Refunds
Refund a successful Orange Money payment, fully or partially, with one API call.
How it works
- The money is sent back to the payer's Orange Money number (an Orange Money credit to the wallet that paid). The payment must therefore have a payer phone number; otherwise the refund is refused with
refund_payer_phone_missing. - The refunded amount is debited from your SOAMTAR balance.
- Partial refunds are allowed: you can refund a payment several times until the sum of the refunds reaches the payment amount.
- The refund is submitted to Orange Money in the same request, but the credit is asynchronous: in live mode the refund usually comes back
processingand its final status reaches you through therefund.success/refund.failedwebhooks (orGET /v1/refunds/{id}). - Your API key needs the
can_refundscope, and refunds must be enabled on your account. - Use a key of the same environment as the payment: a
sk_test_key refunds sandbox payments only, ask_live_key live payments only.
Create a refund
POST /v1/refunds
Headers
Authorization: Bearer sk_(test|live)_…Idempotency-Key: <uuid>(strongly recommended — a retried request must never refund twice; see Idempotency)Content-Type: application/json
Body
{
"transaction_id": "0e85b3c2-7c4f-4f1d-9b3a-1c8a2d4e5f6a",
"amount": "40.00",
"reason": "customer_request",
"description": "Item returned"
}| Field | Type | Required | Notes |
|---|---|---|---|
transaction_id | string | yes | Payment id (UUID) or reference (TXN-…). payment_id is accepted as an alias. |
amount | decimal string | optional | Amount to refund, at most two decimals. Omit it to refund the full remaining amount. |
reason | string, max 100 | optional | Your reason code, e.g. customer_request, duplicate, fraudulent. |
description | string, max 500 | optional | Free text kept with the refund. |
Response 201
The response carries the refund's current status:
- sandbox (
sk_test_):success— the simulator settles immediately; - Orange Money (
sk_live_):processing— the credit is on its way; wait forrefund.success/refund.failed; failedwhen Orange Money refused the credit (seefailure_reason). The amount is released and you can try again.
{
"data": {
"id": "9b1f4c2e-5d6a-4e8b-a1c3-2f4e6a8b0c1d",
"object": "refund",
"reference": "REF-7K2M9Q4XW8T1B3NC",
"transaction_id": "0e85b3c2-7c4f-4f1d-9b3a-1c8a2d4e5f6a",
"amount": "40.00",
"currency": "SLE",
"reason": "customer_request",
"description": "Item returned",
"status": "processing",
"fee_refunded": false,
"fee_refunded_amount": "0.60",
"tax_refunded_amount": "0.00",
"failure_reason": null,
"processed_at": null,
"failed_at": null,
"created_at": "2026-10-22T10:04:58+00:00",
"updated_at": "2026-10-22T10:04:59+00:00"
}
}curl -X POST https://app.so-amtar.com/v1/refunds \
-H "Authorization: Bearer sk_test_..." \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"transaction_id":"TXN-1A2B3C4D-ABCDEF123456","amount":"40.00","reason":"customer_request"}'Retrieve a refund
GET /v1/refunds/{id_or_reference}
Accepts the refund id (UUID) or its reference (REF-…). Returns {"data": Refund}, or 404 refund_not_found.
List refunds
GET /v1/refunds?status=success&transaction_id=0e85b3c2-...&per_page=25&page=1
| Parameter | Notes |
|---|---|
status | pending, processing, success or failed |
transaction_id | Only the refunds of this payment (UUID) |
per_page | 1 to 100, default 25 |
page | Page number, from 1 |
Sorted by created_at descending.
{
"data": [ { "id": "9b1f4c2e-...", "object": "refund", "reference": "REF-7K2M9Q4XW8T1B3NC", "status": "success", "...": "..." } ],
"pagination": { "total": 3, "per_page": 25, "current_page": 1, "last_page": 1 }
}The refund object
| Field | Type | Description |
|---|---|---|
id | UUID | Refund identifier |
object | string | Always refund |
reference | string | REF- followed by 16 characters. Also used as the Orange Money transaction id of the credit. |
transaction_id | UUID | The refunded payment |
amount | decimal string | Refunded amount, e.g. "400.00" |
currency | string | Currency of the payment (SLE) |
reason / description | string | null | As sent on creation |
status | enum | pending, processing, success, failed |
fee_refunded | boolean | Whether the SOAMTAR fee on the refunded share was returned to you |
fee_refunded_amount | decimal string | Pro-rata share of the payment's SOAMTAR fee for this refund |
tax_refunded_amount | decimal string | Pro-rata share of the payment's tax for this refund |
failure_reason | string | null | Why the refund failed |
processed_at | ISO 8601 | null | When the refund succeeded |
failed_at | ISO 8601 | null | When the refund failed |
created_at / updated_at | ISO 8601 | Timestamps |
Statuses
| Status | Meaning |
|---|---|
pending | Created, not yet submitted to Orange Money |
processing | Credit submitted to Orange Money, waiting for its confirmation |
success | The payer's wallet was credited (final) |
failed | Refused or not completed (final); the amount is released |
A refund that stays processing is checked automatically with Orange Money; a credit that Orange Money never registered is marked failed after 60 minutes.
When a refund succeeds, the payment moves to partially_refunded, or to refunded once refunds total the payment amount.
Webhooks
Each refund produces its own events:
| Outcome | Events |
|---|---|
| Success | refund.success + payment.partially_refunded (or payment.refunded when the payment is now fully refunded) |
| Failure | refund.failed |
refund.* events carry the refund object plus transaction_reference, merchant_reference and is_test; payment.*refunded events carry the transaction payload with the refund in data.refund. See Webhooks → Refund events. Subscribe your endpoint to these events (they are not in the SDKs' default subscription).
Errors
| Code | HTTP | Cause |
|---|---|---|
transaction_not_found | 404 | Unknown payment id or reference for this merchant |
refunds_disabled | 422 | Refunds are not enabled on your account (contact support) |
refund_environment_mismatch | 422 | Test key on a live payment, or live key on a sandbox payment |
transaction_not_refundable | 422 | The payment is not success or partially_refunded |
non_payment_transaction_refund | 422 | Only payments can be refunded (not payouts) |
provider_refund_unsupported | 422 | The payment method does not support refunds |
refund_payer_phone_missing | 422 | The payment has no payer phone number to credit |
amount_must_be_positive | 422 | amount is zero or negative |
transaction_already_fully_refunded | 422 | Nothing left to refund on this payment |
refund_exceeds_refundable | 422 | amount is above the remaining refundable amount |
refund_rejected | 422 | The refund could not be created (other business rule) |
scope_insufficient | 403 | The API key lacks the can_refund scope |
refund_not_found | 404 | GET /v1/refunds/{id}: unknown refund id or reference |
SDKs
Every server SDK exposes refunds.create, refunds.retrieve and refunds.list (named according to each language). For example in Node.js:
const { data: refund } = await soamtar.refunds.create({
transaction_id: payment.id, // omit amount to refund everything left
amount: '40.00',
reason: 'customer_request',
}, { idempotencyKey: `refund-${order.id}-1` });
// refund.status: 'success' (sandbox) | 'processing' (Orange Money) | 'failed'See the SDK pages for the other languages, and the CMS plugins for refunds from WooCommerce, Magento, PrestaShop, Drupal Commerce and Odoo.