Refunds
Create full or partial refunds for paid payments via the API. Refunds are processed through the payment provider; your merchant wallet is held while the refund is in flight.
Dashboard: merchants initiate refunds from Payments (requires email verification code).
API: server-to-server with an API key — no 2FA (Authentication).
Endpoints
| Method | Path | Scope |
|---|---|---|
POST | /api/transactions/{id}/refund/ | refunds:write or payments:write |
GET | /api/refunds/ | refunds:read or payments:read |
GET | /api/refunds/{id}/ | refunds:read or payments:read |
Use the same base URL as Create payment (/api/ or /sandbox/api/ for sandbox keys).
POST /api/refunds/ is for the merchant dashboard only. API keys receive 405 — always refund via POST /api/transactions/{id}/refund/.
Create a refund
POST /api/transactions/<payment_id>/refund/
Authorization: Api-Key <your_api_key>
Content-Type: application/json
payment_id — Public ID (e.g. TXabc123) or numeric transaction ID (Get payment by id).
Request body
{
"reason": "Customer request",
"amount": "40.00"
}
| Field | Required | Description |
|---|---|---|
reason | Yes | Shown to the customer and sent to the provider (max 500 characters) |
amount | No | Partial refund in charge currency (TTC). Omit to refund the full remaining refundable amount |
Response (200)
{
"status": "processing",
"refund_id": 42,
"refund_public_id": "xYz9AbC",
"transaction_id": 17,
"amount": "40.00",
"currency": "USD",
"amount_usd": "39.20",
"amount_in_cents": 4000
}
On success the refund is processing while the provider completes it. Subscribe to refund.status_changed for lifecycle updates.
Example
curl -X POST "https://api.sandbox.nd8.com/api/transactions/TXabc123/refund/" \
-H "Authorization: Api-Key YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"reason": "Customer request", "amount": "40.00"}'
List refunds
GET /api/refunds/
Authorization: Api-Key <your_api_key>
Query parameters
| Parameter | Description |
|---|---|
status | Filter: pending, processing, completed, failed, rejected |
search | Search payment ID or reason |
created_at__gte | ISO datetime — created on or after |
created_at__lte | ISO datetime — created on or before |
ordering | e.g. -created_at, amount |
page / page_size | Pagination |
Response (200)
Array or paginated object with refund rows:
[
{
"id": "RFxYz9AbC",
"transaction_id": "TXabc123",
"amount": "40.00",
"currency": "USD",
"amount_in_cents": 4000,
"amount_usd": "39.20",
"reason": "Customer request",
"status": "processing",
"created_at": "2026-03-01T12:00:00Z",
"updated_at": "2026-03-01T12:00:05Z"
}
]
Get refund by ID
GET /api/refunds/<id>/
Authorization: Api-Key <your_api_key>
id — Public refund ID (e.g. RFxYz9AbC) or numeric ID.
Eligibility rules
Refunds are allowed when all of the following hold:
- Payment status is
paidorrefund_pending(partial refund in progress). - Provider has not marked the payment
FULLY_REFUNDEDorPARTIAL_REFUNDED(no further refunds after provider completion). - Remaining refundable amount on the payment is > 0 (prior refunds reduce the cap).
- Merchant wallet can cover the refund — you may issue a partial refund up to the lesser of remaining payment amount and wallet-available amount (balance + rolling reserve, proportional to net settlement).
There is no time limit on when a refund can be requested.
Common 400 errors:
| Message (example) | Cause |
|---|---|
| Refund amount exceeds remaining… | Partial amount above payment remaining |
| Refund amount exceeds wallet-available limit… | Amount above what wallet can cover — use a lower partial refund |
| Insufficient wallet balance to refund any amount… | Wallet empty while payment still has remaining refundable gross |
| A refund is already in progress | Pending/processing refund exists |
| Payment has already been refunded | Provider or platform status blocks new requests |
Refund status lifecycle
| Status | Meaning |
|---|---|
pending | Record created, provider request not yet confirmed |
processing | Accepted by provider; awaiting completion |
completed | Refund settled; customer charged back |
failed | Provider rejected or sync reported failure; wallet hold released |
rejected | Manually rejected (internal/admin) |
Status is synced from the provider (webhooks + periodic poll). You do not need to poll if you handle webhooks.
Webhooks
Configure Webhooks to receive:
refund.status_changed— refundprocessing,completed, orfailed(recommended for refund tracking).transaction.status_changed— parent payment may move torefund_pendingorrefunded.
Example handler (Node.js):
app.post('/webhooks/iflowed', express.raw({ type: 'application/json' }), (req, res) => {
const event = req.header('X-Webhook-Event');
const body = JSON.parse(req.body.toString());
if (event === 'refund.status_changed') {
console.log(body.refund_id, body.status, body.amount, body.transaction_id);
// Fulfill order reversal, CRM update, etc.
}
res.sendStatus(200);
});
API vs dashboard
| API key | Dashboard (Token session) | |
|---|---|---|
| Auth | Authorization: Api-Key … | Authorization: Token … |
| 2FA | Not required | Email verification code required |
| Create refund | POST /transactions/{id}/refund/ | Payments → View → Refund |
| List refunds | GET /refunds/ | Refunds page |
Related
- Get payment by id — payment status includes
refund_pendingandrefunded - Webhooks —
refund.status_changedpayload reference - Authentication — API key scopes