Webhooks
The browser told you the payment succeeded — but browsers close, redirects get
lost, and success_url can be typed into an address bar. Fulfill from the
webhook.
Events
Section titled “Events”| Event | Fires when |
|---|---|
checkout_session.completed | The session was confirmed and payment succeeded |
checkout_session.expired | The session expired (or was expired via API) |
{ "webhook_id": "evt_1a2b3c4d5e6f", "event_type": "checkout_session.completed", "timestamp": 1781200800, "data": { "session_id": "cs_3oNkLp9aBcDeFgHi", "status": "completed", "amount": 14227, "payment": { "transaction_id": "txn_…", "total_amount": 14650, "method": "card", "last4": "4242" }, "metadata": { "invoice": "INV-2026-0611" } }}Amounts are integer minor units.
-
Set your account’s
callback_url(HTTPS) and enable webhooks. -
Subscribe to the events you want:
PUT /api/webhooks/subscriptions{"subscriptions": [{ "event_type": "checkout_session.completed", "enabled": true },{ "event_type": "checkout_session.expired", "enabled": true }]} -
Fetch your signing secret (
GET /api/webhooks/secret) and store it with your server secrets.
Verify the signature
Section titled “Verify the signature”Every delivery is signed with HMAC-SHA256 over timestamp + "." + body and
sent in the X-Webhook-Signature header:
const crypto = require('node:crypto');
app.post('/webhooks/govifi', express.raw({ type: 'application/json' }), (req, res) => { const payload = req.body.toString(); const event = JSON.parse(payload);
const expected = crypto .createHmac('sha256', process.env.GOVIFI_WEBHOOK_SECRET) .update(`${event.timestamp}.${payload}`) .digest('hex'); const provided = String(req.headers['x-webhook-signature']).replace(/^sha256=/, '');
if (!crypto.timingSafeEqual(Buffer.from(provided), Buffer.from(expected))) { return res.status(401).send('invalid signature'); } // Reject stale timestamps to block replays (4-hour window). if (Math.abs(Date.now() / 1000 - event.timestamp) > 240 * 60) { return res.status(401).send('timestamp too old'); }
handleEvent(event); // idempotent — deliveries can repeat res.json({ received: true });});Respond 200 quickly — do slow work (emails, ledger posting) after you’ve
acknowledged, not before.
Testing
Section titled “Testing”POST /api/webhooks/test sends a sample event to your callback URL, and
GET /api/webhooks/deliveries shows delivery history with response codes —
useful while wiring up your endpoint.