.. _authentication: Authentication ============== Every request to the External Payments API must be signed with **HMAC-SHA512** using your API key's secret. There is no ``Authorization`` header — authentication is carried entirely by five ``X-MP-*`` headers. See :ref:`prerequisites` for how to create an API key and obtain your **key ID**, **secret**, and **passphrase**. Request Headers --------------- .. list-table:: :widths: 24 76 :header-rows: 1 * - Header - Value * - ``X-MP-KEY-ID`` - Your API key ID (starts with ``mpay_pk_``). * - ``X-MP-PASSPHRASE`` - The passphrase issued alongside the key. * - ``X-MP-TIMESTAMP`` - Current Unix time in **seconds**, as a string. Must be within ±300 seconds of server time. * - ``X-MP-NONCE`` - A unique value per request (e.g. a random UUID). Each nonce is accepted **once** — reusing one is rejected as a replay (:ref:`E2009 `). * - ``X-MP-SIGNATURE`` - Lowercase hex HMAC-SHA512 signature computed as described below. The Canonical String -------------------- The signature is computed over five fields joined by newline characters (``\n``): .. code-block:: text \n\n\n\n .. code-block:: text signature = hex( HMAC-SHA512( secret, canonicalString ) ) The rules for each field — every one of these must match **byte-for-byte** what you actually send: .. list-table:: :widths: 18 82 :header-rows: 1 * - Field - Rule * - ``timestamp`` - Exactly the value sent in ``X-MP-TIMESTAMP``. * - ``nonce`` - Exactly the value sent in ``X-MP-NONCE``. * - ``METHOD`` - The HTTP method, uppercase (``GET``, ``POST``). * - ``path`` - The full request path **including the** ``/external/v1`` **prefix and any query string** — e.g. ``/external/v1/payments?limit=20&status=successful``. No scheme or host. * - ``rawBody`` - The exact body bytes you transmit. For requests without a body (``GET``), use the **empty string** — the canonical string then simply ends with the newline after ``path``. .. warning:: The two most common causes of ``signature_invalid`` (:ref:`E2007 `): - **Re-serializing the body after signing.** Sign the exact string you send — serialize once, sign that string, send that string. A different key order or whitespace produces a different signature. - **A proxy that rewrites the path.** The server verifies against the path it receives; if a reverse proxy strips or adds a path prefix between you and the API, every signature fails. Point your base URL directly at the API host with no path suffix. Worked Example -------------- Use these fixed inputs to unit-test your signer. For a ``GET`` request to the active blockchains endpoint: .. code-block:: text timestamp: 1750000000 nonce: 3f2c1a9e-8d4b-4c6a-9e2f-1b7d5a3c8e4f method: GET path: /external/v1/blockchains/active rawBody: (empty string) The canonical string is (shown with visible ``\n``): .. code-block:: text 1750000000\n3f2c1a9e-8d4b-4c6a-9e2f-1b7d5a3c8e4f\nGET\n/external/v1/blockchains/active\n Signing this with the secret ``mpay_sk_test``: .. code-block:: bash printf '%s\n%s\n%s\n%s\n%s' \ "1750000000" \ "3f2c1a9e-8d4b-4c6a-9e2f-1b7d5a3c8e4f" \ "GET" \ "/external/v1/blockchains/active" \ "" \ | openssl dgst -sha512 -hmac "mpay_sk_test" must produce this signature — if your signer outputs anything else, one of the five fields differs: .. code-block:: text dded28718cdc654e0680c76809c174eb5b40741282ebd0b58f01c00130031b63464ce4dba8120258cf33d4b7265406568184b229ccb0a524e190cf77967d1659 Code Samples ------------ Node.js / TypeScript ~~~~~~~~~~~~~~~~~~~~ A complete, reusable signed-fetch helper. Keep it **server-side only** — never ship your secret to a browser. .. code-block:: typescript import { createHmac, randomUUID } from "node:crypto"; const BASE_URL = "https://api.miraclecash.info"; const KEY_ID = process.env.MPAY_KEY_ID!; const SECRET = process.env.MPAY_SECRET!; const PASSPHRASE = process.env.MPAY_PASSPHRASE!; export async function miraclePayFetch( method: "GET" | "POST", path: string, // must include /external/v1 and any query string body?: unknown, extraHeaders?: Record, ): Promise { const timestamp = Math.floor(Date.now() / 1000).toString(); const nonce = randomUUID(); // Send the exact string that was signed — re-serializing after signing // (different key order / whitespace) would invalidate the signature. const rawBody = body === undefined ? "" : JSON.stringify(body); const signature = createHmac("sha512", SECRET) .update([timestamp, nonce, method, path, rawBody].join("\n")) .digest("hex"); return fetch(`${BASE_URL}${path}`, { method, headers: { ...(rawBody ? { "Content-Type": "application/json" } : {}), "X-MP-KEY-ID": KEY_ID, "X-MP-PASSPHRASE": PASSPHRASE, "X-MP-TIMESTAMP": timestamp, "X-MP-NONCE": nonce, "X-MP-SIGNATURE": signature, ...extraHeaders, }, body: rawBody || undefined, }); } curl / bash ~~~~~~~~~~~ .. code-block:: bash KEY_ID="mpay_pk_..." SECRET="mpay_sk_..." PASSPHRASE="..." TIMESTAMP=$(date +%s) NONCE=$(uuidgen) METHOD="POST" PATH_="/external/v1/payments/prompt" BODY='{"amount":1099,"blockchainIds":["eth-usdc"]}' SIGNATURE=$(printf '%s\n%s\n%s\n%s\n%s' \ "$TIMESTAMP" "$NONCE" "$METHOD" "$PATH_" "$BODY" \ | openssl dgst -sha512 -hmac "$SECRET" | sed 's/^.* //') curl -X "$METHOD" "https://api.miraclecash.info$PATH_" \ -H "Content-Type: application/json" \ -H "X-MP-KEY-ID: $KEY_ID" \ -H "X-MP-PASSPHRASE: $PASSPHRASE" \ -H "X-MP-TIMESTAMP: $TIMESTAMP" \ -H "X-MP-NONCE: $NONCE" \ -H "X-MP-SIGNATURE: $SIGNATURE" \ -d "$BODY" .. note:: The body string passed to ``curl -d`` must be the exact string that was signed. Do not let a JSON formatter re-serialize it in between. Common Failure Modes -------------------- .. list-table:: :widths: 42 14 44 :header-rows: 1 * - Symptom - Error code - Fix * - Missing one of the five headers - ``E2001`` - Send all five ``X-MP-*`` headers on every request. * - Timestamp not a Unix-seconds integer - ``E2002`` - Send seconds (not milliseconds) as a string. * - Server clock drifted beyond ±300 s - ``E2003`` - Sync your clock (NTP), re-sign, retry. This error is retryable. * - Unknown key ID or wrong passphrase - ``E2004`` - Verify key ID and passphrase from the dashboard. * - Key was revoked - ``E2005`` - Create a new key. * - Key passed its expiry date - ``E2006`` - Rotate or create a new key. * - Signature mismatch (wrong path, re-serialized body, wrong secret) - ``E2007`` - Check the canonical string rules above, byte-for-byte. * - Nonce reused within the window - ``E2009`` - Re-sign with a fresh nonce and timestamp — never resend a signed request verbatim. * - Key lacks the endpoint's scope - ``E2101`` - Grant the required scope to the key in the dashboard. Full details for every code are in the :doc:`Error Reference `. Key Rotation ------------ Rotating a key in the dashboard issues a new secret and passphrase under the **same key ID**. The previous secret/passphrase pair keeps verifying for a **24-hour grace period** (unless you choose immediate rotation), so you can deploy the new credentials without downtime.