Webhooks are how Valtu tells your server that a deposit arrived or a payout confirmed. They're also an attack surface: anyone can send a request to your endpoint. Three habits make them safe to build on.
1. Check the signature
Every delivery is signed twice, over <timestamp>.<raw body>:
| Header | Check it with |
|---|---|
X-Valtu-Signature-Ed25519 | Your workspace's Ed25519 public key. Nothing secret to store. Recommended. |
X-Valtu-Signature | HMAC-SHA256 with the endpoint's secret, shown once when you add the endpoint. |
Verify either one, and reject anything signed more than 5 minutes ago. Verify against the raw body exactly as received: re-serialising the JSON first changes the bytes and breaks the check.
import { constructEvent } from "@valtu/wallet-kit";
const event = constructEvent(rawBody, req.headers, {
publicKey: process.env.VALTU_WEBHOOK_PUBLIC_KEY,
});
2. De-duplicate, and don't rely on order
Delivery is at least once: if your endpoint doesn't answer with a 2xx within 10 seconds, the delivery is retried. So the same event can arrive more than once, and events can arrive out of order.
- Store each event's
idand skip ones you've already handled. - Act on the status in the event, not on the order events arrive in. A
deposit.confirmedthat arrives beforedeposit.pendingis still confirmed. - Answer quickly, then do the slow work in the background.
3. Reconcile with the API
A webhook is a notification, not the record. Networks fail, deploys happen, endpoints go down. Run a regular catch-up:
GET /v1/deposits?after=2026-10-15T00:00:00Z
GET /v1/transfers?after=2026-10-15T00:00:00Z
Anything you missed is there. The dashboard also keeps every delivery attempt for 30 days, with your endpoint's response, and can resend one delivery or everything that was given up.
Putting it together
Verify, de-duplicate on id, credit on the status, and reconcile. Together they make the ledger correct even when the network between us isn't.
See webhooks in the API guide and the delivery guarantees.