Skip to content

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.

EventFires when
checkout_session.completedThe session was confirmed and payment succeeded
checkout_session.expiredThe 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.

  1. Set your account’s callback_url (HTTPS) and enable webhooks.

  2. 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 }
    ]
    }
  3. Fetch your signing secret (GET /api/webhooks/secret) and store it with your server secrets.

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.

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.