Skip to main content
Webhooks keep your systems in sync without polling. When data is synced from a connected platform, a connection changes state, or an end user requests a disconnect, we POST a JSON event to a URL you control.

Event types

Each webhook you configure subscribes to exactly one event type.

Payload envelope

Every event body has the same two-key shape. The event type is in event, and everything else is under data:

Events

Data Model Changes

Fires as records are written during a sync. Use it to mirror our data into your own store.
type is one of:
  • CREATE — data is synced for the first time, or a new record appears on the connected platform.
  • UPDATE — a record actually changed. Not sent on every sync: if we sync daily and the platform reports no changes, you get nothing.
  • DELETE — a record was removed on the connected platform. Only detected during a full sync.
Records are batched and paged. Each delivery carries at most 50 records in records, so a single sync of a large data model produces many deliveries. Records are deduplicated by platform_id within a batch. Treat records as a page, not the complete set for that sync_id.
The same record can arrive more than once in a sync. A record is re-sent as UPDATE whenever any of its fields changed since the last fetch — including fields you may not care about. On Stripe invoices the hosted payment link is regenerated on every fetch, so most invoices arrive as a CREATE and then again as an UPDATE with no other field changed.Deduplicate on platform_id and compare the fields you actually use, rather than treating every UPDATE as a meaningful change.
For a full sync, this event delivers the entire dataset — records that were already up to date arrive as UPDATE.

Sync Started

Sync Completed

Fires once an asynchronous sync finishes. Use it to check which data models succeeded and which failed.
status is derived from the job counts: SUCCESS when every job succeeded, otherwise FAILED, RUNNING, or CREATED. A partial failure surfaces as FAILED with a non-zero failed_jobs_count — inspect the counts rather than treating the event as all-or-nothing. id and sync_id carry the same value; prefer sync_id.

Connection Changed

Fires when a connection is established via an invite link, or when its status changes — including expiry and disconnection. Use it to prompt customers to reconnect.
status is one of HEALTHY, EXPIRED, or DISCONNECTED. connection_id is the composite {company_id}_{integration_type}.

Disconnect Requested

This event does not disconnect anything. An end user in the SDK is requesting that you disconnect their integration. The connection stays active and keeps syncing until an authenticated dashboard user resolves the request. Subscribe to this event so those requests reach a human instead of sitting unnoticed.

Topics

Topics narrow a DATA_MODEL_CHANGES webhook to the changes you care about. A topic is a data model and an operation, both lowercase:
Retrieve the full list of valid topics from GET /v4/core/webhook-config/platform-topics. Invalid topics are rejected when you create or update the config.
A DATA_MODEL_CHANGES webhook with an empty topic list matches every data model and operation. The dashboard requires you to pick at least one topic; the API does not. If you create configs through the API, set topics explicitly unless you truly want everything.
The other event types ignore topics.

Configuring a webhook

  1. Go to Webhooks and click Create Webhook.
  2. Name the webhook and choose the event type.
  3. For Data Model Changes, select the topics to subscribe to.
  4. Enter your endpoint URL. Optionally add custom headers, and a signing key so you can verify that deliveries came from us.
Existing configs are listed on the Webhooks page, where you can enable, disable, or edit them, and open a delivery to see its attempts and response codes.

Delivery

All webhooks are sent as POST requests with a JSON body. The body is exactly the event payload — there is no additional wrapper.

Headers

Any custom headers on the config are also sent. If the originating event carried x-request-id, x-correlation-id, or x-trace-id, those are forwarded too. Our own X-RootFi-* headers take precedence over custom headers with the same name.

Retries

A delivery succeeds on any 2xx. Anything else — including a timeout — is retried up to 5 attempts total, with exponential backoff of min(60 × 2^attempt, 900) seconds: roughly 1 min, 2 min, 4 min, 8 min, then capped at 15 min. Requests time out after 30 seconds. After the final failed attempt the delivery is marked failed and not retried again. Deliveries are also dropped if the config is disabled or deleted before they are sent.
Return a 2xx as soon as you have durably accepted the payload, and do your processing asynchronously. Slow handlers burn into the 30-second timeout and trigger retries you don’t want.
Delivery history, attempt counts, and response codes are available via GET /v4/core/webhook-logs and in the dashboard.

Verifying signatures

When a config has a signing key, we sign each delivery and send the result in X-RootFi-Signature:
t is the unix timestamp and v1 is a hex HMAC-SHA256. The signed message is the timestamp, a literal ., and the raw request body:
To verify: parse t and v1, recompute over the raw body, and compare in constant time. Sign the bytes you received — re-serializing parsed JSON changes key order and whitespace, and the signature will not match.
Rejecting timestamps outside a tolerance window (300 seconds is a reasonable default) limits replay attacks.

Handling events reliably

  • Deduplicate on X-RootFi-Webhook-Delivery-Id. Retries reuse the id, and at-least-once delivery means you may see the same event twice.
  • Don’t assume ordering. Deliveries are queued independently; a SYNC_COMPLETED can arrive before the last DATA_MODEL_CHANGES batch of that sync.
  • Treat records as one page of many. Reconcile against sync_id rather than expecting a single complete payload.
  • Upsert rather than insert. Combined with partial data, the same platform_id will legitimately arrive more than once.