Refunds API¶
The Refunds API creates full or partial refunds for a captured payment and lists the refunds of that payment.
Base URL
https://api.4geeks.io
Authentication¶
All endpoints require the X-Api-Key header:
Every refund is checked against the owner of the payment, so a key can only refund its own payments. See Authentication for details.
How a refund is routed¶
The refund is issued with the credential and the account of the payment’s environment (test or live), so test refunds never touch live money.
- Destination charges (the money sits in the merchant’s connected account and the platform took its fee): the refund reverses the merchant transfer (
reverse_transfer: true) and returns the platform fee (refund_application_fee: true) by default. - Direct charges (the charge was created on the merchant’s connected account): the merchant already funds the refund, so only the platform fee is returned.
reverse_transferis not applied. - Platform charges (no connected account): no transfer or fee is involved.
The API inspects the charge and only sends the flags that apply, so the request never fails because of an inapplicable Connect flag.
Create Refund¶
Creates a refund for a payment. Omit amount for a full refund of the remaining amount.
Headers¶
| Header | Required | Description |
|---|---|---|
| X-Api-Key | Yes | Merchant API key |
| Idempotency-Key | No | Reusing it within 24 hours returns the original refund instead of creating a new one |
Request Body¶
| Field | Type | Required | Description |
|---|---|---|---|
| payment_id | string | Yes | The checkout session ID returned by the Payments API, or the payment’s gateway ID |
| amount | integer | No | Amount to refund in the currency’s minor unit (cents). Defaults to the full remaining amount |
| reason | string | No | duplicate, fraudulent or requested_by_customer |
| reverse_transfer | boolean | No | Destination charges only. Default: true |
| refund_application_fee | boolean | No | Return the platform fee. Default: true |
The refundable amount is what was captured minus what was already refunded. Requesting more answers 409 amount_exceeds_refundable and a payment fully refunded answers 409 already_refunded.
Request Example¶
const response = await fetch('https://api.4geeks.io/v1/refunds', {
method: 'POST',
headers: {
'X-Api-Key': 'sk_test_your_api_key_here',
'Content-Type': 'application/json',
'Idempotency-Key': 'refund-order-1044'
},
body: JSON.stringify({
payment_id: 'sess_1a2b3c4d5e6f',
amount: 2500,
reason: 'requested_by_customer'
})
});
const data = await response.json();
console.log(data);
Response (200 OK)¶
| Field | Type | Description |
|---|---|---|
| id | string | Refund ID |
| object | string | Object type (refund) |
| payment_id | string | Session (or payment) that was refunded |
| payment_intent_id | string | Gateway payment intent that was refunded |
| amount | integer | Refunded amount in the currency’s minor unit |
| currency | string | Currency code |
| status | string | Refund status (pending, succeeded, failed, canceled) |
| reason | string | Reason sent with the refund |
| created_at | string (datetime) | Creation timestamp |
Response Example¶
{
"id": "ref_m3n4o5p6q7r8",
"object": "refund",
"payment_id": "sess_1a2b3c4d5e6f",
"payment_intent_id": "pi_1a2b3c4d5e6f",
"amount": 2500,
"currency": "USD",
"status": "succeeded",
"reason": "requested_by_customer",
"created_at": "2026-11-11T15:04:05Z"
}
Refund a Payment¶
Convenience route for the same operation, with the payment ID in the path. The body has the same fields as Create Refund except payment_id.
Request Example¶
Response (200 OK)¶
Same schema as Create Refund.
Sessions created with the branded checkout
A session created with POST /v1/payments/session then confirmed with POST /v1/payments/intent has no gateway checkout object to look up from the session ID. Pass the payment_intent_id returned by /intent as the path parameter (or as payment_id in POST /v1/refunds).
List Refunds¶
Lists the refunds of one of the caller’s payments.
Query Parameters¶
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| payment_intent_id | string | Yes | - | Payment intent whose refunds are listed |
| limit | integer | No | 10 | Page size, 1–100 |
| offset | integer | No | 0 | Number of results to skip |
The payment intent must belong to the merchant that owns the API key; anything else answers 404.
Because the gateway does not expose counts, total_items is a lower bound for this endpoint and has_more tells whether another page exists.
Request Example¶
Response (200 OK)¶
| Field | Type | Description |
|---|---|---|
| items | array[RefundResponse] | Array of refund objects |
| total_items | integer | Total number of refunds (lower bound) |
| limit | integer | Requested page size |
| offset | integer | Requested offset |
| has_more | boolean | Whether another page exists |
Response Example¶
{
"items": [
{
"id": "ref_m3n4o5p6q7r8",
"object": "refund",
"payment_id": "pi_1a2b3c4d5e6f",
"payment_intent_id": "pi_1a2b3c4d5e6f",
"amount": 2500,
"currency": "USD",
"status": "succeeded",
"reason": "requested_by_customer",
"created_at": "2026-11-11T15:04:05Z"
}
],
"total_items": 1,
"limit": 5,
"offset": 0,
"has_more": false
}
Errors¶
| Status | Meaning |
|---|---|
| 401 | Missing or invalid API key |
| 404 | The payment does not exist or belongs to another merchant |
| 409 | payment_not_refundable, already_refunded, amount_exceeds_refundable, refund_requires_payment_intent, gateway_account_not_configured |
| 422 | Validation error (amount, reason, missing payment_id) |
| 429 | Rate limit exceeded |
| 502 | The gateway rejected the refund |
Error Response (422 Validation Error)¶
{
"detail": [
{
"loc": ["body", "payment_id"],
"msg": "field required",
"type": "value_error.missing"
}
]
}
Still have questions? Ask on Discord or explore the tutorials.