Payment Webhook
This page serves as a guide to help you understand how MoneyHash's payment webhooks work and how to handle them correctly in your integration. For the full list of payment webhook event types, payload structures, and field descriptions, see Webhook Types.
To check how the webhooks work, you need to create an intent, either following our guide or using our sandbox:
When creating an intent, you must provide a webhook_url to which MoneyHash will send notifications at every step of each customer's payment process.
Before you start
Make sure the following are in place before testing payment webhooks:
- A payment intent created through MoneyHash, with a
webhook_urlincluded. This can be passed per intent and acts as an override for the account-level webhook endpoint. - A publicly reachable backend endpoint on your server that can receive POST requests from MoneyHash and return a
2xxresponse. This should be implemented on your backend, not your frontend. - A way to observe incoming webhook events during testing. We recommend webhook.site to get a free URL and inspect payloads in real time.
- If your server has firewall or network restrictions, you may need to whitelist MoneyHash IPs to allow inbound requests.
What to expect
A single payment can generate multiple webhook events. MoneyHash fires webhooks at two levels:
- Transaction webhooks fire on every operation status change. They follow the pattern
transaction.{operation}.{status}, where the operation ispurchase,authorize,capture,void, orrefund. For example, a purchase attempt going through 3DS firestransaction.purchase.pending_authenticationand thentransaction.purchase.successful. - Intent webhooks fire when the intent reaches a terminal state, for example
intent.processed,intent.time_expired, orintent.closed.
The operation events you receive depend on your flow:
- Purchase (auto-capture) flow takes the funds in a single step. You receive
transaction.purchase.successful, and the intent'spayment_status.statusbecomesCAPTURED. - Authorize and capture flow holds the funds first, then captures them later. You receive
transaction.authorize.successfulwithpayment_status.status: AUTHORIZED, and thentransaction.capture.successfulwithpayment_status.status: CAPTUREDonce you capture.
For payment methods that require the customer to complete an action outside the card flow, such as a bank redirect or a wallet, you may also receive pending_external_action or pending_online_external_action events. See the Webhook Types page for the full list of operation events.
The table below shows which events to act on and which are informational.
| Webhook event | Merchant action |
|---|---|
intent.processed | The intent has reached its final state. Read payment_status.status to determine the outcome and act accordingly. This is the recommended event to act on. |
intent.processed with payment_status.status: CAPTURED | Funds have been collected. For a purchase or auto-capture flow, this is the paid state. Fulfill the order. |
intent.processed with payment_status.status: AUTHORIZED | Funds have been held successfully. For an authorize-only flow, this is the successful state. Capture the funds when ready; once capture succeeds the state moves to CAPTURED. |
intent.processed with payment_status.status: ABORTED | The intent ended without a successful payment. Do not fulfill. |
transaction.purchase.successful | A purchase succeeded and funds were captured. |
transaction.authorize.successful | An authorization succeeded and funds are held, awaiting capture. |
transaction.capture.successful | A capture succeeded and funds were collected. |
intent.time_expired | Mark the payment session as expired. Do not fulfill. |
intent.closed | Mark the payment session as closed. Do not fulfill. |
transaction.purchase.failed | Do not fail the order. The intent may still retry, depending on your routing configuration. |
transaction.purchase.pending_authentication | No action. The customer is completing 3DS with their bank. |
transaction.purchase.pending | No action. Wait for the final outcome. |
Wait for intent.processed, then read payment_status.status to know what happened:
CAPTURED- funds were collected. This is the success state for a purchase or auto-capture flow.AUTHORIZED- funds were held successfully and are awaiting capture. This is the success state for an authorize-only flow. Once you capture the funds and the capture succeeds, the state moves toCAPTURED.ABORTED- the intent ended without a successful payment. Do not fulfill.
Do not hardcode CAPTURED as your only success check. If you run an authorize and capture flow, your success signal at authorization time is AUTHORIZED, not CAPTURED. A failed transaction does not mean the intent is done, as the customer may still retry under the same intent.
The diagram below shows the webhook sequence for the two most common payment scenarios.
Exploring payment webhooks
We recommend you use our Sandbox feature to explore each situation in which you will receive a notification. In addition, you can use webhook.site to receive and check the notification's content.
To explore how payment webhooks work, let's create a simple example with MoneyHash's Sandbox to understand better what happens in a successful payment scenario.
- Access the Sandbox feature.
- Select a scenario, you can use the basic embed one, which is the default when accessing the Sandbox feature.
- Replace the
webhook_urlat the payload with yours. Use the webhook.site solution to acquire a URL and further check the webhook notifications. You can use other solutions as well. - Submit the request.
- With the embed experience on the Sandbox, proceed with a successful payment while accompanying your webhook.site page.
You may have noticed that you received two different webhooks on your webhook.site page. To distinguish them, check the JSON of each notification and look for the type field. The first webhook should be a transaction.purchase.successful event, indicating that the transaction was successful. The second webhook should be an intent.processed event, indicating that the intent was processed. In MoneyHash, the intent associated with this transaction moves from UNPROCESSED to PROCESSED.
The goal of the simple example above is to present that with every step taken on a payment, you will be notified through one of our webhook events whenever a status changes. Everything that happens inside the payment - belonging to the intent, transaction, card token, or even the customer entity itself - you will be notified to allow you to handle each situation better as your business sees fit.
Payment webhook scenarios
The following scenarios walk through exactly what webhooks fire, in what order, and what to look for in each payload.
The first webhook below shows the full payload structure. The webhooks that follow show only the key fields that change, since the remaining fields keep the same structure as the first payload.
Scenario 1: 3DS authentication flow
This scenario shows a single intent (LkGoG41) going through a 3DS authentication step before resolving successfully. All three webhooks share the same transaction.id (87ce3591..) and operation.id (9K4YpKL) because this is one transaction with one operation progressing through states.
Webhook 1 - 3DS authentication initiated
type: transaction.purchase.pending_authentication
transaction.status:purchase.pending_authenticationoperation.status:pending_authenticationintent.payment_status.status:AUTHORIZE_ATTEMPT_PENDING
No action needed at this point. The cardholder is completing 3DS with their bank. Do not treat this as a failure.
{
"type": "transaction.purchase.pending_authentication",
"intent": {
"id": "LkGoG41",
"amount": {
"value": "50.00",
"currency": "AED",
"formatted": 50,
"display_value": "50.00 AED"
},
"created": "2026-06-11 10:07:37.065641+00:00",
"aft_data": null,
"flow_data": null,
"split_data": [],
"custom_fields": null,
"payment_status": {
"status": "AUTHORIZE_ATTEMPT_PENDING",
"balances": {
"total_voided": "0.00",
"total_captured": "0.00",
"total_refunded": "0.00",
"total_authorized": "0.00",
"available_to_void": "0.00",
"available_to_refund": "0.00",
"available_to_capture": "0.00"
}
},
"custom_form_answers": null
},
"account": {
"id": "jLYko7Z"
},
"status_id": "ZGyr7dG",
"api_version": "1.4",
"transaction": {
"id": "87ce3591-5e9a-4b87-92c6-c72beee863af",
"ip": {
"country": {
"name": "United States of America",
"iso_code": "US"
},
"ip_address": "192.0.2.1"
},
"type": "payment",
"method": {
"id": "gEOAQj9",
"display_name": "Provider A - Card",
"service_provider": {
"id": "gl7MmWL",
"display_name": "Provider A"
},
"processing_service_provider": {
"id": "gl7MmWL",
"display_name": "Provider A"
}
},
"status": "purchase.pending_authentication",
"created": "2026-06-11 10:07:40.420465+00:00",
"trx_rrn": null,
"bin_lookup": null,
"operations": [
{
"id": "9K4YpKL",
"arn": null,
"rrn": null,
"type": "purchase",
"extra": {},
"amount": {
"value": "50.00",
"currency": "AED",
"formatted": 50,
"display_value": "50.00 AED"
},
"status": "pending_authentication",
"statuses": [
{
"id": "LWJpkOj",
"code": "8000",
"value": "pending",
"created": "2026-06-11 10:07:40.446128+00:00",
"message": "Pending",
"localized_message": "Pending",
"provider_error_code": null,
"provider_error_message": null
},
{
"id": "ZGyr7dG",
"code": "8001",
"value": "pending_authentication",
"created": "2026-06-11 10:07:46.486497+00:00",
"message": "Pending Authentication",
"localized_message": "Pending Authentication",
"provider_error_code": null,
"provider_error_message": null
}
],
"refund_type": null,
"custom_fields": null,
"latest_status": {
"id": "ZGyr7dG",
"code": "8001",
"value": "pending_authentication",
"message": "Pending Authentication",
"localized_message": "Pending Authentication",
"provider_error_code": null,
"provider_error_message": null
},
"authorization_code": null,
"authorization_status": null,
"acquirer_transaction_id": null
}
],
"billing_data": {
"city": "Dubai",
"name": "John Doe",
"email": "[email protected]",
"floor": null,
"state": "Dubai",
"street": "123 Example Street",
"address": "123 Example Street",
"country": "AE",
"address1": null,
"building": null,
"apartment": null,
"bank_code": null,
"last_name": "Doe",
"first_name": "John",
"postal_code": "00000",
"phone_number": "+971500000000",
"mobile_wallet_number": null
},
"device_check": null,
"full_capture": false,
"shipping_data": {
"city": "Dubai",
"state": "Dubai",
"address": "123 Example Street",
"country": "AE",
"last_name": "Doe",
"first_name": "John",
"postal_code": "00000",
"phone_number": "+971500000000"
},
"custom_message": "",
"fraud_decision": null,
"paying_card_token": null,
"authorization_code": null,
"merchant_reference": "order-0001",
"authentication_data": null,
"authorization_status": null,
"payment_method_details": {
"data": {
"bin": "400000",
"type": null,
"brand": "VISA",
"issuer": null,
"last_4": "0002",
"country": null,
"network": null,
"pan_token": null,
"eci_version": null,
"expiry_year": "2030",
"local_brand": null,
"expiry_month": "01",
"funding_method": null,
"card_holder_name": "John Doe",
"cryptogram_format": null
},
"type": "CARD",
"provider_data": null
},
"external_action_message": [],
"provider_unique_reference": {
"key": "provider_reference_id",
"value": null
},
"provider_transaction_fields": {
"provider_checkout_id": null,
"provider_transaction_id": "abc123example",
"extended_error_description": null,
"provider_result_details_object": null
}
},
"operation_id": "9K4YpKL"
}Webhook 2 - 3DS completed, transaction successful
type: transaction.purchase.successful
transaction.status:purchase.successfuloperation.status:successfulintent.payment_status.status:CAPTUREDstatusesarray shows full progression:pending→pending_authentication→successful
This is a valid event to act on. For order fulfillment, intent.processed is recommended as the more reliable terminal signal.
{
"type": "transaction.purchase.successful",
"intent": {
"id": "LkGoG41",
"payment_status": {
"status": "CAPTURED",
"balances": {
"total_voided": "0.00",
"total_captured": "50.00",
"total_refunded": "0.00",
"total_authorized": "50.00",
"available_to_void": "0.00",
"available_to_refund": "50.00",
"available_to_capture": "0.00"
}
}
},
"status_id": "gXo5dK3",
"api_version": "1.4",
"transaction": {
"id": "87ce3591-5e9a-4b87-92c6-c72beee863af",
"status": "purchase.successful",
"operations": [
{
"id": "9K4YpKL",
"type": "purchase",
"status": "successful",
"statuses": [
{
"code": "8000",
"value": "pending",
"message": "Pending"
},
{
"code": "8001",
"value": "pending_authentication",
"message": "Pending Authentication"
},
{
"code": "6000",
"value": "successful",
"message": "Successful"
}
],
"latest_status": {
"code": "6000",
"value": "successful",
"message": "Successful",
"provider_error_code": null,
"provider_error_message": null
}
}
]
},
"operation_id": "9K4YpKL"
}Webhook 3 - Intent processed
type: intent.processed
intent.status:PROCESSEDpayment_status.status:CAPTUREDtransactions_count:1
The active_transaction.status in this webhook may show PENDING_AUTHENTICATION even after a successful outcome. Always use payment_status.status and the operation's latest_status.value as the source of truth for the final result.
{
"type": "intent.processed",
"intent_type": "PAYMENT",
"api_version": "1.4",
"data": {
"intent": {
"id": "LkGoG41",
"status": "PROCESSED",
"payment_status": {
"status": "CAPTURED",
"balances": {
"total_voided": "0.00",
"total_captured": "50.00",
"total_refunded": "0.00",
"total_authorized": "50.00",
"available_to_void": "0.00",
"available_to_refund": "50.00",
"available_to_capture": "0.00"
}
},
"transactions_count": 1,
"active_transaction": {
"id": "87ce3591-5e9a-4b87-92c6-c72beee863af",
"status": "PENDING_AUTHENTICATION",
"method": {
"display_name": "Provider A - Card"
},
"operations": [
{
"id": "9K4YpKL",
"type": "purchase",
"status": "successful",
"latest_status": {
"code": "6000",
"value": "successful",
"message": "Successful"
}
}
]
},
"transactions_history": [
{
"id": "87ce3591-5e9a-4b87-92c6-c72beee863af",
"status": "PENDING_AUTHENTICATION"
}
]
},
"intent_id": "LkGoG41"
}
}Scenario 2: Failed transaction with retry
This scenario shows a single intent (ZGydl8P) that goes through two transactions. The first fails on one provider, MoneyHash automatically retries on another provider, and the intent is ultimately processed successfully. The transaction.id is different across the two transaction webhooks because these are two separate transactions under the same intent.
Webhook 1 - First transaction fails
type: transaction.purchase.failed
transaction.id:d885f2ee..transaction.status:purchase.failedintent.payment_status.status:AUTHORIZE_ATTEMPT_FAILEDlatest_status.code:7001
The intent is still UNPROCESSED at this point. A retry is possible. Do not mark the order as failed.
{
"type": "transaction.purchase.failed",
"intent": {
"id": "ZGydl8P",
"payment_status": {
"status": "AUTHORIZE_ATTEMPT_FAILED",
"balances": {
"total_voided": "0.00",
"total_captured": "0.00",
"total_refunded": "0.00",
"total_authorized": "0.00",
"available_to_void": "0.00",
"available_to_refund": "0.00",
"available_to_capture": "0.00"
}
}
},
"status_id": "LWJpKaJ",
"api_version": "1.4",
"transaction": {
"id": "d885f2ee-11a8-4426-ace5-f83767d19b92",
"status": "purchase.failed",
"method": {
"id": "gMk35A9",
"display_name": "Provider A - Card",
"service_provider": {
"id": "1ZvVnpZ",
"display_name": "Provider A"
}
},
"operations": [
{
"id": "ZpN2BlL",
"type": "purchase",
"status": "failed",
"statuses": [
{
"code": "8000",
"value": "pending",
"message": "Pending"
},
{
"code": "7001",
"value": "failed",
"message": "Transaction declined by the bank.",
"provider_error_code": "BANK_DECLINE",
"provider_error_message": "Transaction declined by the bank."
}
],
"latest_status": {
"code": "7001",
"value": "failed",
"message": "Transaction declined by the bank.",
"provider_error_code": "BANK_DECLINE",
"provider_error_message": "Transaction declined by the bank."
}
}
]
},
"operation_id": "ZpN2BlL"
}Webhook 2 - Retry succeeds
type: transaction.purchase.successful
transaction.id:00f72691..- a new transaction on a different providertransaction.status:purchase.successfulintent.payment_status.status:CAPTURED
This is a valid event to act on. For order fulfillment, intent.processed is recommended as the more reliable terminal signal.
{
"type": "transaction.purchase.successful",
"intent": {
"id": "ZGydl8P",
"payment_status": {
"status": "CAPTURED",
"balances": {
"total_voided": "0.00",
"total_captured": "50.00",
"total_refunded": "0.00",
"total_authorized": "50.00",
"available_to_void": "0.00",
"available_to_refund": "50.00",
"available_to_capture": "0.00"
}
}
},
"status_id": "ZGyrWvA",
"api_version": "1.4",
"transaction": {
"id": "00f72691-ac38-42ad-8c16-de5de9e8a778",
"status": "purchase.successful",
"method": {
"id": "1ZvVjZB",
"display_name": "Provider B - Card",
"service_provider": {
"id": "jZBkM9V",
"display_name": "Provider B"
}
},
"operations": [
{
"id": "L5zV3Xg",
"type": "purchase",
"status": "successful",
"statuses": [
{
"code": "8000",
"value": "pending",
"message": "Pending"
},
{
"code": "6000",
"value": "successful",
"message": "Successful"
}
],
"latest_status": {
"code": "6000",
"value": "successful",
"message": "Successful"
}
}
]
},
"operation_id": "L5zV3Xg"
}Webhook 3 - Intent processed
type: intent.processed
intent.status:PROCESSEDpayment_status.status:CAPTUREDtransactions_count:2active_transaction: the successful second transactiontransactions_history: full history of both transactions
Terminal webhook. Once received, no further transactions will be created under this intent.
{
"type": "intent.processed",
"intent_type": "PAYMENT",
"api_version": "1.4",
"data": {
"intent": {
"id": "ZGydl8P",
"status": "PROCESSED",
"payment_status": {
"status": "CAPTURED",
"balances": {
"total_voided": "0.00",
"total_captured": "50.00",
"total_refunded": "0.00",
"total_authorized": "50.00",
"available_to_void": "0.00",
"available_to_refund": "50.00",
"available_to_capture": "0.00"
}
},
"transactions_count": 2,
"active_transaction": {
"id": "00f72691-ac38-42ad-8c16-de5de9e8a778",
"status": "SUCCESSFUL",
"method": {
"display_name": "Provider B - Card"
}
},
"transactions_history": [
{
"id": "00f72691-ac38-42ad-8c16-de5de9e8a778",
"status": "SUCCESSFUL"
},
{
"id": "d885f2ee-11a8-4426-ace5-f83767d19b92",
"status": "FAILED"
}
]
},
"intent_id": "ZGydl8P"
}
}Important notes
On order fulfillment: Wait for intent.processed, then check payment_status.status. For a purchase or auto-capture flow, CAPTURED is the paid state and the signal to fulfill. For an authorize-only flow, AUTHORIZED is the successful state, and funds are collected later when you capture. Do not rely on CAPTURED alone as a universal success check, as it does not apply to authorize-only flows.
On retries: A transaction.purchase.failed webhook does not mean the intent is done. If the intent has not reached a terminal state (PROCESSED, TIME_EXPIRED, CLOSED), MoneyHash may retry automatically, depending on your routing configuration. Only mark an order as permanently failed when the intent reaches a terminal state without a successful transaction.
On 3DS: The active_transaction.status in the intent.processed webhook may show PENDING_AUTHENTICATION even after a successful outcome. Always use payment_status.status and the operation's latest_status.value as the source of truth for the final result.
You can perform other payment types or operations to explore the webhook notifications further. Access the Webhook Types page to learn more about all available webhook events.
Updated about 1 month ago