Receive real-time notifications when resources in your Mercury account change by configuring webhook endpoints to listen for specific events.
Webhooks are currently not available in the sandbox environment.
The Webhooks API allows you to receive real-time HTTP notifications when changes occur to your Mercury resources, like Transactions. Instead of polling the Events API, webhooks push updates directly to your server as they happen.
How Webhooks Work
When a resource changes in your Mercury account, we'll send an HTTP POST request to your configured webhook endpoint containing the event data. Webhooks use the same event structure as the Events API, following the JSON Merge Patch standard (RFC 7396). Each webhook delivery contains the same event structure as the Events API:
{
"id": "bfa85eaa-afab-11f0-8fea-17d650f2306e",
"resourceType": "transaction",
"resourceId": "1d3042b6-af63-11f0-89d2-3503f2fcfef7",
"operationType": "update",
"resourceVersion": 2,
"occurredAt": "2025-01-01T00:00:00.000000Z",
"changedPaths": [
"status",
"postedAt"
],
"mergePatch": {
"postedAt": "2025-01-01T00:00:00.000000+00:00",
"status": "sent"
},
"previousValues": {
"postedAt": null,
"status": "pending"
}
}See the Events API documentation for complete details on event field descriptions and supported resources.
Webhook Endpoint Configuration
Each webhook endpoint can be configured with:
-
url: The HTTPS URL where webhook events will be delivered. -
eventTypes: Optional array of event types to subscribe to (e.g.,["transaction.created", "transaction.updated"]). If omitted, you'll receive all event types. -
filterPaths: Optional array of resource field paths to filter by (e.g.,["status", "amount"]). When specified, webhooks are only sent when one of these fields changes. If omitted, all changes trigger webhooks. -
status: The current state of the webhook endpoint:"active"- Receiving events normally"paused"- Temporarily stopped, no events will be sent
Webhook Event Types
Currently supported event types:
transaction.created: Fired when a new transaction is created in your accounttransaction.updated: Fired when an existing transaction is modified in your accountcheckingAccount.balance.updated: Fired when available balance, current balance, and/or in flight balance is updated in your checking accountsavingsAccount.balance.updated: Fired when available balance, current balance, and/or in flight balance is updated in your savings accounttreasuryAccount.balance.updated: Fired when available balance, current balance, and/or in flight balance is updated in your treasury accountcreditAccount.balance.updated: Fired when available balance, current balance, and/or in flight balance is updated in your credit accountinvestmentAccount.balance.updated: Fired when available balance, current balance, and/or in flight balance is updated in your investment account
Verifying Webhook Signatures
Every webhook request includes a signature in the Mercury-Signature header. You should always verify this signature to ensure the request is legitimately from Mercury and hasn't been tampered with.
The signature is computed as an HMAC-SHA256 hash using:
- Key: Your webhook endpoint's
secretKey - Message:
<timestamp>.<request_body>where timestamp is a Unix timestamp (seconds since epoch)
The Mercury-Signature header format is: t=<timestamp>,v1=<signature>
t= Unix timestamp (seconds since epoch) when the webhook was sentv1= Hex-encoded HMAC-SHA256 signature
To verify:
- Extract the timestamp and signature from the header
- Construct the signed payload:
timestamp.request_body - Compute HMAC-SHA256 using your secret key
- Compare the computed signature with the received signature using constant-time comparison
You must use the raw request body exactly as received when verifying signatures. Do not parse and re-serialize the JSON, as even minor formatting differences (whitespace, key ordering) will cause signature verification to fail.
To prevent replay attacks, we recommend rejecting webhooks with timestamps older than 5 minutes. Compare the
tvalue from the signature header against your server's current time and reject requests where the difference exceeds your tolerance threshold.
import crypto from 'crypto';
function verifyWebhookSignature(
payload: string,
signatureHeader: string,
secretKey: string
): boolean {
// Parse the signature header (format: "t=<timestamp>,v1=<signature>")
const parts = signatureHeader.split(',');
const timestamp = parts[0]?.split('=')[1];
const signature = parts[1]?.split('=')[1];
if (!timestamp || !signature) {
return false;
}
// Construct the signed payload
const signedPayload = `${timestamp}.${payload}`;
// Compute HMAC-SHA256 using the secret key directly
const expectedSignature = crypto
.createHmac('sha256', secretKey)
.update(signedPayload)
.digest('hex');
// Use constant-time comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}const crypto = require('crypto');
function verifyWebhookSignature(payload, signatureHeader, secretKey) {
// Parse the signature header (format: "t=<timestamp>,v1=<signature>")
const parts = signatureHeader.split(',');
const timestamp = parts[0]?.split('=')[1];
const signature = parts[1]?.split('=')[1];
if (!timestamp || !signature) {
return false;
}
// Construct the signed payload
const signedPayload = `${timestamp}.${payload}`;
// Compute HMAC-SHA256 using the secret key directly
const expectedSignature = crypto
.createHmac('sha256', secretKey)
.update(signedPayload)
.digest('hex');
// Use constant-time comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}import hashlib
import hmac
def verify_webhook_signature(payload: str, signature_header: str, secret_key: str) -> bool:
# Parse the signature header (format: "t=<timestamp>,v1=<signature>")
parts = signature_header.split(',')
timestamp = None
signature = None
for part in parts:
key, _, value = part.partition('=')
if key == 't':
timestamp = value
elif key == 'v1':
signature = value
if not timestamp or not signature:
return False
# Construct the signed payload
signed_payload = f"{timestamp}.{payload}"
# Compute HMAC-SHA256 using the secret key directly
expected_signature = hmac.new(
secret_key.encode('utf-8'),
signed_payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
# Use constant-time comparison to prevent timing attacks
return hmac.compare_digest(signature, expected_signature)require 'openssl'
def verify_webhook_signature(payload, signature_header, secret_key)
# Parse the signature header (format: "t=<timestamp>,v1=<signature>")
parts = signature_header.split(',')
timestamp = nil
signature = nil
parts.each do |part|
key, value = part.split('=', 2)
case key
when 't'
timestamp = value
when 'v1'
signature = value
end
end
return false if timestamp.nil? || signature.nil?
# Construct the signed payload
signed_payload = "#{timestamp}.#{payload}"
# Compute HMAC-SHA256 using the secret key directly
expected_signature = OpenSSL::HMAC.hexdigest('SHA256', secret_key, signed_payload)
# Use constant-time comparison to prevent timing attacks
OpenSSL.secure_compare(signature, expected_signature)
endBest Practices
Responding to Webhooks
Your endpoint should respond with a 2xx status code as soon as possible upon receiving an event. If we receive no response within 5 seconds or we receive a non-2xx status code, Mercury will mark the event delivery as failed and retry up to 10 times using exponential backoff over the course of approximately one day. If we receive a 4xx status code, we will not retry with the exception of 429 (rate limited), which we will retry.
Handling Duplicate Events
Webhooks are delivered at-least-once, meaning you may occasionally receive the same event multiple times. Use the event's id field to track which events you've already processed and implement idempotency. This also protects against replay attacks, where an attacker might attempt to re-send a previously captured webhook request.
Filtering Events
Use eventTypes and filterPaths to reduce noise and only receive events you care about.
Verifying Your Endpoints
After creating a webhook endpoint, use VerifyWebhook to test your webhook configuration before going live. This sends a test event to your endpoint and confirms it's reachable and properly configured.

