결제 취소 API를 사용해서 승인된 결제를 취소하고 환불하는 기능을 구현해보세요.
API를 사용하기 위해 필요한 키 정보와 인증 방식, 보안에 대한 정보는 API 사용하기에서 자세히 알아보세요.
승인된 결제를 취소하려면 결제 승인 요청 결과로 발급 받은 paymentKey
와 결제 취소 이유인 cancelReason
이 필요합니다.
결제 취소 API 엔드포인트에 paymentKey
를 Path 파라미터로 추가하고 cancelReason
은 Request Body 파라미터로 추가하세요.
curl --request POST \
--url https://api.tosspayments.com/v1/payments/WxdQtiCTz74W3Im2a_vqD/cancel \
--header 'Authorization: Basic dGVzdF9za196WExrS0V5cE5BcldtbzUwblgzbG1lYXhZRzVSOg==' \
--header 'Content-Type: application/json' \
--data '{"cancelReason":"고객이 취소를 원함"}'
응답으로 Payment 객체의 cancels
필드에 취소 객체가 배열로 돌아옵니다.
{
"mId": "tosspayments",
"version": "1.4",
"transactionKey": "",
"paymentKey": "",
"orderId": "",
"orderName": "토스 티셔츠 외 2건",
"currency": "KRW",
"method": "카드",
"status": "CANCELED",
"requestedAt": "2022-01-01T11:31:29+09:00",
"approvedAt": "2022-01-01T11:31:51+09:00",
"useEscrow": false,
"cultureExpense": false,
"card": {
"company": "우리",
"number": "100212*****12",
"installmentPlanMonths": 0,
"isInterestFree": false,
"interestPayer": null,
"approveNo": "00000000",
"useCardPoint": false,
"cardType": "신용",
"ownerType": "개인",
"acquireStatus": "READY",
"receiptUrl": "https://dashboard.tosspayments.com/sales-slip?transactionId=bGqvzDSq5OoimabqhwIGRNk5Ks4A%2B2pzVwKxOP0WsjnZ6FZiUqVa4RbnVeqVlxsd&ref=PX"
},
"virtualAccount": null,
"transfer": null,
"mobilePhone": null,
"giftCertificate": null,
"foreignEasyPay": null,
"cashReceipt": null,
"discount": null,
"cancels": [
{
"cancelReason": "고객이 취소를 원함",
"canceledAt": "2022-01-01T11:32:04+09:00",
"cancelAmount": 10000,
"taxFreeAmount": 0,
"taxAmount": null,
"refundableAmount": 0
}
],
"secret": null,
"type": "NORMAL",
"easyPay": "토스페이",
"country": "KR",
"failure": null,
"totalAmount": 10000,
"balanceAmount": 0,
"suppliedAmount": 0,
"vat": 0,
"taxFreeAmount": 0
}
같은 paymentKey
를 이용한 취소 요청이 1초에 2번 이상 연속으로 들어오면 첫 번째 요청만 처리됩니다.
결제 금액 중 일부만 취소하려면 cancelAmount
에 취소할 금액을 추가해서 결제 취소 API를 요청합니다.
cancelAmount
에 값을 넣지 않으면 전액 취소됩니다.
curl --request POST \
--url https://api.tosspayments.com/v1/payments/h__greVcS1OZxT1Dy_WBE/cancel \
--header 'Authorization: Basic dGVzdF9za196WExrS0V5cE5BcldtbzUwblgzbG1lYXhZRzVSOg==' \
--header 'Content-Type: application/json' \
--data '{"cancelReason":"고객이 취소를 원함","cancelAmount":1000}'
부분 취소를 여러 번 하면 아래와 같이 cancels
필드에 취소 객체가 여러 개 쌓입니다.
// Payment 객체
{
// ...
"cancels": [
{
"cancelAmount": 1000,
"cancelReason": "고객이 취소를 원함",
"taxFreeAmount": 0,
"taxAmount": null,
"refundableAmount": 9000,
"canceledAt": "2022-01-01T23:23:52+09:00"
},
{
"cancelAmount": 1000,
"cancelReason": "고객이 다른 품목도 취소를 원함",
"taxFreeAmount": 0,
"taxAmount": null,
"refundableAmount": 8000,
"canceledAt": "2022-01-02T20:00:00+09:00"
}
]
// ...
}
가상계좌 결제를 취소할 때는 환불 받을 고객의 계좌 정보를 취소 요청에 포함해야 합니다.
refundReceiveAccount
에 환불 받을 고객의 계좌 정보를 포함해서 결제 취소를 요청하세요. 요청한 계좌로 취소 금액이 환불됩니다.
curl --request POST \
--url https://api.tosspayments.com/v1/payments/1Y49VfzAot_2KS-Ok61R7/cancel \
--header 'Authorization: Basic dGVzdF9za196WExrS0V5cE5BcldtbzUwblgzbG1lYXhZRzVSOg==' \
--header 'Content-Type: application/json' \
--data '{"cancelReason":"고객이 취소를 원함","cancelAmount":10000,"refundReceiveAccount":{"bank":"우리","accountNumber":"1000123456789","holderName":"김토페"}}'
응답은 다른 취소 요청과 동일하게 Payment 객체의 cancels
로 돌아옵니다.
{
"mId": "tvivarepublica",
"version": "1.4",
"transactionKey": "",
"paymentKey": "",
"orderId": "",
"orderName": "토스 티셔츠 외 2건",
"currency": "KRW",
"method": "가상계좌",
"status": "PARTIAL_CANCELED",
"requestedAt": "2022-01-01T11:48:53+09:00",
"approvedAt": "2022-01-01T11:49:35+09:00",
"useEscrow": false,
"cultureExpense": false,
"card": null,
"virtualAccount": {
"accountNumber": "X6505831718354",
"accountType": "일반",
"bank": "우리",
"customerName": "김토스",
"dueDate": "2022-01-03T11:48:53+09:00",
"expired": true,
"settlementStatus": "INCOMPLETED",
"refundStatus": "NONE"
},
"transfer": null,
"mobilePhone": null,
"giftCertificate": null,
"cashReceipt": null,
"discount": null,
"cancels": [
{
"cancelReason": "고객 변심",
"canceledAt": "2022-01-01T11:51:04+09:00",
"cancelAmount": 10000,
"taxFreeAmount": 0,
"taxAmount": null,
"refundableAmount": 0
}
],
"secret": null,
"type": "NORMAL",
"easyPay": null,
"country": "KR",
"failure": null,
"totalAmount": 10000,
"balanceAmount": 0,
"suppliedAmount": 0,
"vat": 0,
"taxFreeAmount": 0
}
환불할 수 있는 금액 정보인 refundableAmount
파라미터를 요청에 포함하면 취소 요청을 안전하게 처리할 수 있습니다.
취소 요청에 포함된 refundableAmount
와 실제 환불할 수 있는 잔액 정보가 서로 다르면 응답으로 에러가 돌아옵니다. 아래 예시를 확인해보세요.
고객이 15,000원을 결제한 뒤 5,000원을 취소하고 싶습니다. 이 때 취소할 수 있는 금액은 전액이므로 결제 취소를 요청할 때 refundableAmount
값으로 15000
을 넣습니다.
curl --request POST \
--url https://api.tosspayments.com/v1/payments/fPGxdC2rs4AoPcTk_-SeW/cancel \
--header 'Authorization: Basic dGVzdF9za196WExrS0V5cE5BcldtbzUwblgzbG1lYXhZRzVSOg==' \
--header 'Content-Type: application/json' \
--data '{"cancelReason":"고객이 취소를 원함","cancelAmount":5000,"refundReceiveAccount":{"bank":"우리","accountNumber":"1000123456789","holderName":"김토페"},"refundableAmount":15000}'
5,000원이 성공적으로 부분 취소되어 응답으로 돌아오는 refundableAmount
는 원래 결제 금액인 15,000원에서 취소된 5,000원을 뺀 10,000원 입니다.
{
"mId": "tosspayments",
"version": "1.4",
"transactionKey": "",
"paymentKey": "",
"orderId": "",
"orderName": "토스 티셔츠 외 2건",
"currency": "KRW",
"method": "카드",
"status": "CANCELED",
"requestedAt": "2022-01-01T11:31:29+09:00",
"approvedAt": "2022-01-01T11:31:51+09:00",
"useEscrow": false,
"cultureExpense": false,
"card": {
"company": "우리",
"number": "100212*****12",
"installmentPlanMonths": 0,
"isInterestFree": false,
"interestPayer": null,
"approveNo": "00000000",
"useCardPoint": false,
"cardType": "신용",
"ownerType": "개인",
"acquireStatus": "READY",
"receiptUrl": "https://dashboard.tosspayments.com/sales-slip?transactionId=bGqvzDSq5OoimabqhwIGRNk5Ks4A%2B2pzVwKxOP0WsjnZ6FZiUqVa4RbnVeqVlxsd&ref=PX"
},
"virtualAccount": null,
"transfer": null,
"mobilePhone": null,
"giftCertificate": null,
"foreignEasyPay": null,
"cashReceipt": null,
"discount": null,
"cancels": [
{
"cancelReason": "고객이 취소를 원함",
"canceledAt": "2022-01-01T11:32:04+09:00",
"cancelAmount": 5000,
"taxFreeAmount": 0,
"taxAmount": null,
"refundableAmount": 10000
}
],
"secret": null,
"type": "NORMAL",
"easyPay": "토스페이",
"country": "KR",
"failure": null,
"totalAmount": 15000,
"balanceAmount": 5000,
"suppliedAmount": 4545,
"vat": 455,
"taxFreeAmount": 0
}
이후 고객이 한 번 더 5,000원을 취소하려고 합니다. 앞서 돌아온 환불 가능한 잔액은 10,000원이므로 두 번째 취소를 할 때는 refundableAmount
값으로 10000
을 넣어줍니다.
curl --request POST \
--url https://api.tosspayments.com/v1/payments/fPGxdC2rs4AoPcTk_-SeW/cancel \
--header 'Authorization: Basic dGVzdF9za196WExrS0V5cE5BcldtbzUwblgzbG1lYXhZRzVSOg==' \
--header 'Content-Type: application/json' \
--data '{"cancelReason":"고객이 다른 품목도 취소를 원함","cancelAmount":5000,"refundReceiveAccount":{"bank":"우리","accountNumber":"1000123456789","holderName":"김토페"},"refundableAmount":10000}'
응답으로 cancels
에 결제 취소 기록이 돌아옵니다. 마지막에 쌓인 취소 기록의 refundableAmount
값은 전체 결제 금액인 15,000원에서 두 번의 부분 취소 금액을 뺀 5000원 입니다.
{
// ...
"cancels": [
{
"cancelReason": "고객이 취소를 원함",
"canceledAt": "2022-01-01T11:32:04+09:00",
"cancelAmount": 5000,
"taxFreeAmount": 0,
"taxAmount": null,
"refundableAmount": 10000
},
{
"cancelReason": "고객이 다른 품목도 취소를 원함",
"canceledAt": "2022-01-02T20:00:00+09:00",
"cancelAmount": 5000,
"taxFreeAmount": 0,
"taxAmount": null,
"refundableAmount": 5000
}
],
// ...
}
이렇게 현재 환불할 수 있는 잔액을 refundableAmount
값으로 담아 보내고, 응답으로 돌아오는 값을 확인해서 결제 취소가 중복으로 요청되거나 결제된 금액보다 더 많이 취소하는 상황을 방지하는 데 활용할 수 있습니다.