Webhooks
Webhooks let Syntic tell your system when something happens, instead of your system asking over and over. You register an HTTPS URL and the event types you care about; Syntic sends a signed POST to that URL each time one occurs: a batch finishes, an agent session needs a decision, spending crosses a threshold.
When to use them
| Use a webhook | Poll instead |
|---|---|
| Batches that finish hours later | A short script that waits a few seconds |
| Agent sessions waiting for approval while nobody is watching the stream | You already hold an open session stream |
| Alerting finance when spend passes a threshold | A daily report job |
| Reacting to failed scheduled runs | Reviewing runs by hand |
Endpoints
| Method | Path | Purpose |
|---|---|---|
POST | /v1/webhooks | Register an endpoint. |
GET | /v1/webhooks | List endpoints. |
GET | /v1/webhooks/{webhook_id} | Read one. |
PATCH | /v1/webhooks/{webhook_id} | Change URL, events or status. |
DELETE | /v1/webhooks/{webhook_id} | Remove it. |
POST | /v1/webhooks/{webhook_id}/rotate_secret | Issue a new signing secret. The old one keeps working for 24 hours. |
POST | /v1/webhooks/{webhook_id}/test | Send a webhook.test event now. |
GET | /v1/webhooks/{webhook_id}/deliveries | Delivery log: each attempt, status code and timing. |
POST | /v1/webhooks/{webhook_id}/deliveries/{delivery_id}/redeliver | Send a past event again. |
Base URL https://app.syntic.ai/v1 (the paths above already include /v1). Authenticate with a private key and send syntic-beta: webhooks-2026-11-01. Webhooks belong to the key’s workspace and receive only that workspace’s events. Organization-wide events (spend, security) can also be registered with an Admin API key at /v1/organizations/webhooks, which has the same shape.
Register an endpoint
POST /v1/webhooks
| Field | Required | Notes |
|---|---|---|
url | Yes | https:// only, public address, up to 2,048 characters. |
events | Yes | Event types to receive, or ["*"] for all. |
description | No | Up to 200 characters. |
status | No | enabled (default) or disabled. |
The response includes signing_secret (starting whsec_). It is shown once; store it with your other secrets.
curl -sS https://app.syntic.ai/v1/webhooks \
-H "Authorization: Bearer $SYNTIC_API_KEY" \
-H "Content-Type: application/json" \
-H "syntic-beta: webhooks-2026-11-01" \
-d '{"url": "https://hooks.example.com/syntic",
"events": ["batch.completed", "session.requires_action", "deployment.run.failed"]}'What Syntic sends
POST /syntic HTTP/1.1
Host: hooks.example.com
Content-Type: application/json
User-Agent: Syntic-Webhooks/1.0
syntic-webhook-id: whk_01JCA4C6E8G0J2L4N6Q8
syntic-event-id: evt_01JCA5D7F9H1K3M5P7R9
syntic-signature: t=1762300800,v1=3b9a0f5c...{
"id": "evt_01JCA5D7F9H1K3M5P7R9",
"type": "batch.completed",
"created_at": "2026-11-05T10:00:00Z",
"workspace_id": "wrkspc_01JC0A2C4E6G8J0L2N4Q",
"data": {
"object": { "id": "batch_01JC9...", "type": "batch", "status": "completed", "request_counts": { "succeeded": 998, "errored": 2 } }
}
}data.object is the same object the matching GET endpoint returns, at the moment of the event. Payloads hold ids and status, never prompt or output content; fetch details with the API if you need them.
Event types
| Event | Sent when |
|---|---|
batch.completed, batch.failed, batch.expired, batch.canceled | A batch reaches a final state. |
file.expired | A file is deleted by its expiry. |
session.idle | An agent session finishes a turn. |
session.requires_action | A session waits for a tool result or approval. |
session.error | A session’s turn ends with an error. |
deployment.run.failed, deployment.paused | A scheduled or triggered run fails, or a deployment auto-pauses. |
environment.build.succeeded, environment.build.failed | An environment build finishes. |
spend.threshold_reached | Credit spend this month passes 50%, 80% or 100% of a spend limit. |
credits.low_balance | The credit balance falls below the auto-reload trigger and auto-reload is off or failed. |
inference_hook.failing | The inference policy hook has failed for 5 minutes. |
encryption_key.unavailable | Your customer-managed key cannot be used. |
webhook.test | You called /test. |
More event types will appear over time; skip any you do not recognise.
Verify signatures
Always check the signature before trusting a delivery. syntic-signature has a timestamp t and one or more v1 values (more than one during secret rotation). Compute HMAC-SHA256 over the string t + "." + raw request body with your signing secret, hex-encode it, and compare it to each v1 in constant time. Reject the delivery if no value matches or if t is more than 5 minutes from your clock.
import crypto from 'node:crypto'
import express from 'express'
const app = express()
app.post('/syntic', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('syntic-signature') ?? ''
const parts = Object.groupBy(header.split(','), (p) => p.split('=')[0])
const t = parts.t?.[0]?.slice(2)
const sigs = (parts.v1 ?? []).map((p) => p.slice(3))
const expected = crypto
.createHmac('sha256', process.env.SYNTIC_WEBHOOK_SECRET)
.update(`${t}.${req.body}`)
.digest('hex')
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300
const ok = fresh && sigs.some((s) => s.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)))
if (!ok) return res.sendStatus(400)
const event = JSON.parse(req.body)
queue.push(event) // do the work later
res.sendStatus(200) // answer fast
})Delivery and retries
- Respond with any
2xxwithin 10 seconds. Anything else, or a timeout, counts as a failure. - Failed deliveries are retried with exponential back-off for up to 3 days: after about 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, then every 12 hours.
- Delivery is at least once. The same event can arrive more than once; use
syntic-event-idto ignore duplicates. - Order is not guaranteed. Use
created_atand the object’s own status, not arrival order. - If an endpoint fails every delivery for 3 days, it is set to
disabledand Owners are emailed. Fix it, thenPATCHit back toenabledand redeliver what you missed from the delivery log.
Syntic sends from a published set of IP addresses (listed in the Console under Settings → Webhooks) if your firewall needs an allowlist.
Errors
API errors use the error envelope.
| Status | type | code | When |
|---|---|---|---|
| 400 | invalid_request | invalid_url | Not HTTPS, not a public address, or too long. |
| 400 | invalid_request | unknown_event_type | An entry in events is not a known type. |
| 404 | not_found | webhook_not_found | No such webhook in this workspace. |
| 409 | invalid_request | webhook_limit_reached | More than 20 endpoints in the workspace. |
Limits
| Limit | Value |
|---|---|
| Endpoints per workspace | 20 |
| Payload size | Up to 64 KB |
| Delivery log kept | 30 days |
| Response timeout | 10 seconds |
Webhooks are free.
Build notes for the platform team
- One outbound delivery service for every product event; features publish events to a queue and never call customer URLs directly.
- Resolve and validate the destination on every attempt with the existing URL validator (no private, loopback or metadata addresses), and do not follow redirects.
- The signing scheme is shared with inbound deployment triggers and inference hooks.