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
Every endpoint (and every webhook event) returns payments in one canonical shape:
{
"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
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 |
Conditional |
Networks the customer may pay on (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. |
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.
Warning
On standard accounts, every blockchain ID in blockchainIds must be active and configured for your merchant account, otherwise the request fails with E1020. Use GET /external/v1/blockchains/active to check what you can accept.
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
blockchainIds: ["eth-usdc", "tron-usdt"],
note: "Order #12345",
redirectUrl: "https://shop.example.com/order/12345/complete",
customerReferenceId: "order_12345",
},
{ "Idempotency-Key": crypto.randomUUID() },
);
const { prompt, checkoutUrl } = await response.json();
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:
Omit
blockchainIdson create. The customer picks the network on the hosted checkout page; sending the field is rejected with E1001.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 — no blockchainIds
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 (any unique string up to 255 characters, e.g. a UUID):
The first request with a given key is processed normally, and its response is cached for 24 hours.
Retrying with the same key and the same body returns the original response, with the header
Idempotent-Replayed: trueset.The same key with a different body is rejected with
409E4002.If the original request is still in flight, the retry gets
409E4003 with aRetry-Afterheader — wait and retry.
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 ID. 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.
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 page. An unknown cursor ID returns 404 E3001.
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.
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.miraclecash.info/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 { randomUUID } from "node:crypto";
import { miraclePayFetch } from "./miraclepay";
async function handleCheckout(orderId: string, amountCents: number) {
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": randomUUID() },
);
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: Send an
Idempotency-Keyon every create request so network-level retries can never double-charge.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.
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.