# RootFi (Commenda) Webhooks

## When to Use This Skill

* How do I receive RootFi webhooks?
* How do I verify the `X-RootFi-Signature` header?
* How do I handle `DATA_MODEL_CHANGES`, `SYNC_COMPLETED`, or `CONNECTION_CHANGED` events?
* Why are my RootFi deliveries unsigned, or why does signature verification fail?
* How do I deduplicate RootFi deliveries and handle out-of-order events?

RootFi's unified accounting, commerce and payments integrations API is now documented as Commenda integrations (integrations.docs.commenda.io).

## Essential Code (USE THIS)

When a webhook config has a `signing_key`, every delivery carries `X-RootFi-Signature: t=<unix seconds>,v1=<hex>`. `v1` is the HMAC-SHA256 (hex) of `"<t>.<raw body>"`, keyed with the `signing_key` as UTF-8 bytes, used as-is (not hex- or base64-decoded). Use `t` exactly as it appears in the header, sign the raw body bytes (never re-serialized JSON), and accept the delivery if any `v1` matches. Failures return 401.

### Verification (Node.js)

```javascript
const crypto = require('crypto');

// rawBody: Buffer of the exact request bytes (express.raw({ type: '*/*' }), not express.json)
function verifyRootfi(rawBody, header, secret) {
  if (!secret || !header) return false;
  let t; const v1 = [];
  for (const part of header.split(',')) {            // trim: a space after the comma is allowed
    const p = part.trim(), i = p.indexOf('=');
    if (i > 0) { const k = p.slice(0, i).trim(), v = p.slice(i + 1).trim();
      if (k === 't') t = v; else if (k === 'v1') v1.push(v); }   // ignore v0 and other keys
  }
  if (!t || v1.length === 0) return false;
  // Optional replay window, off by default: retries and replays keep the original timestamp
  const expected = crypto.createHmac('sha256', secret)   // signing_key as UTF-8
    .update(`${t}.`).update(rawBody).digest();           // t stays a string; body stays bytes
  // 64 hex chars first: Buffer.from(x, 'hex') silently truncates invalid hex
  return v1.some((s) => /^[0-9a-f]{64}$/i.test(s) &&
    crypto.timingSafeEqual(Buffer.from(s, 'hex'), expected));
}

```

### Verification (Python)

```python
import hashlib, hmac, re

def verify_rootfi(raw_body: bytes, header: str, secret: str) -> bool:
    if not secret or not header:
        return False
    t, v1 = None, []
    for part in header.split(","):
        k, sep, v = part.strip().partition("=")
        if sep and k.strip() == "t": t = v.strip()
        elif sep and k.strip() == "v1": v1.append(v.strip())
    if not t or not v1:
        return False
    expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest().encode()
    # exact hex format first, then compare_digest on bytes (never str)
    return any(re.fullmatch(r"[0-9a-fA-F]{64}", s, re.ASCII) and
               hmac.compare_digest(s.lower().encode(), expected) for s in v1)

```

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/rootfi-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/rootfi-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/rootfi-webhooks/examples/fastapi/).

### Timestamp window: off by default

RootFi's docs suggest rejecting timestamps more than about 300 seconds old. **Hookdeck retries and replays resend the original headers and timestamp, and Hookdeck itself applies no tolerance, so a fixed window would reject legitimate retries.** The examples therefore skip the freshness check unless you set `ROOTFI_WEBHOOK_TOLERANCE_SECONDS` to a positive number of seconds (`t` is Unix seconds; unset or `0` = off). Dedupe on `X-RootFi-Webhook-Delivery-Id` instead. [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/rootfi-webhooks/references/verification.md) shows where the window goes.

## Setup with Hookdeck

Set a signing_key on the RootFi webhook config. Without one, deliveries are unsigned and fail verification.

RootFi does not generate the key: choose a long random string, set it as `signing_key` (dashboard or `POST /v4/core/webhook-config`), and paste the same string into the Hookdeck source. Use the Hookdeck source URL as the config's `url`. See [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/rootfi-webhooks/references/setup.md).

## Event Envelope and Types

Every body is `{ "event": "<TYPE>", "data": { ... } }`, with no extra wrapper. Branch on `event`; no header names the type. Each webhook config subscribes to exactly one event type.

| Event | Fires when |
| --- | --- |
| `DATA_MODEL_CHANGES` | Synced records are created, updated, or deleted (`data.data_model`, `data.type` of `CREATE` / `UPDATE` / `DELETE`, up to 50 `records` per delivery) |
| `SYNC_STARTED` | A sync begins |
| `SYNC_COMPLETED` | A sync finishes (`data.status`: `SUCCESS`, `FAILED`, `RUNNING`, `CREATED`) |
| `CONNECTION_CHANGED` | A company's connection status changes (`HEALTHY`, `EXPIRED`, `DISCONNECTED`) |
| `DISCONNECT_REQUESTED` | An end user requests a disconnect from the SDK (nothing is disconnected until a dashboard user resolves it) |

Delivery is at least once and unordered: a `SYNC_COMPLETED` can arrive before the last `DATA_MODEL_CHANGES` batch of that sync. See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/rootfi-webhooks/references/overview.md) for payloads and retries.

## Important Headers

| Header | Description |
| --- | --- |
| `X-RootFi-Signature` | `t=<unix seconds>,v1=<hex HMAC-SHA256>`; sent only when the config has a signing key |
| `X-RootFi-Webhook-Delivery-Id` | Unique id for the delivery, stable across retries: use it to deduplicate |
| `X-RootFi-Timestamp` | Unix timestamp (seconds) when the delivery was attempted |

## Environment Variables

```bash
ROOTFI_WEBHOOK_SECRET=your_signing_key        # the config's signing_key, used as-is as a UTF-8 HMAC key
ROOTFI_WEBHOOK_TOLERANCE_SECONDS=0            # optional; a positive value opts in to the replay window

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 rootfi --path /webhooks/rootfi

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/rootfi-webhooks/references/overview.md) - RootFi webhook concepts, events, payloads, retries
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/rootfi-webhooks/references/setup.md) - Creating a webhook config, the signing key, Hookdeck setup
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/rootfi-webhooks/references/verification.md) - Signature details, the timestamp window, and debugging