Webhooks
Required Access Level
Admin or Super Admin
What are Webhooks for?
Webhooks let Flowi Agentic notify external systems automatically when events happen on the platform, without the external system having to poll for them.
Typical uses:
- Notify an ERP when a purchase instance is approved
- Push data to an external dashboard when a task is completed
- Trigger automation in another system when a process ends
How they work
When an event happens in Flowi Agentic, the system sends an HTTP POST request to the URL registered on the webhook, with the event data in the body (JSON).
Registering a webhook
- Go to Integrations & automation → Webhooks
- Click New Webhook
- Fill in:
- Name — how you identify the webhook
- URL — the external endpoint that will receive the notifications
- Events — tick the events that fire this webhook, or All events (
*). There is no text field: the screen only offers the events the platform emits - Signing secret (HMAC) — leave it blank to have the platform generate one, or type your own
- Click Create
- The screen shows the signing secret in a dialog with a copy button. Copy it now and store it in the system that receives the webhook: it is not shown again — not in the list, not when editing, not through the API. If you lose it, generate a new one (see below)
Secret kept in the vault
When the installation has a vault connected (see External vault), the Signing secret field gets the Stored value / Vault reference choice. With Vault reference, give where the secret lives — for example vault://kv/flowi/tenants/<your-tenant>/webhooks/erp#hmac — and the platform reads the value from the vault on every delivery to sign the HMAC.
- There is no "copy it now" window: the secret is yours and is already in the vault. The webhook panel shows the reference.
- The reference is checked on save: missing from the vault, or outside your Tenant's prefix, and the webhook is refused.
- If the vault is down when a delivery is due, nothing goes out unsigned: the delivery is marked for Failed, retry scheduled with the reason and follows the normal retry.
Editing, switching off and rotating the secret
Click the webhook in the list (or reach it with Tab and press Enter) to open its panel:
- Edit opens a dialog with name, URL, events, Active and the Signing secret. With the secret field left blank, saving an edit does not change the secret — the receiving system keeps validating with the same one. On a webhook whose secret lives in the vault, the field comes filled with the reference: replace it to point at another secret, or choose Stored value and type a secret to stop using the vault.
- The Active switch on each card in the list turns the webhook on and off without opening anything. A switched-off webhook sends nothing. Switching off is always allowed, even if the host has left the allowed destinations or the webhook is marked Never fires; switching back on checks the list again. An API edit that does not send
enabledkeeps the webhook as it is. - Generate new secret asks for confirmation and shows the new secret once, as on registration. The previous secret stops working immediately: update the receiving system right away.
- On a webhook whose secret lives in the vault, Generate new secret is disabled, with the reason on the panel: generating one here would replace the reference with a value stored by the platform. Rotate the secret in the vault itself, or edit the webhook to point at another reference.
Where a webhook may send
Each tenant declares the allowed destinations for its webhooks, under Integrations & automation → Webhooks → Allowed destinations — one host per line:
erp.customer.com
*.partner.com*.partner.com reaches api.partner.com and does not reach partner.com. The port is ignored in the comparison. It is the same format as the destination list of AI Agents — the two halves of the same control.
Why this exists: before it, a webhook sent the tenant's process events to whatever URL an administrator had typed, and nothing answered "if this webhook fires, where does the data go" before it fired. The list answers that, and the screen shows, for every webhook, its effective host and its state:
| State | What it means |
|---|---|
| Declared destination | the host is on the list — it will be delivered |
| No list declared | nothing bounds the destination; the installation allows it, but nobody declared anything |
| Outside the destination list | the host is not on the list — it will not be delivered |
| No destination declared | the installation requires a list and this tenant has none |
A destination that is not on the list is refused on save, with the host in the error text. If the list changes after the webhook was registered, the delivery is refused at send time and recorded as BLOCKED in the history, with the reason — it is not a network failure, and it is not retried.
The listed state is computed from the host written in the URL, and from nothing else. The check that the address is not internal happens at two other moments — when the webhook is saved and on every send — and is not part of that state: a host that started resolving to an internal address after registration is still shown as Declared destination and is refused at delivery time.
An internal address is never a destination
localhost, 127.0.0.1, the private ranges (10., 172.16-31., 192.168.), link-local and the cloud metadata endpoints (169.254.169.254) are refused even if you write them on the list. The list widens external reach; it does not revoke that protection.
New installation: WEBHOOKS_EGRESS_DEFAULT_POLICY=deny
With allow (the default, for compatibility) a tenant with no list delivers to any external address. With deny, a tenant with no list delivers nothing, and whoever integrates says where. Twin of AI_HTTP_TOOL_DEFAULT_POLICY.
What happened to webhooks that already existed
The upgrade filled each tenant's list with the hosts of the webhooks it already had. Nothing that was delivering stopped delivering, and the list was born true: it is the set of destinations that were actually in use. Registering a new destination became two steps — declare the host, then register the webhook.
HMAC validation
Every delivery is signed — there is no webhook without a secret. Flowi Agentic computes the HMAC-SHA256 of the body with the webhook's secret and sends the signature in the header:
X-FlowIa-Signature: sha256=<signature>
X-FlowIa-Event: instance.completedOn the receiving system, validate the signature with the same secret — the one the screen showed on registration or when you generated a new one: it proves the body came from here and was not altered on the way. Compute the HMAC over the body exactly as it arrived, before re-formatting the JSON.
What the signature does not say is when the request was made. There is no timestamp and no sequence number in the headers, and a retry re-sends the same body with the same signature — the same event can arrive up to three times, all of them valid. Treat every delivery as idempotent, using the event's own data to recognise what has already been processed.
Available events
| Event | Fired when |
|---|---|
instance.started | A process instance is started |
instance.completed | An instance completes |
instance.failed | An instance fails |
task.created | A new task is created |
task.completed | A task is completed |
* | Any of the events above |
task.overdue does not exist
A due date passing is not an engine event: nothing happens at the moment the date goes by. Delivering that event would require a periodic sweep of open tasks, and the platform runs no such sweep — so it is not in the list, and the API refuses a webhook registered with it.
Registering through the API
On POST and PUT /api/a/webhooks, the events field is a JSON array of event names sent as a string:
{ "name": "ERP", "url": "https://erp.cliente.com.br/hook", "events": "[\"instance.completed\", \"task.completed\"]" }The API answers 400 — naming the refused value and listing the valid ones — when events is not an array ("instance.completed", "task.completed,instance.started"), when the array is empty, or when it holds a name not in the table above. Without events, the webhook subscribes to everything (["*"]). Event names are compared whole: instance.completed does not match xinstance.completed.
A webhook that never fires
Before this validation the API accepted any text, and the screen itself sent the events comma-separated — a format that never matched any event. Those registrations were neither deleted nor rewritten: the list shows Never fires on the webhook, with the explanation written right below it, and the API response carries "eventsValid": false. Click Edit on the webhook and choose the events again (or send a PUT with a valid array) — there is no need to delete and re-register it, and the secret stays the same.
When delivery happens
The event records the delivery as pending and a background job sends it — within a minute, not at the instant of the event. The call to your server is deliberately kept outside the process transaction: were it made inside, an instance completing would hold database resources for the whole response time of your endpoint, timeout included.
And if the process rolls its transaction back, the delivery rolls back with it. An instance that never started does not announce that it started.
Retry
If a delivery fails (the external server returns an error or does not respond), Flowi Agentic re-queues it and tries again every minute, up to three attempts. After that the delivery is marked FAILED and is not tried again. The history is kept on each webhook.
There is no exponential backoff — the interval is fixed.
Monitoring deliveries
In the webhook list, click one to see the 50 most recent deliveries of that webhook: the event, the status, the HTTP code your server returned, the number of attempts and the date.
| Status on screen | In the API | What it means |
|---|---|---|
| Delivered | SUCCESS | your server answered 2xx |
| Pending | PENDING | waiting to be sent (up to a minute) |
| Failed, retry scheduled | RETRYING | the server answered an error or did not answer; it will be tried again |
| Failed, no retries left | FAILED | three attempts without success, or a failed test; it will not be tried again |
| Blocked | BLOCKED | the destination is not an allowed destination, or is an internal address; not retried |
Click a delivery to see the reason: an excerpt of your server's response (on a 401 or 500, what it returned in the body), the network error when there was no response, or the reason it was blocked. The excerpt is cut at 1000 characters and, if your server echoes the secret, it shows as ***. The body that was sent is not shown on that screen.
Testing a webhook
In the webhook panel, pick one of the events it subscribes to under Test event and click Test. The platform sends right away — without waiting for the one-minute queue — a body in the same shape as the real event, signed like any delivery:
{ "event": "instance.completed", "processInstanceId": "test-3f2a…", "tenantId": "acme" }The processInstanceId starts with test-: that is how your system recognises a test. The result appears right below the button — status, HTTP code and the reason — and the delivery enters the history under the chosen event name. If it fails, it is marked Failed, no retries left right away: a test never enters the retry, so your system does not receive the fake event again minutes later. Fix the problem and click Test again.