Webhooks
A Webhook is an address of yours that Essentio tells when something happens to a Business’s documents and Payments — a document issued, sent, viewed, paid or cancelled, a Payment recorded or reversed. Each message is signed, so your program knows it came from Essentio, and a message your address does not accept is tried again for three and a half days.
Essentio’s Webhooks follow the Standard Webhooks specification: its headers, its signature and its secrets. Verify a message with one of its libraries rather than by hand.
Adding a Webhook
An Owner or an Admin of the Business adds one in Essentio under Settings → API Keys → Webhooks, with the address and the events it should hear. Its signing secret — whsec_ and 44 characters — is shown once, when it is added; keep it with your program. A Business has at most ten Webhooks, and each has a secret of its own.
The address must be:
https://, on the standard port (443), with no user name, password or#part;- a host name, not an IP address;
- a host every address of which is on the public internet. An address inside a private network, on the machine itself or at a cloud’s metadata service is refused when it is typed — and again at every delivery, since a name can be pointed elsewhere later.
The same Settings card pauses a Webhook, turns it on again, changes its address and events, rolls its secret and deletes it. Below it, Webhook Deliveries lists every message of the last 30 days.
A Webhook of a Test business is delivered like any other: it goes to the address its own developer chose, never to a Client.
An OAuth app’s Webhook
An OAuth app has one Webhook of its own, which hears every Business the app is connected to — so you do not ask each Business to add your address. You, the app’s owner, add it in Essentio under Profile → Apps → Webhooks: choose the app, the address and the events. Its signing secret is shown once, as a Business’s is, and the same rules hold for the address.
- Each message names its Business, in
business_id: the Business’s public id, a UUID that never changes — thebusiness_idthatGET /v1/businessanswers each of the app’s access tokens. Keep it beside the tokens you hold for that Business, and route every message by it. - It hears only what the app may read there. A Business is heard while the app has a Connected app in it that may act — not revoked or disconnected, its person still a member, the Business not deleted, used within the last 90 days — by an API call or a message it took. Every Role may read documents and Payments, so every event is sent. A document’s
actionsare answered for the app’s Role, as the API answers the app’s token — again at every attempt, so a retry sent after the Role was lowered offers only what the app may do then. When several people connected the app to one Business, one message is sent, with the lowest of their Roles. - Listening is using. Every message about a Business your address answers with a
2xxcounts as a use of the app’s connections to that Business, as an API call does. A connection with neither — no API call and no message taken — for 90 days ends, and with it the Webhook’s messages about that Business. A message that failed, was refused or was not sent is not a use, and neither is a message to a Business’s own Webhook. - It stops at once when the app leaves a Business, and forgets what it was sent. Once the app is disconnected, revoked, left behind by its person, unused for 90 days there, or the Business is deleted, nothing more about that Business is sent — not even a retry of a message written before, which is marked failed unsent and cannot be resent — and the bodies of the deliveries about it are erased from Webhook Deliveries: their time, event, status and attempts stay.
- You are emailed, not the Business, when a message fails after its last attempt or your address answers
410 Gone— at most one failure email a day. Deleting the app, or your account, deletes its Webhook with its deliveries.
Events
- Name
document.issued- Type
- document
- Description
A document was issued and took its number — by Issue, by a Send that issued a Draft, by a Payment entered on a Draft, by a Proforma Invoice converted, or through the API.
- Name
document.sent- Type
- document
- Description
The document reached its Client: Essentio emailed it (Send, in Essentio or through the API), or someone marked it sent by hand in Essentio. Taking the mark back raises nothing.
- Name
document.viewed- Type
- document
- Description
The Client opened the document’s page for the first time. Later opens raise nothing.
- Name
document.paid- Type
- document
- Description
The document was paid in full: its Amount due reached zero, by a Payment or by the Payments on the Proforma Invoice it was converted from.
- Name
document.cancelled- Type
- document
- Description
The document was cancelled. It keeps its number.
- Name
payment.recorded- Type
- payment
- Description
A Payment was recorded against a document, with its Receipt.
- Name
payment.reversed- Type
- payment
- Description
A Payment was reversed: it is
voided, and its Receipt keeps its number with avoided_at.
An event is raised once the change it describes is saved; a change that fails raises nothing. One change can raise two: recording the Payment that settles an Invoice raises payment.recorded and document.paid. Messages are not ordered — two of them may arrive in either order — so read the state from the message, not from the order they came in.
A message carries its document or Payment as it stands when the message is built, after the change is committed — so it already shows whatever else the same save did. A Draft saved in Essentio with a Payment is issued and paid in one save: its document.issued carries the document paid, and a document.paid follows.
Inside v1 events are added and never renamed or removed. Ignore a type you do not know.
The message
Each event is a POST to the address, with a JSON body of four fields:
- Name
type- Type
- string
- Description
The event, such as
document.paid.
- Name
timestamp- Type
- string
- Description
When it happened, in UTC (ISO 8601, with microseconds).
- Name
business_id- Type
- string
- Description
The Business the message is about: its public id, a UUID — the
business_idGET /v1/businessanswers. A Business’s own Webhook always receives its own; an OAuth app’s (above) hears every Business the app is connected to, and routes by it.
- Name
data- Type
- object
- Description
{"document": …}for adocument.event,{"payment": …}for apayment.one: the document or the Payment as it stands when the message is built, after the change is committed, exactly as the API answers it — money as decimal strings. A document’sactionsare answered as they would be to an Admin key on a Business’s Webhook, and to the app’s token on an OAuth app’s, at each attempt.
payment.recorded
{
"type": "payment.recorded",
"timestamp": "2026-09-27T10:00:00.123456Z",
"business_id": "5c92e0ef-5111-492f-af6e-9dcbafc9587a",
"data": {
"payment": {
"id": 57,
"document_id": 118,
"status": "completed",
"amount": "100.00",
"currency": "EUR",
"payment_date": "2026-09-25",
"payment_method": "bank_transfer",
"bank_account_id": 3,
"reference": "SEPA 881",
"notes": null,
"recorded_by": {"type": "api_key", "name": "Bank sync"},
"amended_by": null,
"receipt": {
"id": 41,
"receipt_number": "R-0041",
"payment_id": 57,
"document_id": 118,
"amount": "100.00",
"currency": "EUR",
"payment_date": "2026-09-25",
"payment_method": "bank_transfer",
"reference": "SEPA 881",
"issue_date": "2026-09-27",
"voided_at": null,
"voided_by": null,
"created_at": "2026-09-27T10:00:00+00:00",
"updated_at": "2026-09-27T10:00:00+00:00"
},
"created_at": "2026-09-27T10:00:00+00:00",
"updated_at": "2026-09-27T10:00:00+00:00"
}
}
}
The contract declares every event under webhooks in the OpenAPI file, with the body as DocumentEvent or PaymentEvent.
The headers
- Name
webhook-id- Type
- string
- Description
The message’s id:
msg_and 26 characters. It is the same on every retry and every resend of the message, and for every Webhook of the Business that heard it with the same body. Keep the ids you have handled and handle a message once.
- Name
webhook-timestamp- Type
- string
- Description
When this attempt was made, in Unix seconds. A retry carries its own.
- Name
webhook-signature- Type
- string
- Description
v1,and the base64 of the HMAC-SHA256 of{webhook-id}.{webhook-timestamp}.{body}under the secret — the bytes the base64 afterwhsec_decodes to. For a day after the secret is rolled there are two, separated by a space: one with the new secret and one with the old.
Verifying a message
Verify the signature against the body exactly as it arrived, before parsing it: a body parsed and written out again is not the one that was signed. The Standard Webhooks libraries take the secret as Essentio shows it, whsec_ included, and also refuse a webhook-timestamp more than five minutes from your clock, so a message recorded and sent again later is refused.
Verify a message
// npm install standardwebhooks
import { Webhook } from 'standardwebhooks'
const webhook = new Webhook(process.env.ESSENTIO_WEBHOOK_SECRET)
// `body` is the raw request body, a string; `headers` the request's headers
const message = webhook.verify(body, headers) // throws when the signature is wrong
Answering
Answer with any 2xx status within 15 seconds; do the work after answering. What you answer is not read beyond its status, and the first kilobyte of its body is kept for Webhook Deliveries.
Anything else is a failure: another status, a redirect (it is not followed), no answer within 15 seconds, or no connection. A failed message is tried again after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours, 14 hours, 20 hours and 24 hours — ten attempts in all, each wait lengthened by up to a tenth so many retries do not arrive at once — and marked failed after the tenth.
Answering 410 Gone pauses the Webhook: nothing more is sent to it until an Owner or Admin turns it on again in Settings — or, for an OAuth app’s, its owner in Profile → Apps.
Deliveries and Resend
Webhook Deliveries, under the Webhooks in Settings, lists every message of the last 30 days: when, the event, the address, whether it was delivered, is being retried (and when next) or failed, and each attempt — its time, how long it took, the status or why there was none, and the start of the answer. Viewing one shows the message exactly as it was sent.
Resend sends a stored message again as a new delivery, with the same body and the same webhook-id, and retries it on the same schedule. Deliveries are erased after 30 days.
Webhooks need a paid plan. While the Business a message is about is on the Free plan or Paused, nothing about it is sent — to its own Webhooks or to an OAuth app’s: a change raises no delivery, and a retry already waiting is marked failed unsent, saying why, with its message kept. The Webhooks are not deleted or paused: once the Business is on Pro or Pro Plus again they deliver as before, to the same addresses with the same secrets, and a message failed that way can be resent (a resend before then is refused with webhooks_need_a_paid_plan).
Rolling the secret
Roll the secret in Settings makes a new one, shown once. For the next 24 hours every message is signed with both, so you can change the secret in your program without missing one; after that only the new secret signs.