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.

1

Event occurs

Client created, payment settled, account blocked, etc.

2

Mercury POSTs

JSON payload sent to your registered URL

3

You respond 200

Acknowledge receipt. Body content ignored.

4

Or Mercury retries

Non-200 responses trigger retry with backoff

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"
}
Type

Event identifier string. Use this to route to the correct handler. Note: Type values do not always match the event name (see gotchas).

Payload

Event-specific data. Structure varies by event type. Always contains enough context to identify the affected resource.

Nonce

Unique value per delivery. Store processed nonces and skip duplicates to ensure idempotent handling.

Registering your endpoint

Engineering input needed

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

Engineering input needed

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.

Engineering input needed

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}), 200
const 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));
    }
}
Best practice

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)

Event Type value Description
ClientCreated ClientCreated New client onboarded. Payload includes corporate details, contacts, risk rating.
ClientUpdated ClientUpdated Client details modified (name, address, contacts, etc.).
ClientStatusChanged ClientStatusChanged Status transition (e.g. Active to Suspended). Includes FromStatus, ToStatus, and Reason.
ClientRiskRatingChanged ClientRiskRatingChanged Client risk rating updated (Low, Medium, High, etc.).
ClientAssignedAccount ClientAssignedAccount Virtual IBAN linked to client. Includes IBAN and BBAN identifiers.

Account events (7)

Event Type value Description
AccountCreated AccountCreated Virtual IBAN provisioned and active (GBP or EUR).
AccountCreationFailed AccountCreationFailed Provisioning failed. Payload includes Errors object with details.
AccountUpdated AccountUpdated Account details modified.
AccountUpdatingFailed AccountUpdatingFailed Account update operation failed.
AccountBlocked AccountBlocked Account frozen. Status = "Blocked".
AccountUnBlocked AccountBlocked Type says "AccountBlocked". Check Status = "UnBlocked".
AccountClosed AccountClosed Account permanently closed. Irreversible.

GBP payment events (10)

Event Type value Description
TransactionSettled PaymentTransactionSettled Outbound FPS payment confirmed settled.
TransactionRejected PaymentTransactionRejected Payment rejected. Includes CancellationReason and CancellationCode.
PaymentStatusChanged PaymentStatusChanged Status transition during processing (e.g. Pending to Approved).
PaymentValidationFailed PaymentValidationFailed Scheme-level validation failed post-submission. Check ValidationFailure array.
PaymentAssesmentFailed PaymentAssesmentFailed Compliance assessment failed. Note: "Assesment" spelling is intentional in the API.
InboundTransaction PaymentInboundTransaction Inbound FPS payment received and credited.
InboundTransactionHeld PaymentInboundTransactionHeld Incoming payment held for review before crediting.
OutboundTransactionHeld PaymentOutboundTransactionHeld Outbound payment held for review before release.
InboundInternalTransaction PaymentInboundInternalTransaction Internal transfer received (between Mercury accounts).
InternalTransferSettled PaymentInternalTransferSettled Internal transfer between Mercury accounts settled.

EUR payment events (6)

Event Type value Description
MccyOutboundPaymentComplete MccyOutboundPaymentComplete Outbound SEPA CT settled.
MccyOutboundPaymentFailed MccyOutboundPaymentFailed Outbound SEPA CT failed.
MccyInboundPaymentComplete MccyInboundPaymentComplete Inbound SEPA CT received and credited.
MccyInboundInternalTransaction MccyInboundInternalTransaction Internal EUR transfer received.
MccyPaymentStatusChanged MccyPaymentStatusChanged EUR payment status transition.
MccyInternalTransferSettled MccyInternalTransferSettled Internal EUR transfer settled.

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=="
}

Client status changed

{
  "Type": "ClientStatusChanged",
  "Payload": {
    "ClientReference": "CLIENT-001",
    "Name": "Acme Ltd",
    "FromStatus": "Active",
    "ToStatus": "Suspended",
    "Reason": "AML review initiated"
  },
  "Nonce": "XrNcCDMvPN3KvfcU4f+U9Q=="
}

Outbound GBP payment settled

{
  "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=="
}

Inbound GBP payment received

{
  "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=="
}

Payment rejected

{
  "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=="
}

EUR outbound SEPA settled

{
  "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=="
}

EUR inbound SEPA received

{
  "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

Type vs event name

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.

AccountUnBlocked Type field

The unblock webhook sends Type: "AccountBlocked". Distinguish block from unblock using Payload.Status ("Blocked" vs "UnBlocked"). Do not rely on Type alone.

Idempotency via Nonce

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.

Respond fast

Return HTTP 200 before doing heavy processing. Queue the payload for async handling. If your endpoint times out, Mercury will retry, creating duplicate work.

Handle unknown types

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.

PaymentAssesmentFailed spelling

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

View the full webhook reference

See complete schemas and test payloads in the API reference.

Webhook reference

Did this page help you?