PovitoDevelopers

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#

curlbash
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"
  }'
Node (fetch)js
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()
Response 201 Createdjson
{
  "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. amount plus every refund on this payment that is requested, processing or succeeded must not exceed payment.captured_amount; otherwise 409 refund_exceeds_captured with the remaining amount in the message. Concurrent refunds are serialised, so two requests cannot both squeeze under the ceiling.
  • Capability. Each method's capabilities.refunds in GET /payment_methods is partial, full or none. For full vendors (ZainCash, AsiaPay) amount must equal captured_amount; for none vendors (FIB, FIB Card, NassPay) the API cannot refund. Both cases are 422 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_amount grows, and payment_status becomes partially_refunded or, 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#

text
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.

Read and listbash
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:

  1. Refund through the vendor's merchant portal, by bank transfer, or in cash — whatever your agreement with the vendor and the shopper allows.
  2. Record it on your side against payment.gateway_reference. Povito will not know, so the session stays paid.

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.