A webhook trigger gives a flow a private URL. When your system POSTs JSON to it, Natiq starts a run of the published flow and answers straight away with a run id. You can poll the same URL for the outcome.
Runs started this way are headless: there is no contact and nothing is sent to a channel. What the flow would have said is captured on the run, and you read it back.
Set it up
In the Agent Builder, open the flow and select its start point. Under Triggers, switch Webhook on.
If your sender can sign requests, turn signing on and copy the signing secret when it is shown. It appears once only. (The exact control labels can differ; the API calls below are the reference.)
Publish the flow. A webhook can only start a flow that is published and lists
WEBHOOKamong its start triggers.Copy the Webhook address.
Anyone with this address can start the flow
The URL token is the credential. Keep the address private, and turn signing on when the sender can sign. To replace a leaked address, rotate the trigger (below).
The same trigger is available over the API: list an agent's triggers with GET /api/v1/bots/{botId}/triggers (needs builder.bots:read), and create, rotate, update and delete under /api/v1/bots/{botId}/triggers/webhook (needs builder.bots:write). See Triggers.
curl -s -X POST https://api.cx.na6iq.com/api/v1/bots/42/triggers/webhook \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"signing": true}'{
"trigger": {
"id": 9,
"botId": 42,
"url": "https://api.cx.na6iq.com/webhooks/flows/<urlToken>",
"status": "ENABLED",
"signingEnabled": true,
"signatureHeader": null,
"secretHint": "********"
},
"secret": "shown-once"
}null means the default header, X-Signature-SHA256.
Use url exactly as returned. Send signatureHeader together with "signing": true at creation ({"signing": true, "signatureHeader": "X-Hub-Signature-256"}, up to 100 characters); without signing it is ignored. It can't be changed afterwards, and the agent builder doesn't set it — it's API-only.
Fire the flow
curl -X POST 'https://api.cx.na6iq.com/webhooks/flows/<urlToken>' \
-H 'Content-Type: application/json' \
-H 'X-Natiq-Delivery-Id: order-A-1001' \
-d '{"order_id":"A-1001","email":"layla@example.com"}'202 Accepted:
{ "runId": "8f7f6a2e-3c1b-4b8e-9d57-0a1b2c3d4e5f" }No Authorization header: the URL token is the credential. If signing is on, add the signature header; see Verifying signatures.
Rules for the request
| Rule | Detail |
|---|---|
| Body | A JSON object, or empty. An array or a plain value is refused with 400. |
| Keys | The flow reads values by name, for example trigger.body.order_id. Questions the flow always asks must be present, or the call fails with 422 TRIGGER_PAYLOAD_INCOMPLETE and error.details naming them. |
| Size | At most 262,144 bytes by default. Larger bodies get 413 WEBHOOK_BODY_TOO_LARGE. |
| Headers the flow sees | Headers starting with x-, plus content-type and user-agent, as trigger.headers. The signature header is never passed on. |
| Rate | By default each URL token may burst 30 deliveries and then 10 per second, on each server. Polling /runs/{id} and the GET handshake draw on the same allowance. Beyond that, 429 with Retry-After. These are server defaults and can differ in your deployment. |
Make retries safe
Send X-Natiq-Delivery-Id with a value unique to the event. A retry with the same id returns the run the first attempt created instead of starting another. Without the header, Natiq treats an identical body to the same URL within the same clock minute as a duplicate; do not depend on that for events that can legitimately repeat.
Read the result
curl 'https://api.cx.na6iq.com/webhooks/flows/<urlToken>/runs/8f7f6a2e-3c1b-4b8e-9d57-0a1b2c3d4e5f'{
"runId": "8f7f6a2e-3c1b-4b8e-9d57-0a1b2c3d4e5f",
"status": "SUCCEEDED",
"result": {
"messages": ["Your order A-1001 has shipped."],
"variables": { "order_id": "A-1001" }
},
"error": null,
"startedAt": "2026-11-07T09:00:00Z",
"finishedAt": "2026-11-07T09:00:02Z"
}status is one of PENDING, RUNNING, SUCCEEDED, FAILED or SKIPPED. A failed run has error set. The same URL token reads only runs it started; any other id is a 404. Poll every second or two and stop when the status is final.
A GET on the webhook URL itself answers ok, or echoes a ?challenge= value, for senders that verify an address before saving it.
Errors
| Status | Code | Meaning |
|---|---|---|
400 | INVALID_REQUEST | The body is not a JSON object. |
401 | WEBHOOK_SIGNATURE_INVALID | Signing is on and the signature is missing or wrong. |
404 | WEBHOOK_TRIGGER_NOT_FOUND | Unknown token, or the trigger is switched off. |
404 | BOT_NOT_FOUND | The agent was deleted. |
409 | FLOW_NOT_PUBLISHED | The agent has no published flow. |
409 | TRIGGER_TYPE_NOT_ACCEPTED | The flow does not list WEBHOOK among its triggers. Add it and publish again. |
413 | WEBHOOK_BODY_TOO_LARGE | Body over the size limit. |
422 | TRIGGER_PAYLOAD_INCOMPLETE | The flow always asks for values the body lacks. |
429 | RATE_LIMITED | Too many requests for this URL token. Honour Retry-After. |
Rotate or switch off
POST /api/v1/bots/{botId}/triggers/webhook/{triggerId}/rotate issues a new URL token. Add {"rotateSecret": true} to replace the signing secret too. Both are returned once. PATCH …/{triggerId} with {"status": "DISABLED"} keeps the token but makes the URL answer 404 within about a minute; {"signing": false} removes signing. A PATCH cannot turn signing on; rotate with rotateSecret instead.
These changes are not instant. Disabling, deleting, turning off signing and rotating take effect within about a minute, so the old token or secret can still be honoured for that long.