Developers
Webhooks
Webhooks let Kumry tell your systems when something happens in your books. Kumry sends a signed JSON POST to an HTTPS endpoint you choose, and retries it if your endpoint does not answer.
Setting up an endpoint
- An owner or admin opens Settings → Integrations and adds a webhook: a name, an
https://URL, and the event types to receive. - Kumry asks them to confirm with multi-factor authentication, because the endpoint will receive the organisation's data.
- Kumry shows the signing secret once. It starts with
whsec_. Store it with your other secrets; Kumry keeps it encrypted and cannot show it again. You can rotate it at any time.
The URL must be HTTPS and must resolve to a public address. Kumry checks the address again when it delivers and does not follow redirects.
What Kumry sends
Each delivery is a POST with a JSON body and these headers:
| Header | Value |
|---|---|
| kumry-signature | t=<unix seconds>,v1=<hex HMAC-SHA256> |
| kumry-event-id | Stable identifier of the event. Use it to ignore duplicates. |
| kumry-event-type | The event type, for example invoicing.invoice.paid |
| kumry-delivery-id | Identifier of this delivery to this endpoint |
| kumry-attempt | Attempt number, starting at 1 |
| user-agent | Kumry-Webhooks/1 |
| content-type | application/json |
{
"id": "6f1c2a8e-…", // same as the kumry-event-id header
"type": "invoicing.invoice.paid",
"createdAt": "2026-10-01T03:12:45.120Z",
"tenantId": "…", // your organisation
"attempt": 1,
"data": { … } // the event's details
}Verifying the signature
The kumry-signature header holds a Unix timestamp t and a signature v1. The signature is the hex HMAC-SHA256, keyed with your signing secret, of the timestamp, a full stop, and the raw request body: {t}.{body}.
- Read the raw body before parsing it; re-serialised JSON will not match.
- Reject the request if the timestamp is more than five minutes from your clock.
- Compute the HMAC and compare it to
v1in constant time.
import crypto from 'node:crypto';
// rawBody: the request body exactly as received (a string, before JSON.parse)
export function verifyKumrySignature(secret, rawBody, header, toleranceSeconds = 300) {
const parts = Object.fromEntries(
(header ?? '').split(',').map((part) => part.trim().split('=')),
);
const timestamp = Number(parts.t);
if (!Number.isSafeInteger(timestamp)) return false;
if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`, 'utf8')
.digest('hex');
const received = String(parts.v1 ?? '');
return received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
}Responding and retries
Answer with any 2xx status within 10 seconds to acknowledge the delivery. Do the real work after you respond. Anything else, including a timeout or a redirect, counts as a failed attempt.
After a failed attempt, Kumry tries again after:
- 2. 30 seconds
- 3. 2 minutes
- 4. 10 minutes
- 5. 1 hour
- 6. 6 hours
If the sixth attempt fails, the delivery is marked dead and not retried. If an endpoint fails 20 times in a row, Kumry disables that webhook and records why; an owner or admin can re-enable it once the endpoint is fixed. The delivery history for each webhook is in Settings → Integrations.
Because an event can arrive more than once (for example if your endpoint processed it but timed out), use kumry-event-id to skip events you have already handled.
Event types
Invoices and credit notes
invoicing.invoice.sentA sales invoice was sent to its customerinvoicing.invoice.paidA sales invoice was fully paidinvoicing.invoice.payment_recordedA payment was recorded against a sales invoiceinvoicing.invoice.updatedA sales invoice was changedinvoicing.invoice.voidedA sales invoice was voidedinvoicing.credit_note.createdA customer credit note was createdinvoicing.credit_note.approvedA customer credit note was approvedinvoicing.credit_note.allocatedA customer credit note was allocated to an invoice
Bills and payments
bills.bill.approvedA supplier bill was approved for paymentbills.bill.paidA supplier bill was paidbills.bill.payment_recordedA payment was recorded against a supplier billbills.bill.rejectedA supplier bill was rejectedbills.bill.voidedA supplier bill was voidedbills.payment_run.authorisedA supplier payment run was authorisedbills.payment_run.completedA supplier payment run completedbills.supplier_credit.approvedA supplier credit note was approved
Banking
banking.reconciliation.completedA bank reconciliation was completed
Expenses and close
expenses.expense.submittedAn expense claim was submittedexpenses.expense.approvedAn expense claim was approvedexpenses.expense.rejectedAn expense claim was rejectedclose.period.completedAn accounting period was closedclose.period.reopenedA closed accounting period was reopened
Projects and inventory
projects.budget.exceededA project exceeded its budgetinventory.goods_receipt.postedGoods were received against a purchase order
API access is available on request
Organisation owners and admins create API keys and webhooks under Settings → Integrations. If you are building an integration and want to talk it through first, get in touch.

