Mercuryo Integration — Test Plan

Document type
QA reference · scenarios, readiness gates, vendor-side risk catalog
Status
Draft · pre-implementation
Last updated
May 26, 2026
Companion documents
Overview · Technical Design

Sandbox readiness — what we need before testing starts

Tick each item as it lands. Progress is saved in this browser. None of these need to ship in the same week — but the earliest test cycle cannot begin until items marked From vendor arrive.

Pre-flight checklist

Eight items. Six come from the vendor; two are internal preparation.
0 of 8
Sandbox base URL provided From vendor
Expected form: https://sandbox-cryptosaas.mrcr.io/v1.6. Confirm exact version with vendor; version bump invalidates request shapes.
Partner-level token issued From vendor
Used as Sdk-Partner-Token header on every request. Stable, doesn't expire.
Widget ID + widget secret + sign_key obtained From vendor
All three come from the Mercuryo dashboard. Widget secret feeds the v2 signature; sign_key feeds webhook HMAC verification.
Test cards confirmed working From vendor
Vendor publishes 4444 4444 4444 3333 (success) and 5555 4444 3333 1111 with CVV variants for fail-path testing. Validate one round-trip before scenario runs begin.
Sandbox crypto address realism confirmed From vendor
Critical · the vendor's documentation indicates test-network addresses for BTC and ETH-Sepolia, but vendor sandboxes have returned mainnet addresses in past integrations. Send a probe + verify against a public chain RPC before any off-ramp test sends real value.
Identity-share client identifier supplied From vendor
Mercuryo's integration manager provides the value to be passed as forClientId on the share-token generation call.
Test customer provisioned end-to-end Internal
A platform user with verified identity, in a Mercuryo-supported country, with a non-zero crypto balance for off-ramp testing.
UAT environment receives signed webhooks from vendor Internal
Vendor's dashboard must point at our UAT webhook URL, and the URL must be reachable from outside our VPC. Validate with a dashboard-fired test webhook before scenario runs.

Test coverage matrix

What needs to be exercised on each payment-method × scenario combination. Click a cell to jump to the related scenario card below.

Card IBAN-invoice IBAN-exchange
On-ramp · happy path
▸ S1
▸ S2
▸ S3
On-ramp · rate moved ≥1%
▸ S4
n/a
n/a
On-ramp · quote expired
▸ S5 — covers all methods
Off-ramp · happy path
▸ S6
n/a
▸ S7
Off-ramp · rate moved >5%
▸ S8 — vendor returns crypto
Off-ramp · address-signature mismatch
▸ S9 — refuse-to-send · security
Identity reuse (no re-KYC)
▸ S10 — covers all methods
Auth · bearer refresh
▸ S11 — server-side
Country gate · unsupported
▸ S12 — covers all methods
Compliance gate · withdrawal freeze
▸ S13 — off-ramp only
Webhook · forged signature
▸ S14 — security
Webhook · duplicate eventId
▸ S15 — idempotency
To run Pass Fail Skipped Not applicable

Test scenarios

Each scenario is independent; all are run on a fresh customer where indicated. Setup describes preconditions; Execute is the action under test; Expect is the externally-visible outcome; Verify is what to inspect to confirm internal correctness.

1
On-ramp · happy path · card
On-rampCard
High
Setup
  • Customer is logged in, identity verification complete on the platform side
  • Customer's country is in the Mercuryo-supported list
  • Test card 4444 4444 4444 3333 handy
Execute
  • Wallet → Deposit → Mercuryo route
  • Enter fiat amount and crypto target
  • In the embedded widget, complete card 3DS
Expect
  • Widget surfaces success state
  • Crypto appears in the platform balance within the vendor's stated settlement window
  • No second identity prompt inside the widget
Verify
  • Payment row in completed state, matched by merchant_transaction_id
  • Webhook event logged with HMAC verified
  • No duplicate side effects on second webhook delivery (replay test)
2
On-ramp · happy path · IBAN invoice
On-rampIBAN
High
Setup
  • Same as S1, except the customer is in a SEPA / IBAN-eligible country
  • Customer has access to a bank that can send an IBAN transfer
Execute
  • Deposit → Mercuryo route → IBAN invoice method
  • Follow vendor-provided bank-transfer instructions
Expect
  • Widget displays IBAN invoice details
  • Customer receives crypto when bank settlement completes
  • Widget can be closed without losing the in-flight transaction
Verify
  • Payment row tracks pending → completed transitions on vendor webhooks
  • Re-opening the widget mid-flight shows the same transaction state
3
On-ramp · happy path · IBAN exchange
On-rampIBAN
Medium
Setup
  • Customer in IBAN-eligible country with a saved IBAN method on the vendor
Execute
  • Deposit → Mercuryo → IBAN exchange method
Expect
  • Single-click confirmation without re-entering bank details
  • Customer receives crypto on settlement
Verify
  • No duplicate IBAN payment-method records on vendor side
4
On-ramp · rate moved ≥1% between quote and commit
SlippageCard
High
Setup
  • Live sandbox rate that's expected to move within minutes
  • Quote token captured but commit delayed deliberately
Execute
  • Wait until visible rate has moved ≥1%
  • Submit the buy with the old quote
Expect
  • Customer sees a clear "rate expired · please retry" message
  • Customer is offered a fresh quote without restarting the whole flow
  • No charge to the card
Verify
  • Vendor responds with error code 403004
  • Payment row stays at created; no completed transition
  • Frontend automatically requests a fresh quote
5
On-ramp · quote token expired (1h)
Quote expiry
Medium
Setup
  • Customer fetches a quote, then leaves the widget idle for > 1 hour
Execute
  • Return to widget, click Submit on the stale quote
Expect
  • Customer is automatically re-quoted; old quote token discarded silently
  • Customer is not asked to re-enter amount or destination
Verify
  • Frontend never submits an expired trx_token to the vendor
  • Re-quote is initiated automatically when the customer becomes active again
6
Off-ramp · happy path · payout to card
Off-rampCard
High
Setup
  • Customer holds enough crypto on the platform to satisfy the sell amount + estimated fees
  • Customer's destination card is registered on the vendor side
Execute
  • Wallet → Withdraw → Mercuryo route → Card payout
  • Confirm amount and destination
Expect
  • Crypto leaves the customer's platform balance immediately
  • Customer receives fiat to the chosen card within the vendor's stated settlement window
Verify
  • Payment row transitions created → processing → completed
  • Vendor's deposit-address signature was verified before crypto was sent
  • Withdrawal job chain ran all compliance steps before reaching the vendor call
7
Off-ramp · payout to IBAN
Off-rampIBAN
Medium
Setup
  • Same as S6, except a SEPA-eligible IBAN is registered as the payout destination
Execute
  • Withdraw → Mercuryo → IBAN destination
Expect
  • Customer receives fiat via SEPA within stated window
Verify
  • Payment row reflects settlement window — long-tail of pending state is expected for IBAN
8
Off-ramp · rate moved >5% · vendor returns crypto
SlippageOff-ramp
High
Setup
  • Manufactured rate-movement scenario, or naturally occurring >5% swing
  • Customer has confirmed the sell and the platform has sent crypto to the vendor
Execute
  • Wait for the rate to move > 5% before vendor settles
Expect
  • Vendor refuses the sell and sends the crypto back to the deposit address
  • Customer's platform balance is credited with the returned crypto, not the fiat
  • Original payment record is marked refunded (or similar terminal-cancelled state)
Verify
  • Existing crypto-deposit pipeline handles the return — no custom refund logic involved
  • No duplicate side effects: customer is not credited both fiat and crypto
9
Off-ramp · vendor returns address with bad signature
SecurityOff-ramp
High
Setup
  • Synthetic vendor response with a tampered signature value
  • Reproduced via traffic interception or vendor-side switch (per vendor sandbox capability)
Execute
  • Off-ramp confirmation flow runs up to the point where vendor returns the deposit address
Expect
  • Platform refuses to send any crypto
  • Customer sees a clear error; transaction is aborted server-side
  • No partial state — payment record either stays pre-send or marked failed cleanly
Verify
  • Signature mismatch is logged with enough detail to forward to the vendor
  • Custody layer never receives a send instruction for the bad address
10
Identity reuse · no re-KYC inside the widget
Identity
High
Setup
  • Customer with complete identity verification on the platform
  • Customer has never used Mercuryo before (fresh on the vendor side)
Execute
  • Start any on-ramp or off-ramp flow
  • Observe the widget's identity-check state
Expect
  • Widget does not prompt for documents
  • Vendor recognises the customer as identity-complete on its side
Verify
  • Identity share-token was passed to the widget at mount time
  • Vendor's KYC-status endpoint returns complete for the customer's feature set after first interaction
11
Auth · per-user bearer expiry and refresh
Auth
Medium
Setup
  • Returning customer whose cached bearer is past its TTL (24h in production)
Execute
  • Initiate any per-user vendor call (sell, share-token, kyc-status)
Expect
  • Customer-facing flow runs to completion without showing any auth state to the customer
Verify
  • Server-side refresh path was hit before the vendor call
  • New bearer cached with correct TTL
  • No two concurrent refresh calls for the same customer (race protection)
12
Country gate · customer in unsupported country
Compliance
High
Setup
  • Customer whose country is not in Mercuryo's allowed list
Execute
  • Customer attempts to open the Mercuryo route in the wallet
Expect
  • Mercuryo route is either not shown, or the open-action returns a clear "not available in your region" message
  • No vendor API call is ever made
Verify
  • Country check happens before any vendor request (no outbound HTTP to Mercuryo in trace)
  • Same customer in a supported country sees the route normally — gate is per-customer, not per-tenant
13
Compliance gate · platform-wide withdrawal freeze
ComplianceOff-ramp
High
Setup
  • Platform's withdrawal freeze is set globally for a maintenance window
Execute
  • Customer attempts an off-ramp via Mercuryo
Expect
  • Customer sees the platform's standard withdrawals-paused message
  • No vendor API call is made
Verify
  • Freeze gate runs first, before any Mercuryo-specific logic
  • Releasing the freeze allows the same flow to proceed without state cleanup
14
Webhook · forged signature
Security
High
Setup
  • Crafted webhook payload with valid shape but invalid X-Signature
Execute
  • POST to the platform's Mercuryo webhook endpoint with the forged signature
Expect
  • HTTP 401 or 403
  • No side effects · no payment row update, no balance change, no other downstream event
Verify
  • Signature check happens before any body parsing or DB lookup
  • Failure is logged at warn or higher
15
Webhook · duplicate eventId (idempotency)
Security
High
Setup
  • A successful on-ramp webhook delivery (S1 just ran)
Execute
  • POST the exact same webhook body + signature again
  • Repeat with concurrent timing if possible
Expect
  • Second delivery returns 200 OK
  • No double-credit · the customer's balance changes once and only once
Verify
  • Concurrency lock is acquired on the vendor's eventId
  • Payment row is touched exactly once even under concurrent retries

Likely vendor-side risks — areas where PSP integrations historically break

Based on past PSP integrations on this platform, these are the surfaces most likely to fail unexpectedly on the vendor side. Each one is worth a dedicated probe before the build cycle begins.

!
Sandbox crypto addresses may be mainnet
Vendor docs reference test-network addresses, but past PSP sandboxes have returned mainnet addresses with no warning. If QA sends real assets to a mainnet address during smoke testing, it's gone.
Mitigation: verify every returned address against a public chain RPC before the first off-ramp test. Document the result in the readiness checklist.
!
Webhook delivery may stop earlier than advertised
Vendor docs state up to 15 retries with decreasing frequency. Past vendors have stopped at 5–8 retries silently. If our endpoint is down briefly, we may lose terminal-state notifications without a reconciliation safety net.
Mitigation: instrument webhook receipt with a count per merchant_transaction_id; flag any payment in processing for > 24h with no further events.
!
Bearer token TTL behaviour may differ in production
Vendor docs claim no expiry in sandbox, 24h in production. The transition can hide refresh-path bugs that only surface in production. Refresh must be exercised in sandbox by manually invalidating the cached bearer.
Mitigation: dedicated S11 scenario forces a refresh path by clearing the cache before a per-user call.
!
Slippage thresholds may differ from docs
Docs state 1% for buy and 5% for sell. Past vendors have applied tighter thresholds without notice. The customer-facing error message must be generic enough to remain accurate.
Mitigation: log every 403004 with the actual rate movement so we can build empirical evidence over time.
!
Quote token may invalidate before stated TTL
Vendor docs state 1-hour validity. Past vendors have invalidated tokens earlier under high price volatility. Customers can see "quote expired" after a few minutes if the market is moving.
Mitigation: automatic re-quote before submit, regardless of remaining TTL.
!
Identity-share import may fail silently for some customer states
If the customer was previously rejected on the vendor side under a different identity (manual signup), the share-token import can succeed at the API level but leave the customer with vendor-side failed_attempt state.
Mitigation: after share-token import, read the vendor's KYC status and surface mismatches to support.

Expected error catalog

Vendor-side error codes that QA should recognise. Anything not in this table is novel and worth a vendor-side ticket.

CodeMeaningCustomer-facingTriggering scenario
400001Request validation error"Please check the entered amount."Most often: amount below minimum or non-supported pair
400011Invalid identity share token"We couldn't validate your identity. Please try again."Stale share-token or token issued for a different vendor
400015Applicant not approved (identity side)"Your identity verification is not complete on our side."Share-token from an unapproved applicant
400064User not found(should not be customer-facing — internal error)Bearer token for a deleted vendor-side user
401000Auth failed(internal — bearer refresh or partner-token rotation)Wrong partner token or expired bearer that didn't refresh
403003Rate limit"Too many requests. Please wait a moment."Quote calls in a tight loop, e.g. live re-quote bug
403004Rate slippage exceeded threshold"The price has changed. Please try again at the new rate."≥1% buy or >5% sell between quote and commit
403007Active transaction conflict"You have an active transaction. Please complete or cancel it first."Customer attempts a second buy or sell with one already in flight
403020IP blocked"Service not available from your network."Customer on a vendor-blocked IP (e.g. VPN region mismatch)
403022KYC already complete(internal — share-token re-import attempt)Customer's vendor-side identity already imported
403023Applicant already exists(internal — share-token re-import for known customer)Race condition on first share-token import

What QA does not need to test

These belong to the vendor or to existing platform pipelines. Time spent on them does not improve coverage for this integration.

Out of QA scope

  • Card data security inside the vendor's iframe — vendor is PCI-DSS certified.
  • The 3-D Secure flow itself — the vendor handles the cardholder challenge.
  • The identity-provider's iframe UX inside the vendor's widget — owned by the identity provider.
  • The vendor's internal sanctions / PEP screen — handled by the vendor's AML pipeline.
  • On-chain transaction confirmations during off-ramp settlement — owned by the custody layer.
  • Existing platform compliance pipelines (transaction limits, KYT, Travel Rule) — covered by their own test plans.
  • The identity-provider's underlying verification result — already covered by the platform's identity tests.