Guide to Exact Online Webhooks: Features and Best Practices
Exact Online webhooks notify your systems when accounting data changes in a division: an account, item, financial transaction, or stock position is created, updated, or deleted. If you're integrating with Exact Online, webhooks are how you react to changes without polling the REST API.
This guide covers how Exact Online webhooks work, the topics you'll subscribe to, its unusual verification model (the signature is a field inside the body), the thin payloads, and the best practices for production.
What are Exact Online webhooks?
Exact Online webhooks are HTTP POSTs delivered to a CallbackURL you subscribe per topic (per division). The verification model is unusual: there's no signature header. The POST body is {"Content": {...}, "HashCode": "<hex>"}, and you verify by recomputing an HMAC over the Content object and comparing it to the HashCode field.
Payloads are thin (they name the changed entity, not its data), so you fetch the full record from the REST API, and the payload hands you a ready-made ExactOnlineEndpoint URL to fetch it with.
Exact Online webhook features
| Feature | Details |
|---|---|
| Configuration | OAuth2: POST /api/v1/{division}/webhooks/WebhookSubscriptions (Topic + CallbackURL) |
| Scope | Per division (company); one subscription per topic per app |
| Verification | HashCode field in the body (HMAC-SHA256 over the Content JSON); no signature header |
| Encoding | Hex, uppercase; key is the app webhook secret |
| Payload | Thin Content: Topic, Action (Create/Update/Delete), Key (GUID), Division, ClientId, ExactOnlineEndpoint (ready-made fetch URL), EventCreatedOn |
| Creation validation | An empty POST (content-length 0) arrives ~1s after subscription creation, return 2xx and don't choke on it |
| OAuth token | Access token lifetime ~600s; refresh is rate-limited while the current token is still valid |
| Delivery | Return 2xx quickly; Exact retries failed deliveries |
| SDK | None |
Common topics
Confirmed Exact Online topics include:
| Topic | Fires when |
|---|---|
FinancialTransactions | A financial transaction changes |
Accounts | An account (customer/supplier) changes |
Items | An item changes |
StockPositions | A stock position changes |
GoodsDeliveries | A goods delivery changes (supports near-instant delivery via IsInstant) |
Contacts | A contact changes |
Exact's docs list around 30 topics; the six above are confirmed. Subscribe to the topics you process (one subscription per topic per app), and consult the official topics list for the full, exact spellings.
Setting up Exact Online webhooks
Subscribe with an OAuth2 bearer token via POST /api/v1/{division}/webhooks/WebhookSubscriptions, passing a Topic and a CallbackURL. Subscriptions are scoped per division (company). Set your app's Webhook secret in the Exact App Center when registering the OAuth app, that's the HMAC key.
Handle the creation validation POST. About a second after you create the subscription, Exact sends an empty POST (content-length 0) to your CallbackURL to validate it. Return a 2xx and make sure your handler doesn't choke on a body with no Content/HashCode, verification should be skipped for it.
Securing Exact Online webhooks
There's no signature header. Instead, the body is {"Content": {...}, "HashCode": "<hex>"}, and you verify by computing an HMAC-SHA256 over the raw JSON string of the Content object (the exact substring between {"Content": and ,"HashCode":), keyed with your app's webhook secret, hex-encoded and uppercased, then comparing to HashCode.
const crypto = require("crypto");
const WEBHOOK_SECRET = process.env.EXACT_WEBHOOK_SECRET;
function verify(rawBody) {
const body = rawBody.toString();
// The signed content is the raw JSON of the Content object,
// i.e. the substring between `{"Content":` and `,"HashCode":`
const start = body.indexOf('{"Content":') + '{"Content":'.length;
const end = body.lastIndexOf(',"HashCode":');
const contentJson = body.slice(start, end);
const hashCode = JSON.parse(body).HashCode;
const expected = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(contentJson)
.digest("hex")
.toUpperCase();
const a = Buffer.from(expected);
const b = Buffer.from(hashCode || "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
// Creation validation: Exact posts an EMPTY body (~1s after subscribe).
// Acknowledge it and skip verification.
if (!req.body || req.body.length === 0) {
return res.sendStatus(200);
}
if (!verify(req.body)) {
return res.sendStatus(401);
}
res.sendStatus(200); // acknowledge fast
const { Content } = JSON.parse(req.body);
// Content.ExactOnlineEndpoint is the ready-made URL to fetch the record
processQueue.add(Content); // fetch via ExactOnlineEndpoint, async
});
Because the signed substring must match Exact's serialization exactly, extract it from the raw body (as above) rather than re-serializing the parsed Content. This scheme, HMAC-SHA256 over the raw Content substring with uppercase hex, is verified against real deliveries. (A single delivery can't prove the raw-substring approach byte-for-byte beats re-serialization, but extracting the raw substring is the reliable choice, it can't drift from Exact's serialization the way a reparse can.)
Exact Online webhook limitations and pain points
The signature is a body field, over an exact substring
The Problem: There's no header to check, and the HMAC is over the raw JSON of the Content object, an exact substring of the body. Parsing and re-serializing Content (which can reorder keys or change spacing) breaks verification.
Why It Happens: Exact signs the Content JSON and carries the result as HashCode in the same body.
Workarounds:
- Extract the
Contentsubstring from the raw body and HMAC that exact string, uppercase the hex.
How Hookdeck Can Help: Hookdeck verifies against the bytes as received, so your app doesn't have to slice the raw body to match Exact's serialization.
Thin payloads require an API fetch (with a short-lived token)
The Problem: Content names the entity but not its data, so you fetch the full record from the REST API. Two practical wrinkles: the OAuth access token lives only ~600 seconds, and refresh is rate-limited while the current token is still valid, so a naive "refresh on every event" approach trips the limiter.
Why It Happens: Exact keeps payloads minimal and enforces a short token lifetime with throttled refresh.
Workarounds:
- Use the
ExactOnlineEndpointURL in the payload to fetch the record directly, rather than constructing it fromKey/Division. - Cache and reuse the access token until it's near expiry; refresh once, not per event.
How Hookdeck Can Help: Hookdeck durably queues each event so your worker fetches records at a controlled rate, giving you room to manage token refresh within Exact's limits.
An empty validation POST on creation
The Problem: About a second after you create a subscription, Exact sends an empty POST (content-length 0) to validate the callback. A handler that assumes every request has a Content/HashCode body, or tries to verify a signature on it, errors out, and validation (sometimes the subscription) fails.
Why It Happens: Exact validates the callback URL with an empty probe before delivering real events.
Workarounds:
- Detect an empty body, return 2xx, and skip verification for it (as the handler above does).
How Hookdeck Can Help: Hookdeck accepts the validation probe cleanly and forwards only real, verified events to your handler.
Division scoping and topic exactness
The Problem: Subscriptions are per division, and topic spellings must be exact (only some are well-documented). A wrong topic string or missing division subscription silently delivers nothing.
Why It Happens: Exact scopes webhooks per division and exposes many topics.
Workarounds:
- Subscribe per division with confirmed topic spellings, and verify against the official topic list.
How Hookdeck Can Help: Hookdeck centralizes multiple division/topic subscriptions into one ingestion point with routing.
Retries and duplicates
The Problem: Exact retries failed deliveries, so the same event can arrive more than once.
Why It Happens: At-least-once delivery favors eventual delivery.
Workarounds:
- Dedupe on
Key+Action(and a timestamp), and make side effects idempotent.
How Hookdeck Can Help: Hookdeck deduplicates deliveries at the edge. See our guide to webhook idempotency.
Best practices
Verify the HashCode over the raw Content substring
Extract the Content JSON substring from the raw body, HMAC-SHA256 it with your app webhook secret, uppercase the hex, and compare to HashCode in constant time.
Handle the empty validation POST
Detect the content-length-0 probe Exact sends on creation, return 2xx, and skip verification for it.
Acknowledge fast, fetch via ExactOnlineEndpoint
Return 2xx immediately and fetch the full entity off a queue using the ExactOnlineEndpoint URL from the payload, caching the OAuth token between events (it lives ~600s and refresh is throttled). See why to process webhooks asynchronously.
Subscribe per division with exact topics
Use confirmed topic spellings, one subscription per topic per app per division.
Dedupe
Dedupe on Key + Action so retries don't double-process.
Make Exact Online webhooks production-ready
Hookdeck verifies the HashCode, normalizes payloads, and durably queues every change event
Conclusion
Exact Online webhooks verify not with a header but with a HashCode field inside the body, an HMAC-SHA256 over the raw Content JSON, uppercase hex (verified against real deliveries). Extract the exact Content substring to verify, handle the empty validation POST sent on creation, fetch full records via the ready-made ExactOnlineEndpoint URL (caching the short-lived OAuth token), subscribe per division with exact topics, and dedupe.
Hookdeck verifies the HashCode, normalizes payloads, deduplicates, and durably queues every event at the edge, so your app only ever processes verified, unique change events.
Get started with Hookdeck for free and handle Exact Online webhooks reliably in minutes.