API Console credentials live only in this browser

Mercuryo Integration — Technical Design

Document type
Engineering reference · for backend, frontend, infrastructure
Status
Draft · pending sandbox alignment
Last updated
May 26, 2026
Vendor
Mercuryo OOR · sandbox v1.6 · sandbox-cryptosaas.mrcr.io
Companion documents
Overview · Test Plan

Architecture at a glance

Five participants: customer-facing frontend, platform backend, the vendor's API, the identity provider already in use, and the existing custody layer. Mercuryo plugs in as one more payment route alongside the existing ones — no new compliance pipeline, no fork in withdrawal mechanics.

┌───────────────────┐ │ Customer │ browser session in the wallet │ (web · widget) │ └─────────┬─────────┘ │ 1. open buy / sell ↓ ┌──────────────────────────────────────────────────┐ │ Platform backend │ │ │ │ PSP module — Mercuryo route │ │ ▸ widget-session prepare │ │ ▸ bearer-token cache │ │ ▸ signature helpers (×3 schemes) │ │ ▸ identity-sharing bridge │ │ ▸ webhook handler │ │ │ │ Existing payments pipeline │ │ ▸ off-ramp job chain (one new branch) │ │ ▸ deposit credit orchestration │ │ ▸ platform compliance checks │ └────┬────────────────┬──────────────────┬─────────┘ │ HTTPS ↑ HTTPS (signed) │ HTTPS ↓ │ ↓ ┌──────────────┐ webhook lifecycle ┌──────────────────┐ │ Mercuryo │──────────────────────│ Identity │ │ OOR API │ │ provider │ │ + widget │ │ (existing) │ └──────────────┘ └──────────────────┘ │ │ crypto deposit (off-ramp settlement) ↓ ┌──────────────────┐ │ Custody layer │ existing │ (existing) │ └──────────────────┘
What changes structurally: a new payment-route option appears for the platform's customers and a new branch is added inside the off-ramp processor. Everything else — compliance, custody, identity verification — is reused unchanged.

Sequence — on-ramp (buy)

From a customer click to a crypto balance increase. Sixteen messages across five participants. Signature generation and identity-token forwarding happen server-side before the widget is even visible.

Buy flow · fiat in → crypto credited
Customer Frontend Platform BE Identity Mercuryo 1. Click Buy 2. Prepare widget session 3. Re-check eligibility · maintenance · KYC 4. Sign-up (first use) or sign-in 5. Vendor user id · session tokens 6. Mint identity share token 7. Share token 8. Quote / rates 9. Quote token (1h validity) 10. Sign widget params · persist payment shell 11. Widget config bundle 12. Load + mount embedded widget 13. Pay (card 3DS / IBAN inside widget) Vendor processes payment + KYC via shared token 14. Signed webhook · event with terminal status 15. Verify signature · idempotency lock · credit 16. Crypto balance visible to customer
Reading: purple = our outbound to the vendor or to the frontend; orange = vendor response or async webhook; green = identity provider traffic plus final balance update; grey = internal work on a single participant. The same numeric merchant_transaction_id minted at step 10 is what re-attaches the inbound webhook at step 14 to the payment record we created.

Sequence — off-ramp (sell)

Customer confirms a sell in the widget; platform clears compliance, then sends crypto to the vendor's settlement address; vendor dispatches fiat. The platform never bypasses its own withdrawal pipeline.

Sell flow · crypto out → fiat delivered
Customer Frontend Platform BE Custody Mercuryo 1. Click Sell 2. Prepare widget session (sell) 3. Re-check eligibility + withdrawal-freeze gate 4. Widget config bundle 5. Mount widget · sell mode 6. Confirm payout destination + amount 7. Webhook · sell created 8. Enqueue platform withdrawal pipeline 9. Auto-approve · limits · KYT · Travel Rule 10. Confirm sell with quote 11. Deposit address · address signature 12. Verify deposit address signature 13. Send crypto 14. Settlement 15. Webhook · sell succeeded 16. Fiat dispatched · transaction done
Slippage abort path: if the rate moves beyond the vendor's slippage tolerance between the sell-confirmation and the deposit address being funded, the vendor cancels the sale and sends the crypto back. The existing crypto-deposit pipeline credits the customer back without any custom refund logic.

Payment state machine

Every Mercuryo-driven transaction (on-ramp or off-ramp) maps to one row in the platform's payments table and progresses through the same state machine. Transitions are driven by webhooks; idempotency is enforced on the vendor's eventId.

created payment shell minted processing vendor working completed happy path · terminal rejected terminal · balance restored refunded off-ramp only · crypto returned via standard deposit pipeline expired quote token timeout submit webhook ok webhook fail off-ramp · slippage abort no submit · 1h LEGEND internal transition success webhook failure webhook vendor abort + return Idempotency: every webhook delivery acquires a lock on the vendor's eventId; the same eventId arriving twice is a 200-OK no-op. A second webhook for a payment already in a terminal state is logged and ignored.

Signature schemes — three, each protecting something different

The integration uses three independent signature schemes. They are not interchangeable; each protects a different attack surface and runs in a different direction.

Mercuryo · 3 signature schemes
A · Widget signature
Algorithm
SHA-512 with v2: prefix
Input
destination address + widget secret + customer IP + transaction id
Generated by
Platform backend, server-side
Verified by
Mercuryo widget at load
Protects against widget tampering. Widget refuses to mount if the signature doesn't match the parameters.
B · Webhook signature
Algorithm
HMAC-SHA-256
Input
raw request body
Generated by
Mercuryo
Verified by
Platform backend before any payload parsing
Protects against forged webhooks. Rejection happens before idempotency lookup — no work is done on unverified bodies.
C · Deposit address signature
Algorithm
SHA-256
Input
vendor-returned address + transaction id
Generated by
Mercuryo · sell-confirmation response
Verified by
Platform backend before sending any crypto
Off-ramp only. Protects against address swap by a compromised intermediary. Mismatch aborts the sell.

API console — interactive endpoint reference

Each card describes one Mercuryo endpoint we use, with the real request/response shape from vendor docs. Configure credentials in the bar at the top of the page; expand any card to inspect its example body, fire it against the configured base URL, or copy a ready-to-paste curl command. Browser CORS may block direct fires — curl works regardless.

Heads-up on CORS: Mercuryo's API does not advertise CORS allowlist for arbitrary browser origins. If Fire returns a network error, the call did reach the network but the browser blocked the response from being read — use Copy curl and run from a terminal instead. Both produce identical request payloads.
POST /sdk-partner/sign-up BE → vendor
First-touch customer creation. Establishes the vendor-side user identifier and returns the initial session tokens.
Partner token
Not in the public v1.6 reference — schema below is from integration guidance. Confirm field set with Mercuryo's integration manager before first call.
Response
POST /sdk-partner/login BE → vendor
Refresh path for an existing user when the cached bearer is near expiry or has lapsed.
Partner token
Not in the public v1.6 reference — schema below is inferred. Confirm with Mercuryo before first call.
Response
GET /b2b/oor/buy-rates BE → vendor
Returns a frozen on-ramp quote plus a quote token valid for one hour. The token is required to commit the buy later.
Partner token
Known errors: 400001 validation · 401000 auth · 403003 rate-limit · 403020 IP blocked
Response
GET /b2b/oor/sell-rates BE → vendor
Mirror of buy-rates for off-ramp pricing.
Partner token
Known errors: 400001 · 401000 · 403003 · 403020
Response
POST /b2b/oor/buy BE → vendor
Commits an on-ramp purchase using a previously issued trx_token. Returns a payment-page URL for the customer to complete.
Partner tokenPer-user bearer
Known errors: 400001 validation · 403004 slippage ≥1% · 403007 active-transaction-conflict · 403020 IP blocked
Response
POST /b2b/oor/sell BE → vendor
Commits an off-ramp sale. Vendor responds with a settlement crypto address plus a signature to verify before sending funds.
Partner tokenPer-user bearer
Known errors: 400001 validation · 403004 slippage >5% (returns crypto) · 403007 active conflict · 403020 IP blocked
Response
POST /b2b/user/kyc-share-token BE → vendor
Imports a verified identity applicant on the vendor side via SumSub share-token. Used as a fallback if widget-param sharing is unavailable.
Per-user bearer
Known errors: 400001 validation · 400011 invalid share token · 400015 not approved · 403022 already complete · 403023 already exists
Response
GET /b2b/user/kyc-status BE → vendor
Read the vendor's current KYC state for the authenticated user. Useful before showing the buy or sell UI.
Per-user bearer
Status values: incomplete · failed_attempt · failed · complete · under_review
Known errors: 400064 user not found · 401000 auth · 403020 IP blocked
Response
POST (our /psp/<ours>/webhook) vendor → BE
Inbound notification of transaction status changes. Verified by HMAC, deduplicated by event id, dispatched to the payments pipeline.
HMAC-SHA-256 · header X-Signature
Inbound endpoint — not fireable from this console. Below is the canonical payload shape Mercuryo sends.
Buy statuses: new · pending · cancelled · paid · order_failed · order_scheduled · order_verified_not_complete · failed_exchange
Sell statuses: new · pending · succeeded · failed · cancelled
Retries: up to 15 with decreasing frequency · BE must return 200 to stop retries
Out of scope in v1: recurring on-ramp deposits, PCI-DSS direct card flow, business / corporate customers, the vendor's spend-card lifecycle events, and any reconciliation jobs. Reconciliation is added only if drift is observed in production — not preemptively.

State storage — where things live

Four storage planes. Each has a single owner and a documented lifecycle. Nothing about Mercuryo creates a new persistence layer or a new table outside what the platform already manages.

Persistent · per-user identity

Vendor user identifier

  • Stored in the existing per-user property store, scoped to the platform user.
  • Minted once at first interaction, immutable afterwards.
  • Used as the join key for all subsequent vendor calls and webhook reverse-lookup.
Cache · short-lived

Per-user bearer tokens

  • Cached with a TTL aligned to the vendor's bearer expiry (24h in production).
  • Refreshed lazily on next use if expiry is within a safety margin.
  • Never returned to the frontend — server-side only.
Persistent · transaction records

Payment rows

  • One row per buy or sell, in the platform's existing payments table.
  • Shell row created at session-prepare time; status transitions on webhook.
  • Joined to the vendor's events via the transaction identifier we generate.
Cache · idempotency

Webhook deduplication locks

  • Lock keyed on the vendor's eventId at webhook ingest.
  • TTL covers the worst-case vendor retry window plus a safety buffer.
  • Same eventId arriving twice produces a single side effect.
Configuration · platform-wide

Vendor credentials

  • Stored as an opaque JSON blob in the platform's secret store, never in the database.
  • Hot-reloaded on a defined interval — no service restart needed for rotation.
  • Single shared credential set across all platform tenants that opt in.
Configuration · per-tenant

Route enablement

  • One row per tenant that enables Mercuryo, in the existing payment-routes table.
  • Presence of the row gates whether the rail appears in the tenant's UI.
  • No code change required to enable a new tenant — only a config row.

Open questions — Phase 2 probe items

Items the design holds open until first sandbox round-trip. None of these block design approval; all of them block the start of the build cycle.

Resolve on first sandbox run

  • Bearer token TTL in sandbox. Vendor docs claim no expiry in sandbox. Confirm experimentally — affects how we exercise the refresh path during testing.
  • Webhook retry schedule. Vendor docs state up to fifteen retries with decreasing frequency. Exact back-off must be captured to size the idempotency lock TTL correctly.
  • Error code coverage. Only the slippage error is documented. Other errors will surface live; each one needs a mapping to a customer-visible message.
  • Business / corporate customers. If the vendor supports KYB in this product line, design an additional provisioning branch. Otherwise KYB stays explicitly out of scope.
  • Identity-sharing scope identifier. The share-token generation requires one identifier supplied by Mercuryo's integration manager — get it before first probe.
  • Sandbox crypto address realism. Whether the vendor's sandbox returns test-network or real-network addresses for the off-ramp settlement step. Determines what QA needs to do during smoke tests.
  • Fixed network per route vs customer-selectable. Recommend fixing per route; confirm the option is available from the vendor side.

Decisions already locked

These are settled and will not be re-opened without explicit input from product.

Copied to clipboard