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 four-phase integration flow

Map your IDs to Mercury IDs

Webhook-driven design

Idempotency, retries, reconciliation

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.

Phase 1
Onboard client
POST /api/v1/client
→
Phase 2
Open accounts
GBP + EUR virtual IBANs
→
Phase 3
Move money
FPS + SEPA CT payments
→
Phase 4
Reconcile
Webhooks + statements
Phase 1: Onboard client

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 ID

Client starts with Active status immediately

Webhook: Clients.ClientCreated confirms onboarding
Phase 2: Open accounts

Create 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
Phase 3: Move money

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
Phase 4: Reconcile

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 PaymentId

Reference 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.

Do: event-driven
Listen for webhooks to confirm state changes
Process events idempotently (same event twice = same result)
Use statements as a daily reconciliation safety net
Avoid: polling
Polling GET endpoints to check payment status
Assuming account is ready immediately after creation call
Treating the 200 response on payment submit as settlement

Webhook event map

Key events to handle at each phase of the integration. For full payload details and handler code, see the Webhooks guide.

Clients (5 events)
ClientCreatedClientUpdatedClientStatusChangedClientRiskRatingChangedClientAssignedAccount
Accounts (7 events)
AccountCreatedAccountCreationFailedAccountUpdatedAccountBlockedAccountUnBlockedAccountClosedAccountUpdatingFailed
Payments (14 events)
TransactionSettledTransactionRejectedPaymentStatusChangedInboundTransactionMccyOutboundPaymentCompleteMccyOutboundPaymentFailedMccyInboundPaymentComplete

  • 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.

Idempotency

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.

Pattern: your_payment_id + attempt_date
Example: pay_xyz789-2026-04-19
Never: random UUID per retry attempt
Retry strategy

Not all errors are retryable. Match your retry logic to the HTTP status code.

200Success. Do not retry.
400Validation error. Fix the request, do not retry as-is.
500Server error. Retry with exponential backoff.
Reconciliation

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/statements

EUR: POST /api/v1/euro/vibans/statements
SEPA operating hours

EUR SEPA CT payments are only processed during TARGET2 operating hours. Submissions outside this window are rejected. Build this constraint into your payment scheduling.

Window: Mon-Fri, 06:00-14:25 UK time
Closed: 1 Jan, 1 May, 25-26 Dec
GBP FPS: 24/7, no restrictions

Complete integration sequence

End-to-end flow from client onboarding to first settled payment. This is the critical path for your integration.

1
POST /api/v1/client

Onboard client with your externalReference. Store the returned Identifier.

2
Receive Clients.ClientCreated webhook

Confirms onboarding complete. Contains full client details including ExternalReference for matching.

3
POST /api/v1/sterling-account (and/or /euro/vibans)

Request account(s). Response returns Processing status. Do not proceed until webhook confirms.

4
Receive Accounts.AccountCreated webhook

Contains the IBAN you need for payments. Store it against your client record. Account is now Active.

5
POST /api/v1/payments (or /euro/payments/sepa-ct)

Submit payment with unique endToEndIdentifier. Store the returned PaymentId. Status: Pending.

6
Receive Payments.TransactionSettled webhook

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.

SANDBOX
Base URL: api.external.test.mercury.global
Separate API key and secret
Full webhook delivery
No real money moves
PRODUCTION
Base URL: provided during go-live
Separate API key and secret
Same API surface, same webhook events
Real money. Real consequences.
import 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.

Ready to build?

Start with client onboarding, work through each phase, and use webhooks to drive your state machine.


Did this page help you?