You can expect the below object when you register a webhook for the following events:
- payment.failed
- payment.succeeded
- payment.refund_initiated
- payment.refunded
- payment.refund_failed
- payment.card_verified
- payment.authorized
- payment.voided
Raw sample
{
"status": "captured",
"id": "PAY-3C4D5E6F7A",
"amount": 33.99,
"amount_currency": "AED",
"refund_amount": 0,
"refund_status": "No refund",
"refunds": [],
"max_refund_amount": 33.99,
"billing_descriptor": "Mamo*Merchant",
"custom_data": {},
"created_date": "2023-12-25-14-38-53",
"subscription_id": null,
"settlement_amount": "31.99",
"settlement_currency": "AED",
"settlement_date": "2024-01-01",
"customer_details": {
"name": "",
"email": "[email protected]",
"phone_number": "-",
"comment": "-"
},
"payment_method": {
"card_id": null,
"method": "Card",
"type": "CREDIT VISA",
"card_holder_name": "John Doe",
"card_last4": "1111",
"card_expiry_month": "12",
"card_expiry_year": "2028",
"origin": "International card"
},
"settlement_fee": "AED 1.90",
"settlement_vat": "AED 0.10",
"payment_link_id": "MB-LINK-5E6F7A8B9C",
"payment_link_url": "https://sandbox.dev.business.mamopay.com/pay/acme-chocolates-4d5e6f",
"external_id": null,
"error_code": null,
"error_message": null,
"next_payment_date": null,
"event_type": "payment.succeeded"
}Field descriptions
| Field | Type | Description |
|---|---|---|
| status | String | Status of the payment: confirmation_required, processing, authorized, card_verified, captured, failed or voided. Refunds don't change status: a refunded payment stays captured. refund_initiated and refunded are legacy values that only payments created before February 2022 can return. See the lifecycle below. |
| redirect_url | String | URL the customer must visit to complete authentication. Present only while status is confirmation_required, so it never appears on webhooks |
| id | String | Mamo payment ID |
| amount | Number | Amount |
| amount_currency | String | Currency |
| refund_amount | Number | Total amount refunded on this payment so far. 0 if none |
| refund_status | String | Refund state: initiated, success, failed, or No refund (no refund, or one still pending) |
| refunds | Array | Breakdown of each individual refund issued (id-amount-amount_currency-billing_amount-billing_amount_currency) |
| max_refund_amount | Number | The maximum amount that can still be refunded on this payment |
| custom_data | Dictionary | Custom Data sent upon creation |
| created_date | String (Format: YYYY-MM-DD-HH-MM-SS) | payment creation time |
| subscription_id | String | Subscription ID (in case of a subscription payment) |
| settlement_amount | String | Amount settled to you, without currency, formatted with thousands separators (for example "1,356.42"). "0.00" until settled |
| settlement_currency | String | Settlement Currency |
| settlement_date | String (Format: YYYY-MM-DD) | Expected settlement date, or - until a settlement is scheduled. Never null |
| customer_details | Dictionary | Customer Details (name-email-phone_number-comment). Each value is - when unknown |
| external_id | String | External ID if sent |
| payment_method | Object | Payment Method (card_id-method-type-card_holder_name-card_last4-card_expiry_month-card_expiry_year-origin). method is Card, Apple Pay, Google Pay or Recurring; type is Tabby, or the card type and brand upper-cased, for example CREDIT VISA |
| settlement_fee | String | Mamo fee with currency, for example AED 1.90. Never null |
| settlement_vat | String | VAT on the Mamo fee with currency, for example AED 0.10. Never null |
| payment_link_id | String | ID of payment link used |
| payment_link_url | String | Payment Link Used |
| error_code | String | Error Code (In case of failure) |
| error_message | String | Error Message (In case of failure) |
| next_payment_date | String (Format: DD/MM/YYYY) | Next renewal date for subscription payments; null otherwise |
| event_type | String | Corresponding Webhook Event |
Payment status lifecycle
The status field tells you where a payment is in its lifecycle and, crucially, whether money has actually been received. On webhooks the status is reported raw — it is never remapped — so an authorized hold arrives as authorized (not captured) and a payment voided by Mamo's fraud screening arrives as voided (not failed). Note that confirmation_required and processing do not trigger a webhook, so you will not receive this object for those states; a payment you hear about via webhook has reached captured, failed, authorized, card_verified, or voided.
Refunds don't change status: a refunded payment stays captured, so you can refund it again up to max_refund_amount. Track refunds with refund_status (initiated, success, failed, or No refund), refund_amount, max_refund_amount and refunds[]. Payments made before February 2022 may still show the legacy values refund_initiated or refunded.
| Status | Funds received? | Meaning |
|---|---|---|
confirmation_required | No | Initial state. The payment is waiting on the customer to complete authentication / confirmation (for example 3-D Secure or a redirect). No money has moved. Does not trigger a webhook, so it does not arrive via this object. |
processing | No (pending) | The payment has been submitted to the gateway and the outcome is not yet known. Do not treat this as success. Does not trigger a webhook, so it does not arrive via this object. |
authorized | Held, not captured | Funds have been authorized (placed on hold on the customer's card) but not captured. This happens on hold_and_charge_later links, and on future-dated subscription links — see the note below. |
captured | Yes | The payment succeeded and funds were captured. This is the terminal success state and the amount will be settled to you. |
card_verified | No (no charge) | The card was successfully verified (for example a card-saving or verification flow). No amount was captured. |
failed | No | The payment attempt failed. Inspect error_code / error_message for the reason. |
voided | No (hold released) | An authorization that was released before capture: either you released a hold with Reverse payment, or Mamo's fraud screening declined the payment after the card authorized it. Only the fraud-screening void sends a payment.voided webhook (with the raw voided status, never remapped to failed); releasing a hold yourself sends no webhook. |
Authorize vs. capture (hold_and_charge_later)On a normal payment link the charge is captured immediately, so a successful payment goes straight to
capturedand the funds are received.On a payment link configured with
hold_and_charge_later, the payment is only authorized first: the amount is placed on hold on the customer's card and the charge sits inauthorizeduntil you capture it later. You can release the hold with Reverse payment, which moves the charge tovoided; no webhook is sent for that, so use the Reverse Payment response as your confirmation. On webhooks these states are reported raw — an authorized hold is sent asauthorized(notcaptured) — so rely on thestatustogether with the link configuration to know whether a given payment is a hold or a completed capture.
What differs per event
The raw sample above is a payment.succeeded payload. Other events carry the same object shape — only these fields change:
| Event | status | Notable fields |
|---|---|---|
payment.failed | failed | error_code / error_message populated; settlement_date is the placeholder string "-" and settlement_amount is the string "0.00" — these fields are always present, never actually null |
payment.authorized | authorized | Same settlement_date/settlement_amount placeholders as above until captured |
payment.voided | voided | Same as authorized, but Mamo's fraud screening voided the authorization — no money was taken and no further action is possible on this payment. Not sent when you release a hold with Reverse Payment |
payment.card_verified | card_verified | amount reflects the verification amount (if any); settlement_* fields are still present with the same placeholder values, since no revenue has been recorded |
payment.refund_initiated | captured | refund_status: "initiated", refund_amount > 0, refunds has at least one entry, max_refund_amount reduced accordingly |
payment.refunded | captured | refund_status: "success". Two partial refunds send two payment.refunded events with the same id; tell them apart by refunds[].id |
payment.refund_failed | captured | refund_status: "failed" |
Marketplace / split paymentsThis object does not include
payouts_share— the split amount is available on the payment link object instead. For a split payment, the recipient's share is credited to their recipient ledger on the payment's settlement date (at capture if your account settles same-day); it is paid out to their bank account later on a schedule (by default monthly). Use Fetch recipient balance to check a recipient's accrued ledger balance.