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

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

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 the platform deposit, with a commission cut) 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

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

Conditional

Networks the customer may pay on (e.g. ["eth-usdc", "tron-usdt"]). Required for standard accounts — must be non-empty and every entry must be a supported blockchain ID (see Prerequisites). Must be omitted on US merchant accounts (rejected with E1001) — the customer picks 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. Max 2048 characters; the domain must be on your allowed redirect domains list; https required in production.

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.

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 blockchainIds on 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, 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 — 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: true set.

  • The same key with a different body is rejected with 409 E4002.

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

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

  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: Send an Idempotency-Key on every create request so network-level retries can never double-charge.

  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.

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.