Skip to content

Refunds

A Refund returns all or part of the amount of a successful Transaction to the buyer. The refund is issued via the same payment method as the original payment — you cannot refund through a different channel.

You can issue multiple partial refunds on the same transaction, as long as the total refunded does not exceed the original amount.

Operator refund delays

The time it takes for funds to be returned depends on the operator: Mobile Money (Airtel, Moov): 24–72h. Bank card: 3–10 business days depending on the issuing bank. PayPal: instant to 24h. Sangho sends a refund.succeeded webhook as soon as the operator confirms.

Lifecycle

pendingprocessingsucceeded / failed / cancelled / expired
StatusMeaning
pendingRefund created, awaiting processing.
processingSent to the operator, being processed.
succeededFunds returned to the buyer.
failedRefund failed on the operator's side. Funds are not debited.
cancelledRefund cancelled before processing (via /cancel/).
expiredProcessing deadline exceeded — the funds are returned to your balance.

Endpoints

MéthodeEndpointDescription
POST/refunds/Create a refund
GET/refunds/List refunds
GET/refunds/{id}/Retrieve a refund
POST/refunds/{id}/cancel/Cancel a pending refund

Object schema

ResponseRefund object
json
{
  "id": "refd_xxxxxxxxxxxx",
  "object": "refund",
  "transaction": "trans_3Nx8mLKZ2eZvKYlo28m",
  "amount": 5000,
  "currency": "XAF",
  "status": "succeeded",
  "reason": "customer_request",
  "description": "Partial refund — item unavailable",
  "failure_reason": null,
  "payment_method_type": "mobile_money",
  "refunded_at": "2026-03-16T09:00:00Z",
  "metadata": {
    "ticket": "SUPPORT-00412"
  },
  "livemode": true,
  "created_at": "2026-03-15T14:30:00Z",
  "updated_at": "2026-03-16T09:00:00Z"
}

Create a refund

Initiates a refund for an existing transaction. The transaction must be in completed status. A partial refund does not change the status of the original transaction — only a full refund moves the transaction to refunded.

POSTRequest body
transactionRequis
stringtrans_...ex :trans_3Nx8mLKZ2eZvKYlo28m

ID of the Transaction to refund. The transaction must be in completed status.

A transaction in pending, failed, or disputed status cannot be refunded. To cancel a payment that hasn’t been processed yet, use POST /payment-intents/{id}/cancel/.

amountOptionnel
integercents

Amount to refund, in cents. If omitted, refunds the full refundable amount of the transaction.

En savoir plus

The amount cannot exceed transaction.amount - amounts_already_refunded. You can accumulate multiple partial refunds. Example: a transaction of 15,000 XAF → a refund of 5,000, then a second refund of 3,000 is accepted (remaining: 7,000).

reasonOptionnel
string

Reason for the refund. Used for internal reporting and compliance.

En savoir plus

Accepted values: duplicate (duplicate payment), fraudulent (fraudulent transaction), customer_request (customer request), product_issue (product issue), service_not_rendered (service not rendered). The reason is included in the refund.succeeded webhook.

descriptionOptionnel
string

Internal note about the refund. Not visible to the buyer.

En savoir plus

Ideal for referencing a support ticket, an internal decision, or a communication with the customer.

metadataOptionnel
object

Free-form data associated with the refund. Returned in the refund.succeeded webhook.

Always use an Idempotency-Key

A duplicate call without an Idempotency-Key creates two separate refunds. With an identical key, Sangho returns the existing refund instead of creating a new one. Use a unique UUID for each refund attempt.

Create a refund

bash
curl -X POST https://api.sangho.ga/v1/refunds/ \
  -H "Authorization: Bearer sk_prod_xxxx" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: refund-trans_xxx-$(date +%s)" \
  -d '{
    "transaction": "trans_3Nx8mLKZ2eZvKYlo28m",
    "amount": 5000,
    "reason": "customer_request",
    "description": "Partial refund — item unavailable",
    "metadata": { "ticket": "SUPPORT-00412" }
  }'
Response201 Created — Refund initiated
The refund is created with processing status. The refund.succeeded webhook will confirm the return of funds.
json
{
  "id": "refd_xxxxxxxxxxxx",
  "object": "refund",
  "transaction": "trans_3Nx8mLKZ2eZvKYlo28m",
  "amount": 5000,
  "currency": "XAF",
  "status": "processing",
  "reason": "customer_request",
  "description": "Partial refund — item unavailable",
  "failure_reason": null,
  "metadata": { "ticket": "SUPPORT-00412" },
  "created_at": "2026-03-15T14:30:00Z",
  "updated_at": "2026-03-15T14:30:00Z"
}
Response400 — Amount exceeds available
The requested amount exceeds what's refundable. Check the amount already refunded before submitting.
Retrieve the transaction via GET /transactions/{id}/ and check amount_refunded to find out the remaining refundable amount.
json
{
  "error": {
    "type": "validation_error",
    "code": "refund_amount_exceeds_available",
    "message": "The refund amount exceeds the refundable amount (10000 XAF remaining).",
    "param": "amount"
  }
}
Response409 — Transaction not refundable
The transaction is in a state that does not allow refunding (failed, pending, disputed).
Only transactions in completed status can be refunded.
json
{
  "error": {
    "type": "conflict_error",
    "code": "transaction_not_refundable",
    "message": "This transaction is in failed status and cannot be refunded."
  }
}