Refund a payment
Pays amount of the payment’s asset back from the master wallet to destination, as a payout. Requires the Idempotency-Key header; an API key needs refunds:write and payouts:write (neither is given to a key by default) and is restricted as for POST /payouts: only to the destinations on its payout allowlist, or, with an IP allowlist alone, to any valid address when called from those IP addresses. The refunds of a payment cannot exceed it. When a refund completes, its invoice becomes refunded, or keeps its status and gains the flag partially_refunded when only part went back. Test mode: the refund of a simulated payment is played, not paid: it is completed at once with simulated: true, emits refund.created and refund.completed, and creates no payout and no transaction.
Authorization
Section titled “Authorization”Send your API key as a bearer token in the Authorization header. Secret API key: Authorization: Bearer ctp_live_sk_…. A publishable key (ctp_…_pk_…) is accepted by these operations only, which serve reference data: getRate, listAssets, listNetworks (each marked x-publishable-key: true). Every other operation refuses it with 403 publishable_key_not_allowed, whatever Origin the request carries: that header binds browsers, not other clients, and nothing on this API relies on it.
Headers
Section titled “Headers”| Name | Type | Description |
|---|---|---|
Idempotency-Key required |
string | Required: this operation moves money, and without the header it answers 400 idempotency_key_required. Use one key per payout or refund and send the same key again when you retry: the same key with the same request returns the payout or refund that exists and never pays twice; the same key with a different request is 422 idempotency_key_reused. For 24 hours the first response is replayed as it was (Idempotent-Replayed: true); while the first request is still running, 409 idempotency_request_in_progress with Retry-After. At most 255 characters. Pattern ^[!-~]+$. |
Request body
Section titled “Request body”| Field | Type | Description |
|---|---|---|
payment_id required |
string | At most 100 characters. |
amount required |
string | Base units of the payment’s asset. A whole number written as a string of digits. |
destination required |
string | The payer’s address. At most 128 characters. |
reason required |
string | At most 200 characters. |
Example request
Section titled “Example request”curl -X POST "https://api.ctpeway.com/v1/refunds" \ -H "Authorization: Bearer $CTPEWAY_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: <a-unique-key>" \ -d '{ "payment_id": "<payment-id>", "amount": "25000000", "destination": "<payer-address>", "reason": "Order canceled"}'import { CtpeWay } from '@ctpeway/sdk';
const ctpeway = new CtpeWay(process.env.CTPEWAY_API_KEY);
const result = await ctpeway.refunds.create({ payment_id: '<payment-id>', amount: '25000000', destination: '<payer-address>', reason: 'Order canceled',});Response
Section titled “Response”201 Created
Section titled “201 Created”| Field | Type | Description |
|---|---|---|
id |
string | |
status |
string | One of pending, completed, failed. |
payment_id |
string | |
invoice_id optional |
string | |
payout_id optional |
string | |
network |
string | |
asset |
string | |
asset_id |
string | |
contract optional |
string | |
decimals |
integer | |
amount |
string | Base units of asset, the asset of the refunded payment. A whole number written as a string of digits. |
destination |
string | |
reason |
string | |
tx_hash optional |
string | |
failure_code optional |
string | |
created_at |
string (date-time) | |
completed_at optional |
string (date-time) | |
failed_at optional |
string (date-time) | |
livemode |
boolean | |
simulated |
boolean | true: the refund of a simulated payment (test mode). It was played, not paid: no payout, no transaction, and tx_hash is a sim_… placeholder. |
failure_message optional |
string | What failure_code means, in a sentence for a person. Absent for codes that need no explanation. |
{ "id": "string", "status": "pending", "payment_id": "string", "invoice_id": "string", "payout_id": "string", "network": "string", "asset": "string", "asset_id": "string", "contract": "string", "decimals": 0, "amount": "1000000", "destination": "string", "reason": "string", "tx_hash": "string", "failure_code": "string", "created_at": "2026-01-01T00:00:00.000Z", "completed_at": "2026-01-01T00:00:00.000Z", "failed_at": "2026-01-01T00:00:00.000Z", "livemode": true, "simulated": true, "failure_message": "string"}The values are placeholders generated from the schema. They show the shape, not real data.
Errors
Section titled “Errors”A failed request answers with the error format described under Errors.
Besides the codes any endpoint can return, this endpoint can answer with these code values:
idempotency_key_requiredidempotency_key_reusedmaster_wallet_missingmerchant_on_holdpayment_not_foundpayment_not_refundablepayout_allowlist_requiredpayout_amount_below_dustpayout_balance_unavailablepayout_destination_invalidpayout_destination_not_allowlistedpayout_destination_not_payablepayout_destination_own_addresspayout_insufficient_balancepayout_not_authorizedpayouts_pausedrefund_exceeds_payment

