Integrate Webhooks

A webhook is an HTTP request Dotfile sends to your server when something changes in a workspace, so an integration learns about a case, an entity or a check without polling for it.

There are 46 subscribable events, across nine families:

FamilyEventsCovers
Case17Creation, deletion, every property update, periodic review, client portal wait steps
CaseReport1Report generation
Note3Notes on a case
NoteComment3Comments on a note
Individual6Creation, deletion, property updates, relevance
Company6Creation, deletion, property updates, relevance
Check6Start, review needed, approval, rejection, expiry, deletion
DocumentOrder2Company document retrieval
AutonomyChatRun2Terminal outcome of an Autonomy run

Configuring webhooks

Receiving webhooks takes a dedicated endpoint on your server and a webhook registered against it through Create a webhook. Dotfile then sends POST requests with a raw JSON body to that endpoint. A workspace holds up to 50 webhooks.

Every delivery is recorded. The log — the payload sent, and each attempt with its response — is readable in the Dotfile console and through List all webhook logs. Logs are retained for 30 days by default.

Events

Case events

  • Case.Created: When a new case is created (empty case).
  • Case.Updated: When any case property (such as name, external_id, status, flags, risk, tags, template_id, metadata, custom_properties, assignee) is updated. A sub_event is always provided in Case.Updated events and can be valued to any of the following events:
    • Case.StatusUpdated: When case status is updated from one status to another one.
      Example: case openapproved
    • Case.FlagsUpdated: When case flags are updated (any flag changes can trigger this event).
    • Case.ContactHasActionsUpdated: When contact_has_actions is updated.
    • Case.ReviewerHasActionsUpdated: When reviewer_has_actions is updated.
    • Case.InfoUpdated: When case info is updated (such as name, external_id or custom_properties).
    • Case.TemplateUpdated: When case template is updated.
    • Case.RiskUpdated: When the case current risk is updated.
    • Case.MetadataUpdated: When case metadata is updated.
    • Case.TagsUpdated: When tags have been updated on case. Adding a tag and removing a tag both surface as this single event.
    • Case.AssigneeUpdated: When the case assignee is set, changed or cleared.
  • Case.ReviewUpdated: When the case periodic review is updated (for example when the case’s risk changes and the next_review_at date is updated).
  • Case.ReviewDue: When the case periodic review is due.
  • Case.ReviewConfirmed: When the case periodic review is confirmed.
  • Case.ClientPortalWaitStepTriggered: When an applicant reaches a wait step in a client portal workflow, the first time that step is reached. The case is parked until you call Complete client portal wait step.
  • Case.Deleted: When a case is deleted.

Case report events

  • CaseReport.Generated: When a case report link is generated. Only report generations requested through the API emit this event.

Note events

  • Note.Created: When a new note is created.
  • Note.Updated: When any note content is updated.
  • Note.Deleted: When a note is deleted.

Note comment events

  • NoteComment.Created: When a new note comment is created.
  • NoteComment.Updated: When any note comment content is updated.
  • NoteComment.Deleted: When a note comment is deleted.

Individual events

  • Individual.Created: When a new individual is created.
  • Individual.Updated: When any individual property (not check related) is updated, for example risk. A sub_event is always provided in Individual.Updated events and can be valued to any of the following events:
    • Individual.InfoUpdated: When individual properties are updated.
    • Individual.MarkedAsRelevant: When an irrelevant individual is marked as relevant.
    • Individual.MarkedAsNotRelevant: When a relevant individual is marked as not relevant.
  • Individual.Deleted: When an individual is deleted.

Company events

  • Company.Created: When a new company is created.
  • Company.Updated: When any company property (not check related) is updated, for example risk. A sub_event is always provided in Company.Updated events and can be valued to any of the following events:
    • Company.InfoUpdated: When company properties are updated.
    • Company.MarkedAsRelevant: When an irrelevant company is marked as relevant.
    • Company.MarkedAsNotRelevant: When a relevant company is marked as not relevant.
  • Company.Deleted: When a company is deleted.

Check events

  • Check.Started: When a check is started on an entity.
    Example: an AML check is started on an individual.
  • Check.ReviewNeeded: When a check needs manual review.
    Example: an AML check found hits and a reviewer needs to look at them.
  • Check.Approved: When a check is approved.
    Example: all hits are ignored as false positives and the reviewer approves the AML check.
  • Check.Rejected: When a check is rejected.
    Example: a document is invalid and the reviewer rejects the document check.
  • Check.Expired: When a check is expired.
    Example: a document check has passed its expiration date.
  • Check.Deleted: When a check is deleted.
    Example: a document check has been removed by a user.

A review recorded through the API — PATCH /v1/checks/{type}/{id}/review — emits the same Check.Approved and Check.Rejected events as a review made in the Console App. The payload’s reviewer is api instead of a user.

Document order events

  • DocumentOrder.Completed: When a document order for a company has been completed.
    Example: the KBIS for a French company has been retrieved.
  • DocumentOrder.Failed: When a document order for a company has failed.
    Example: annual accounts are unavailable for a company.

Autonomy chat run events

  • AutonomyChatRun.Completed: When a chat run reaches its end normally. The payload carries the thread_id, the run_id, the outcome, the run’s last_message, and the routine_id and routine_slug when the run was fired by a routine.
  • AutonomyChatRun.Failed: When a run ends on aborted or error.

A thread runs once per user message or wake-up, and each terminal run fires its own event. POST /v1/routines/{slug}/trigger returns the thread_id these events carry, so an execution you started is matched to its outcome without a lookup.

Note: Deleted events do not trigger the deleted event of sub-entities.

Example: deleting a case triggers a Case.Deleted event, but no Individual.Deleted, Company.Deleted nor Check.Deleted event.

Note: events can have sub events for fine-grained filtering.

For instance, the top-level event Case.Updated has a sub-event Case.StatusUpdated that lets you subscribe specifically to updates of the case status and ignore updates of other properties.

The top-level event is always triggered, so you never need to subscribe to a sub-event you already cover through its parent. Subscribing one webhook to both a parent event and one of its sub-events is refused: the request returns 400 with code INVALID_WEBHOOK_CONFIGURATION. Pick one level per webhook.


Main Event vs. Sub event

For example, if you subscribe to the event Case.Updated, the event structure is as follows:

  • Main Event: Case.Updated
  • Sub event: Case.StatusUpdated — or any other Case.*Updated sub event

You can also subscribe specifically to a sub event such as Case.StatusUpdated, then the event structure will be as follows:

  • Main Event: Case.StatusUpdated
  • Sub event: None

Best Practices

  • Fine-grained control: Subscribe to specific events like Case.StatusUpdated if you only need information about status changes.
  • Broad updates: Subscribe to Case.Updated if you want to receive all types of case updates, including status changes.
  • Filtering: If subscribed to Case.Updated, implement filtering on your side to handle specific sub events as needed.

Payload

Payload structure:

{
  "event": "ENTITY_NAME.ACTION_NAME",
  "sub_event": "ENTITY_NAME.SUB_ACTION_NAME",
  "context": { "...": "..." },
  "entity_name": { "...": "..." }
}

sub_event is only present on the three parent events (Case.Updated, Individual.Updated, Company.Updated).

The key carrying the entity depends on the event:

EventEntity key
Case.*case — the full case object, or id, name and external_id only for Case.Deleted
CaseReport.*report
Note.*note
NoteComment.*noteComment
Individual.*individual
Company.*company
Check.*check
DocumentOrder.*document_order
AutonomyChatRun.*no wrapper — thread_id, run_id, outcome, last_message, routine_id and routine_slug sit at the top level

Context

You can find useful extra properties in the context such as:

  • timestamp a UNIX timestamp of the time when the webhook is processed. You can verify that this timestamp is within 1-2 minutes of the time your system receives it to prevent replay attacks.
  • event_id UUID of the event. When a webhook is retried the event id is the same as the initial call.
  • retry_count Webhook retry counter. 0 for the initial call.
  • remaining_retry_count Webhook automatic remaining retry counter.

The context also contains additional properties depending on the entity targeted by the webhook.

Eventcontext properties
Case.*workspace
CaseReport.*workspace, case
Note.*workspace, case
NoteComment.*workspace, case, note
Individual.*workspace, case
Company.*workspace, case
Check.*workspace, case, individual, company
DocumentOrder.*workspace
AutonomyChatRun.*workspace
  • workspace: contains a workspace subset with its id and name.
  • case: contains a case subset with its id, external_id, last_activity_at, name, tags, contact_has_actions, reviewer_has_actions, flags, metadata and status. If the case was created via a client portal, the case context will also include a client_portal subset, with id, name and type.
  • note: contains a note subset with its id and author (author.id, author.first_name, author.last_name, author.email). author is null for a note that was not written by a user.
  • individual: contains an individual subset with id, first_name, last_name.
  • company: contains a company subset with id, name, country.

On Check.* events, individual and company are mutually exclusive: the one the check does not run on is null.

The context can be useful to verify that the webhook comes from a specific workspace or build URL to the Console App.

You can see all payload definition documented in the Callbacks section (latest section of Create a webhook).

Testing a webhook

A webhook needs a reachable URL at creation time, which makes it awkward to develop against.

webhook.site generates a unique URL and displays the incoming HTTP request in the browser, which is enough to inspect a payload.

ngrok generates a unique URL that forwards to a local port, so a webhook created with that URL delivers to a local machine.

> ngrok http http://localhost:4000/
# will generate an URL like https://0ebb-92-8-30-12.eu.ngrok.io

Securing a webhook

Every payload is signed. The signature is an HMAC-SHA256 of the request body, keyed with the webhook secret and hex-encoded, sent in the Dotfile-Signature header.

The secret is returned when the webhook is created. Leave secret out of the payload and Dotfile generates one, prefixed dotsecret.. Sending a new secret on Update a webhook rotates it.

Note: secret rotation happens with no downtime.

After a rotation, a second header Dotfile-Old-Signature carries the signature computed with the previous secret, so an endpoint validating against either one keeps accepting deliveries during the switch. See the example below.

To verify a payload, compute the signature from the request body using the webhook secret and compare. Hash the raw request body: parsing the JSON first may alter it.

A minimal Express implementation:

import express from 'express';
import bodyParser from 'body-parser';
import { createHmac } from 'crypto';

const app = express();
const port = 3000;

// Replace with your webhook secret
const WEBHOOK_SECRET = 'dotsecret.XXXXXXXXXX';

// Add middleware to extract raw request body
app.use(
  express.json({
    verify: (req, res, buf) => {
      req.rawBody = buf.toString();
    },
  })
);

// Parse the request body
app.use(bodyParser.json());

// Receive HTTP POST requests
app.post('/dotfile-webhook', (req, res) => {
  const payload = req.body;
  const rawBody = req.rawBody;

  // Verify signature
  const signature = createHmac('sha256', WEBHOOK_SECRET)
    .update(rawBody)
    .digest('hex');
  
  let isSignatureValid = false;

	if (signature === req.headers['dotfile-signature']) {
		isSignatureValid = true;
		console.log('Valid signature');
	}

	if (signature === req.headers['dotfile-old-signature']) {
		isSignatureValid = true;
		console.log('Valid signature but has been rotated, update your config');
	}

	if (!isSignatureValid) {
		res.sendStatus(400);
		console.error('Invalid signature');
		return;
	}
	// Handle the payload
	console.log(payload);

	// Respond 200 to acknowledge receipt
	res.sendStatus(200);
});

app.listen(port, () =>
  console.log(`Webhook consumer listening on port ${port}`)
);

context.timestamp carries the UNIX timestamp at which the payload was processed. Rejecting a payload whose timestamp is more than one or two minutes older than the time of receipt guards against replay attacks.

Webhook ordering

Overview

Webhooks are delivered asynchronously, so events are not guaranteed to arrive in the order they were triggered. Network delays, retries and asynchronous processing all reorder deliveries, and this is common to webhook systems generally. Background on the trade-off: Svix on guaranteeing webhook ordering and the accompanying discussion.

Design decision

Delivery is guaranteed; order is not. That trade-off is deliberate, and it is what allows the system to absorb network and endpoint failures without degrading.

Enforcing strict ordering would mean holding every subsequent event for an entity until the blocking one had been delivered successfully and acknowledged without error. A single unhealthy endpoint would then accumulate an unbounded backlog, which moves the reliability problem onto the integration rather than removing it.

Some integrations do need ordered state. The two techniques below reconcile that with unordered delivery.

Synchronization best practices

  1. Re-read the current state

    After receiving a webhook, call GET /cases/{id} to retrieve the case as it stands. The webhook becomes a trigger rather than a source of truth, and late deliveries cannot corrupt local state.

  2. Compare on last_activity_at

    Every case carries a last_activity_at timestamp. Storing it locally and applying an update only when the incoming last_activity_at is greater than or equal to the stored one discards stale deliveries without discarding the event.

Failure and retry

A delivery is successful when the endpoint answers 2XX. Any other response code is a failure, as is a call that cannot complete at all — connection refused, DNS failure, timeout — which is recorded as 502. Failures are retried with an exponential backoff until a 2XX is returned or the retries are exhausted.

There are 4 automatic retries after the initial call, at 1 hour, 4 hours, 13 hours and 40 hours from that call:

RetryAfter the initial callAfter the previous retry
1st1 hour
2nd4 hours3 hours
3rd13 hours9 hours
4th40 hours27 hours

Retries read the webhook's URL at the moment they run, so correcting a broken URL redirects the retries still pending.

Retry state travels in both the payload context and the request headers:

Context propertyHeaderValue
context.event_iddotfile-event-idUUID of the event. A retry carries the same id as the initial call.
context.retry_countdotfile-retry-countRetry counter. 0 on the initial call.
context.remaining_retry_countdotfile-remaining-retry-countAutomatic retries left, out of a maximum of 4.

Note: a webhook call can be inspected and retried manually from the Dotfile console.

A successful manual retry cancels the automatic retries still pending for that call.

An unsuccessful one counts against the automatic retry budget, and the next automatic retry, if any, still falls on its interval from the initial call.

Warning: a webhook is set offline automatically

Once a webhook reaches 50 failed calls within a rolling 24h window, it enters a high error rate state and a notification is sent. The window never reaches further back than the moment the webhook was last set online.

A webhook that is not LIVE is then set offline. Correcting the URL and setting the webhook back online, through the API or the console, restarts delivery.

A webhook promoted to LIVE stays online through a high error rate, and still raises the notification. Promotion is done in the console, and a workspace holds up to 3 LIVE webhooks. LIVE is also the condition for bulk-retrying a range of failed calls from the console.