Gareth Wilson Gareth Wilson

Guide to RootFi (Commenda) Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationDashboard Webhooks page, or POST /v4/core/webhook-config
ScopeOne event type per config; topics for DATA_MODEL_CHANGES
Signaturehex HMAC-SHA256
Signature headerx-rootfi-signature (t=<unix>,v1=<hex>)
Signed content<t>.<raw body>
Keysigning_key you set on the config. No header without one
BodyJSON {event, data}
Event routingBody field event
Delivery IDX-RootFi-Webhook-Delivery-Id, stable across retries
Timeout30 seconds
Retries5 attempts in total, backoff capped at 15 minutes
OrderingNot guaranteed

Events and payloads

EventTriggered WhenCommon Use Cases
DATA_MODEL_CHANGESSynced records are created, updated, or deletedMirror RootFi data (invoices, contacts) into your own store
SYNC_STARTEDA sync beginsShow sync progress, track sync_id
SYNC_COMPLETEDA sync finishesCheck which data models succeeded or failed
CONNECTION_CHANGEDA company's connection is established, expires, or is disconnectedPrompt customers to reconnect
DISCONNECT_REQUESTEDAn end user requests a disconnect from the SDKRoute 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

  1. Go to Webhooks and click Create Webhook.
  2. Name it and choose the event type.
  3. For Data Model Changes, select topics.
  4. 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_id and reconcile through the API after SYNC_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-logs after 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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.