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. |
invoice.overdue | An invoice passed its due date unpaid. |
payment.received | A payment was recorded (includes partial payments). |
payment.failed | A payment attempt failed. |
contract.signed | Both parties have signed a contract. |
recurring_invoice.generated | A recurring invoice generated a new invoice. |
Payload format
{
"event": "invoice.paid",
"created_at": "2026-05-01T14:22:00Z",
"data": {
"invoice_id": 101,
"invoice_number": "INV-0042",
"total": 3000,
"currency": "GHS",
"client": { "id": 42, "name": "Kofi Mensah", "email": "kofi@example.com" },
"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 2 more times (3 total) with fixed delays of 60s then 120s. Respond with 200 as quickly as possible and process the event asynchronously to avoid timeouts.