External Payments API
The External Payments API allows you to create and track payment prompts programmatically from your backend. When a payment is created, you receive a checkout URL to redirect your customer to.
Base path:
/external/v1Authentication: every request is HMAC-signed — see Authentication
Errors: structured envelope with typed codes — see the Error Reference
Rate limit: 60 requests per 60 seconds (Prerequisites)
How a payment is processed depends on your account configuration, not on the request: standard accounts use the on-chain crypto flow described throughout this guide, while US merchant accounts use a regulated payment processing flow with a few field-level differences — see US Merchant Accounts below. The endpoints, authentication, webhooks, and status lifecycle are identical for both.
The Payment Object
The merchant payment endpoints return payments in one canonical shape. Payment webhook snapshots use the same fields plus externalId. The Merchant-Partner API reporting endpoints use a separate response contract.
{
"object": "payment_prompt",
"id": "550e8400-e29b-41d4-a716-446655440000",
"amount": 1099,
"commissionAmount": 22,
"commissionPayer": "merchant",
"settlementModel": "split",
"status": "pending",
"sessionStatus": "pending",
"blockchainId": null,
"tokenAmount": null,
"transactionHash": null,
"paidAt": null,
"expiresAt": "2026-07-21T15:00:00.000Z",
"createdAt": "2026-07-21T14:30:00.000Z",
"redirectUrl": "https://shop.example.com/order/12345/complete",
"customerReferenceId": "order_12345",
"cancellationReason": null,
"lastError": null
}
Field |
Type |
Description |
|---|---|---|
|
string |
Always |
|
string |
Unique payment prompt ID (UUID). |
|
integer |
Charged amount in USD minor units (cents): |
|
integer or null |
Commission in cents. |
|
string |
Who bears the commission: |
|
string or null |
How funds reach you on the crypto flow: |
|
string |
|
|
string or null |
Checkout session state: |
|
string or null |
The network the customer selected; |
|
string or null |
Crypto amount as a decimal string (never a float), e.g. |
|
string or null |
On-chain transaction hash once the payment is observed. On US merchant accounts, the transaction reference reported once the payment settles. |
|
string or null |
ISO 8601 timestamp when the payment was confirmed. |
|
string |
ISO 8601 timestamp when the prompt expires. Expiry is configured per network (typically minutes, not hours) — always read this field rather than assuming a duration. |
|
string |
ISO 8601 creation timestamp. |
|
string or null |
The redirect URL you provided, or |
|
string or null |
Your own correlation ID, echoed back exactly as you sent it. |
|
string or null |
Why the payment was cancelled, when it was. |
|
object or null |
|
Create a Payment Prompt
Creates a new payment prompt and returns a checkout URL.
Endpoint: POST /external/v1/payments/prompt
Required scope: payment_prompts:write
Accepts merchant-admin or merchant-partner keys. Partner keys must supply merchantId for an active merchant assigned to that partner; merchant keys must omit it. Partner-linked merchants using their own keys require the partner's self-service API payment setting to be enabled, otherwise E2103 is returned. See Merchant-Partner API for partner reporting routes and credentials.
Headers:
Header |
Required |
Description |
|---|---|---|
|
Yes |
Must be |
|
Yes |
See Authentication. |
|
No |
Makes the request safe to retry — see Idempotency below. |
Request Body:
Field |
Type |
Required |
Description |
|---|---|---|---|
|
integer |
Yes |
Amount in USD minor units (cents): |
|
array of strings |
No |
Restricts the payment to specific networks (e.g. |
|
string |
No |
Optional description or reference. Max 500 characters. |
|
string |
No |
URL to send the customer back to after payment. |
|
string |
No |
Your own correlation ID (e.g. your order ID). Echoed on the payment object and webhook events, and filterable on the list endpoint. Max 255 characters. |
|
UUID |
Partner keys only |
Required for merchant-partner keys, forbidden for merchant keys. The merchant must belong to the authenticated partner. |
Warning
amount is an integer amount of cents, not a dollar decimal. 1099 means $10.99 — sending 10.99 is rejected with E1001.
Note
Unknown request fields are rejected (E1002), not silently ignored.
Tip
Leaving blockchainIds out is the simplest integration and the one we recommend: the same request body then works on every account type, and networks you add or remove in the dashboard take effect without a code change.
Warning
On standard accounts, an explicit blockchainIds list requires a configured external deposit address on the default Main branch for every requested ID; missing addresses return E1020. GET /external/v1/blockchains/active lists platform availability, not the merchant's configured addresses. When omitted, the list is resolved from the intersection of active networks and configured addresses; an empty intersection returns E1020, even if addresses exist for inactive networks. A supplied list may contain at most 24 entries (the current supported-ID count).
Example Request:
// miraclePayFetch is the signed-fetch helper from the Authentication page
const response = await miraclePayFetch(
"POST",
"/external/v1/payments/prompt",
{
amount: 1099, // $10.99 in cents
note: "Order #12345",
redirectUrl: "https://shop.example.com/order/12345/complete",
customerReferenceId: "order_12345",
},
{ "Idempotency-Key": crypto.randomUUID() },
);
const { prompt, checkoutUrl } = await response.json();
To restrict the payment to particular networks, add blockchainIds:
{
amount: 1099,
blockchainIds: ["eth-usdc", "tron-usdt"],
}
Example Response (201 Created):
{
"prompt": {
"object": "payment_prompt",
"id": "550e8400-e29b-41d4-a716-446655440000",
"amount": 1099,
"commissionAmount": 22,
"commissionPayer": "merchant",
"settlementModel": "split",
"status": "pending",
"sessionStatus": "pending",
"blockchainId": null,
"tokenAmount": null,
"transactionHash": null,
"paidAt": null,
"expiresAt": "2026-07-21T15:00:00.000Z",
"createdAt": "2026-07-21T14:30:00.000Z",
"redirectUrl": "https://shop.example.com/order/12345/complete",
"customerReferenceId": "order_12345",
"cancellationReason": null,
"lastError": null
},
"checkoutUrl": "https://checkout.miraclecash.info/?sessionId=550e8400-e29b-41d4-a716-446655440000"
}
Redirect your customer to checkoutUrl — always use the returned value, never construct the URL yourself. See Redirect to Checkout.
US Merchant Accounts
US merchant accounts accept payments through a regulated payment processing flow instead of the direct on-chain flow. The integration is the same — same endpoints, signing, idempotency, webhooks, and status lifecycle — with these differences:
A valid
blockchainIdsis ignored. The customer picks the network on the hosted checkout page. Validation still runs first: an empty array, unsupported ID, or oversized array is rejected, even on US merchant accounts.Some payment-object fields stay
null:settlementModel,blockchainId, andtokenAmountare alwaysnull.transactionHashfills in with the reported transaction reference once the payment settles.The
checkoutUrlhas a different format. Treat it as opaque and always redirect to the returned value.The customer is not redirected back to your
redirectUrlafter payment — confirm the outcome via Webhooks orGET /external/v1/payments/:id.Merchant Onboarding must be approved first. Until it is, every signed API request is rejected with E2102.
Example request (US merchant account):
const response = await miraclePayFetch(
"POST",
"/external/v1/payments/prompt",
{
amount: 1099, // $10.99 in cents
note: "Order #12345",
customerReferenceId: "order_12345",
},
{ "Idempotency-Key": crypto.randomUUID() },
);
const { prompt, checkoutUrl } = await response.json();
Idempotency
Network failures can leave you unsure whether a POST reached the API. To retry safely, send an Idempotency-Key header (a nonblank unique string up to 255 characters, e.g. a UUID), persisted with the order before the first attempt:
Keys are scoped to the authenticated account, HTTP method and route (not to a particular API key).
Completed successful responses and execution-time 5xx errors are cached for 24 hours. 4xx errors release the claim so the input can be corrected.
Retrying with the same key and exact serialized body replays the response with
Idempotent-Replayed: true. Even changes to JSON whitespace or key order count as a different body and return409E4002.If the original request is still in flight, the retry gets
409E4003 withRetry-After: 5— wait and retry.Each retry must use a fresh nonce, timestamp and signature. Reuse the idempotency key, not the signed request headers.
Replayed 5xx errors retain their error HTTP status and return
X-MP-Should-Retry: false. Another same-key retry will not re-execute the payment. Reconcile the order/payment history before deciding to create with a new key; changing keys automatically after an uncertain failure risks duplicates.
const idempotencyKey = crypto.randomUUID(); // persist alongside your order
await miraclePayFetch("POST", "/external/v1/payments/prompt", body, {
"Idempotency-Key": idempotencyKey,
});
Retrieve a Payment
Endpoint: GET /external/v1/payments/:id
Required scope: payment_prompts:read
Returns the full payment object for the given prompt UUID. Requires merchant credentials; partners use Merchant-Partner API. Payments are merchant-scoped: an ID belonging to another merchant returns the same 404 E3001 as a nonexistent one.
const response = await miraclePayFetch(
"GET",
`/external/v1/payments/${promptId}`,
);
const payment = await response.json();
List Payments
Endpoint: GET /external/v1/payments
Required scope: payment_prompts:read
Returns your payments, newest first, with cursor-based pagination. Requires merchant credentials. Unknown query parameters are rejected with E1002.
Query Parameters:
Parameter |
Type |
Description |
|---|---|---|
|
integer |
Page size, 1–100. Default 10. |
|
UUID |
Cursor: return payments created before this payment (the next page when reading newest-first). |
|
UUID |
Cursor: return payments created after this payment (the previous page). Mutually exclusive with |
|
string |
Filter by payment status ( |
|
string |
Filter by the correlation ID you set at creation. |
|
ISO 8601 date |
Only payments created after this time. |
|
ISO 8601 date |
Only payments created before this time. |
Response:
{
"object": "list",
"data": [
{ "object": "payment_prompt", "id": "..." }
],
"hasMore": true
}
Pagination:
While hasMore is true, pass the last item's id as startingAfter to fetch the next, older page. To fetch the adjacent newer page, use the first item's ID as endingBefore. Results remain newest-first in either direction; hasMore describes the requested direction. Ordering uses creation time and ID, and cursors are exclusive. An unknown or foreign cursor ID returns 404 E3001. Date filters use strict boundaries; customerReferenceId matches exactly and is at most 255 characters.
async function allSuccessfulPayments() {
const payments = [];
let cursor: string | undefined;
while (true) {
const query = new URLSearchParams({ limit: "100", status: "successful" });
if (cursor) query.set("startingAfter", cursor);
// The query string is part of the signed path
const response = await miraclePayFetch(
"GET",
`/external/v1/payments?${query}`,
);
const page = await response.json();
payments.push(...page.data);
if (!page.hasMore) return payments;
cursor = page.data[page.data.length - 1].id;
}
}
Note
The query string is part of the signed path — sign /external/v1/payments?limit=100&status=successful exactly as sent.
Refunds
The External Payments API has no refund endpoint. Once a payment reaches successful it is final: it cannot be reversed, cancelled, or refunded through the API, and there is no refund-related webhook event.
If you need to return funds to a customer, handle the refund outside the API through your own process. Keep the payment's id, customerReferenceId, and transactionHash on your side so you can reference the original payment when you do.
Get Active Blockchains
Endpoint: GET /external/v1/blockchains/active — documented in Prerequisites.
Errors
Failed requests return a structured envelope:
{
"error": {
"type": "invalid_request_error",
"code": "blockchain_not_configured",
"errorCode": "E1020",
"message": "Requested blockchains are not configured: btc",
"docUrl": "https://docs.miraclepay.com/errors#E1020",
"userSafeMessage": false,
"requestId": "8f14e45f-ceea-4f3a-9a5a-1c0d2e3f4a5b"
}
}
The X-MP-Should-Retry response header tells you whether the same request may succeed on retry. Every code is documented in the Error Reference.
End-to-End Example
Using the miraclePayFetch helper from Authentication:
import { miraclePayFetch } from "./miraclepay";
async function handleCheckout(orderId: string, amountCents: number, idempotencyKey: string) {
// Load the persisted key for this intended payment; do not regenerate on retry.
const response = await miraclePayFetch(
"POST",
"/external/v1/payments/prompt",
{
amount: amountCents,
blockchainIds: ["eth-usdc", "tron-usdt"],
note: `Order #${orderId}`,
redirectUrl: `https://shop.example.com/orders/${orderId}/complete`,
customerReferenceId: orderId,
},
{ "Idempotency-Key": idempotencyKey },
);
if (!response.ok) {
const { error } = await response.json();
throw new Error(`MiraclePay ${error.errorCode}: ${error.message}`);
}
const { prompt, checkoutUrl } = await response.json();
// Store prompt.id in your database linked to the order
await savePaymentToOrder(orderId, prompt.id);
// Redirect customer to checkout
return { redirectUrl: checkoutUrl };
}
Best Practices
Store payment IDs: Always save
prompt.idin your database linked to the corresponding order for reconciliation.customerReferenceIdlets you find payments by your own order ID later.Use idempotency keys: Persist and reuse one
Idempotency-Keyand the exact body per intended payment during the 24-hour retention window. Reconcile uncertain outcomes before starting a new payment.Amounts are cents: Convert dollar decimals once, at the boundary (
Math.round(dollars * 100)), and work in integers everywhere else.Handle errors by
code: Branch on the machine-readableerror.codeslug, honorX-MP-Should-Retry, and logrequestIdfor support.Test first: Always test your integration using testnet blockchain IDs before going live.
Do not trust the redirect: When using
redirectUrl, the customer is redirected with?promptId=...&status=...query parameters. Never use these parameters as proof of payment. Always verify server-side viaGET /external/v1/payments/:idbefore fulfilling orders.Configure allowed redirect domains: Before using
redirectUrl, add the domain to your allowed redirect domains list in the dashboard.Plan for refunds outside the API: There is no refund endpoint — see Refunds. Decide up front how your operations team will handle a customer refund request.
Allowed Redirect Domains
For security reasons, redirect URLs are validated against an allowlist of domains configured in your merchant account.
Dashboard Configuration:
Configure your allowed redirect domains in the merchant panel under Developer Settings. Each domain must be a valid fully qualified domain name (FQDN).
Example allowed domains:
shop.example.comstore.mysite.comcheckout.yourdomain.com
Limits:
Maximum 20 domains per merchant account
Only FQDNs are accepted (no paths, protocols, or wildcards)
The hostname of your
redirectUrlmust exactly match one of the configured domains
Related errors: no domains configured → E1010; invalid URL → E1011; domain not on the list → E1012.