Events & Integration
Outbound webhooks
Configure system.Webhook listeners that POST Antly events to your URL with retries, delivery headers, and attempt logs.
Outbound webhooks send Antly events to an HTTPS endpoint you control. The models are system.Webhook (the URL and headers), system.WebhookListener (which event to subscribe to), system.WebhookListenerEvent (one delivery), and system.WebhookListenerEventLog (one HTTP attempt).
The workspace console is Settings → Application → Webhooks. Create a webhook there, copy the whsec_… signing secret once, add listeners, then watch Deliveries. Overview, health, pause/resume, replay, rotate secret, and send-test live on the webhook detail page. Old links to /update and /request-logs still resolve.
Delivery is always asynchronous. Creating a listener does not POST from the request that fired the event. Attempt 1 is queued as soon as the delivery row is committed; later attempts follow the retry schedule. The immediate field on a listener is deprecated and no longer blocks the originating request.
Pause a webhook with active: false on system.Webhook, or call pause / resume. Inactive webhooks and listeners are not fanned out and are not retried.
Create a webhook target
1{
2 "__meta__": {
3 "schema": "model",
4 "namespace": "system.Webhook",
5 "intent": "consequence",
6 "consequence": "createWebhook",
7 "sideEffects": { "consequenceType": "class" }
8 },
9 "name": "CRM sync",
10 "webhook_url": "https://crm.example.com/antly/events",
11 "header_config": [
12 { "key": "X-Integration", "value": "antly" }
13 ]
14}The payload returns { "id": …, "secret": "whsec_…" }. Store the secret immediately; retrieve never returns it.
A plain create still works but will mint the secret in post_created without returning it — prefer createWebhook.
Add a listener
1{
2 "__meta__": {
3 "schema": "model",
4 "namespace": "system.WebhookListener",
5 "intent": "create",
6 "authenticationClass": "jwt",
7 "return": {
8 "id": null,
9 "namespace": null,
10 "intent": null,
11 "event": null
12 }
13 },
14 "webhook": 12,
15 "namespace": "tickets.Ticket",
16 "intent": "create",
17 "event": "tickets.Ticket:create",
18 "fields": ["id", "status", "modified"],
19 "filter": "{}",
20 "exclude": "{}",
21 "include_metadata": true
22}Listeners can reduce payload noise with fields, filter, and exclude. Set include_metadata to false to POST only the data object instead of the full envelope.
Rapid updates to the same record within a five-minute checksum bucket collapse into one delivery carrying the latest payload.
What Antly POSTs
Each attempt is POST with Content-Type: application/json, a 10 s connect timeout, a 20 s read timeout (overridable per webhook), and no redirects. Only HTTP 2xx is success. 3xx, 4xx, 5xx, timeouts, TLS errors, and connection failures are retried.
Headers
| Header | Meaning |
|---|---|
User-Agent | Antly-Webhooks/1.0 |
X-Antly-Webhook-Id | The system.Webhook id |
X-Antly-Delivery-Id | Stable UUID for this delivery. Use it as the idempotency key |
X-Antly-Event | Listener event, e.g. tickets.Ticket:create |
X-Antly-Attempt | 1-based attempt number |
X-Antly-Timestamp | Unix seconds used in the signature |
X-Antly-Signature | t=<timestamp>,v1=<hex HMAC> — see below |
| Custom keys | From header_config on the webhook |
Treat X-Antly-Delivery-Id as the replay key, not the attempt number. A crashed worker can retry the same delivery; your endpoint should be idempotent on that id.
Signature
Each webhook has a secret (whsec_…), shown once by createWebhook and again by rotateSecret / revealSecret. It is never returned by retrieve.
The signature is HMAC-SHA256(secret, "<timestamp>." + raw body bytes), hex-encoded. During the 24-hour rotation grace the header carries two v1= values; accept if either matches.
1import hmac, hashlib, time
2
3def verify(secret: str, header: str, body: bytes, tolerance=300) -> bool:
4 parts = [p.split("=", 1) for p in header.split(",")]
5 ts = next(v for k, v in parts if k == "t")
6 sigs = [v for k, v in parts if k == "v1"]
7 if abs(time.time() - int(ts)) > tolerance:
8 return False
9 expected = hmac.new(secret.encode(), f"{ts}.".encode() + body, hashlib.sha256).hexdigest()
10 return any(hmac.compare_digest(expected, sig) for sig in sigs)Body
When include_metadata is true (default):
1{
2 "id": "9f1c…",
3 "event": "tickets.Ticket:create",
4 "namespace": "tickets.Ticket",
5 "intent": "create",
6 "trigger": "tickets.Ticket:create:42",
7 "occurredAt": "2026-09-18T21:00:00+00:00",
8 "attempt": 1,
9 "webhook": { "id": 12, "name": "CRM sync" },
10 "listener": { "id": 7 },
11 "data": { "id": 42, "status": "pending" },
12 "meta": { "checksum": 76716, "variations": {} }
13}When include_metadata is false, the body is only data (optionally projected to fields). Stored delivery payload is never rewritten between attempts.
Retry schedule
Failed deliveries retry up to seven times:
| Attempt | When |
|---|---|
| 1 | Immediately after the event is committed |
| 2 | ~15 minutes later |
| 3 | ~30 minutes later |
| 4 | ~6 hours later |
| 5 | ~24 hours later |
| 6 | ~72 hours later |
| 7 | ~1 week later |
Each delay is jittered by ±10 %. After attempt 7 the delivery is failed and is not retried. Statuses: pending → failing (retries remaining) → failed or success.
A webhook that exhausts 10 deliveries in a row (WEBHOOK_AUTO_PAUSE_AFTER_EXHAUSTED) is paused automatically (health: paused, pausedReason: auto). Fix the endpoint, then resume. sendTest posts a one-shot event through an existing listener. replay on system.WebhookListenerEvent re-queues that delivery; replayFailed on the webhook replays exhausted rows.
Inspect deliveries
List deliveries for a webhook:
1{
2 "__meta__": {
3 "namespace": "system.WebhookListenerEvent",
4 "schema": "model",
5 "intent": "retrieve",
6 "filter": { "webhook": 12 },
7 "ordering": ["-created"],
8 "limit": 25
9 },
10 "id": null,
11 "deliveryId": null,
12 "event": null,
13 "trigger": null,
14 "status": null,
15 "attempts": null,
16 "maxAttempts": null,
17 "processAt": null,
18 "lastAttemptAt": null,
19 "lastHttpStatus": null,
20 "lastError": null,
21 "created": null,
22 "listener": { "id": null, "eventName": null }
23}processAt is the next attempt time. Attempt rows live on system.WebhookListenerEventLog (attempt, httpStatus, ok, durationMs, error, response). Custom request headers are stored redacted.
Receiver checklist
- Return 2xx only when you have accepted the event. Do not return 3xx.
- Deduplicate on
X-Antly-Delivery-Id. - Respond within 20 seconds.
- Keep the URL stable; a changed
webhook_urlonly affects future attempts.
For the admin-facing setup guide, see /guides/channel-integrations or /guides/workspace-administration. For JQL model operations, see /reference/crud-operations.