External API
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:
- Access your MoneyHash Organization and Account from the dashboard
- Connect your payment providers to the account
- Configure your Payment Defaults and Flow
- 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.
| State | What it means | What to do |
|---|---|---|
METHOD_SELECTION | Intent created, no method selected yet | Read state_details.payment_methods[], render your own method selection UI, call update-method with the customer's choice |
FORM_FIELDS | Billing fields required before proceeding | Collect billing data and pass in intent creation, or render the embed URL to collect them |
URL_TO_RENDER | Redirect or 3DS required | Redirect customer to state_details.embed_url |
INTENT_PROCESSED | Payment complete - terminal | Confirm via webhook and fulfill order |
TRANSACTION_FAILED | Transaction failed | Check intent status - retry may be possible |
EXPIRED | Intent expired - terminal | Render expired state |
CLOSED | Intent closed - terminal | Render closed state |
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
Required parameters
| Parameter | Type | Description |
|---|---|---|
amount | string | The amount to charge. Max 14 digits, max 2 decimal places. |
amount_currency | string | ISO 4217 currency code e.g. AED, USD, SAR. |
webhook_url | string | Your backend endpoint that receives payment event notifications. |
operation | string | purchase for immediate charge · authorize for auth-only. Required if flow_id is not provided. |
Recommended parameters
| Parameter | Purpose |
|---|---|
billing_data | Pass upfront to skip FORM_FIELDS state for methods that require billing data |
customer | Associate with an existing MoneyHash customer ID |
allow_tokenize_card | Set to true to allow saving the card for future use |
payment_type | Set to recurring for non-present cardholder payments |
merchant_initiated | Set to true for merchant-initiated transactions |
successful_redirect_url | Redirect after successful payment |
failed_redirect_url | Redirect after failed payment |
pending_external_action_redirect_url | Redirect when external action is pending |
processed_redirect_url | Redirect after intent is processed |
time_expired_redirect_url | Redirect when intent expires |
closed_redirect_url | Redirect 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.
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.
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.
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
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
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'])
]])
);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
| Field | Expected value | Purpose |
|---|---|---|
type | transaction.purchase.successful | Confirms the event type |
intent.payment_status.status | CAPTURED | Confirms money was collected |
intent.id | Your order's intent ID | Links the webhook to your order |
Webhook 2 - Intent processed
type: intent.processed
| Field | Expected value | Purpose |
|---|---|---|
type | intent.processed | Confirms the event type |
data.intent.status | PROCESSED | Confirms intent is terminal |
data.intent.payment_status.status | CAPTURED | Confirms 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
| Endpoint | Method | Purpose |
|---|---|---|
/api/v1.4/external/payments/intent/ | POST | Create payment intent |
/api/v1.4/external/payments/intents/{intent_id}/ | GET | Get intent state |
/api/v1.4/external/payments/intents/{intent_id}/update-method/ | POST | Set payment method |
/api/v1.4/external/payments/intents/{intent_id}/card_token/ | POST | Pay with saved token |
/api/v1.4/external/payments/intents/{intent_id}/receipt/ | POST | Submit native pay receipt |
Next steps
| I want to... | Go here |
|---|---|
| Understand Intent, Transaction, and Operation statuses | Payment Components |
| Set up webhook signature verification | Webhook Signature |
| Configure redirect URLs | Redirects |
Updated about 1 month ago