External API is an API-driven integration where your backend controls the payment flow - method selection, billing data collection, and native pay receipt submission - while MoneyHash handles card data collection securely through the embedded experience. It is suited for merchants who want programmatic control over the payment flow without PCI certification requirements.

Prerequisites

Before starting your integration:

  1. Access your MoneyHash Organization and Account from the dashboard
  2. Connect your payment providers to the account
  3. Configure your Payment Defaults and Flow
  4. Retrieve your API keys from the dashboard

Authentication: Include your API key in the X-Api-Key header on every request.

Base URL: https://web.moneyhash.io/api/v1.4/


How it works

External API is state-driven. Every API response returns a state field and a state_details object that tells you what to do next. You advance the payment by reading state and acting on it.

StateWhat it meansWhat to do
METHOD_SELECTIONIntent created, no method selected yetRead state_details.payment_methods[], render your own method selection UI, call update-method with the customer's choice
FORM_FIELDSBilling fields required before proceedingCollect billing data and pass in intent creation, or render the embed URL to collect them
URL_TO_RENDERRedirect or 3DS requiredRedirect customer to state_details.embed_url
INTENT_PROCESSEDPayment complete - terminalConfirm via webhook and fulfill order
TRANSACTION_FAILEDTransaction failedCheck intent status - retry may be possible
EXPIREDIntent expired - terminalRender expired state
CLOSEDIntent closed - terminalRender closed state
Note

METHOD_SELECTION is always the initial state on intent creation. Pass billing_data at intent creation to skip FORM_FIELDS for methods that require billing information before proceeding.


Step 1 - Create a payment intent

Create an intent on your backend to initiate the payment. API Reference

POST /api/v1.4/external/payments/intent/

Required parameters

ParameterTypeDescription
amountstringThe amount to charge. Max 14 digits, max 2 decimal places.
amount_currencystringISO 4217 currency code e.g. AED, USD, SAR.
webhook_urlstringYour backend endpoint that receives payment event notifications.
operationstringpurchase for immediate charge · authorize for auth-only. Required if flow_id is not provided.

Recommended parameters

ParameterPurpose
billing_dataPass upfront to skip FORM_FIELDS state for methods that require billing data
customerAssociate with an existing MoneyHash customer ID
allow_tokenize_cardSet to true to allow saving the card for future use
payment_typeSet to recurring for non-present cardholder payments
merchant_initiatedSet to true for merchant-initiated transactions
successful_redirect_urlRedirect after successful payment
failed_redirect_urlRedirect after failed payment
pending_external_action_redirect_urlRedirect when external action is pending
processed_redirect_urlRedirect after intent is processed
time_expired_redirect_urlRedirect when intent expires
closed_redirect_urlRedirect when intent is closed

Example request

curl --request POST \
  --url https://web.moneyhash.io/api/v1.4/external/payments/intent/ \
  --header 'X-Api-Key: <your_api_key>' \
  --header 'Content-Type: application/json' \
  --data '{
    "amount": "500",
    "amount_currency": "AED",
    "operation": "purchase",
    "webhook_url": "https://yourbackend.com/webhooks/moneyhash"
  }'
const response = await fetch('https://web.moneyhash.io/api/v1.4/external/payments/intent/', {
  method: 'POST',
  headers: { 'X-Api-Key': '<your_api_key>', 'Content-Type': 'application/json' },
  body: JSON.stringify({
    amount: '500',
    amount_currency: 'AED',
    operation: 'purchase',
    webhook_url: 'https://yourbackend.com/webhooks/moneyhash'
  })
});
const data = await response.json();
import requests

response = requests.post(
    'https://web.moneyhash.io/api/v1.4/external/payments/intent/',
    headers={'X-Api-Key': '<your_api_key>', 'Content-Type': 'application/json'},
    json={
        'amount': '500',
        'amount_currency': 'AED',
        'operation': 'purchase',
        'webhook_url': 'https://yourbackend.com/webhooks/moneyhash'
    }
)
data = response.json()
$response = file_get_contents('https://web.moneyhash.io/api/v1.4/external/payments/intent/', false, stream_context_create([
    'http' => [
        'method' => 'POST',
        'header' => "X-Api-Key: <your_api_key>\r\nContent-Type: application/json",
        'content' => json_encode([
            'amount' => '500',
            'amount_currency' => 'AED',
            'operation' => 'purchase',
            'webhook_url' => 'https://yourbackend.com/webhooks/moneyhash'
        ])
    ]
]));
$data = json_decode($response, true);

Example response

{
  "status": { "code": 200, "message": "success", "errors": [] },
  "data": {
    "intent": {
      "id": "Ln5N0Ey",
      "status": "UNPROCESSED",
      "intent_secret": "1fccb9d7589b5eea2adf",
      "amount": { "value": "500.00", "currency": "AED" }
    },
    "state": "METHOD_SELECTION",
    "state_details": {
      "payment_methods": [
        {
          "payment_method_name": "Card",
          "payment_method": "CARD",
          "checkout_icons": ["..."],
          "confirmation_required": false,
          "required_billing_fields": []
        },
        {
          "payment_method_name": "Tamara",
          "payment_method": "TAMARA",
          "checkout_icons": ["..."],
          "confirmation_required": false,
          "required_billing_fields": []
        }
      ],
      "express_methods": [
        {
          "payment_method_name": "Google Pay",
          "payment_method": "GOOGLE_PAY",
          "use_for_express_checkout": true
        }
      ],
      "saved_cards": [],
      "customer_balances": []
    },
    "payment_status": { "status": "NO_AUTHORIZE_ATTEMPTS" }
  }
}

Retain data.intent.id as your intent_id and data.intent.intent_secret for constructing the embed URL.

The embed URL is constructed as:

https://embed.moneyhash.io/embed/payment/{intent_id}?mh_intent_secret={intent_secret}

Step 2 - Render method selection

Read state_details.payment_methods[] and state_details.express_methods[] from the intent response and render your own method selection UI. Each method includes payment_method_name, payment_method, and checkout_icons for display. API Reference

Once the customer selects a method, call update-method with their choice.

POST /api/v1.4/external/payments/intents/{intent_id}/update-method/
curl --request POST \
  --url https://web.moneyhash.io/api/v1.4/external/payments/intents/{intent_id}/update-method/ \
  --header 'X-Api-Key: <your_api_key>' \
  --header 'Content-Type: application/json' \
  --data '{"payment_method": "CARD"}'
const response = await fetch(`https://web.moneyhash.io/api/v1.4/external/payments/intents/${intentId}/update-method/`, {
  method: 'POST',
  headers: { 'X-Api-Key': '<your_api_key>', 'Content-Type': 'application/json' },
  body: JSON.stringify({ payment_method: 'CARD' })
});
response = requests.post(
    f'https://web.moneyhash.io/api/v1.4/external/payments/intents/{intent_id}/update-method/',
    headers={'X-Api-Key': '<your_api_key>', 'Content-Type': 'application/json'},
    json={'payment_method': 'CARD'}
)
$response = file_get_contents(
    "https://web.moneyhash.io/api/v1.4/external/payments/intents/{$intentId}/update-method/",
    false,
    stream_context_create(['http' => [
        'method' => 'POST',
        'header' => "X-Api-Key: <your_api_key>\r\nContent-Type: application/json",
        'content' => json_encode(['payment_method' => 'CARD'])
    ]])
);

The response returns the next state - proceed based on what it returns.


Card flow

When the customer selects CARD, the update-method response returns state: FORM_FIELDS with the required card fields and billing fields in state_details. Render the embed URL to your customer - MoneyHash renders the card form securely and handles card data collection and submission.

https://embed.moneyhash.io/embed/payment/{intent_id}?mh_intent_secret={intent_secret}

MoneyHash handles 3DS inside the embed if required. Once the payment is complete, webhooks are delivered to your webhook_url.

Note

Billing fields vary per provider and payment method. You can consult the MoneyHash team on the requirements for each provider, or retrieve them dynamically by reading state_details.billing.fields from the update-method response. Passing billing_data at intent creation skips the FORM_FIELDS state entirely.


Redirect and wallet flow

For payment methods that require a redirect - such as Tamara, NAPS, bank redirects, or wallets - the flow proceeds to state: URL_TO_RENDER once billing data is available. You can either pass billing_data at intent creation to skip FORM_FIELDS entirely, or let the embed collect it from the customer before proceeding to the redirect.

When state is URL_TO_RENDER, redirect the customer to state_details.embed_url:

{
  "state": "URL_TO_RENDER",
  "state_details": {
    "embed_url": "https://embed.moneyhash.io/embed/payment/<intent_id>"
  }
}

The customer completes the payment on the provider's page and is redirected back to your successful_redirect_url on completion. Webhooks are delivered to your webhook_url to confirm the outcome.

Note

Pass successful_redirect_url, failed_redirect_url, and processed_redirect_url at intent creation to control where the customer lands after completing the provider's payment page. See Redirects for details.


Native pay flow (Apple Pay / Google Pay)

For Apple Pay and Google Pay, your frontend generates a native pay receipt using the device's native payment sheet, then submits it to MoneyHash via the receipt endpoint. API Reference

POST /api/v1.4/external/payments/intents/{intent_id}/receipt/
curl --request POST \
  --url https://web.moneyhash.io/api/v1.4/external/payments/intents/{intent_id}/receipt/ \
  --header 'X-Api-Key: <your_api_key>' \
  --header 'Content-Type: application/json' \
  --data '{
    "receipt": "<native_pay_receipt>"
  }'
const response = await fetch(`https://web.moneyhash.io/api/v1.4/external/payments/intents/${intentId}/receipt/`, {
  method: 'POST',
  headers: { 'X-Api-Key': '<your_api_key>', 'Content-Type': 'application/json' },
  body: JSON.stringify({ receipt: '<native_pay_receipt>' })
});
response = requests.post(
    f'https://web.moneyhash.io/api/v1.4/external/payments/intents/{intent_id}/receipt/',
    headers={'X-Api-Key': '<your_api_key>', 'Content-Type': 'application/json'},
    json={'receipt': '<native_pay_receipt>'}
)
$response = file_get_contents(
    "https://web.moneyhash.io/api/v1.4/external/payments/intents/{$intentId}/receipt/",
    false,
    stream_context_create(['http' => [
        'method' => 'POST',
        'header' => "X-Api-Key: <your_api_key>\r\nContent-Type: application/json",
        'content' => json_encode(['receipt' => '<native_pay_receipt>'])
    ]])
);

After receipt submission, MoneyHash processes the payment and delivers webhooks confirming the outcome.


Paying with a saved card token

If the customer has a previously saved card token - Create an intent first, then call the card token endpoint directly instead of update-method. API Reference

POST /api/v1.4/external/payments/intents/{intent_id}/card_token/
curl --request POST \
  --url https://web.moneyhash.io/api/v1.4/external/payments/intents/{intent_id}/card_token/ \
  --header 'X-Api-Key: <your_api_key>' \
  --header 'Content-Type: application/json' \
  --data '{"card_token_id": "<hashid>", "cvv": "123"}'
const response = await fetch(`https://web.moneyhash.io/api/v1.4/external/payments/intents/${intentId}/card_token/`, {
  method: 'POST',
  headers: { 'X-Api-Key': '<your_api_key>', 'Content-Type': 'application/json' },
  body: JSON.stringify({ card_token_id: '<hashid>', cvv: '123' })
});
response = requests.post(
    f'https://web.moneyhash.io/api/v1.4/external/payments/intents/{intent_id}/card_token/',
    headers={'X-Api-Key': '<your_api_key>', 'Content-Type': 'application/json'},
    json={'card_token_id': '<hashid>', 'cvv': '123'}
)
$response = file_get_contents(
    "https://web.moneyhash.io/api/v1.4/external/payments/intents/{$intentId}/card_token/",
    false,
    stream_context_create(['http' => [
        'method' => 'POST',
        'header' => "X-Api-Key: <your_api_key>\r\nContent-Type: application/json",
        'content' => json_encode(['card_token_id' => '<hashid>', 'cvv' => '123'])
    ]])
);
Note

card_token_id must be the hashid - available as data.card_token.hashid in the card_token.created webhook, or as saved_cards[].id in the intent response. CVV is required if requires_cvv: true was returned in the card_token.created webhook.


Step 3 - Handle webhooks and fulfill the order

MoneyHash sends webhooks to your webhook_url as the payment progresses. For a basic payment flow, there are two webhooks to act on.

Webhook 1 - Transaction successful

type: transaction.purchase.successful
FieldExpected valuePurpose
typetransaction.purchase.successfulConfirms the event type
intent.payment_status.statusCAPTUREDConfirms money was collected
intent.idYour order's intent IDLinks the webhook to your order

Webhook 2 - Intent processed

type: intent.processed
FieldExpected valuePurpose
typeintent.processedConfirms the event type
data.intent.statusPROCESSEDConfirms intent is terminal
data.intent.payment_status.statusCAPTUREDConfirms final monetary state

Key rules for webhook handling

On order fulfillment: Always wait for transaction.purchase.successful with payment_status.status: CAPTURED before releasing an order. Do not act on intermediate webhooks like transaction.purchase.pending_authentication.

On failures: A transaction.purchase.failed webhook does not mean the intent is done. Check intent.status - if it is still UNPROCESSED, MoneyHash may automatically retry on another provider. Only mark an order as permanently failed when the intent reaches CLOSED or EXPIRED without a successful transaction.

On idempotency: Your webhook handler should be idempotent. MoneyHash retries delivery until it receives a successful acknowledgment - you may receive the same event more than once. Use status_id or operation_id to deduplicate.

On source of truth: Always use payment_status.status combined with operations[].latest_status.value as your source of truth - not active_transaction.status, which can be misleading after 3DS flows.

For the full webhook reference - event types, signatures, and retry behavior - see Webhooks.


API reference

EndpointMethodPurpose
/api/v1.4/external/payments/intent/POSTCreate payment intent
/api/v1.4/external/payments/intents/{intent_id}/GETGet intent state
/api/v1.4/external/payments/intents/{intent_id}/update-method/POSTSet payment method
/api/v1.4/external/payments/intents/{intent_id}/card_token/POSTPay with saved token
/api/v1.4/external/payments/intents/{intent_id}/receipt/POSTSubmit native pay receipt

Next steps

I want to...Go here
Understand Intent, Transaction, and Operation statusesPayment Components
Set up webhook signature verificationWebhook Signature
Configure redirect URLsRedirects

Did this page help you?