Payments
How payments work
Payment intents, attempts, the statuses a payment goes through, and which signal to trust before you deliver.
On this page
Every payment, by card or in cash, is a payment intent: one object for one amount you want to
collect, from creation to the money received. You create it yourself, or a checkout session or a
payment link creates it for you. Its status tells you where it stands; your code reads it rather
than guessing.
Payment intents and attempts
A payment intent can be tried several times. Each try is an attempt, also called a charge
(ch_…): a card payment page opened, or a cash code issued. A declined card or an expired code ends
the attempt, not the payment intent: your customer can try again, with the same method or another.
- The intent's
latest_chargenames its most recent attempt. - A payment intent succeeds once. Even if a confirmation reaches us several times, there is one
success, one
payment_intent.succeededevent and one entry in your balance. amount_receivedis set when the payment succeeds;amount_refundedgrows with each refund.
The lifecycle
- requires_payment_method → requires_action: card page or cash code
- requires_action → processing: customer paid
- processing → succeeded: confirmed, amount matches
- requires_action → requires_payment_method: declined, failed or expired
- processing → requires_payment_method
- requires_payment_method → canceled: canceled or expired
- requires_action → canceled: you cancel
A card payment confirmed at once can also go straight from requires_action to succeeded.
Each status, and what to do
| Status | What it means | What you do |
|---|---|---|
requires_payment_method | Nothing is being paid: the payment was just created, or the last attempt failed. | Send your customer to pay, or let them try again. last_payment_error says why the last attempt failed. |
requires_action | Your customer has to act: finish on the card payment page, or pay the cash code at an agent. | Wait. With your own page, show next_action (see below). |
processing | Your customer has paid; the confirmation is on its way. | Wait for the webhook. Do not deliver yet. |
succeeded | The payment is confirmed and the amount matches. | Deliver the order. This is final. |
canceled | You canceled the payment, or its checkout session expired unpaid. | Nothing: no money moves. Create a new payment if your customer comes back. |
When something does not add up
If a payment is confirmed for another amount than the one asked, or money arrives after the payment
was canceled or its code expired, nothing is credited automatically. The payment intent stays in
processing and gains a review object, with reason set to amount_mismatch or
paid_after_end, and you receive review.opened. A person on our side looks at it. In the sandbox,
the cash scenarios let you trigger both.
Attempts
An attempt's status is pending, succeeded, failed, canceled or expired. A failed
attempt carries failure_code and failure_message, and the intent copies them into
last_payment_error:
| Code | Method | What happened |
|---|---|---|
card_declined | Card | The bank declined. decline_code may say more, such as insufficient_funds. |
expired_card | Card | The card has expired. |
authentication_failed | Card | 3-D Secure did not succeed. |
processing_error | Card, cash | The payment could not be processed. Your customer can try again. |
voucher_expired | Cash | The code was not paid before its deadline. |
voucher_refused | Cash | The payment network refused the code. |
An attempt has a limited life: about 30 minutes for a card payment page, and the deadline of the
code for cash (about three days). An attempt that runs out ends expired.
Next actions
While a payment intent is in requires_action, its next_action says what your customer must do.
With hosted checkout or a payment link you never read it: our pages do the work. With your own page:
form_post, for a card: an address and fields that your customer's browser posts to reach the card payment page.display_voucher, for cash: thecode, theamount_to_pay(your amount plus a small stamp fee your customer pays),expires_at, a printableslip_url, andhosted_voucher_url, our voucher page.
Amounts and currency
- Every amount is an integer in centimes:
25000is 250.00 MAD. Never send decimals. currencyis alwaysmad, in lowercase.- A cash payment is at least 100.00 MAD (
10000); a card payment at least 1.00 MAD (100). A smaller amount is refused withamount_too_small, in test mode as in live mode.
Redirects and webhooks: which one to trust
Your customer comes back to your success_url after paying, but that return proves nothing: they can
close the tab first, a cash payment is made days later at an agent, and anyone can type the address.
Deliver on one of these two signals only:
- the webhook
payment_intent.succeeded, with a verified signature; - the payment intent read from the API, with your secret key, showing
status: "succeeded".
Use the return to thank your customer and show "We are confirming your payment"; then let the webhook do the rest.
Refunds
A succeeded card payment can be refunded, in full or in part, as long as the total refunded
stays within the amount received. A refund is pending, then succeeded, failed or canceled.
A cash payment cannot be refunded through AdProMaxPay: the money was handed over in cash, and there
is no card to return it to. Settle it with your customer directly. See Refunds.
Events you receive
| Event | When |
|---|---|
payment_intent.requires_action | A card page was opened or a cash code issued. |
payment_intent.processing | Your customer paid; the confirmation is on its way. |
payment_intent.succeeded | The payment is confirmed. Deliver. |
payment_intent.payment_failed | An attempt failed: declined, expired or refused. |
payment_intent.canceled | The payment was canceled. |
charge.succeeded, charge.failed | An attempt succeeded or failed. |
review.opened | Something does not add up; nothing was credited. |
The full list is in Webhooks.