Webhooks
Webhooks
Receive real-time notifications for client, account, and payment events. 26 event types across three categories.
How webhooks work
Mercury sends HTTP POST requests to your registered endpoint whenever something happens in your account. Every webhook follows the same envelope structure, making it simple to build a single handler.
Payload envelope
Every webhook uses the same three-field structure. Route on Type, process from Payload, and use Nonce for deduplication.
{
"Type": "EventName",
"Payload": { ... },
"Nonce": "unique-value"
}Event identifier string. Use this to route to the correct handler. Note: Type values do not always match the event name (see gotchas).
Event-specific data. Structure varies by event type. Always contains enough context to identify the affected resource.
Unique value per delivery. Store processed nonces and skip duplicates to ensure idempotent handling.
Registering your endpoint
How are webhook endpoints registered? Options may include: API endpoint, Mercury dashboard, or configuration during distributor onboarding. Contact the Mercury team at [email protected] to confirm the registration process for your integration.
Verifying webhook signatures
What signature verification mechanism is used for incoming webhooks? If HMAC-based, what header carries the signature and what is the signing algorithm? Always verify signatures before processing webhook payloads to prevent spoofed requests.
Retry policy
If your endpoint returns a non-200 status code (400 or 500), Mercury retries delivery with exponential backoff.
How many retry attempts? What is the backoff schedule? Is there a maximum retry window (e.g. 24 hours)? What happens after all retries are exhausted?
Building your handler
A minimal webhook handler should: accept POST requests, parse the JSON body, route on the Type field, check the Nonce for deduplication, and return 200 immediately.
from flask import Flask, request, jsonify
app = Flask(__name__)
processed_nonces = set()
HANDLERS = {
"ClientCreated": handle_client_created,
"AccountCreated": handle_account_created,
"PaymentTransactionSettled": handle_payment_settled,
"PaymentInboundTransaction": handle_inbound_payment,
"MccyOutboundPaymentComplete": handle_eur_outbound_settled,
"MccyInboundPaymentComplete": handle_eur_inbound,
# Add handlers for all event types you need
}
@app.route("/webhooks/mercury", methods=["POST"])
def webhook():
data = request.get_json()
nonce = data.get("Nonce")
# Deduplicate using nonce
if nonce in processed_nonces:
return jsonify({"received": True}), 200
processed_nonces.add(nonce)
event_type = data.get("Type")
payload = data.get("Payload", {})
handler = HANDLERS.get(event_type)
if handler:
handler(payload)
else:
app.logger.warning(f"Unhandled webhook type: {event_type}")
return jsonify({"received": True}), 200const express = require("express");
const app = express();
app.use(express.json());
const processedNonces = new Set();
const handlers = {
ClientCreated: handleClientCreated,
AccountCreated: handleAccountCreated,
PaymentTransactionSettled: handlePaymentSettled,
PaymentInboundTransaction: handleInboundPayment,
MccyOutboundPaymentComplete: handleEurOutboundSettled,
MccyInboundPaymentComplete: handleEurInbound,
// Add handlers for all event types you need
};
app.post("/webhooks/mercury", (req, res) => {
const { Type, Payload, Nonce } = req.body;
// Deduplicate using nonce
if (processedNonces.has(Nonce)) {
return res.status(200).json({ received: true });
}
processedNonces.add(Nonce);
const handler = handlers[Type];
if (handler) {
handler(Payload);
} else {
console.warn(`Unhandled webhook type: ${Type}`);
}
res.status(200).json({ received: true });
});@RestController
public class WebhookController {
private final Set<String> processedNonces =
ConcurrentHashMap.newKeySet();
@PostMapping("/webhooks/mercury")
public ResponseEntity<Map<String, Boolean>> handleWebhook(
@RequestBody Map<String, Object> body) {
String nonce = (String) body.get("Nonce");
// Deduplicate using nonce
if (!processedNonces.add(nonce)) {
return ResponseEntity.ok(Map.of("received", true));
}
String type = (String) body.get("Type");
Map<String, Object> payload =
(Map<String, Object>) body.get("Payload");
switch (type) {
case "ClientCreated" -> handleClientCreated(payload);
case "AccountCreated" -> handleAccountCreated(payload);
case "PaymentTransactionSettled" -> handleSettled(payload);
case "PaymentInboundTransaction" -> handleInbound(payload);
case "MccyOutboundPaymentComplete" -> handleEurOut(payload);
case "MccyInboundPaymentComplete" -> handleEurIn(payload);
default -> log.warn("Unhandled type: {}", type);
}
return ResponseEntity.ok(Map.of("received", true));
}
}Return 200 immediately, then process asynchronously. If your handler throws an error before responding, Mercury will retry. Use a message queue or background job for heavy processing to avoid timeouts.
All 26 events
Events are grouped into three categories: Clients, Accounts, and Payments. The Type column shows the actual value in the payload, which may differ from the event name.
Client events (5)
Account events (7)
GBP payment events (10)
EUR payment events (6)
Example payloads
Representative payloads for the most common events. Full schema details are in the API reference.
Client created
{
"Type": "ClientCreated",
"Payload": {
"ClientType": "Corporate",
"Identifier": "CLIENT-001",
"ExternalReference": "EXT-REF-001",
"ExpectedMonthlySpend": 5000,
"RiskRating": "Low",
"Contacts": [
{
"Identifier": "b7809e48-6407-4b44-b613-921049f2d7e8",
"FirstName": "Jane",
"LastName": "Smith",
"PhysicalAddress": {
"Street": "1 High Street",
"City": "London",
"Region": "Greater London",
"Postcode": "EC1A 1BB",
"CountryCode": "GB"
},
"DateOfBirth": "1985-06-15"
}
],
"CorporateDetails": {
"Name": "Acme Ltd",
"RegistrationNumber": "12345678",
"RegisteredAddress": {
"Street": "1 High Street",
"City": "London",
"Region": "Greater London",
"Postcode": "EC1A 1BB",
"CountryCode": "GB"
},
"IndustryCode": "6419"
}
},
"Nonce": "XrNcCDMvPN3KvfcU4f+U9Q=="
}{
"Type": "ClientStatusChanged",
"Payload": {
"ClientReference": "CLIENT-001",
"Name": "Acme Ltd",
"FromStatus": "Active",
"ToStatus": "Suspended",
"Reason": "AML review initiated"
},
"Nonce": "XrNcCDMvPN3KvfcU4f+U9Q=="
}{
"Type": "PaymentTransactionSettled",
"Payload": {
"Status": "Settled",
"Scheme": "FasterPayments",
"EndToEndTransactionId": "E2E-20250225-001",
"Amount": 1000,
"TimestampCreated": "2025-08-11T08:38:16.757Z",
"TimestampSettled": "2025-08-11T08:38:17.997Z",
"CurrencyCode": "GBP",
"DebitCreditCode": "Debit",
"Reference": "Invoice 2025-001",
"PaymentId": "764b7d22-9fa0-43bb-920e-326e8634b0f8",
"Account": {
"IBAN": "GB71CLRB04082700001159",
"BBAN": "CLRB04082700001159",
"OwnerName": "Acme Ltd",
"TransactionOwnerName": "Acme Ltd",
"InstitutionName": null
},
"CounterpartAccount": {
"IBAN": "GB47MONZ04000423900685",
"BBAN": "MONZ04000423900685",
"OwnerName": "John Smith",
"TransactionOwnerName": "John Smith",
"InstitutionName": "Monzo"
},
"IsReturn": false
},
"Nonce": "j8obouk8aHbZbInEMNVkmg=="
}{
"Type": "PaymentInboundTransaction",
"Payload": {
"TransactionTimestamp": "2025-02-25T10:00:00Z",
"PaymentId": "TXN-1234",
"EndToEndTransactionId": "E2E-20250225-001",
"AccountId": "GB29NWBK60161331926819",
"Debtor": {
"AccountName": "John Smith",
"Iban": "GB60BARC20000055779911",
"Bban": "20000055779911",
"AccountNumber": "55779911",
"SortCode": "200000",
"InstitutionName": "Barclays Bank"
},
"Creditor": {
"Iban": "GB29NWBK60161331926819",
"Amount": 500,
"Currency": "GBP",
"Reference": "Invoice payment"
}
},
"Nonce": "XrNcCDMvPN3KvfcU4f+U9Q=="
}{
"Type": "PaymentTransactionRejected",
"Payload": {
"Status": "Rejected",
"Scheme": "FasterPayments",
"EndToEndTransactionId": "E2E-20250225-001",
"Amount": 1000,
"CurrencyCode": "GBP",
"DebitCreditCode": "Debit",
"Reference": "Invoice 2025-001",
"CancellationReason": "InvalidAccount",
"CancellationCode": "AC03",
"TimestampModified": "2025-02-25T10:06:00Z",
"Account": {
"IBAN": "GB29NWBK60161331926819",
"BBAN": "60161331926819"
},
"CounterpartAccount": {
"BBAN": "20000055779911",
"IBANClosed": "GB60BARC20000055779911"
}
},
"Nonce": "XrNcCDMvPN3KvfcU4f+U9Q=="
}{
"Type": "MccyOutboundPaymentComplete",
"Payload": {
"TransactionId": "085440ed-1326-41f4-b4a6-6188dbd42409",
"EndToEndIdentification": "PPP10d0124",
"InstructedCurrency": "EUR",
"InstructedAmount": 1000,
"TimestampCreated": "2026-02-12T10:22:06",
"TimestampSettled": "2026-02-12T10:45:54.5127358Z"
},
"Nonce": "f9ALRB5rLEpVwm6bQckAYg=="
}{
"Type": "MccyInboundPaymentComplete",
"Payload": {
"TransactionTimestamp": "2026-02-13T10:46:41.6133333+00:00",
"TransactionId": "c6e61fc1-c436-472e-bb2e-803b6a891f71",
"EndToEndIdentification": "Funding",
"AccountId": "GB31CLRB04274817818438",
"Amount": 1000,
"Currency": "EUR",
"Reference": "Funding",
"Debtor": {
"AccountName": "Sender GmbH",
"AccountIdentifiers": [
{ "Identifier": "FR7630006000011234567890189", "Kind": "Iban" }
]
},
"Creditor": {
"AccountName": "Acme Ltd",
"AccountIdentifiers": [
{ "Identifier": "GB31CLRB04274817818438", "Kind": "Iban" }
]
}
},
"Nonce": "t3/6hc08IoPkRTasElnO1Q=="
}Gotchas and best practices
The Type field does not always match the event name. GBP payment events prepend "Payment" (e.g. PaymentTransactionSettled). Route on the actual Type value, not the event name.
The unblock webhook sends Type: "AccountBlocked". Distinguish block from unblock using Payload.Status ("Blocked" vs "UnBlocked"). Do not rely on Type alone.
Mercury may deliver the same webhook more than once (retries after timeouts). Store processed Nonce values and skip duplicates. In production, use a persistent store (Redis, DB) rather than in-memory sets.
Return HTTP 200 before doing heavy processing. Queue the payload for async handling. If your endpoint times out, Mercury will retry, creating duplicate work.
New event types may be added. Your handler should log and return 200 for unrecognised types rather than returning an error, which would trigger unnecessary retries.
The event name and Type both use "Assesment" (single 's'). This is the actual API spelling. Match it exactly in your handler routing.
Next steps
GBP account management
Create and manage Sterling virtual IBANs.
→EUR accountsEUR virtual IBAN management
Create and manage EUR multi-currency accounts.
→Error handlingError handling best practices
Parse errors, implement retries, build resilient integrations.
→View the full webhook reference
See complete schemas and test payloads in the API reference.
Webhook referenceUpdated about 2 hours ago
