.. _external_payments_api: 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 :ref:`authentication` - **Errors:** structured envelope with typed codes — see the :doc:`Error Reference ` - **Rate limit:** 60 requests per 60 seconds (:ref:`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: .. code-block:: json { "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 } .. list-table:: :widths: 24 18 58 :header-rows: 1 * - 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 :ref:`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:** .. list-table:: :widths: 24 15 61 :header-rows: 1 * - Header - Required - Description * - ``Content-Type`` - Yes - Must be ``application/json``. * - ``X-MP-*`` signing headers - Yes - See :ref:`authentication`. * - ``Idempotency-Key`` - No - Makes the request safe to retry — see `Idempotency`_ below. **Request Body:** .. list-table:: :widths: 22 16 10 52 :header-rows: 1 * - 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 :ref:`prerequisites`). **Must be omitted on US merchant accounts** (rejected with :ref:`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 :ref:`E1001 `. .. note:: Unknown request fields are **rejected** (:ref:`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 :ref:`E1020 `. Use ``GET /external/v1/blockchains/active`` to check what you can accept. **Example Request:** .. code-block:: typescript // 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``): .. code-block:: json { "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 :ref:`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 :ref:`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 :ref:`webhooks` or ``GET /external/v1/payments/:id``. - **Merchant Onboarding must be approved first.** Until it is, every signed API request is rejected with :ref:`E2102 `. **Example request** (US merchant account): .. code-block:: typescript 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`` :ref:`E4002 `. - If the original request is still in flight, the retry gets ``409`` :ref:`E4003 ` with a ``Retry-After`` header — wait and retry. .. code-block:: typescript 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 <#the-payment-object>`_ for the given prompt ID. Payments are **merchant-scoped**: an ID belonging to another merchant returns the same ``404`` :ref:`E3001 ` as a nonexistent one. .. code-block:: typescript 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:** .. list-table:: :widths: 26 16 58 :header-rows: 1 * - 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 :ref:`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:** .. code-block:: json { "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`` :ref:`E3001 `. .. code-block:: typescript 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 :ref:`prerequisites`. Errors ------ Failed requests return a structured envelope: .. code-block:: json { "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 :doc:`Error Reference `. End-to-End Example ------------------ Using the ``miraclePayFetch`` helper from :ref:`authentication`: .. code-block:: typescript 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 → :ref:`E1010 `; invalid URL → :ref:`E1011 `; domain not on the list → :ref:`E1012 `.