Skip to content
You are offline. Some features may be unavailable.

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

EventDescription
invoice.createdAn invoice was created via the API.
invoice.sentAn invoice was sent to a client.
invoice.viewedA client opened an invoice.
invoice.paidAn invoice was fully paid.
invoice.overdueAn invoice passed its due date unpaid.
payment.receivedA payment was recorded (includes partial payments).
payment.failedA payment attempt failed.
contract.signedBoth parties have signed a contract.
recurring_invoice.generatedA 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.