Skip to main content

Receiving Webhooks in Production

Everything here follows from two facts about how BlockWyre delivers: a delivery attempt times out after 30 seconds, and a delivery that fails six attempts in a row — the first and five retries, over roughly 7 to 14 minutes — disables the target, which then stops receiving anything.

A handler that is merely slow eventually becomes a handler that receives nothing.

Acknowledge first, work afterwards​

Verify the signature, answer 200, and do the real work outside the request. Anything you do before responding is spending your 30 seconds.

Verify against the raw body: register the route with the raw bytes (express.raw), not a JSON parser. See Validating Webhooks for verifyWebhook.

app.post("/webhook", express.raw({ type: "application/json" }), async (req, res) => {
const event = await verifyWebhook(req.body, req.headers); // fast, and must happen first
res.status(200).send("OK"); // acknowledge
await queue.add("process-webhook", event);
});

Expect the same webhook twice​

A retry can arrive after your handler already succeeded — a response lost on the way back looks identical to a handler that never ran. Deduplicate on webhookId: it is unique per event per target, and stays the same across that event's retries and resends. It is also your replay protection, because it is part of the signed body.

async function processWebhook(payload) {
const { webhookId } = payload;

if (await db.webhooks.findOne({ webhookId })) return;

await handleWebhookEvent(payload);
await db.webhooks.create({ webhookId, processedAt: new Date() });
}

Record the id in the same transaction as the work it guards. Recorded before the work, a crash in between loses the event; recorded after it, a crash in between processes the event twice. Keep processed ids for at least as long as you would want to refuse a replay.

Do not count on the timestamp for replay protection​

X-BlockWyre-Timestamp carries the time of the delivery attempt in Unix seconds, but it is not covered by the signature, and BlockWyre sets it fresh on every attempt. A replayed request can carry any timestamp, so a time window does not stop replays: deduplicating on webhookId does.

Use the header only as a freshness hint — for example, to log or alert on deliveries that arrive late:

const timestamp = parseInt(headers["x-blockwyre-timestamp"], 10);
const now = Math.floor(Date.now() / 1000);

if (Math.abs(now - timestamp) > 300) {
logger.warn("webhook attempt arrived late or with clock skew", { webhookId: event.webhookId });
}

Do not reject on the body's timestamp either: it is when the event occurred, and a retry or a resend keeps it.

Fail on the signature, not on your own bugs​

These two failures deserve opposite answers:

What failedRespondWhy
Signature verification401The request is not ours. Retrying will not change that
Your own processing200Retrying replays a payload your code already could not handle, and six failed attempts disable the target

Answer 200 and route the failure to your own alerting. Spending delivery attempts on a bug in your handler costs you unrelated events later.

app.post("/webhook", express.raw({ type: "application/json" }), async (req, res) => {
let event;
try {
event = await verifyWebhook(req.body, req.headers);
} catch {
return res.status(401).send("Unauthorized");
}

res.status(200).send("OK");

try {
await processWebhook(event);
} catch (error) {
logger.error("webhook processing failed", { error, webhookId: event.webhookId });
}
});

A 401 is a failed attempt like any other, so a webhook BlockWyre really sent but your code fails to verify — for example, checked against the wrong environment's JWKS — is retried and can disable the target. Make sure verification works before you go live.

Watch the target, not just the handler​

Your logs show what arrived. They cannot show what stopped arriving. Poll GET /api/v2/webhooks/targets and alert on disabledAt appearing — that field is the difference between a quiet day and a silently disabled integration.

Worth tracking as well: delivery success rate, handler processing time, and X-BlockWyre-Retry-Count. A retry count climbing above zero is the warning before the disable.

Testing locally​

Targets must be reachable from the internet, so expose your local server with a tunnel:

ngrok http 3000

Register the tunnel URL as a target, and remember to delete it afterwards — an expired tunnel is a failing target, and one delivery that fails all six attempts disables it.

Verify your signature code against the sandbox JWKS (https://api-sandbox.blockwyre.xyz/.well-known/jwks.json) before going near production, and switch to the production JWKS (https://api.blockwyre.com/.well-known/jwks.json) for production targets: each environment signs with its own key. See Validating Webhooks.

Troubleshooting​

SymptomCause
Deliveries stopped entirelyThe target was disabled after a delivery failed all six attempts. Check disabledAt, fix the endpoint, then re-enable it — see Managing Webhook Targets
Signature verification failsThe signature is over the raw body. A framework that parses and re-serializes JSON changes the bytes and invalidates it. Capture the raw body before any parsing middleware. Also check you use the JWKS of the environment that sent the webhook: sandbox and production sign with different keys
Some events never arriveA target created or edited in the dashboard receives only its selected categories: check them under Notifications › Webhook. A target created through /api/v2/webhooks/targets receives every event. Some events do not exist: there are no card events, and no separate event for an incoming deposit (it arrives as crypto_transaction_created or bank_transaction_created with details.type deposit). Otherwise reconcile against the API
X-BlockWyre-Retry-Count keeps climbingYour endpoint is answering outside 2xx/3xx, or taking longer than 30 seconds
Nothing arrives at a new targetConfirm the URL resolves to a public address (private and internal addresses are refused), presents a valid certificate, and redirects at most 3 times

Support and Resources​

If you need assistance or have any questions, our support team is here to help. You can contact our support team at support@blockwyre.com.

Stay Updated​

Stay up-to-date with the latest news, updates, and features from BlockWyre by following us on social media:

We are excited to have you on board and look forward to seeing how you leverage BlockWyre's powerful tools to enhance your financial operations. Happy integrating!