Guide to RootFi (Commenda) Webhooks: Features and Best Practices
RootFi, now documented as Commenda integrations, is a unified API over accounting, commerce and payments platforms: connect a customer's Xero, QuickBooks or similar once, and RootFi syncs invoices, contacts and other records into one data model. Fintech and lending products use it to read their customers' books. Since syncs run in the background, RootFi's webhooks are how your product learns that data changed, a sync finished or a connection broke.
This guide covers RootFi's five event types, how record changes are batched, the optional X-RootFi-Signature, and the ordering and retry behaviour you need to design for.
What are RootFi (Commenda) webhooks?
A RootFi webhook config subscribes to exactly one event type and sends it to one URL. You create configs on the dashboard's Webhooks page or with POST /v4/core/webhook-config, so a full integration usually has several. For DATA_MODEL_CHANGES, a config can narrow delivery to particular topics, such as invoice.create.
Each delivery is a JSON POST whose body is simply {event, data}. Headers carry a delivery ID that stays the same across retries, the attempt's timestamp and, if the config has a signing key, X-RootFi-Signature.
The signature uses the Stripe-style t=...,v1=... layout: a Unix timestamp and a hex HMAC-SHA256 of the timestamp and raw body. RootFi doesn't generate the key; you set signing_key on the config, and without it nothing is signed.
RootFi (Commenda) webhook features
| Feature | Details |
|---|---|
| Configuration | Dashboard Webhooks page, or POST /v4/core/webhook-config |
| Scope | One event type per config; topics for DATA_MODEL_CHANGES |
| Signature | hex HMAC-SHA256 |
| Signature header | x-rootfi-signature (t=<unix>,v1=<hex>) |
| Signed content | <t>.<raw body> |
| Key | signing_key you set on the config. No header without one |
| Body | JSON {event, data} |
| Event routing | Body field event |
| Delivery ID | X-RootFi-Webhook-Delivery-Id, stable across retries |
| Timeout | 30 seconds |
| Retries | 5 attempts in total, backoff capped at 15 minutes |
| Ordering | Not guaranteed |
Events and payloads
| Event | Triggered When | Common Use Cases |
|---|---|---|
DATA_MODEL_CHANGES | Synced records are created, updated, or deleted | Mirror RootFi data (invoices, contacts) into your own store |
SYNC_STARTED | A sync begins | Show sync progress, track sync_id |
SYNC_COMPLETED | A sync finishes | Check which data models succeeded or failed |
CONNECTION_CHANGED | A company's connection is established, expires, or is disconnected | Prompt customers to reconnect |
DISCONNECT_REQUESTED | An end user requests a disconnect from the SDK | Route the request to a human; it does not disconnect anything |
DATA_MODEL_CHANGES
data.data_model names the model (for example INVOICE) and data.type says whether records were created, updated or deleted. Records are batched, at most 50 per delivery and deduplicated by platform_id within a batch, so records is one page rather than every change in the sync.
The same record can arrive more than once, for example as a CREATE and then an UPDATE with no meaningful change. UPDATE is sent only when something changed, and deletions are detected only during a full sync. RootFi's example elides the record fields, so check a real delivery for each data model.
SYNC_COMPLETED
Marks the end of a sync and reports how each data model fared, so you can see which succeeded or failed. Because delivery is unordered, it can arrive before the last DATA_MODEL_CHANGES batch for the same sync_id.
DISCONNECT_REQUESTED
data holds disconnect_request_id, connection_id, company_id and integration_type. The event only records a request from an end user in the SDK: the connection stays active and keeps syncing until an authenticated dashboard user resolves it.
Published payload example
A CONNECTION_CHANGED delivery, from the webhook samples library.
{
"event": "CONNECTION_CHANGED",
"data": {
"connection_id": "1234_XERO",
"company_id": 1234,
"integration_type": "XERO",
"status": "HEALTHY"
}
}
Setting up RootFi (Commenda) webhooks
Prerequisites
- A RootFi (Commenda integrations) account with access to the Webhooks page, or API access to
POST /v4/core/webhook-config - A public HTTPS endpoint
Get Your Signing Secret
Set a signing_key on the RootFi webhook config. Without one, deliveries are unsigned and fail verification.
Choose a long random string, for example with openssl rand -hex 32, and store the same value as WEBHOOK_SECRET. RootFi uses it as UTF-8 bytes, so don't decode it.
Register Your Endpoint
Dashboard
- Go to Webhooks and click Create Webhook.
- Name it and choose the event type.
- For Data Model Changes, select topics.
- Enter your endpoint URL, any custom headers and the signing key.
API
POST /v4/core/webhook-config
{
"name": "Invoice changes",
"webhook_type": "DATA_MODEL_CHANGES",
"url": "https://api.example.com/hooks/rootfi",
"topics": ["invoice.create", "invoice.update"],
"headers": [{ "name": "X-Tenant", "value": "acme" }],
"signing_key": "<random secret>"
}
Create one config per event type: DATA_MODEL_CHANGES, SYNC_STARTED, SYNC_COMPLETED, CONNECTION_CHANGED and DISCONNECT_REQUESTED. Valid topics come from GET /v4/core/webhook-config/platform-topics. The dashboard requires at least one topic; through the API, an empty list matches everything. The Webhooks page also lets you enable, disable or edit configs and shows each delivery's attempts and response codes.
Test a delivery
Start a local receiver and forward requests with the Hookdeck CLI:
npx hookdeck-cli listen 3000 rootfi --path /webhooks/rootfi
Create a SYNC_STARTED and a SYNC_COMPLETED config pointing at the CLI's URL, then trigger a sync on a test connection.
Securing RootFi (Commenda) webhooks
Split X-RootFi-Signature into t and v1, build the message from the t text, a dot and the raw body, compute HMAC-SHA256 with the signing_key, and compare it with v1 in constant time. RootFi warns that re-serialised JSON won't match. See the webhooks concept page.
const crypto = require('node:crypto');
function verifySignature(rawBody, headers, secret) {
if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
const signature = headers['x-rootfi-signature'];
if (typeof signature !== 'string') return false;
const key = Buffer.from(secret, 'utf8');
const parts = signature.split(',').map((part) => part.trim());
const times = parts.filter((part) => part.startsWith('t=')).map((part) => part.slice(2));
if (times.length !== 1 || !/^[0-9]+$/.test(times[0])) return false;
const message = Buffer.concat([Buffer.from(`${times[0]}.`, 'utf8'), rawBody]);
const candidates = parts.filter((part) => part.startsWith('v1=')).map((part) => part.slice(3));
const expected = crypto.createHmac('sha256', key).update(message).digest();
return candidates.some((candidate) => {
if (!/^[0-9a-fA-F]{64}$/.test(candidate)) return false;
const received = Buffer.from(candidate, 'hex');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}
The handler deduplicates on the delivery ID, upserts records by platform_id, and treats SYNC_COMPLETED as a signal to reconcile rather than as proof that every batch has arrived.
const express = require('express');
function createRootFiApp({ claimDelivery, records, syncs, connections }) {
const app = express();
app.post('/webhooks/rootfi', express.raw({ type: '*/*' }), async (req, res) => {
const secret = process.env.WEBHOOK_SECRET;
if (!secret) return res.sendStatus(500);
if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);
const deliveryId = req.headers['x-rootfi-webhook-delivery-id'];
let parsed;
try {
parsed = JSON.parse(req.body.toString('utf8'));
} catch {
return res.sendStatus(400);
}
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return res.sendStatus(400);
const { event, data } = parsed;
if (typeof event !== 'string' || !data || typeof data !== 'object') return res.sendStatus(400);
try {
if (!(await claimDelivery(deliveryId))) return res.sendStatus(200);
switch (event) {
case 'DATA_MODEL_CHANGES':
for (const record of Array.isArray(data.records) ? data.records : []) {
await records.upsertOrDelete(data.data_model, data.type, record);
}
break;
case 'SYNC_STARTED':
case 'SYNC_COMPLETED':
await syncs.record(event, data);
break;
case 'CONNECTION_CHANGED':
case 'DISCONNECT_REQUESTED':
await connections.handle(event, data);
break;
default:
console.log('Unhandled RootFi event', event);
}
} catch {
return res.sendStatus(503);
}
return res.sendStatus(200);
});
return app;
}
A batch of 50 upserts can be slow; if it risks the 30-second timeout, store the body and process it from a queue.
RootFi's docs describe a 300-second replay window for t. The verifier above doesn't apply it, so that retries and replays through Hookdeck still verify. If you receive directly from RootFi, you can add the age check, and the delivery ID remains your guard against duplicates either way.
On a RootFi source, Hookdeck parses t and v1 and checks the HMAC of {t}.{body} with your signing key. RootFi always receives 200; a failed check is recorded as failed verification and the request isn't forwarded. Hookdeck doesn't enforce the 300-second window. If a config has no signing key by design, leave verification off on the source.
Make RootFi (Commenda) webhooks production-ready. Hookdeck Event Gateway collects all your one-event RootFi configs on one source, verifies them, and keeps record batches replayable after RootFi's five attempts.
RootFi (Commenda) webhook limitations and pain points
Out-of-order sync events
The Problem: Your app marks a sync complete while record batches are still arriving.
Why It Happens: Delivery is at-least-once with no ordering, and RootFi notes that SYNC_COMPLETED can arrive before the last DATA_MODEL_CHANGES batch.
Workarounds:
- Track batches per
sync_idand reconcile through the API afterSYNC_COMPLETED. - Make every record write an upsert keyed on
platform_id.
How Hookdeck Can Help: Hookdeck's event log shows the arrival order of every event for a sync, which helps when reconciling.
A short retry window
The Problem: Record changes are lost after a brief outage.
Why It Happens: RootFi makes 5 attempts in total, with backoff that reaches about 15 minutes, then marks the delivery failed.
Workarounds:
- Check
GET /v4/core/webhook-logsafter incidents. - Run a full sync to catch up when deliveries have failed.
How Hookdeck Can Help: Hookdeck accepts the first attempt and holds the event, retrying your service on your schedule.
Signing is optional
The Problem: Unsigned configs accept forged data changes.
Why It Happens: X-RootFi-Signature is sent only when the config has a signing_key.
Workarounds:
- Set a signing key on every config, including ones created through the API.
How Hookdeck Can Help: With verification on, unsigned requests are recorded as failed and not forwarded.
One event per config
The Problem: A full integration needs five configs, each with its own settings to keep in sync.
Why It Happens: Each webhook config subscribes to exactly one event type.
Workarounds:
- Manage configs through the API from code, with the same URL and key on each.
How Hookdeck Can Help: Point every config at one Hookdeck source, and use connection filters on event to route each type.
Best practices
Deduplicate on the delivery ID
X-RootFi-Webhook-Delivery-Id stays the same across retries, and the body has no event ID. Store it with the change, following our idempotency guide.
Don't act on DISCONNECT_REQUESTED automatically
The event records a request, not a disconnection. Route it to a person who can review and resolve it in the dashboard.
Use full syncs for deletions
DELETE changes are only detected during full syncs, so schedule them if deletions matter to you.
Acknowledge within 30 seconds
Store the body and return 2xx, then process batches in the background, as the asynchronous processing guide explains.
Conclusion
RootFi's webhooks report the background work of a unified accounting API: record changes in batches, sync boundaries and connection status. A good receiver upserts by platform_id, deduplicates on the delivery ID, doesn't trust SYNC_COMPLETED to arrive last, and sets a signing key on every config.
Hookdeck Event Gateway brings RootFi's single-event configs together on one verified source and keeps their deliveries replayable.
Get started with Hookdeck for free and handle RootFi (Commenda) webhooks reliably in minutes.