Webhooks and the API

API and MCP mutations do not fire webhook events — the rule, the reasoning, and what still fires normally.

Updated 2026-08-09

The big rule

Mutations through the Customer API do NOT fire webhook events. The same is true of writes made through the MCP server.

If you call:

POST /api/v1/organizations/{org}/forms/{form}/records

…and your organization has an active record.created webhook subscription, that subscription will not receive a delivery for this submission.

The same goes for every other API-originated mutation:

EventFires for dashboard / mobile?Fires for API or MCP?
record.created / .updated / .deletedYesNo
form.published / .deletedYesNo
form.assigned / .unassignedYesNo
organization.created / .updated / .deactivatedYesNo
user.invited / .removed / .roles_updatedYesNo

Why

Webhooks exist to notify external systems of state changes the external system didn't already know about. If you're the integrator submitting a record through your own API call, you already know it happened — the response body confirms it. Firing a webhook in that case creates two real problems:

  1. Loop risk. If your webhook handler ever re-calls the API (e.g. an enrichment pipeline that adds a field after submission), the API call's resulting webhook would re-trigger the handler. We chose to break this category of loop at the source rather than trust every integrator to guard against it.
  2. Duplicate effort. Most integrators end up writing webhook-handler code that dedupes against their own submissions anyway. Skipping the fire on API origin removes that work.

How it's enforced

Every API mutation that can produce one of the events above runs through a database function that sets a transaction-local Postgres setting (app.api_origin = 'true') as its first statement. The webhook queue insert function (public.queue_webhook_event) checks that setting at the top and returns early when it's set. The check is in the single chokepoint every webhook insert flows through — there's no path to bypass it.

This is why the MCP server behaves the same way. Its write tools call the same database functions the REST routes call, so they inherit the suppression rather than reimplementing it. Anything that reaches those functions is treated as integration-origin, whichever door it came through.

Webhooks for dashboard + mobile + native triggers still work

Internal mutations (a dashboard user creating a record, the mobile app submitting offline-collected records, an admin inviting a user from the dashboard) all continue to fire webhooks normally. Only integration-origin writes are excluded — the /api/v1/* HTTP path and the MCP server's write tools.

One consequence catches people out: you cannot test a new subscription by writing through the API or an AI tool, because that write is exactly the kind that stays silent. Submit from the mobile app or the dashboard instead, or use the Send test event button on the subscription.

Future: opt-in webhook firing

Some integrators (CRM sync flows, audit pipelines) do want the webhook even on API mutations. A future ?fire_webhooks=true query param will let you opt-in per-request without changing the default. Not in v1.