구독 계약을 해지해요.
고객이 해지 요청
구독 계약의 중도 해지를 요청해요. 구독 설정에 따라 즉시 해지되거나 관리자 승인 후 해지돼요.
해지 처리 방식
| 설정 | 동작 | 설명 |
|---|---|---|
use_termination_approval: false |
즉시 해지 | 요청 즉시 구독 종료 |
use_termination_approval: true |
승인 대기 | 관리자 승인 후 해지 |
플로우
- 해지 요청 -> 승인 필요?
- 승인 필요? -> 즉시 해지 / 환불 처리 (false)
- 승인 필요? -> 승인 대기 (true)
- 승인 대기 -> 관리자 승인 / 해지 + 환불 (승인)
API 엔드포인트
요청 파라미터
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
order_subscription_id |
String | 선택 | 구독 계약 ID (요청 본문). 없으면 order_number가 필수예요 |
order_number |
String | 선택 | 구독 주문번호. order_subscription_id가 없으면 필수예요 |
user_id |
String | 선택 | 로그인한 회원 ID 또는 이 몰의 외부 회원 ID. 보내면 구독 소유자와 대조해요. Bootpay-User-JWT 헤더로 보내도 돼요 |
reason |
String | 선택 | 중도 해지 사유. 템플릿의 use_termination_reason 이 켜져 있으면 필수 |
termination_fee |
Number | 선택 | 중도 해지 수수료. 수수료 계산 값이 0 이 아니면 필수 |
last_bill_refund_price |
Number | 선택 | 마지막 회차 환불 금액. 수수료 계산 값이 0 이 아니면 필수 |
final_fee |
Number | 선택 | 최종 청구 금액. 수수료 계산 값이 0 이 아니면 필수 |
service_end_at |
String | 선택 | 서비스 종료일 |
수수료 계산이 돌려준 termination_fee·last_bill_refund_price·final_fee 중 0 이 아닌 값을 빼고 보내면 거절돼요. 계산 → 고객 동의 → 해지 요청 순서로 붙여요.
코드 예제
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.orderSubscription.terminateRequest(
'687a1b2c3d4e5f6789012345',
{
reason: '개인 사정으로 인한 해지 요청',
termination_fee: 30000, // 수수료 계산 API 값 그대로
last_bill_refund_price: 15000,
final_fee: 15000
}
)
console.log(response)javascriptfrom bootpay_backend import BootpayCommerce
commerce = BootpayCommerce(
client_key='your-commerce-client-key',
secret_key='your-commerce-secret-key',
mode='production'
)
response = commerce.order_subscription.terminate_request(
'687a1b2c3d4e5f6789012345',
reason='개인 사정으로 인한 해지 요청',
termination_fee=30000,
last_bill_refund_price=15000,
final_fee=15000
)
print(response)pythonuse Bootpay\ServerPhp\BootpayCommerceApi;
$commerce = new BootpayCommerceApi("your-commerce-client-key", "your-commerce-secret-key");
$response = $commerce->orderSubscription->terminateRequest('687a1b2c3d4e5f6789012345', [
'reason' => '개인 사정으로 인한 해지 요청',
'termination_fee' => 30000,
'last_bill_refund_price' => 15000,
'final_fee' => 15000
]);
print_r($response);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.request.orderSubscription.request.ing.OrderSubscriptionTerminationParams;
TokenPayload tp = new TokenPayload("your-commerce-client-key", "your-commerce-secret-key");
BootpayStore commerce = new BootpayStore(tp).withToken();
OrderSubscriptionTerminationParams params = new OrderSubscriptionTerminationParams();
params.orderSubscriptionId = "687a1b2c3d4e5f6789012345";
params.reason = "개인 사정으로 인한 해지 요청";
params.terminationFee = 30000.0;
params.lastBillRefundPrice = 15000.0;
params.finalFee = 15000.0;
BootpayStoreResponse response = commerce.orderSubscription.requestIng.termination(params);
System.out.println(response);javarequire 'bootpay'
commerce = BootpayStore::RestClient.new(client_key: 'your-commerce-client-key', secret_key: 'your-commerce-secret-key')
response = commerce.order_subscription_requests_ing_termination(
order_subscription_id: '687a1b2c3d4e5f6789012345',
reason: '개인 사정으로 인한 해지 요청',
termination_fee: 30000,
last_bill_refund_price: 15000,
final_fee: 15000
)
puts responserubycommerce := bootpay.NewCommerceApi("your-commerce-client-key", "your-commerce-secret-key")
response, err := commerce.OrderSubscription.TerminateRequest("687a1b2c3d4e5f6789012345", bootpay.OrderSubscriptionTerminateRequestParams{
Reason: "개인 사정으로 인한 해지 요청",
})
fmt.Println(response)gousing Bootpay.Commerce;
var commerce = new BootpayCommerceApi("your-commerce-client-key", "your-commerce-secret-key");
var response = await commerce.OrderSubscription.TerminateRequest("687a1b2c3d4e5f6789012345", new {
reason = "개인 사정으로 인한 해지 요청",
termination_fee = 30000,
last_bill_refund_price = 15000,
final_fee = 15000
});
Console.WriteLine(response);csharp응답
성공 응답
요청이 반영된 구독 계약 상세 전체를 돌려줘요. 아래는 일부만 추린 예시예요.
{
"order_subscription_id": "687a1b2c3d4e5f6789012345",
"status": "manual_terminated",
"service_end_at": "2025-07-31T23:59:59+09:00",
"request_histories": [
{
"request_type": "request_termination",
"request_status": "request_approved_auto",
"termination_fee": 30000,
"last_bill_refund_price": 15000,
"final_fee": 15000
}
]
}json응답 필드 설명
| 필드 | 타입 | 설명 |
|---|---|---|
order_subscription_id |
String | 구독 계약 고유 ID |
status |
String | 구독 상태. 자동 승인이면 manual_terminated(직접 해지), 관리자 승인이 필요하면 승인 전까지 그대로예요 |
service_end_at |
String | 서비스 종료일 |
request_histories |
Array | 이 구독의 요청 이력. 해지 요청은 request_type 이 request_termination (내부값 41)이고, 확정된 수수료·환불액이 함께 담겨요. 요청 유형 전체 |
에러 코드
인증·권한 관련 에러는 에러 코드표를 참고해요.
| 코드 | 메시지 | 대처 방법 |
|---|---|---|
ORDER_SUBSCRIPTION_TERMINATE_UNABLE |
이미 해지되었거나 해지할 수 없는 구독 상태예요 | 구독 상태를 확인해요 |
SUBSCRIPTION_TERMINATION_NOT_ALLOWED |
이미 중도해지 되었거나, 정책상 중도해지 요청을 할 수 없는 상태예요. | 구독 상태를 확인해요 |
SUBSCRIPTION_TERMINATION_REASON_REQUIRED |
중도해지 사유를 입력해야 해요. | reason 파라미터를 입력해요 |
SUBSCRIPTION_TERMINATION_FEE1_INVALID |
중도해지 수수료가 유효하지 않아요. | 수수료 계산 값의 termination_fee 를 넣어요 |
SUBSCRIPTION_TERMINATION_FEE2_INVALID |
마지막 회차 환불금액이 유효하지 않아요. | 수수료 계산 값의 last_bill_refund_price 를 넣어요 |
SUBSCRIPTION_TERMINATION_FEE3_INVALID |
최종 수수료가 유효하지 않아요. | 수수료 계산 값의 final_fee 를 넣어요 |
ORDER_SUBSCRIPTION_TERMINATION_REQUEST_DUPLICATE |
이미 대기 중인 중도해지 요청이 있어요. | 기존 요청 처리를 기다려요 |
ORDER_SUBSCRIPTION_TERMINATE_CANNOT_ABLE |
구독해지를 승인할 수 없는 상태예요. | 이미 해지·만료된 구독인지 확인해요 |
지금은 고객 해지 요청이 즉시 해지되거나 관리자 승인으로 해지돼도 subscription.terminated 웹훅이 나가지 않아요. 이 API의 응답 status 나 계약 상세의 status 로 해지 여부를 확인해요.
use_termination 설정이 false인 구독 상품은 해지 요청이 불가능해요. 부트페이 관리자 상품 설정에서 구독 설정을 확인해요.
관리자가 해지 처리
관리자가 직접 구독 계약을 해지해요. 고객 요청 없이 관리자가 즉시 해지할 수 있으며, 수수료와 환불 금액을 직접 설정할 수 있어요.
이 API는 관리자(Supervisor) 권한이 필요해요. 해지된 구독을 되살리는 API는 없어요 — 복구는 부트페이 관리자에서만 할 수 있고, 복구되면 subscription.restored 웹훅이 와요.
고객 요청 vs 관리자 직접 해지
| 구분 | 고객 요청 | 관리자 직접 해지 |
|---|---|---|
| API | 고객 해지 요청 | 이 API |
| 승인 과정 | 관리자 승인 필요 (설정에 따라) | 즉시 해지 |
| 수수료 설정 | 자동 계산 | 관리자가 직접 설정 가능 |
| 환불 금액 | 자동 계산 | 관리자가 직접 설정 가능 |
API 엔드포인트
https://api.bootapi.com/v1/order_subscriptions/:order_subscription_id/terminateBasic Auth (supervisor 권한 필요)요청 파라미터
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
order_subscription_id |
String | 필수 | 구독 계약 ID (URL 파라미터) |
reason |
String | 선택 | 해지 사유 |
termination_fee |
Number | 선택 | 해지 수수료 (관리자 직접 설정) |
last_bill_refund_price |
Number | 선택 | 마지막 회차 환불 금액 |
final_fee |
Number | 선택 | 최종 정산 금액. 양수면 추가 결제, 음수면 그 금액만큼 환불해요. 실제 환불·추가 결제는 이 값으로 실행돼요 |
service_end_at |
String | 선택 | 서비스 종료일 (ISO 8601, 미입력 시 즉시) |
cancel_date |
String | 선택 | 해지 기준일 (ISO 8601). service_end_at 을 비우면 이 날짜가 서비스 종료일이 돼요 |
지금은 termination_fee·last_bill_refund_price·final_fee 를 보내지 않으면 서버 계산값이 아니라 0 으로 확정돼요. 특히 final_fee 가 0 이면 last_bill_refund_price 를 넣었어도 환불이 실행되지 않아요. 수수료 계산에서 받은 값(또는 직접 정한 값)으로 세 값을 모두 함께 보내요.
코드 예제
curl -X PUT "https://api.bootapi.com/v1/order_subscriptions/{order_subscription_id}/terminate" \
-H "Authorization: Basic {base64(client_key:secret_key)}" \
-H "Content-Type: application/json" \
-d '{
"reason": "서비스 정책 변경에 따른 해지",
"termination_fee": 0,
"last_bill_refund_price": 15000,
"final_fee": -15000
}'bashconst { BootpayCommerce } = require('@bootpay/backend-js');
const commerce = new BootpayCommerce({
client_key: '{client_key}',
secret_key: '{secret_key}'
});
const response = await commerce.orderSubscription.adminTerminate(
'sub_abc123',
{
reason: '서비스 정책 변경에 따른 해지',
termination_fee: 0,
last_bill_refund_price: 15000,
final_fee: -15000
}
);
console.log(response);javascriptfrom bootpay_backend.commerce import BootpayCommerce
commerce = BootpayCommerce('{client_key}', '{secret_key}')
response = commerce.order_subscription_admin_terminate(
'sub_abc123',
{
'reason': '서비스 정책 변경에 따른 해지',
'termination_fee': 0,
'last_bill_refund_price': 15000,
'final_fee': -15000
}
)
print(response)pythonuse Bootpay\ServerPhp\BootpayCommerceApi;
$commerce = new BootpayCommerceApi("{client_key}", "{secret_key}");
$response = $commerce->orderSubscriptionAdminTerminate('sub_abc123', [
'reason' => '서비스 정책 변경에 따른 해지',
'termination_fee' => 0,
'last_bill_refund_price' => 15000,
'final_fee' => -15000
]);
print_r($response);phpimport kr.co.bootpay.store.BootpayStore;
import kr.co.bootpay.store.model.request.TokenPayload;
import kr.co.bootpay.store.model.request.orderSubscription.SupervisorTerminateParams;
import kr.co.bootpay.store.model.response.BootpayStoreResponse;
TokenPayload tokenPayload = new TokenPayload("{client_key}", "{secret_key}");
BootpayStore commerce = new BootpayStore(tokenPayload).withToken();
SupervisorTerminateParams params = new SupervisorTerminateParams();
params.reason = "서비스 정책 변경에 따른 해지";
params.terminationFee = 0.0;
params.lastBillRefundPrice = 15000.0;
params.finalFee = -15000.0;
BootpayStoreResponse response = commerce.orderSubscription.supervisorTerminate("sub_abc123", params);
System.out.println(response);javarequire 'bootpay'
commerce = BootpayStore::RestClient.new(client_key: '{client_key}', secret_key: '{secret_key}')
response = commerce.supervisor_request_order_subscription_terminate(
order_subscription_id: 'sub_abc123',
reason: '서비스 정책 변경에 따른 해지',
termination_fee: 0,
last_bill_refund_price: 15000,
final_fee: -15000
)
puts responserubycommerce := bootpay.NewCommerceApi("{client_key}", "{secret_key}")
response, err := commerce.OrderSubscriptionAdminTerminate("sub_abc123", map[string]interface{}{
"reason": "서비스 정책 변경에 따른 해지",
"termination_fee": 0,
"last_bill_refund_price": 15000,
"final_fee": -15000,
})
fmt.Println(response)govar commerce = new BootpayCommerceApi("{client_key}", "{secret_key}");
var response = await commerce.OrderSubscriptionAdminTerminate("sub_abc123", new {
reason = "서비스 정책 변경에 따른 해지",
termination_fee = 0,
last_bill_refund_price = 15000,
final_fee = -15000
});
Console.WriteLine(response);csharp응답
{
"order_subscription_id": "687a1b2c3d4e5f6789012345",
"status": "admin_terminated",
"service_end_at": "2025-07-11T15:00:00+09:00"
}json응답 필드 설명
| 파라미터 | 타입 | 설명 |
|---|---|---|
| order_subscription_id | String | 구독 계약 ID |
| status | String | 변경된 상태 (admin_terminated: 관리자 해지) |
| service_end_at | String | 서비스 종료 시각 |
해지 사유와 확정된 수수료·환불액은 신청 관리의 해지 요청 이력(request_type: request_termination)에 남아요. 관리자 해지 웹훅은 subscription.admin_terminated 로 와요.
에러 코드
인증·권한 관련 에러는 에러 코드표를 참고해요.
| 코드 | 메시지 | 대처 방법 |
|---|---|---|
ORDER_SUBSCRIPTION_NOT_FOUND |
구독결제 건을 찾을 수 없어요. | order_subscription_id를 확인해요 |
ORDER_SUBSCRIPTION_TERMINATE_CANNOT_ABLE |
구독해지를 승인할 수 없는 상태예요. | 이미 해지·만료된 구독인지 확인해요 |
TIME_INVALID |
시간 포맷이 잘못되었어요. ISO 8601 표준시 포맷으로 보내요. | service_end_at·cancel_date 형식을 확인해요 |
