Webhooks
Handling deliveries
Process incoming events reliably.
Rules
- Verify the signature first.
- Return 2xx within 30 seconds. Queue heavy work for a background job.
- Be idempotent on
webhook_id— duplicate deliveries must be safe.
Example
JavaScript
app.post('/webhooks/harpoon', express.raw({ type: 'application/json' }), async (req, res) => {
const signature = req.headers['x-harpoon-signature'];
const timestamp = parseInt(req.headers['x-harpoon-timestamp'], 10);
const payload = req.body.toString();
try {
verifyWebhookSignature(payload, signature, timestamp, process.env.WEBHOOK_SECRET);
const event = JSON.parse(payload);
if (await alreadyProcessed(event.webhook_id)) {
return res.status(200).json({ received: true });
}
switch (event.event) {
case 'transaction.completed':
if (event.data.status === 'SUCCESS') {
await fulfillOrder(event.data.meta.order_id);
} else if (event.data.status === 'PARTIAL') {
await notifyPartialPayment(event.data);
} else if (event.data.status === 'OVERPAID') {
await fulfillOrderAndLogOverpayment(event.data);
}
break;
case 'transaction.failed':
await notifyPaymentFailed(event.data);
break;
case 'transaction.expired':
await cancelOrder(event.data.meta.order_id);
break;
}
await markProcessed(event.webhook_id);
res.status(200).json({ received: true });
} catch (error) {
res.status(400).json({ error: error.message });
}
});Reading transaction.completed
SUCCESS, PARTIAL, and OVERPAID all arrive on transaction.completed. Branch on data.status:
| Status | Meaning |
|---|---|
SUCCESS | Exact match |
PARTIAL | Underpaid — see data.difference |
OVERPAID | Overpaid — see data.difference |