Refunds and disputes#
POST /v1/refunds returns money on a paid session — all of it or part of it, as many times as needed up to what was captured. Whether a refund can be partial, or made through the API at all, depends on the vendor that took the payment.
Create a refund#
curl -X POST https://api.checkout.povito.com/v1/refunds \
-H "Authorization: Bearer $POVITO_SECRET_KEY" \
-H "Idempotency-Key: 4b6a1d2e-8c3f-4e7a-9b0d-5f6e7a8b9c0d" \
-H "Content-Type: application/json" \
-d '{
"session_id": "cs_test_7KQ4M2XA9BC3DEFGH1JK",
"amount": 5000,
"reason": "Customer returned the delivery item, ticket #1"
}'const res = await fetch(`${API}/refunds`, {
method: "POST",
headers: { ...headers, "Idempotency-Key": randomUUID() },
body: JSON.stringify({
session_id: "cs_test_7KQ4M2XA9BC3DEFGH1JK",
amount: 5000, // minor units of the CAPTURED currency
reason: "Customer returned the delivery item, ticket #1",
metadata: { ticket: "1" },
}),
})
const refund = await res.json(){
"id": "rf_test_9BC3DEFGH1JK7KQ4M2XA",
"object": "refund",
"livemode": false,
"session_id": "cs_test_7KQ4M2XA9BC3DEFGH1JK",
"attempt_id": "pa_test_3M8XQ2K7A9BC4DEFGH1J",
"amount": 5000,
"currency": "IQD",
"reason": "Customer returned the delivery item, ticket #1",
"initiated_by": "merchant",
"status": "succeeded",
"gateway_reference": "test_rf_test_pa_test_3M8XQ2K7A9BC4DEFGH1J",
"failure_reason": null,
"metadata": { "ticket": "1" },
"created_at": "2026-09-10T14:10:02.000Z",
"updated_at": "2026-09-10T14:10:02.000Z"
}| Field | Rules |
|---|---|
session_id |
A session of this mode with a captured payment. |
amount |
Positive integer minor units of the captured currency (payment.captured_currency — USD cents for a Stripe payment on an IQD basket). |
reason |
10–1500 characters. Shown to Povito ops and, for vendors that carry it, sent to the vendor. |
metadata |
Optional; your correlation keys, echoed back. |
Idempotency-Key header |
Required, exactly as for session creation. |
The call is synchronous with the vendor: the response already carries the outcome for vendors that answer immediately (the simulated gateway, ZainCash reversal, Stripe), or processing for those that settle later.
Rules#
- The ceiling.
amountplus every refund on this payment that isrequested,processingorsucceededmust not exceedpayment.captured_amount; otherwise409 refund_exceeds_capturedwith the remaining amount in the message. Concurrent refunds are serialised, so two requests cannot both squeeze under the ceiling. - Capability. Each method's
capabilities.refundsinGET /payment_methodsispartial,fullornone. Forfullvendors (ZainCash, AsiaPay)amountmust equalcaptured_amount; fornonevendors (FIB, FIB Card, NassPay) the API cannot refund. Both cases are422 method_not_refundable. - Only paid sessions. A session that is unpaid, failed, or completed by cash on delivery has nothing to refund:
409 session_not_open. - A refund changes the session only when it succeeds:
refunded_amountgrows, andpayment_statusbecomespartially_refundedor, once the whole capture is refunded,refunded.
Worked example on the 45,000 IQD session from the quickstart:
| Call | Result | Session afterwards |
|---|---|---|
| refund 5,000 | 201, status: succeeded |
payment_status: partially_refunded, refunded_amount: 5000 |
| refund 41,000 | 409 refund_exceeds_captured — "Only 40000 IQD remains refundable." |
unchanged |
| refund 40,000 | 201, status: succeeded |
payment_status: refunded, refunded_amount: 45000 |
Refund states#
requested ──▶ processing ──▶ succeeded
│
├──▶ failed (the vendor tried and could not)
└──▶ rejected (the vendor refused: outside its window, unknown reference, …)refund.succeeded and refund.failed events fire on the terminal states; refund.failed carries status (failed or rejected) and failure_reason. A refund that stays processing is re-checked by Povito; poll GET /v1/refunds/{id} or wait for the event.
curl https://api.checkout.povito.com/v1/refunds/rf_test_9BC3DEFGH1JK7KQ4M2XA \
-H "Authorization: Bearer $POVITO_SECRET_KEY"
curl "https://api.checkout.povito.com/v1/refunds?session_id=cs_test_7KQ4M2XA9BC3DEFGH1JK" \
-H "Authorization: Bearer $POVITO_SECRET_KEY"initiated_by is merchant for refunds you create and povito_admin for ones Povito ops make on your behalf; both appear in your list and both emit events.
When the API cannot refund#
FIB, FIB Card and NassPay expose no refund API to Povito, and cash on delivery never captured anything. For those:
- Refund through the vendor's merchant portal, by bank transfer, or in cash — whatever your agreement with the vendor and the shopper allows.
- Record it on your side against
payment.gateway_reference. Povito will not know, so the session stayspaid.
Povito's refund and dispute policy (L-09, in review) sets the customer-facing expectations your process must meet.
Disputes and chargebacks#
Card payments (fib_card, stripe) can be disputed by the cardholder with their issuer. There is no dispute API in v1: Povito ops receive the vendor's notice, open a case with you by email, and refund or contest per the chargeback procedure in L-09. Keep the session_id, payment.gateway_reference, delivery proof and your customer correspondence per order — they are what wins a chargeback. Wallet payments (ZainCash, FastPay, AsiaPay) have no chargeback mechanism; complaints reach you through Povito support or the vendor.