Webhooks
Receive real-time notifications when events happen in EsperWorks. Set a webhook URL when creating an API key and we'll POST a signed JSON payload to your endpoint.
Events
| Event | Description |
|---|---|
invoice.created | An invoice was created via the API. |
invoice.sent | An invoice was sent to a client. |
invoice.viewed | A client opened an invoice. |
invoice.paid | An invoice was fully paid. |
payment.received | A payment was recorded (includes partial payments). |
contract.signed | A contract was signed. |
Payload format
{
"event": "invoice.paid",
"idempotency_key": "d1c2a4f0-7c8b-4f3a-9d2e-1b6c8f0a3d9e",
"created_at": "2026-05-01T14:22:00Z",
"data": {
"invoice_id": 101,
"invoice_number": "INV-0042",
"total": 3000,
"currency": "GHS",
"client_id": 42,
"paid_at": "2026-05-01T14:22:00Z"
}
}Signature verification
Every webhook request includes an X-EsperWorks-Signature header, a sha256= prefixed HMAC-SHA256 hex digest of the raw request body, signed with your webhook secret. Always verify this before processing the event.
Use the raw request body (before JSON parsing) when computing the HMAC. Parsing and re-serializing JSON can change whitespace and break the signature check.
const crypto = require('crypto');
function verifyWebhook(rawBody, signatureHeader, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signatureHeader),
Buffer.from(expected)
);
}
// Express example
app.post('/webhooks/esperworks', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-esperworks-signature'];
if (!verifyWebhook(req.body, sig, process.env.WEBHOOK_SECRET)) {
return res.status(401).end();
}
const event = JSON.parse(req.body);
console.log(event.event, event.data);
res.status(200).end();
});Retries
If your endpoint returns a non-2xx status or times out (10s), EsperWorks retries up to 4 more times (5 total) with exponential backoff: 10s, 30s, 90s, 4.5m, then 13.5m. Respond with 200 as quickly as possible and process the event asynchronously to avoid timeouts.