Webhooks

Some systems can be given a URL and nothing else: no custom header, no auth configuration, no retry policy you control. For those, each source can issue a webhook URL: a dedicated webhook key in the path, instead of an API key in the Authorization header.

The endpoint

POST /v1/webhooks/{key}

curl https://api.sinio.ai/v1/webhooks/$SINIO_WEBHOOK_KEY \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      {
        "source_event_id": "evt_88213",
        "type": "badge_in",
        "occurred_at": "2026-08-28T07:58:12+02:00",
        "roles": { "device": "BORNE-A12", "subject": "P-4582" },
        "payload": { "badge": "04A2B9C1" }
      }
    ]
  }'

Same body, same limits, same counters, same rejection codes as POST /v1/events — mode included: a history sent with "mode": "backfill" stays silent here too (Backfill). Like an API key, the webhook key belongs to one source: everything posted to the URL lands in it, no header needed.

A webhook key is not an API key. It carries the ingest:webhook scope only: it opens this URL and nothing else, and it is refused as a Bearer key on every other route. The other way round, an API key does not work in a webhook URL. A URL leaks far more easily than a header, so the secret it carries must be worth as little as possible. Create one from the source's page in the project settings, and revoke it there.

What it is not

It is not a generic receiver. The body must be the sinio.ai event format. Pointing a third-party product's native webhook at this URL will not work: nothing normalises an arbitrary payload for you. If the emitting system cannot be told what shape to post, put a small adapter in front — a function, a queue consumer, anything that reshapes and forwards.

Only event ingestion is reachable this way. Entities, locations and assignments all require a Bearer API key.

Treat the URL as a secret

The webhook key is in the path, which means the whole URL is a credential:

  • It may appear in the emitting tool's configuration screen, in proxy logs and in browser history. Keep it out of anything shared. sinio.ai masks it in its own logs.
  • Never put it in client-side code or a public repository.
  • Use one webhook URL per emitting system, so revoking one does not silence the others.
  • Prefer an API key in the Authorization header whenever the emitting system supports it.

Replays are free

Webhook delivery is unreliable by nature: a timeout on your side, a retry policy you do not control, a queue that redelivers. All of it is expected.

Ingestion is idempotent per (project, source, source_event_id): a redelivered event with the same content as one already stored is recognised, counted in duplicates, and stored once. What you have to guarantee is that source_event_id is stable — the same fact must always carry the same id, and a redelivery the same content. Different content under a known id is a correction, counted in revisions (Idempotency).

A redelivered batch

{
  "batch_id": "0192f3a4-7b1c-7e2d-9a10-4c5b6d7e8f90",
  "accepted": 0,
  "pending_classification": 0,
  "duplicates": 3,
  "revisions": 0,
  "rejected": 0,
  "rejections": [],
  "test_mode": false
}

Three duplicates, nothing rejected, nothing stored twice. That response is a success.

Reading the response

Whatever the emitting system does with the response, log it if you can. A 200 with rejected: 3 looks exactly like a 200 with rejected: 0 to a system that only checks the status code — and the three lost events will only surface weeks later, as a hole in a report.

If the system genuinely cannot inspect a response body, the console shows the same rejections; check it during the first days of an integration.

Was this page helpful?