결제 링크(Invoice)를 생성하여 고객에게 전송해요. 고객은 링크를 통해 결제 페이지에 접속해 결제를 완료할 수 있어요.
개념 배경
- 결제만 붙이면 되나요, 주문까지 해야 하나요? — 링크 결제만으로 충분한지 판단하는 기준
- 정산 흐름 이해하기 — 링크로 받은 대금이 언제 들어오나
상품을 등록하지 않아도 돼요
링크페이는 상품 없이 결제가 성립하는 유일한 경로예요. 주문명(name)·금액(price)·고객(user.user_id)·식별자(request_id) 넷이면 링크가 나와요.
products 를 넣으면 상품 기준으로 금액이 다시 계산되고 재고·구독 설정이 함께 걸려요. 넣지 않으면 넘긴 price 가 그대로 청구돼요. 상담 결제·후불 청구처럼 품목이 매번 달라지는 흐름이라면 상품을 안 넣는 편이 단순해요.
코드 없이 생성하려면
관리자 콘솔에서 UI로 링크페이를 생성할 수도 있어요. → 관리자에서 생성
API 엔드포인트
활용 시나리오
| 시나리오 | 설명 |
|---|---|
| 비대면 결제 | 고객에게 결제 링크를 SMS/이메일로 발송 |
| 오프라인 연동 | 매장에서 결제 링크를 생성하여 고객에게 전송 |
| 반복 청구 | 정기적으로 청구서를 생성하여 발송 |
처리 흐름
- 결제 안내 메시지 발송 (이메일, SMS, 알림톡)
- 고객이 링크 클릭 후 결제 진행
- 결제 완료 시 Webhook으로 결과 전달
- 고객에게 완료 알림 발송
링크 생성 전 결제 대상 고객 정보(이메일, 휴대폰 번호, 이름)를 먼저 등록해두면 user 파라미터로 바로 연결할 수 있어요.
요청 파라미터
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
name |
String | 필수 | 주문명 |
price |
Integer | 필수 | 결제 금액 |
tax_free_price |
Integer | 선택 | 면세 금액 |
delivery_price |
Integer | 선택 | 배송비 |
memo |
String | 선택 | 내부 메모 |
request_id |
String | 필수 | 가맹점 고유 식별자 (웹훅 매칭용) — 비우면 INVOICE_REQUEST_ID_BLANK 로 거절돼요 |
redirect_url |
String | 선택 | 결제 완료 후 이동할 URL |
webhook_url |
String | 선택 | 결제 결과를 받을 웹훅 URL |
header_content_type |
String | 선택 | 웹훅 전송 시 Content-Type |
usage_api_url |
String | 선택 | 사용량 기반 구독의 사용량 조회 URL |
use_notification |
Boolean | 선택 | 알림 자동 발송 여부 |
use_auto_login |
Boolean | 선택 | 링크 진입 시 자동 로그인 여부 |
expired_at |
String | 선택 | 링크 만료 시간 (ISO 8601). 생략하면 생성일로부터 3일 뒤 자정이에요. 과거 시각은 INVOICE_EXPIRED_DATE_INVALID 로 거절돼요 |
metadata |
Object | 선택 | 추가 데이터 |
extra |
Object | 선택 | 결제창 부가 설정 |
user |
Object | 필수 | 고객 정보 — 없으면 INVOICE_TARGET_NOT_FOUND 로 거절돼요 |
└─ membership_type |
String | 선택 | 고객 유형 (member 기본 / guest) |
└─ user_id |
String | 필수 | 고객 ID — 이 값이 없으면 user 객체 전체가 무시돼 요청이 거절돼요. member 면 부트페이에 등록된 고객의 user_id(몰 설정에 따라 로그인 ID·이메일도 가능)여야 하고, 없는 회원이면 INVOICE_TARGET_NOT_FOUND 예요. guest 면 가맹점 쪽 식별자이고, 처음 보는 값이면 비회원 고객을 새로 만들어요 |
└─ name |
String | 선택 | 고객 이름 (guest 로 새로 만들 때 쓰여요. 비우면 비회원) |
└─ corporate_type |
String | 선택 | member 일 때 개인·기업 구분 (individual 기본 / corporate) |
└─ email |
String | 선택 | 고객 이메일 |
└─ phone |
String | 선택 | 고객 전화번호 |
products |
Array | 선택 | 상품 목록 |
└─ product_id |
String | 선택 | 상품 ID |
└─ product_option_id |
String | 선택 | 옵션 ID. 옵션이 있는 상품이면 필수(INVOICE_PRODUCT_OPTION_ID_REQUIRED) |
└─ quantity |
Integer | 선택 | 수량 |
└─ duration |
Integer | 선택 | 구독 회차 수. 구독 기간이 설정된 구독 상품이면 필수 — 상품에 등록된 회차 중 하나여야 해요(INVOICE_PRODUCT_SUBSCRIPTION_DURATION_NOT_FOUND). 수시결제·사용량 구독은 생략해요. subscription_duration 으로 보내면 이 API 는 읽지 않아요 |
코드 예제
const { BootpayCommerce } = require('@bootpay/backend-js')
const commerce = new BootpayCommerce({
client_key: 'your-commerce-client-key',
secret_key: 'your-commerce-secret-key',
mode: 'production'
})
const response = await commerce.invoice.create({
name: 'Professional 플랜 구독',
price: 29900,
request_id: 'invoice_20250801_0001',
use_notification: true,
expired_at: '2025-08-01T23:59:59Z',
redirect_url: 'https://myshop.com/payment/complete',
user: {
membership_type: 'guest', // 부트페이에 등록된 고객이면 생략하고 그 고객의 user_id 를 넣어요
user_id: 'user_20250801',
email: 'user@example.com',
phone: '01012345678'
},
products: [
{
product_id: '67c95e64d01640bb9859c629',
quantity: 1,
duration: 12
}
]
})
console.log('invoice_url:', response.invoice_url)javascriptfrom bootpay_backend import BootpayCommerce
commerce = BootpayCommerce(
client_key='your-commerce-client-key',
secret_key='your-commerce-secret-key',
mode='production'
)
response = commerce.invoice.create(
name='Professional 플랜 구독',
price=29900,
request_id='invoice_20250801_0001',
use_notification=True,
expired_at='2025-08-01T23:59:59Z',
redirect_url='https://myshop.com/payment/complete',
user={
'membership_type': 'guest',
'user_id': 'user_20250801',
'email': 'user@example.com',
'phone': '01012345678'
},
products=[
{
'product_id': '67c95e64d01640bb9859c629',
'quantity': 1,
'duration': 12
}
]
)
print('invoice_url:', response['invoice_url'])pythonuse Bootpay\ServerPhp\BootpayCommerceApi;
$commerce = new BootpayCommerceApi('your-commerce-client-key', 'your-commerce-secret-key');
$response = $commerce->invoice->create([
'name' => 'Professional 플랜 구독',
'price' => 29900,
'request_id' => 'invoice_20250801_0001',
'use_notification' => true,
'expired_at' => '2025-08-01T23:59:59Z',
'redirect_url' => 'https://myshop.com/payment/complete',
'user' => [
'membership_type' => 'guest',
'user_id' => 'user_20250801',
'email' => 'user@example.com',
'phone' => '01012345678'
],
'products' => [
[
'product_id' => '67c95e64d01640bb9859c629',
'quantity' => 1,
'duration' => 12
]
]
]);
echo 'invoice_url: ' . $response['invoice_url'];phpimport kr.co.bootpay.store.BootpayStore;
import kr.co.bootpay.store.model.request.TokenPayload;
import kr.co.bootpay.store.model.response.BootpayStoreResponse;
import kr.co.bootpay.store.model.pojo.SInvoice;
import kr.co.bootpay.store.model.pojo.SInvoiceProduct;
import kr.co.bootpay.store.model.pojo.SInvoiceUser;
import java.util.Arrays;
TokenPayload tp = new TokenPayload("your-commerce-client-key", "your-commerce-secret-key");
BootpayStore commerce = new BootpayStore(tp).withToken();
SInvoiceUser user = new SInvoiceUser();
user.membershipType = SInvoiceUser.MEMBERSHIP_TYPE_GUEST; // 등록된 고객이면 생략하고 그 고객의 user_id 를 넣어요
user.userId = "user_20250801"; // 필수 — 이 값이 없으면 user 객체 전체가 무시돼요
user.email = "user@example.com";
user.phone = "01012345678";
SInvoiceProduct product = new SInvoiceProduct();
product.productId = "67c95e64d01640bb9859c629";
product.quantity = 1;
product.duration = 12;
SInvoice invoice = new SInvoice();
invoice.name = "Professional 플랜 구독";
invoice.price = 29900.0;
invoice.requestId = "invoice_20250801_0001"; // 필수 — 가맹점 고유 식별자
invoice.useNotification = true;
invoice.expiredAt = "2025-08-01T23:59:59Z";
invoice.redirectUrl = "https://myshop.com/payment/complete";
invoice.user = user;
invoice.products = Arrays.asList(product);
BootpayStoreResponse response = commerce.invoice.create(invoice);
System.out.println("invoice_url: " + response.getData().get("invoice_url"));javarequire 'bootpay'
commerce = BootpayStore::RestClient.new(client_key: 'your-commerce-client-key', secret_key: 'your-commerce-secret-key')
response = commerce.request_checkout(
name: 'Professional 플랜 구독',
price: 29900,
request_id: 'invoice_20250801_0001',
use_notification: true,
expired_at: '2025-08-01T23:59:59Z',
redirect_url: 'https://myshop.com/payment/complete',
user: {
membership_type: 'guest',
user_id: 'user_20250801',
email: 'user@example.com',
phone: '01012345678'
},
products: [
{
product_id: '67c95e64d01640bb9859c629',
quantity: 1,
duration: 12
}
]
)
puts "invoice_url: #{response['invoice_url']}"rubyimport "github.com/bootpay/backend-go/v2"
commerce := bootpay.NewCommerceApi("your-commerce-client-key", "your-commerce-secret-key")
response, err := commerce.Invoice.Create(bootpay.InvoiceCreateParams{
Name: "Professional 플랜 구독",
Price: 29900,
RequestId: "invoice_20250801_0001",
UseNotification: true,
SendTypes: []int{1, 2},
ExpiredAt: "2025-08-01T23:59:59Z",
RedirectUrl: "https://myshop.com/payment/complete",
User: bootpay.UserParams{
UserId: "user_20250801",
Email: "user@example.com",
Phone: "01012345678",
},
Products: []bootpay.ProductParams{
{
ProductId: "67c95e64d01640bb9859c629",
Quantity: 1,
Duration: 12,
},
},
})
fmt.Println("invoice_url:", response["invoice_url"])gousing Bootpay.Commerce;
var commerce = new BootpayCommerceApi("your-commerce-client-key", "your-commerce-secret-key");
var response = await commerce.Invoice.Create(new {
name = "Professional 플랜 구독",
price = 29900,
request_id = "invoice_20250801_0001",
use_notification = true,
expired_at = "2025-08-01T23:59:59Z",
redirect_url = "https://myshop.com/payment/complete",
user = new {
membership_type = "guest",
user_id = "user_20250801",
email = "user@example.com",
phone = "01012345678"
},
products = new[] {
new {
product_id = "67c95e64d01640bb9859c629",
quantity = 1,
duration = 12
}
}
});
Console.WriteLine($"payment_url: {response.PaymentUrl}");csharp응답
성공 응답
주요 필드만 추린 예시예요.
{
"invoice_id": "687a1b2c3d4e5f6789012345",
"name": "Professional 플랜 구독",
"price": 29900,
"status": 1,
"invoice_url": "https://i.bootpay.co.kr/i/{client_key}/687a1b2c3d4e5f6789012345",
"user_id": "68707c59b0eacea5cd974efd",
"expired_at": "2025-08-01 23:59:59",
"c_at": "2025-07-29T10:00:00+09:00"
}json응답 필드 설명
| 필드 | 타입 | 설명 |
|---|---|---|
invoice_id |
String | 생성된 링크페이 ID (24자리) |
invoice_url |
String | 고객에게 전달할 결제 페이지 URL |
status |
Integer | 링크페이 상태 (1 결제 가능 — 링크페이 상태) |
user_id |
String | 연결된 부트페이 고객 ID |
expired_at |
String | 링크 만료 시간 (YYYY-MM-DD HH:MM:SS) |
c_at |
String | 생성 시각 (ISO 8601) |
`extra.create_order_immediately: true` 면 응답이 달라요
링크페이와 함께 주문 초안까지 바로 만들고, 위 링크페이 정보 대신 주문 정보(주문 상세와 같은 필드, 결제 URL 은 order_url)를 돌려줘요.
에러 코드
공통 에러
인증·권한 관련 에러는 에러 코드표를 참고해요.
| 코드 | 메시지 | 대처 방법 |
|---|---|---|
INVOICE_TARGET_NOT_FOUND |
링크페이에 등록된 사용자 정보가 없어요 | user.user_id 를 넣었는지, member 라면 부트페이에 등록된 고객인지 확인해요 |
INVOICE_REQUEST_ID_BLANK |
요청 ID가 필요해요 | request_id 를 넣어요 |
INVOICE_EXPIRED_DATE_INVALID |
만료일이 현재 시간보다 과거예요 | expired_at 을 미래 시각으로 넣어요 |
PRODUCT_NOT_FOUND |
존재하지 않는 상품이에요 | product_id 가 이 프로젝트의 상품인지 확인해요 |
INVOICE_PRODUCT_NOT_FOUND |
존재하지 않는 상품이 포함되어 있어요 | 판매 중(노출) 상품인지 확인해요 |
INVOICE_ITEM_QTY_LT_ZERO |
상품 수량은 0보다 커야 해요 | quantity 를 1 이상으로 넣어요 |
INVOICE_PRODUCT_OPTION_ID_REQUIRED |
옵션이 필요한 상품이 포함되어 있어요 | product_option_id 를 넣어요 |
INVOICE_PRODUCT_OPTION_NOT_FOUND |
존재하지 않는 옵션이 포함되어 있어요 | product_option_id 를 확인해요 |
INVOICE_SUBSCRIPTION_REQUEST_ONLY_ONE |
구독 상품은 하나만 청구서에 포함될 수 있어요 | 구독 상품은 한 줄만 넣어요 |
INVOICE_PRODUCT_SUBSCRIPTION_DURATION_NOT_FOUND |
존재하지 않는 구독 기간이 포함되어 있어요 | duration 을 상품에 등록된 회차로 넣어요 |
INVOICE_NEED_USAGE_API_URL |
사용량 기반 구독 상품은 사용량 API URL이 필요해요 | 상품에 사용량 조회 URL 을 등록해요 |
use_notification: true로 설정하면 고객에게 SMS, 카카오톡, 이메일로 결제 링크가 자동 발송돼요.
