Triggers & Webhooks
The two ways Agentic Fabriq calls you — both signed with the same scheme, so one verification function covers both.
- Connection webhooks tell your backend when a user finishes connecting a tool — so you learn it even if they close the tab before your redirect runs.
- Triggers react to events inside a connected tool and deliver them to your endpoint.
The signature
Every delivery carries two headers:
| Header | Value |
|---|---|
X-AF-Signature | HMAC-SHA256(secret, "{timestamp}.{body}"), hex |
X-AF-Timestamp | Unix seconds — the same value that was signed |
The timestamp is inside the signed material, so a captured signature cannot be replayed with a fresh one. Reject any delivery whose timestamp is more than 300 seconds from now, then compare signatures in constant time.
Verify it (TypeScript)
The TypeScript SDK ships the helper. Pass the raw body — re-serialising parsed JSON reorders keys and breaks the signature.
import { verifyWebhookRequest } from "@agenticfabriq/sdk";
app.post("/hooks/af", express.raw({ type: "application/json" }), (req, res) => {
const ok = verifyWebhookRequest({
payload: req.body.toString(), // RAW body, not the parsed object
secret: process.env.AF_WEBHOOK_SECRET!,
headers: req.headers,
});
if (!ok) return res.status(401).end();
const event = JSON.parse(req.body.toString());
// ... handle event ...
res.sendStatus(200);
});Verify it (Python)
The Python SDK does not ship a verifier. The construction is small enough to write against the standard library:
import hashlib, hmac, time
def verify(raw_body: bytes, signature: str, timestamp: str, secret: str) -> bool:
if abs(int(time.time()) - int(timestamp)) > 300:
return False
expected = hmac.new(
secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)Connection webhooks
Configure a webhook URL and generate a signing secret under Settings → Connect & webhooks. The secret is shown once. When a user finishes connecting, Agentic Fabriq POSTs:
{
"event": "connection.completed",
"app_id": "org-xxx_my-app",
"external_user_id": "sarah_42",
"provider": "notion",
"connection_id": "default",
"method": "api_key",
"external_account": { "id": "bot-1", "name": "Sarah's Notion" },
"occurred_at": "2026-08-16T15:04:05Z"
}method and external_account are present when known. external_account is how you confirm the connection landed on the account you expected — if the workspace is not Sarah's, disconnect it.
connection.needs_reauth
Fires once when a provider rejects a stored credential. The connection is now expired; mint a new connect link with replace and send the user there.
{
"event": "connection.needs_reauth",
"app_id": "org-xxx_my-app",
"external_user_id": "sarah_42",
"provider": "notion",
"connection_id": "default",
"method": "api_key",
"reason": "Provider rejected the credential (401)",
"occurred_at": "2026-08-16T15:04:05Z"
}Triggers
A trigger says "when this provider event happens for my users, do this." Create one under Triggers in the console, or over the API. Triggers require a paid plan.
Subscribe
curl -X POST https://dashboard.agenticfabriq.com/api/v1/triggers \
-H "Authorization: Bearer $TOKEN" \
-H "X-Organization-Id: $ORG_ID" \
-H "Content-Type: application/json" \
-d '{
"app_id": "org-xxx_my-app",
"provider": "slack",
"event_type": "app_mention",
"destination": "webhook",
"destination_config": { "url": "https://example.com/hooks/af" },
"scope": "all_users"
}'| Field | Meaning |
|---|---|
app_id | The agent this subscription belongs to. |
provider | One of slack, gmail, github, notion. |
event_type | Matched exactly against the provider's own event type. For Slack: message, app_mention, reaction_added, reaction_removed, member_joined_channel, member_left_channel, pin_added. |
destination | webhook or agent. |
destination_config | { "url": ... } for a webhook (must be a public address — private and loopback ranges are refused, at configuration time and again at delivery time); { "agent_id": ... } for an agent. |
scope | all_users (default) or explicit_users, which additionally requires scope_user_ids. |
The response is the created subscription, and it is the only time signing_secret is returned. Every trigger gets its own secret; store it when you create the trigger.
Only Slack receives events today. The other three providers are accepted by the API and shown in the console, but their receivers are not live yet — Gmail and Notion need provider-side infrastructure rather than just code. A subscription created for them will not fire.
Agent destinations are not dispatched yet either. They are accepted and recorded as pending rather than silently marked delivered. A webhook destination is the working path today.
The delivery payload
{
"event_id": "Ev09ABCDEF",
"org": "org-xxx",
"app": "org-xxx_my-app",
"provider": "slack",
"event_type": "app_mention",
"subject_user": "sarah_42",
"occurred_at": "2026-08-16T15:04:05",
"payload": { /* the provider's own event, verbatim */ }
}subject_user is the user the event happened to, resolved by matching the provider account against your connected users. If it cannot be resolved, the event is dropped rather than delivered to the wrong person — and the drop is recorded.
Retries and idempotency
- Up to 3 attempts with a 5-second per-request timeout, backing off 1s then 2s. A 4xx is not retried — the receiver rejected us, and retrying will not help.
- Those attempts all happen while the provider's event is being handled. There is no background retry queue, so a delivery that fails all three is terminal and shows in the log as
failed. - Providers redeliver. Each event carries the provider's own event id, and Agentic Fabriq will not deliver the same subscription/event-id pair twice. Your handler should still be idempotent, because your own 200 can be lost in transit.
"Did my webhook arrive?"
Two logs answer it. Both take X-Organization-Id and require the triggers.view scope.
| Endpoint | Shows |
|---|---|
GET /api/v1/triggers/ingest-events | What arrived from the provider, and whether it matched anything — outcome is matched, no_subject or no_matching_trigger, with matched_count. Metadata only, no payloads, kept 48 hours. |
GET /api/v1/triggers/deliveries | What Agentic Fabriq tried to send you — status (pending, delivered, failed, dead), attempts, last_error, delivered_at. Filter by subscription_id and status; paginate with limit and offset. |
The same two views are in the console under Triggers, and from the terminal:
afctl triggers list
afctl triggers list --provider slack --json
afctl triggers deliveries --status failed --limit 25The remaining management endpoints are GET /api/v1/triggers, PATCH /api/v1/triggers/{trigger_id} (enable, disable, or change scope) and DELETE /api/v1/triggers/{trigger_id}. Creating, changing and deleting all require the triggers.manage scope.
Need help?
Our team is here to help you get started.