Refund a customer in Morocco: in full or in part, from the portal or through the API
Refund a customer in Morocco, in full or in part, from the portal or the API: refundReference, statuses, balance impact, export line and customer message.
To refund a customer in Morocco with ChariPay, you use the portal or the API, in full or partially: you point to the original payment and the amount to give back, and that amount is deducted from your available balance. Refunds are free. Through the API, a refundReference derived from the order guarantees that the same refund never goes out twice, even if your code replays the call. And if the customer has not paid yet, there is nothing to refund: you cancel the link or the session instead. This guide covers both cases, webhook tracking, the effect on your balance and on the next payout, the export line and the message to send the customer.
Cancel before payment, refund after
ChariPay is a Moroccan payment platform, a PSP operated by Chari Money, a payment institution licensed by Bank Al-Maghrib. Before looking for how to refund, ask one question: has the money already moved? As long as the customer has not paid, there is nothing to give back — you only need to stop them from paying. Once a card payment has succeeded, a refund is the only option.
| Situation | The right move | What happens |
|---|---|---|
| Payment link sent, not paid yet | Cancel the link — through the API, POST /v1/payment-links/{reference}/cancel | The link turns "canceled"; a canceled link can no longer be paid |
| Checkout session created, not paid yet | Cancel the session — POST /v1/payment-sessions/{sessionId}/cancel | The session is canceled; without that call, it expires on its own, after 72 hours by default or at its expiresAt |
| Card payment succeeded | Refund, in full or partially | The amount is deducted from your available balance; the return on the bank side is not instantaneous |
Canceling is the cleanest move: no money movement, no line to reconcile. Cancel a link as soon as it no longer matches the order — out of stock, a customer who changes their mind, a wrong amount — instead of letting it live until it expires: a customer in a hurry could still pay it.
If you sell on Shopify or WooCommerce, how to reflect the refund inside the platform itself is covered in Shopify in Morocco and WooCommerce in Morocco. This guide covers the refund on the ChariPay side, whatever tool you sell with.
Refund a customer from the portal
The portal lets you refund a customer in full or partially, without a line of code. Not everyone on your team can do it, and that is by design: portal roles separate viewing from acting. The owner or an authorized manager triggers outgoing operations, refunds included; the cashier collects without seeing the balance or transfers, and does not refund; the accountant reads everything without triggering anything. A refund request made at the counter therefore goes up to the manager.
Before you confirm, four decisions are yours:
- 1The payment concerned. Identify it among your transactions, and check the amount paid and the customer.
- 2Full or partial. Are you giving everything back, or only part of it? The answer depends on your return policy and on what was delivered.
- 3The amount. For a partial refund, work it out before you start: a missing item, a service not delivered, a goodwill gesture.
- 4The reason. Record it in your own follow-up: your accountant, and sometimes your customer, will ask later.
The amount is then deducted from your available balance. Roles filter upstream; downstream, the audit log is cryptographically chained: history cannot be rewritten.
Refund through the API with a deterministic refundReference
Through the API, a refund is a single call: POST /v1/refunds, with the identifier of the original payment, your refund reference and a reason.
curl -X POST 'https://api-psp.charipay.ma/v1/refunds' \
-H 'X-CHARI-PAY-API-KEY: chari_sk_test_...' \
-H 'Content-Type: application/json' \
-d '{
"operationId": 8123,
"refundReference": "refund-ORD-2026-0421-1",
"refundAmount": 100.00,
"reason": "CUSTOMER_CANCELLATION",
"note": "Article retourne"
}'Field by field:
- `operationId` identifies the payment to refund. Read it from
GET /v1/transactionsorGET /v1/transactions/{operationId}. Alternatively,externalIdaccepts the value of yourorderIdfor a session payment — careful, this is not theexternalIdyou sent when creating the session. The guide on matching a payment to its order shows where each identifier comes back. - `refundReference` (64 characters at most) is your own reference, deterministic, built from the order number, such as
refund-ORD-2026-0421-1. It is the idempotency key: creation returns202 Accepted, and replaying the same reference returns the existing refund with200, without debiting a second time — never201. The principle is explained in payment idempotency. - `refundAmount` sets the amount of a partial refund; when it is absent, the refund is for the full amount.
- `reason` takes an enumeration value:
MERCHANT_CANCELLATION,CUSTOMER_CANCELLATION,PLATFORM_ORCHESTRATIONorOTHER. Free text is rejected there: it goes into `note`, 128 characters at most.metadataalso accepts your own keys.
The full reference for the three endpoints — create, retrieve, list — is in the Refunds module of the API docs.
You can run this loop risk-free in the sandbox: Start in test mode. Three fields (name, work email, company), an activation link received by email to choose your password, then the portal, where you create your own test key. Pay an order with the test card, refund it through the API, replay the call to see the 200: no real money moves, and your business verification (KYB) only gates the move to production.
Partial or full refund: refundAmount decides
A partial refund is not a separate operation: it is the same call with one more field. Without refundAmount, you give back the whole payment; with refundAmount, you give back that amount, in dirhams and in major units (100.00 = MAD 100.00).
Example: order ORD-2026-0421 was paid MAD 250.00 and the customer sends back an item worth MAD 100.00. That is the call above: "refundAmount": 100.00, the reason CUSTOMER_CANCELLATION and the note "Article retourne" (item returned). The customer keeps the rest of the order, and your system knows exactly what was given back, and why.
| Situation | refundAmount | Suggested reason | note |
|---|---|---|---|
| You cancel before shipping (out of stock) | absent: full | MERCHANT_CANCELLATION | "Out of stock" |
| The customer cancels before delivery | absent: full | CUSTOMER_CANCELLATION | "Customer canceled" |
| The customer sends back an item | item amount | CUSTOMER_CANCELLATION | "Item returned" |
| Item missing from the parcel | item amount | MERCHANT_CANCELLATION | "Missing item" |
| Service partly delivered | undelivered part | depends on who withdraws | short description |
| Goodwill gesture after a delay | gesture amount | OTHER | "Goodwill gesture" |
The golden rule: decide the exact amount before the call, with the person who knows the order. Your refundReference, your note and your refundAmount should tell the same story.
Track a refund: status and events
The 202 Accepted response means the request is recorded, not that the money is back with the customer: a refund is not instantaneous on the bank side. To learn the outcome, you have two complementary routes:
- Webhook events.
refund.succeededwhen the refund went through,refund.failedwhen it failed. If your endpoint declares an explicitenabledEventslist, add both events to it — an empty list receives every event. Route on theChari-Event-Typeheader and deduplicate onChari-Event-Id. - Direct reads.
GET /v1/refunds/{reference}returns a refund from yourrefundReference(or the identifier assigned by the platform), andGET /v1/refundslists your refunds, most recent first.
In your system, mark the order "refunded" when refund.succeeded arrives, not at the moment of the call. In between, show "refund in progress": that is the real state. If a refund does not go through, check your available balance first — if it is not enough, the operation is blocked — then contact the team if the cause is not obvious.
No developer? The portal is enough: there, every account movement, refunds included, carries the reference of the sale, refund or payout that explains it.
The effect on your balance and on the next payout
Refunding a customer is free: no refund fee is added, as the pricing page states. The refunded amount is deducted from your available balance.
ChariPay is not a bank: your payment account is held by Chari Money. It complements your bank; it does not replace it. It has neither overdraft nor credit: if the available balance is not enough to cover the refund, the operation is blocked rather than creating an overdraft. Two habits follow:
- Before a large refund, check your available balance — in the portal, or with
GET /v1/wallet. - If it does not cover the amount, wait for new collections or fund the account with
POST /v1/wallet/cash-ins, with a mandatoryIdempotency-Keyheader.
The payout to your bank is a separate operation: automatic every night once a settlement account is configured in the portal, or by transfer on demand, and each payout carries a reference that appears on your bank statement. A refund can reduce the next payout, without a separate line on the bank statement: you will see a smaller payout, not a "refund" line. The explanation lives in the account export, not on the statement. The full timeline, from successful payment to your bank, is in payout timing.
The refund line in the export
A refund does not erase a sale: it follows it. In the export, each refund is a separate movement, linked to the original transaction and carrying the refund's reference. The portal separates amount paid, fees and net, and the CSV export keeps those references — from the portal or with GET /v1/transactions/export.csv, up to 10,000 rows; beyond that, split by period with from and to.
For your accountant, that means two dated entries: the sale, then the refund. Three habits keep this simple:
- 1Keep the `refundReference` in your order system: it is what links the export line to the customer's file.
- 2Reconcile the smaller payout against the export for the same period, not against the refunded sale.
- 3Convert timestamps: they are in UTC, and Morocco is UTC+1. A refund made just after midnight, Moroccan time, is dated the previous day in the export.
The full method — gaps between collected and paid out, refunds, time zone — is in reconciling your collections.
Tell the customer: the message to send
A refund the customer does not understand turns into a support call. Send a short message when you start the operation, with what they need to know and nothing more:
Hello Amine, we have started a refund of MAD 100.00 on your order ORD-2026-0421 (partial refund: item returned). A refund is not instantaneous on the bank side: the amount will come back to you according to your bank's processing. Our reference: refund-ORD-2026-0421-1. We will never ask you for your card details.
What the message must include: the amount, the order reference, the type of refund (full or partial), the sentence about bank processing with no number in it — you do not control that timing — and your refund reference, which the customer can quote. What it must never include: a request for a card number, a code or a RIB "to speed things up." A refund is tied to the original payment and needs none of that; a customer used to you asking for nothing spots a scam in your name faster.
If your tool allows it, a second, shorter message when refund.succeeded arrives closes the loop. For the wider customer relationship online — confirmations, receipts, return policy — see the guide to e-commerce payments in Morocco.
Frequently asked questions
How much does a refund cost?
Nothing: refunding a customer is free, in full or partially, from the portal as well as through the API. No refund fee is added to the amount you give back; that amount is simply deducted from your available balance. Test mode, also free and with no time limit, lets you run a complete refund, from payment to export, before you serve real customers.
Can I make a partial refund?
Yes. Through the API, add refundAmount with the amount to give back, in dirhams and major units; without that field, the refund is for the full amount. In the portal, you choose between the full amount and a partial one in the same way. Typical cases: a missing item, a service partly delivered, a goodwill gesture. Work out the exact amount before starting.
What happens if my balance is not enough to refund?
The operation is blocked rather than creating an overdraft: the payment account has neither overdraft nor credit. Check your available balance in the portal or with GET /v1/wallet, then wait for new collections or fund the account with POST /v1/wallet/cash-ins. Meanwhile, let the customer know their refund is being prepared rather than leaving them without news.
When does the customer see the money back on their card?
A refund is not instantaneous on the bank side, and we publish no timeframe: the return depends on processing by the customer's bank. On your side, the refund.succeeded event, or a read with GET /v1/refunds/{reference}, tells you the refund went through. Tell the customer the operation has started, give them your reference, and do not promise a date.
Who can refund a customer in the portal?
The owner or an authorized manager: they are the ones who trigger outgoing operations. The cashier collects without seeing the balance or transfers, and does not refund; the accountant reads everything without triggering anything. A request received at the counter therefore goes up to the manager. Through the API, the call goes out with your API key, a secret that never leaves your server.
Should I refund or cancel an unpaid payment link?
Cancel it. As long as the link is unpaid, there is nothing to refund: cancel it, and a canceled link can no longer be paid. Through the API, the call is POST /v1/payment-links/{reference}/cancel. Same logic for an unpaid checkout session, with POST /v1/payment-sessions/{sessionId}/cancel. Refunds only concern successful payments; of the two moves, only a refund changes your balance.
Does the refund show up on my bank statement?
Not as a separate line. The payout to your bank is a separate operation, with its own reference on your statement; a refund can reduce the next payout, without a separate line on the bank statement. The detail is in the export of your payment account, where each refund is a separate movement, linked to the original transaction and carrying the refund's reference.
What reason should I give in an API refund?
The reason field only accepts an enumeration value: MERCHANT_CANCELLATION when you cancel, CUSTOMER_CANCELLATION when the customer withdraws, OTHER for any other reason; the list also includes PLATFORM_ORCHESTRATION. Free text is rejected there. Put your explanations in note, up to 128 characters — "Missing item," "Goodwill gesture" — and keep the same wording in your own system.
Next step
Create your test account — Start in test mode — then pay an order with the test card, refund it partially through the API, wait for refund.succeeded and find the line in the export. The Refunds module of the API docs details the three endpoints, and matching a payment to its order helps you find each payment's operationId.
Written by ChariPay team.