Architecture patterns
Architecture patterns
Proven patterns for building a production-grade Mercury integration. From reference mapping to event-driven reconciliation, everything you need to design it right the first time.
Who this is for
Technical architects and senior engineers designing a Mercury BaaS integration. This guide assumes you have read the Overview and Authentication guides.
The integration lifecycle
Every Mercury integration follows four phases. Each phase builds on the previous one and produces references you will need in subsequent steps.
Create a client record for each end-customer your platform serves. Mercury returns a Identifier (e.g. CLIENT-001) that you will use in every subsequent call.
Your
externalReference maps to your internal customer IDClient starts with
Active status immediatelyWebhook:
Clients.ClientCreated confirms onboardingCreate virtual IBANs for your client. Account creation is asynchronous. The API returns a Processing status. Wait for the Accounts.AccountCreated webhook before proceeding.
One GBP account per client (max)
One EUR account per client (max)
IBAN arrives in webhook payload, not the create response
Submit outbound payments via FPS (GBP) or SEPA CT (EUR). Each payment requires a unique endToEndIdentifier that can never be reused. Payments start in Pending status.
GBP FPS: near-instant settlement, 24/7
EUR SEPA CT: Mon-Fri 06:00-14:25 UK time only
Optional: CoP check before GBP payments
Use webhooks for real-time updates and statements for end-of-day reconciliation. Mercury fires 26 distinct webhook events covering clients, accounts, and payments.
Webhooks for real-time state changes
Statement endpoints for batch reconciliation
Match on
endToEndIdentifier and PaymentIdReference mapping
Mercury uses several identifiers across the API. A clean mapping between your internal IDs and Mercury references is critical for reconciliation and support escalations.
| Mercury reference | You provide | Example | Where you will need it |
|---|---|---|---|
Identifier |
externalReference |
CLIENT-001 | Account creation, payment submission, GET client |
IBAN |
n/a (assigned) | GB71CLRB... | Payment debtor field, account block/unblock/close |
PaymentId |
endToEndIdentifier |
764b7d22-... | Webhook correlation, reconciliation, support |
RequestId |
n/a (assigned) | a1b2c3d4... | Track async account creation requests |
Store every reference Mercury returns. Your database should maintain a mapping table linking your internal IDs to Mercury's Identifier, IBANs, and PaymentId values. You will need these for every subsequent API call and for matching webhook events back to your records.
Recommended data model
A minimal schema for tracking Mercury entities alongside your own records:
# Recommended mapping tables for your database
clients = {
"your_customer_id": "cust_abc123", # Your internal ID
"mercury_identifier": "CLIENT-001", # From POST /api/v1/client response
"external_reference": "EXT-REF-001", # What you sent in externalReference
"status": "Active", # Track via Clients.ClientStatusChanged
"gbp_iban": "GB71CLRB04082700001159", # From Accounts.AccountCreated webhook
"eur_iban": "GB85CLRB04274893940059", # From Accounts.AccountCreated webhook
}
payments = {
"your_payment_id": "pay_xyz789", # Your internal ID
"mercury_payment_id": "764b7d22-...", # From payment submission response
"end_to_end_id": "E2E-20260419-001", # What you sent (must be unique, ever)
"client_reference": "CLIENT-001", # Links back to client
"currency": "GBP",
"amount": 500.00,
"status": "Pending", # Track via webhook events
"created_at": "2026-04-19T10:30:00Z",
"settled_at": None, # Set when TransactionSettled arrives
}// Recommended mapping tables for your database
const client = {
yourCustomerId: "cust_abc123", // Your internal ID
mercuryIdentifier: "CLIENT-001", // From POST /api/v1/client response
externalReference: "EXT-REF-001", // What you sent in externalReference
status: "Active", // Track via Clients.ClientStatusChanged
gbpIban: "GB71CLRB04082700001159", // From Accounts.AccountCreated webhook
eurIban: "GB85CLRB04274893940059", // From Accounts.AccountCreated webhook
};
const payment = {
yourPaymentId: "pay_xyz789", // Your internal ID
mercuryPaymentId: "764b7d22-...", // From payment submission response
endToEndId: "E2E-20260419-001", // What you sent (must be unique, ever)
clientReference: "CLIENT-001", // Links back to client
currency: "GBP",
amount: 500.00,
status: "Pending", // Track via webhook events
createdAt: "2026-04-19T10:30:00Z",
settledAt: null, // Set when TransactionSettled arrives
};// Recommended mapping tables for your database
public class MercuryClient {
private String yourCustomerId; // "cust_abc123" - Your internal ID
private String mercuryIdentifier; // "CLIENT-001" - From POST /api/v1/client
private String externalReference; // "EXT-REF-001" - What you sent
private String status; // "Active" - Track via ClientStatusChanged
private String gbpIban; // From Accounts.AccountCreated webhook
private String eurIban; // From Accounts.AccountCreated webhook
}
public class MercuryPayment {
private String yourPaymentId; // "pay_xyz789" - Your internal ID
private String mercuryPaymentId; // "764b7d22-..." - From payment response
private String endToEndId; // "E2E-20260419-001" - Must be unique, ever
private String clientReference; // "CLIENT-001" - Links back to client
private String currency; // "GBP"
private BigDecimal amount; // 500.00
private String status; // "Pending" - Track via webhook events
private Instant createdAt;
private Instant settledAt; // Set when TransactionSettled arrives
}Event-driven architecture
Mercury is an event-driven API. Many operations are asynchronous: you submit a request, receive an acknowledgement, and then get the final result via a webhook. Design your integration around this pattern from the start.
Webhook event map
Key events to handle at each phase of the integration. For full payload details and handler code, see the Webhooks guide.
ClientCreatedClientUpdatedClientStatusChangedClientRiskRatingChangedClientAssignedAccountAccountCreatedAccountCreationFailedAccountUpdatedAccountBlockedAccountUnBlockedAccountClosedAccountUpdatingFailedTransactionSettledTransactionRejectedPaymentStatusChangedInboundTransactionMccyOutboundPaymentCompleteMccyOutboundPaymentFailedMccyInboundPaymentComplete- 7 more (see Webhooks guide)
Webhook processing pattern
Your webhook handler should acknowledge fast, process async, and handle duplicates gracefully.
import json
import hashlib
from flask import Flask, request, jsonify
app = Flask(__name__)
# Track processed events to handle duplicates
processed_events = set()
@app.route("/webhooks/mercury", methods=["POST"])
def handle_webhook():
payload = request.get_json()
event_type = payload.get("Type")
nonce = payload.get("Nonce")
# 1. Deduplicate using nonce
if nonce in processed_events:
return jsonify({"received": True}), 200
# 2. Acknowledge immediately (Mercury checks HTTP status only)
# In production, queue the event for async processing
processed_events.add(nonce)
# 3. Route by event type
handlers = {
"ClientCreated": handle_client_created,
"AccountCreated": handle_account_created,
"PaymentTransactionSettled": handle_payment_settled,
"PaymentTransactionRejected": handle_payment_rejected,
"MccyOutboundPaymentComplete": handle_mccy_payment_complete,
"MccyOutboundPaymentFailed": handle_mccy_payment_failed,
}
handler = handlers.get(event_type)
if handler:
handler(payload["Payload"])
return jsonify({"received": True}), 200
def handle_account_created(data):
"""Store the IBAN - you need it for payments."""
client_ref = data["OwnerReference"]
iban = data["Identifiers"][0]["Identifier"]
currency = data["Currencies"][0]
# Update your database: link IBAN to client
print(f"Account ready: {client_ref} -> {iban} ({currency})")
def handle_payment_settled(data):
"""Mark payment as complete in your system."""
payment_id = data["PaymentId"]
e2e_id = data["EndToEndTransactionId"]
settled_at = data["TimestampSettled"]
# Update your database: payment status -> Settled
print(f"Payment settled: {payment_id} (E2E: {e2e_id})")const express = require("express");
const app = express();
app.use(express.json());
// Track processed events to handle duplicates
const processedEvents = new Set();
app.post("/webhooks/mercury", (req, res) => {
const { Type: eventType, Payload: payload, Nonce: nonce } = req.body;
// 1. Deduplicate using nonce
if (processedEvents.has(nonce)) {
return res.json({ received: true });
}
// 2. Acknowledge immediately (Mercury checks HTTP status only)
// In production, queue the event for async processing
processedEvents.add(nonce);
// 3. Route by event type
const handlers = {
ClientCreated: handleClientCreated,
AccountCreated: handleAccountCreated,
PaymentTransactionSettled: handlePaymentSettled,
PaymentTransactionRejected: handlePaymentRejected,
MccyOutboundPaymentComplete: handleMccyPaymentComplete,
MccyOutboundPaymentFailed: handleMccyPaymentFailed,
};
const handler = handlers[eventType];
if (handler) handler(payload);
res.json({ received: true });
});
function handleAccountCreated(data) {
// Store the IBAN - you need it for payments
const clientRef = data.OwnerReference;
const iban = data.Identifiers[0].Identifier;
const currency = data.Currencies[0];
// Update your database: link IBAN to client
console.log(`Account ready: ${clientRef} -> ${iban} (${currency})`);
}
function handlePaymentSettled(data) {
// Mark payment as complete in your system
const { PaymentId: paymentId, EndToEndTransactionId: e2eId, TimestampSettled: settledAt } = data;
// Update your database: payment status -> Settled
console.log(`Payment settled: ${paymentId} (E2E: ${e2eId})`);
}Resilience patterns
Financial integrations demand reliability. These patterns ensure your integration handles failures gracefully.
The endToEndIdentifier on payment endpoints is your idempotency key. Mercury rejects any payment with a previously submitted identifier. Generate it deterministically from your payment record so retries are safe.
Not all errors are retryable. Match your retry logic to the HTTP status code.
Webhooks are your primary data source. Statements are your safety net. Run a daily reconciliation job that compares your records against Mercury statements.
GBP:
POST /api/v1/sterling-account/statementsEUR:
POST /api/v1/euro/vibans/statementsEUR SEPA CT payments are only processed during TARGET2 operating hours. Submissions outside this window are rejected. Build this constraint into your payment scheduling.
Complete integration sequence
End-to-end flow from client onboarding to first settled payment. This is the critical path for your integration.
Onboard client with your externalReference. Store the returned Identifier.
Confirms onboarding complete. Contains full client details including ExternalReference for matching.
Request account(s). Response returns Processing status. Do not proceed until webhook confirms.
Contains the IBAN you need for payments. Store it against your client record. Account is now Active.
Submit payment with unique endToEndIdentifier. Store the returned PaymentId. Status: Pending.
Payment confirmed. Match on PaymentId or EndToEndTransactionId. Update your records. Done.
Sandbox vs production
Mercury provides a sandbox environment that mirrors production behaviour. Your integration code should use environment variables to switch between them.
api.external.test.mercury.globalimport os
# Environment-driven configuration
MERCURY_CONFIG = {
"base_url": os.environ.get(
"MERCURY_BASE_URL",
"https://www.api.external.test.mercury.global" # sandbox default
),
"api_key": os.environ.get("MERCURY_API_KEY"),
"secret_key": os.environ.get("MERCURY_SECRET_KEY"),
}
# Same code, different credentials
def get_base_url():
return MERCURY_CONFIG["base_url"]// Environment-driven configuration
const mercuryConfig = {
baseUrl: process.env.MERCURY_BASE_URL
|| "https://www.api.external.test.mercury.global", // sandbox default
apiKey: process.env.MERCURY_API_KEY,
secretKey: process.env.MERCURY_SECRET_KEY,
};
// Same code, different credentials
const getBaseUrl = () => mercuryConfig.baseUrl;Common pitfalls
Issues we see most often in new integrations:
Using lookup endpoints to test authentication
Lookup endpoints (/api/v1/lookup/*) require no authentication. A successful call does not prove your signing is correct. Use GET /api/v1/client instead.
Sending payments before account webhook confirms
Account creation returns Processing, not Active. Wait for Accounts.AccountCreated before submitting payments on that IBAN.
Reusing endToEndIdentifier values
This field is globally unique and can never be resubmitted. Generate it deterministically from your payment record, not randomly per attempt. If a payment fails validation (400), you still cannot reuse that identifier.
Submitting EUR SEPA CT outside operating hours
SEPA payments are rejected outside Mon-Fri 06:00-14:25 UK time and on TARGET2 holidays. Queue EUR payments and submit during the window. GBP FPS has no such restriction.
Including separators in HMAC signing payload
The signing input is apiKey\nmethod\npath\nbody\nnonce, joined with newlines. GET requests leave out the body line. See the Authentication guide.
Updated about 2 hours ago
