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_url included. 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 2xx response. 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 is purchase, authorize, capture, void, or refund. For example, a purchase attempt going through 3DS fires transaction.purchase.pending_authentication and then transaction.purchase.successful.
  • Intent webhooks fire when the intent reaches a terminal state, for example intent.processed, intent.time_expired, or intent.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's payment_status.status becomes CAPTURED.
  • Authorize and capture flow holds the funds first, then captures them later. You receive transaction.authorize.successful with payment_status.status: AUTHORIZED, and then transaction.capture.successful with payment_status.status: CAPTURED once 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 eventMerchant action
intent.processedThe 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: CAPTUREDFunds have been collected. For a purchase or auto-capture flow, this is the paid state. Fulfill the order.
intent.processed with payment_status.status: AUTHORIZEDFunds 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: ABORTEDThe intent ended without a successful payment. Do not fulfill.
transaction.purchase.successfulA purchase succeeded and funds were captured.
transaction.authorize.successfulAn authorization succeeded and funds are held, awaiting capture.
transaction.capture.successfulA capture succeeded and funds were collected.
intent.time_expiredMark the payment session as expired. Do not fulfill.
intent.closedMark the payment session as closed. Do not fulfill.
transaction.purchase.failedDo not fail the order. The intent may still retry, depending on your routing configuration.
transaction.purchase.pending_authenticationNo action. The customer is completing 3DS with their bank.
transaction.purchase.pendingNo action. Wait for the final outcome.
Reading the 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 to CAPTURED.
  • 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.

  1. Access the Sandbox feature.
  2. Select a scenario, you can use the basic embed one, which is the default when accessing the Sandbox feature.
  3. Replace the webhook_url at 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.
  4. Submit the request.
  5. 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.

What this means

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.

Note

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_authentication
  • operation.status: pending_authentication
  • intent.payment_status.status: AUTHORIZE_ATTEMPT_PENDING
Note

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.successful
  • operation.status: successful
  • intent.payment_status.status: CAPTURED
  • statuses array shows full progression: pendingpending_authenticationsuccessful
Note

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: PROCESSED
  • payment_status.status: CAPTURED
  • transactions_count: 1
Callout

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.failed
  • intent.payment_status.status: AUTHORIZE_ATTEMPT_FAILED
  • latest_status.code: 7001
Note

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 provider
  • transaction.status: purchase.successful
  • intent.payment_status.status: CAPTURED
Note

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: PROCESSED
  • payment_status.status: CAPTURED
  • transactions_count: 2
  • active_transaction: the successful second transaction
  • transactions_history: full history of both transactions
Note

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.


Did this page help you?