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/v1

  • Authentication: 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

object

string

Always payment_prompt.

id

string

Unique payment prompt ID (UUID).

amount

integer

Charged amount in USD minor units (cents): 1099 = $10.99.

commissionAmount

integer or null

Commission in cents. null until determined; 0 for direct settlement.

commissionPayer

string

Who bears the commission: merchant or shopper. Comes from your merchant configuration.

settlementModel

string or null

How funds reach you on the crypto flow: split (via an on-chain settlement/sweep; commission can be zero) or direct (straight to your own wallet, no commission — e.g. BTC). Always null on US merchant accounts' payments.

status

string

pending, successful, unsuccessful, expired, or cancelled. See Checking Payment Status.

sessionStatus

string or null

Checkout session state: pending, cancelled, expired, or completed.

blockchainId

string or null

The network the customer selected; null until they choose one. Always null on US merchant accounts.

tokenAmount

string or null

Crypto amount as a decimal string (never a float), e.g. "0.00312". Always null on US merchant accounts.

transactionHash

string or null

On-chain transaction hash once the payment is observed. On US merchant accounts, the transaction reference reported once the payment settles.

paidAt

string or null

ISO 8601 timestamp when the payment was confirmed.

expiresAt

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.

createdAt

string

ISO 8601 creation timestamp.

redirectUrl

string or null

The redirect URL you provided, or null.

customerReferenceId

string or null

Your own correlation ID, echoed back exactly as you sent it.

cancellationReason

string or null

Why the payment was cancelled, when it was.

lastError

object or null

{ "code", "message", "occurredAt" } — diagnostic detail for the most recent failure, useful for unsuccessful payments.

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

Content-Type

Yes

Must be application/json.

X-MP-* signing headers

Yes

See Authentication.

Idempotency-Key

No

Makes the request safe to retry — see Idempotency below.

Request Body:

Field

Type

Required

Description

amount

integer

Yes

Amount in USD minor units (cents): 1099 = $10.99. Minimum 50 ($0.50), maximum 100000000 ($1,000,000).

blockchainIds

array of strings

No

Restricts the payment to specific networks (e.g. ["eth-usdc", "tron-usdt"]). Omit it and the customer is offered every network your account can be paid on. When present it must be non-empty and every entry must be a supported blockchain ID (see Prerequisites). US merchant accounts ignore the field — their customers pick the network on the hosted checkout page.

note

string

No

Optional description or reference. Max 500 characters.

redirectUrl

string

No

URL to send the customer back to after payment. promptId and status query parameters are appended automatically on the standard checkout flow. Max 2048 characters; the hostname must exactly match your allowed redirect domains list. HTTPS is required; non-production allows HTTP only for allowlisted loopback hosts.

customerReferenceId

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.

merchantId

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 blockchainIds is 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, and tokenAmount are always null. transactionHash fills in with the reported transaction reference once the payment settles.

  • The checkoutUrl has a different format. Treat it as opaque and always redirect to the returned value.

  • The customer is not redirected back to your redirectUrl after payment — confirm the outcome via Webhooks or GET /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 return 409 E4002.

  • If the original request is still in flight, the retry gets 409 E4003 with Retry-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

limit

integer

Page size, 1–100. Default 10.

startingAfter

UUID

Cursor: return payments created before this payment (the next page when reading newest-first).

endingBefore

UUID

Cursor: return payments created after this payment (the previous page). Mutually exclusive with startingAfter — sending both is rejected with E1001.

status

string

Filter by payment status (pending, successful, unsuccessful, expired, cancelled).

customerReferenceId

string

Filter by the correlation ID you set at creation.

createdAfter

ISO 8601 date

Only payments created after this time.

createdBefore

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

  1. Store payment IDs: Always save prompt.id in your database linked to the corresponding order for reconciliation. customerReferenceId lets you find payments by your own order ID later.

  2. Use idempotency keys: Persist and reuse one Idempotency-Key and the exact body per intended payment during the 24-hour retention window. Reconcile uncertain outcomes before starting a new payment.

  3. Amounts are cents: Convert dollar decimals once, at the boundary (Math.round(dollars * 100)), and work in integers everywhere else.

  4. Handle errors by code: Branch on the machine-readable error.code slug, honor X-MP-Should-Retry, and log requestId for support.

  5. Test first: Always test your integration using testnet blockchain IDs before going live.

  6. 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 via GET /external/v1/payments/:id before fulfilling orders.

  7. Configure allowed redirect domains: Before using redirectUrl, add the domain to your allowed redirect domains list in the dashboard.

  8. 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.com

  • store.mysite.com

  • checkout.yourdomain.com

Limits:

  • Maximum 20 domains per merchant account

  • Only FQDNs are accepted (no paths, protocols, or wildcards)

  • The hostname of your redirectUrl must exactly match one of the configured domains

Related errors: no domains configured → E1010; invalid URL → E1011; domain not on the list → E1012.