BuildWebhooks

Webhooks

⚠️
Preview — this feature is being built; the design below is the spec.

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 webhookPoll instead
Batches that finish hours laterA short script that waits a few seconds
Agent sessions waiting for approval while nobody is watching the streamYou already hold an open session stream
Alerting finance when spend passes a thresholdA daily report job
Reacting to failed scheduled runsReviewing runs by hand

Endpoints

MethodPathPurpose
POST/v1/webhooksRegister an endpoint.
GET/v1/webhooksList 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_secretIssue a new signing secret. The old one keeps working for 24 hours.
POST/v1/webhooks/{webhook_id}/testSend a webhook.test event now.
GET/v1/webhooks/{webhook_id}/deliveriesDelivery log: each attempt, status code and timing.
POST/v1/webhooks/{webhook_id}/deliveries/{delivery_id}/redeliverSend 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

FieldRequiredNotes
urlYeshttps:// only, public address, up to 2,048 characters.
eventsYesEvent types to receive, or ["*"] for all.
descriptionNoUp to 200 characters.
statusNoenabled (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

EventSent when
batch.completed, batch.failed, batch.expired, batch.canceledA batch reaches a final state.
file.expiredA file is deleted by its expiry.
session.idleAn agent session finishes a turn.
session.requires_actionA session waits for a tool result or approval.
session.errorA session’s turn ends with an error.
deployment.run.failed, deployment.pausedA scheduled or triggered run fails, or a deployment auto-pauses.
environment.build.succeeded, environment.build.failedAn environment build finishes.
spend.threshold_reachedCredit spend this month passes 50%, 80% or 100% of a spend limit.
credits.low_balanceThe credit balance falls below the auto-reload trigger and auto-reload is off or failed.
inference_hook.failingThe inference policy hook has failed for 5 minutes.
encryption_key.unavailableYour customer-managed key cannot be used.
webhook.testYou 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 2xx within 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-id to ignore duplicates.
  • Order is not guaranteed. Use created_at and the object’s own status, not arrival order.
  • If an endpoint fails every delivery for 3 days, it is set to disabled and Owners are emailed. Fix it, then PATCH it back to enabled and 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.

StatustypecodeWhen
400invalid_requestinvalid_urlNot HTTPS, not a public address, or too long.
400invalid_requestunknown_event_typeAn entry in events is not a known type.
404not_foundwebhook_not_foundNo such webhook in this workspace.
409invalid_requestwebhook_limit_reachedMore than 20 endpoints in the workspace.

Limits

LimitValue
Endpoints per workspace20
Payload sizeUp to 64 KB
Delivery log kept30 days
Response timeout10 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.
Was this page helpful?