Refunds

Issue full or partial refunds against captured transactions, track refund lifecycle status, and reconcile refunded amounts with the Tokeflow API.

A refund returns funds for a transaction that has already been captured. With Tokeflow you issue refunds against a single, normalized API — the orchestration layer relays the request to the connected payment provider, tracks the result, and reflects it back on both the refund record and the parent transaction.

Tokeflow standardizes refunds across providers: one request shape, one status lifecycle, one reconciliation model. The connected payment provider performs the actual movement of funds.

Tokeflow is a payment orchestration layer. It records, routes, and tracks refund requests and normalizes the result; the connected payment provider executes the refund and settles funds.

How refunds work

Every refund is a child of a captured transaction. You can refund the full remaining captured amount, or issue several partial refunds over time until the transaction is fully refunded.

  • Full refund — omit amount to refund the entire remaining captured amount.
  • Partial refund — pass amount (in minor units) to refund less than the remaining captured amount. The remaining balance stays captured and can be refunded later.
  • Multiple partial refunds — repeat partial refunds until the remaining captured amount reaches zero. Each call creates a new refund record.

A refund can only be initiated when the parent transaction is in authorized or refund_pending status. Once a refund is initiated, the transaction moves to refund_pending while the provider confirms the result.

Refund results are typically asynchronous. The synchronous response confirms the request was accepted (refund_pending); subscribe to webhooks to learn the final outcome instead of polling.

Refund lifecycle

A refund record has its own status, independent of the parent transaction.

StatusMeaning
pendingRefund initiated, not yet confirmed by the provider.
succeededRefund confirmed by the provider.
failedRefund failed at the provider; see failure_reason.
cancelledRefund was cancelled before processing.

How refund status maps to the transaction

The parent transaction reflects the aggregate refund state:

  • While any refund is pending, the transaction is refund_pending.
  • When the captured amount has been fully refunded, the transaction becomes refunded.
  • When only part of the captured amount has been refunded, the transaction becomes partially_refunded.

Endpoints

POST/api/v1/transactions/:id/refund

Refund a captured transaction. Supports full and multiple partial refunds.

OrgMerchant

Auth: Organization key (with merchant_id) or Merchant key. Required scope: transactions:write.

Refund a previously captured transaction. Omit amount for a full refund of the remaining captured amount, or pass amount for a partial refund.

The transaction must be in authorized or refund_pending status, and the refund amount must not exceed the remaining captured amount. Other states return 422 business_rule_error; an amount over the remaining balance returns 400 validation_error.

Path parameters

FieldTypeRequiredDescription
idstringYesTransaction ID (tx_…) to refund.

Query parameters

FieldTypeRequiredDescription
merchant_idstringConditionalTarget merchant (mrc_…). Required when using an Organization key; inferred from a Merchant key.

Body

FieldTypeRequiredDescription
amountintegerNoAmount to refund in minor units. Omit for a full refund. Must be ≥ 1 and ≤ the remaining captured amount.
reasonenumNoWhy the refund was issued. One of duplicate, fraudulent, requested_by_customer.

Send an Idempotency-Key header on refund requests so an automatic retry never issues a second refund. Replaying the same key returns the original result. See Idempotency & rate limits.

curl -X POST "https://api.tokeflow.com/api/v1/transactions/tx_777/refund" \
  -H "Authorization: Bearer sk_live_mer_…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 3f1c9a7e-2b4d-4e0a-9c6f-7d1e2a3b4c5d" \
  -d '{
    "amount": 5000,
    "reason": "requested_by_customer"
  }'

For a full refund, omit amount:

curl -X POST "https://api.tokeflow.com/api/v1/transactions/tx_777/refund" \
  -H "Authorization: Bearer sk_live_mer_…" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "duplicate" }'

Response 200 OK

The response confirms the refund was accepted and echoes the reconciliation totals on the transaction. Money fields are in minor units.

{
  "success": true,
  "data": {
    "id": "tx_777",
    "refund_id": "ref_abc123",
    "status": "refund_pending",
    "amount_captured": 15000,
    "amount_refunded": 5000,
    "total_refunded": 5000,
    "updated_at": "2026-01-15T12:30:00.000Z"
  },
  "request_id": "req_8f3c2a1b9d4e",
  "timestamp": "2026-01-15T12:30:00.000Z"
}
FieldTypeDescription
idstringParent transaction ID (tx_…).
refund_idstringThe refund record created by this request (ref_…).
statusenumTransaction status after the request — refund_pending.
amount_capturedintegerTotal amount captured on the transaction, unchanged by the refund.
amount_refundedintegerAmount refunded by this request.
total_refundedintegerCumulative amount refunded across all refunds on the transaction.
updated_atstringISO 8601 UTC timestamp of the update.

On a replayed Idempotency-Key, this endpoint returns the original result with HTTP 200 rather than creating a new refund.


GET/api/v1/transactions/:id/refunds

List all refunds for a transaction, newest first.

OrgMerchant

Auth: Organization key (with merchant_id) or Merchant key. Required scope: transactions:read.

Use this to view refund history, check the status of partial refunds, or correlate refund events with webhook notifications.

Path parameters

FieldTypeRequiredDescription
idstringYesTransaction ID (tx_…).

Query parameters

FieldTypeRequiredDescription
pageintegerNoPage number (1-indexed). Default 1, min 1.
limitintegerNoItems per page. Default 20, max 100.
merchant_idstringConditionalTarget merchant (mrc_…). Required with an Organization key.
curl "https://api.tokeflow.com/api/v1/transactions/tx_777/refunds?page=1&limit=20" \
  -H "Authorization: Bearer sk_live_mer_…" \
  -H "Accept: application/json"

Response 200 OK

{
  "success": true,
  "data": [
    {
      "id": "ref_def456",
      "payment_transaction_id": "tx_777",
      "amount": 4000,
      "currency": "BRL",
      "status": "succeeded",
      "reason": "requested_by_customer",
      "provider_refund_id": "rfnd_7Qx9KpL2",
      "failure_reason": null,
      "created_at": "2026-01-15T13:10:00.000Z",
      "updated_at": "2026-01-15T13:11:42.000Z"
    },
    {
      "id": "ref_abc123",
      "payment_transaction_id": "tx_777",
      "amount": 5000,
      "currency": "BRL",
      "status": "succeeded",
      "reason": "requested_by_customer",
      "provider_refund_id": "rfnd_3Mt0VbN8",
      "failure_reason": null,
      "created_at": "2026-01-15T12:30:00.000Z",
      "updated_at": "2026-01-15T12:31:05.000Z"
    }
  ],
  "meta": {
    "pagination": {
      "page": 1,
      "limit": 20,
      "total": 2,
      "total_pages": 1,
      "has_next": false,
      "has_prev": false
    }
  },
  "request_id": "req_5a2f7b1c8e0d",
  "timestamp": "2026-01-15T13:12:00.000Z"
}

GET/api/v1/transactions/:id/refunds/:refundId

Retrieve a single refund.

OrgMerchant

Auth: Organization key (with merchant_id) or Merchant key. Required scope: transactions:read.

Use this in support flows — for example, to inspect a refund after a webhook notification, or when a refund fails or takes time to settle.

Path parameters

FieldTypeRequiredDescription
idstringYesTransaction ID (tx_…).
refundIdstringYesRefund ID (ref_…).

Query parameters

FieldTypeRequiredDescription
merchant_idstringConditionalTarget merchant (mrc_…). Required with an Organization key.
curl "https://api.tokeflow.com/api/v1/transactions/tx_777/refunds/ref_abc123" \
  -H "Authorization: Bearer sk_live_mer_…" \
  -H "Accept: application/json"

Response 200 OK

{
  "success": true,
  "data": {
    "id": "ref_abc123",
    "payment_transaction_id": "tx_777",
    "amount": 5000,
    "currency": "BRL",
    "status": "succeeded",
    "reason": "requested_by_customer",
    "provider_refund_id": "rfnd_3Mt0VbN8",
    "failure_reason": null,
    "created_at": "2026-01-15T12:30:00.000Z",
    "updated_at": "2026-01-15T12:31:05.000Z"
  },
  "request_id": "req_9c4e1d7a2b6f",
  "timestamp": "2026-01-15T12:32:00.000Z"
}

The refund object

FieldTypeDescription
idstringRefund ID (ref_…).
payment_transaction_idstringParent transaction ID (tx_…).
amountintegerRefund amount in minor units.
currencystringISO 4217 currency code (e.g. BRL).
statusenumpending, succeeded, failed, or cancelled.
reasonenum | nullduplicate, fraudulent, or requested_by_customer. null if not provided.
provider_refund_idstring | nullOpaque identifier for the refund at the connected provider. Treat as an opaque string. null until assigned.
failure_reasonstring | nullHuman-readable failure detail when status is failed; otherwise null.
created_atstringISO 8601 UTC creation timestamp.
updated_atstringISO 8601 UTC last-update timestamp.

Errors

Refund endpoints use the standard error envelope.

HTTPTypeWhen
400validation_erroramount exceeds the remaining captured amount, or is malformed.
401authentication_errorMissing or invalid API key.
403authorization_errorKey lacks the required scope, or the transaction is outside the key's scope.
404not_found_errorTransaction or refund not found, or not owned by the merchant.
409conflict_errorA conflicting refund is already in progress.
422business_rule_errorTransaction is not in authorized or refund_pending status.
429rate_limit_errorPer-key rate limit exceeded; back off and retry.
{
  "error": {
    "type": "business_rule_error",
    "code": "TRANSACTION_NOT_REFUNDABLE",
    "message": "Transaction must be in 'authorized' or 'refund_pending' status to be refunded.",
    "details": { "current_status": "failed" },
    "request_id": "req_2b8d4f6a1c3e",
    "timestamp": "2026-01-15T12:40:00.000Z"
  }
}

On this page