Skip to main content

Refunds

Refund a payment in full or in part with POST /refunds. Refunds settle back against the original collection, and post against it on the same ledger that backs your balance, so a refunded payment nets out cleanly in your reporting and exports.

Before you build a refund flow

Refund availability depends on the payment method and on what is enabled for your account. Confirm it before you write code against this page, because it is not visible from the API response shape alone.

Refunds are enabled per account. The account flag gates the Reporting API's refund surface: if refunds are not enabled, POST /v1/refunds returns 403 with code ERR_1012 and the message "Refunds are not enabled for this account. Contact support.", regardless of the payment or the amount. POST /refunds — the endpoint this page documents — is not behind that flag, so an integration built against this route keeps working either way. Either way, ask support to confirm refunds are on for you before you build: whether the underlying rail can carry the reversal is a separate question from whether the route answers.

Method-level support is activated per account too. A refund is a capability of the underlying rail as well as of the API, so a method you can collect with is not automatically a method you can reverse through this endpoint. Your dashboard shows what is enabled on your account; ask us and we will confirm refundability for the methods you plan to use. GET /v1/capabilities answers a different question — it is the unauthenticated, platform-wide catalogue of what TensorRail supports, not a per-account list.

Build your customer-service flow around what your account actually has, rather than around the assumption that every collection can be reversed through the API. Where the API cannot carry the reversal for a given method, returning the money out of band and reconciling it against the original collection remains an option. The sandbox exercises the full refund path, so you can build and test the flow while method-level availability is being confirmed.

Create a refund

curl -X POST https://api.tensorrail.com/refunds \
-H "api-key: rail_full_test_xxx" \
-H "Idempotency-Key: 9b1c7f4e-2d6a-4f8b-8c1e-3a5d9e7b2f10" \
-H "Content-Type: application/json" \
-d '{
"payment_id": "pay_N5c…",
"amount": 50000,
"reason": "requested_by_customer",
"metadata": { "rma": "RMA-4471" }
}'

Request fields

FieldTypeRequiredNotes
payment_idstringYesThe payment to refund
amountintegerNoMinor units; defaults to the full amount. Must be a positive integer, not exceeding the unrefunded remainder
refund_idstringNoYour id for the refund; auto-generated if omitted; creation is idempotent on it
reasonstringNoSome methods require duplicate, fraudulent, or requested_by_customer
refund_typeinstant | scheduledNoDefault instant
metadataobjectNoYour own key-value data

The response

{
"refund_id": "ref_8Kj2…",
"payment_id": "pay_N5cPeGw6uS2QIMjnsjVF",
"amount": 50000,
"currency": "INR",
"status": "pending",
"reason": "requested_by_customer",
"metadata": { "rma": "RMA-4471" },
"error_code": null,
"error_message": null,
"unified_code": null,
"unified_message": null,
"created_at": "2026-07-22T10:15:00.000Z",
"updated_at": "2026-07-22T10:15:00.000Z"
}

Refunds are asynchronous like collections: the create response is usually pending, and the terminal outcome arrives as a refund_succeeded / refund_failed webhook (and on GET /refunds/{refund_id}).

Idempotency (live)

Refund creation carries the same live idempotency contract as payment creation:

  • Send an Idempotency-Key header; retrying with the same key returns the original refund, so a network retry can never refund twice.
  • Reusing a key with a different body returns that same original refund too — this surface does not compare bodies, so the second body is silently ignored rather than rejected. Use one key per logical refund. (POST /v1/refunds on the Reporting API does compare bodies and rejects a mismatch with ERR_3002.)
  • You can also supply your own refund_id; creation is idempotent on it, which is the natural fit when your system already has a returns reference (like an RMA number).

For a money-out operation, this is the property that matters most: your retry logic cannot over-refund a customer.

Partial refunds

Refund less than the collected amount by passing amount, and repeat for multiple partials. Give each partial its own refund_id (or its own Idempotency-Key) so each is individually idempotent:

# Refund INR 150.00 of a INR 500.00 collection
curl -X POST https://api.tensorrail.com/refunds \
-H "api-key: rail_full_test_xxx" \
-H "Idempotency-Key: 41d2a6a0-88a7-4b0d-9d3e-7c2f8b4a1c55" \
-H "Content-Type: application/json" \
-d '{ "payment_id": "pay_N5c…", "amount": 15000, "refund_id": "RMA-4471-part-1" }'

Guardrails the platform enforces for you:

  • The total refunded can never exceed the captured amount; exceeding it is rejected with ERR_3005 (refund_exceeds_capture).
  • Each refund must be at least 100 minor units; below that the request is rejected as too small.
  • There is a limit on the number of partial refunds per payment (ERR_3025 when exceeded).
  • Refunding a payment that is not in a refundable state returns a business-state error rather than silently queueing.

The refund object

The stable field set on every refund:

refund_id, payment_id, amount, currency, status, reason, metadata, error_code, error_message, unified_code, unified_message, created_at, updated_at.

unified_code / unified_message are TensorRail's normalized, cross-rail error surface for a refund that did not succeed. They mean the same thing whatever rail executed the refund; read those rather than any rail-specific detail. Full per-field documentation is in the API reference.

Refund status

StatusMeaning
pendingSubmitted; outcome not yet final
reviewHeld for review
succeededRefunded
failedDid not complete; read unified_code / unified_message

Retrieve, update, and list

  • GET /refunds/{refund_id} retrieves a single refund.
  • POST /refunds/{refund_id} updates a refund's reason or metadata.
  • POST /refunds/list lists refunds; the list shape is { "count": …, "total_count": …, "data": [ … ] }.

Tips

  • Send payment_method_type explicitly on the original confirm. Omitting it can block a later refund with a missing-field error. See Accept a payment.
  • Drive your customer communication ("your refund is on its way" vs "refund complete") off the refund status transitions from the webhook, not off the create call returning 2xx.
  • Reconcile refunds in your exports the same way as collections; each refund row references its payment_id. See Money integrity.

Next steps

You can issue and track a refund. Related paths:

TensorRail, Limassol, Cyprus.