How to build reliable webhook delivery for agent payments
A payment API is only useful if the product hears about money movement reliably. This guide shows how to receive Viaclave webhooks in a normal backend without relying on MCP or manual polling.
What you are building
You are building a webhook receiver for a bot platform, marketplace, or SaaS backend that needs to react when payments complete, deposits are credited, withdrawals change state, or a transfer fails.
The receiver must be boring and strict. It should verify signatures, reject malformed payloads, write every event once, and process business logic asynchronously. The goal is not to make a clever webhook handler. The goal is to make sure a missed HTTP request does not become a missed payment.
Why this has ROI
Without webhooks, teams often poll payment status on a timer. Polling wastes requests, adds delay, and still misses edge cases when jobs crash. A reliable webhook pipeline lets the rest of the product react immediately: unlock access, update balances, notify users, or retry settlement.
The operational savings show up in support. If a user pays and the product does not update, someone has to investigate. Logging and replaying webhooks turns those incidents into a searchable event history instead of a guessing game.
Receiver table
Store the raw event before doing product logic. This gives you a replay point if your downstream code fails after accepting the webhook.
type WebhookEventRecord = {
eventId: string;
eventType: string;
signature: string;
rawBody: string;
receivedAt: string;
processedAt: string | null;
processingError: string | null;
};Verify the signature
Treat unsigned webhooks as untrusted internet traffic. Verify the HMAC signature against the exact raw request body before parsing JSON or updating state.
import crypto from "node:crypto";
function verifyWebhookSignature(rawBody: string, signature: string, secret: string) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature, "hex"),
Buffer.from(expected, "hex"),
);
}Handle the request
A good receiver acknowledges only after the event is durably stored. Product logic can run in a queue, worker, or background job after the response.
async function handleWebhook(request: Request) {
const rawBody = await request.text();
const signature = request.headers.get("x-viaclave-signature") || "";
if (!verifyWebhookSignature(rawBody, signature, process.env.WEBHOOK_SECRET!)) {
return new Response("invalid signature", { status: 401 });
}
const event = JSON.parse(rawBody);
await saveWebhookEvent({
eventId: event.id,
eventType: event.type,
signature,
rawBody,
receivedAt: new Date().toISOString(),
processedAt: null,
processingError: null,
});
return new Response("ok");
}Processing rules
- Make event IDs unique in the database so retries do not duplicate work.
- Process events in a background queue when possible.
- Record the business object updated by each event, such as payment ID or withdrawal ID.
- Keep a dead-letter state for events that fail repeatedly.
- Build an admin replay button before you need it during an incident.
Production checklist
- Use HTTPS only.
- Verify signatures with the raw body.
- Store raw payloads for audit and replay.
- Return quickly and process heavy work asynchronously.
- Alert on repeated failures, signature failures, and dead-letter growth.
Build this workflow in test mode
Create a test API key, connect the MCP server, or call the REST API directly. Viaclave's test mode lets you try wallet creation and test stablecoin payments without real funds.