Refunds via the Reporting API
The Reporting API has its own refund surface at /v1/refunds. It creates the same refund as POST /refunds on the main API, but the object it returns is the account-ledger view of that refund: it carries the refund fee applied to your balance and is listable account-wide alongside your balances.
Which refund surface should I use?
Main API — /refunds | Reporting API — /v1/refunds | |
|---|---|---|
| Base URL / auth | api.tensorrail.com, api-key header | api.tensorrail.com, Authorization: Bearer |
| Object returned | The processing refund (ref_*, status: pending → succeeded/failed) | The ledger record (numeric id, fee fields, balance impact) |
| Best for | The standard integration: refund, then confirm via refund_succeeded webhook | Programmatic refunds when you also reconcile balances via the Reporting API |
Use one surface per refund — both roads lead to the same refund being processed, and the two records are linked: the ledger record's refund_id field carries the same ref_* identifier the main API returns, so you can join the two views on it.
Create a refund
POST /v1/refunds — scope refunds:write
The Idempotency-Key header is required (a missing key returns ERR_2001). The refundable amount is enforced: the sum of succeeded and in-flight refunds can never exceed the captured amount, even under concurrent requests.
Headers
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer rail_… |
Idempotency-Key | Yes | Reuse the same key to retry safely; see behavior below |
Body
| Field | Type | Required | Description |
|---|---|---|---|
payment_id | string | Yes | The captured payment to refund |
amount_minor | integer | Yes | Refund amount in minor units, > 0 |
currency | string | No | Must match the payment's currency; defaults to it |
reason | string | No | Free-text reason, stored on the record |
/v1/refunds requires refunds enabled on your account. Until it is, the call returns
403 ERR_1012 — Refunds are not enabled for this account, which no request change will fix;
ask support to enable it. The engine-surface
POST /refunds is not behind this flag, so an integration built against that
route keeps working either way.
curl -X POST "https://api.tensorrail.com/v1/refunds" \
-H "Authorization: Bearer rail_full_live_xxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: refund-order-12345" \
-d '{
"payment_id": "pay_N5cPeGw6uS2QIMjnsjVF",
"amount_minor": 5000,
"reason": "requested_by_customer"
}'
Response 200 OK
{
"refund": {
"id": 314,
"status": "accepted",
"amount_minor": 5000,
"currency": "EUR",
"parent_payment_id": "pay_N5cPeGw6uS2QIMjnsjVF",
"refund_id": "ref_xyz789",
"fee_amount_minor": 25,
"fee_type": "voluntary_refund",
"profile_id": "pro_...",
"reason": "requested_by_customer",
"idempotency_key": "refund-order-12345",
"created_at": "2026-08-03T10:00:00.000000000Z"
}
}
| Field | Type | Description |
|---|---|---|
id | integer | The ledger refund id — use it with GET /v1/refunds/{id} |
status | string | See statuses |
amount_minor / currency | integer / string | The refund amount |
parent_payment_id | string | The refunded payment |
refund_id | string | The refund's identifier in the main API (ref_*), as returned by POST /refunds. Empty until the refund is accepted for processing |
fee_amount_minor | integer | The refund fee applied to your balance, in minor units (0 when no fee applies) |
fee_type | string | Fee classification, e.g. voluntary_refund |
idempotency_key | string | The key this refund was created under |
created_at | string | RFC 3339 timestamp |
The refund is recorded against your balance immediately; the final outcome then arrives asynchronously (poll GET /v1/refunds/{id} or subscribe to the refund_succeeded / refund_failed webhooks). If the refund is rejected outright, the ledger entries are reversed and the record ends failed.
Refund statuses
| Status | Meaning |
|---|---|
initiated | Recorded on your ledger; being submitted for processing |
accepted | Accepted for processing; refund_id is now set |
pending | Processing in progress |
succeeded | Refund completed (terminal) |
failed | Refund rejected or could not be processed; ledger impact reversed (terminal) |
cancelled | Refund cancelled (terminal) |
Idempotency behavior
The Idempotency-Key is persisted with the refund, so it deduplicates across retries indefinitely (not just within the 24-hour replay window):
- Same key + same body, refund reached a terminal state →
200with the existing refund. - Same key + same body, refund still in flight →
409ERR_3032(refund_in_flight, retriable) — retry with the same key to fetch the result. - Same key + different body →
409ERR_3002(duplicate_idempotency_key).
A replayed response served from the short-term cache also carries the Idempotent-Replay: true header.
Retrieve and list refunds
GET /v1/refunds/{id} and GET /v1/refunds — scope refunds:read
curl "https://api.tensorrail.com/v1/refunds?limit=50&offset=0" \
-H "Authorization: Bearer rail_full_live_xxx"
The list returns data (array of refund objects as above) plus a pagination envelope (total, limit, offset) — the same shape as /v1/transactions. limit defaults to 50, max 500.
GET /v1/refunds/{id} returns {"refund": {...}} or ERR_3017 (404) when the id does not exist on your account.
Errors
| Code | HTTP | Cause |
|---|---|---|
ERR_2001 | 400 | Missing Idempotency-Key, payment_id, or a positive amount_minor |
ERR_3002 | 409 | Idempotency key reused with a different body |
ERR_3032 | 409 | A refund with this key is still being processed; retry shortly |
ERR_3033 | 404 | The payment was not found on your account |
ERR_3034 | 422 | The payment is not in a refundable state (must be captured/succeeded) |
ERR_3035 | 422 | Amount exceeds the remaining refundable balance on the payment |
ERR_3039 | 422 | The refund was rejected during processing |
ERR_3017 | 404 | Refund id not found (GET /v1/refunds/{id}) |
Refunds issued from the dashboard require a fresh two-factor verification of the operator. API-key calls are not subject to this — the key's refunds:write scope is the authority.
Related
- Refunds: the main-API refund surface
- Reporting API: base URL and auth
- Balances: the balance the fee applies to
- Idempotency
- Error catalog