Overview
A webhook is push on top of the v1 API: the moment something happens in the organization, Smengo sends a POST to your URL. No need to poll /api/v1/* on a schedule.
Webhooks vs polling
| Polling the API | Webhooks | |
|---|---|---|
| Who initiates | you, on a schedule | Smengo, when the event happens |
| Latency | equals your polling interval | a single change arrives within a minute |
| Idle cost | requests keep going even when nothing changed | no requests while there are no events |
| What you get | the current state of a resource | the fact of a change: type, ids, date |
The event body is thin: identifiers and dates, no details. Details are read back with your API key — most events carry a ready-made related.url for exactly that. That way a webhook never exposes more than the API would to the same key, and the body cannot go stale between retries.
The usual integration shape: webhook as the trigger, API as the source of truth.
How to set it up
- Open Settings → Integrations → the Webhooks block. The section requires the Organization settings permission (
manage_org). - Click Add webhook, enter the receiver URL, tick the events and (optionally) a description.
- Copy the
whsec_…secret — it is shown exactly once. Every request is signed with it, see Verifying the signature. - Click Send test event — a service
pingevent goes out and your server's response code appears in the card right away.
You can only subscribe to events whose data you can see in Smengo yourself — see the event-to-permission table in Events. Unavailable checkboxes are disabled with an explanation.
A step-by-step guide for non-technical users is in the help center.
Delivery format
- Method
POST, body is compact JSON (no indentation, no line breaks). Content-Type: application/json; charset=utf-8,User-Agent: Smengo-Webhooks/1.0.https://and port 443 only. The scheme and port are checked when the URL is saved and again before every delivery.- Redirects are not followed. A
3xxresponse is a failed delivery, not a hop toLocation. - Timeout — 15 seconds for the whole delivery (connect, TLS, response).
- Success is strictly
2xx. The response body has no effect on the outcome: we read at most 512 bytes of it — those go to the log for debugging, and the rest is never read at all (the connection is torn down at that point). - One event, one request. There is no batching.
Request headers
| Header | Value |
|---|---|
Smengo-Event-Id |
Event UUID. One per event: if two endpoints subscribe to the same event, both receive the same Smengo-Event-Id. It never changes across retries — this is the deduplication key. |
Smengo-Delivery-Id |
<delivery uuid>:<attempt number>, e.g. …:3. There is one delivery row per endpoint; a retry reuses it and only bumps the number. |
Smengo-Event-Sequence |
Integer, monotonically increasing. Sort events by it, not by arrival time. |
Smengo-Event-Type |
schedule.published, schedule_entry.changed, checkin.recorded, swap.decided or the service ping — routing without parsing the body. |
Smengo-Timestamp |
Unix seconds of the signing moment. Same value as t= in the signature. |
Smengo-Signature |
t=<unix>,v1=<hex>. During secret rotation there are two signatures separated by a space: t=…,v1=<hex> v1=<hex>. See Verifying the signature. |
We also set Content-Length and Accept-Encoding: identity (we don't need a compressed response — only its first 512 bytes go to the log).
Example request
POST /hooks/smengo HTTP/1.1
Host: example.com
Content-Type: application/json; charset=utf-8
User-Agent: Smengo-Webhooks/1.0
Smengo-Event-Id: 3f1c8b52-9a4d-4f7e-b2c1-8e5d0a6b4c39
Smengo-Delivery-Id: 7b2e4d16-0c58-4a93-9f41-2d6b8c3e5a70:1
Smengo-Event-Sequence: 10427
Smengo-Event-Type: schedule_entry.changed
Smengo-Timestamp: 1785921323
Smengo-Signature: t=1785921323,v1=5a0f…c81b
{"id":"3f1c8b52-9a4d-4f7e-b2c1-8e5d0a6b4c39","type":"schedule_entry.changed","created":"2026-08-01T09:15:23.481Z","sequence":10427,"org_id":"9c1a2e34-5678-4abc-9def-0123456789ab","api_version":"v1","data":{"action":"updated","employee_id":"5e6d9f2a-3b7c-4e2f-9a1d-7c8e2f6b4a10","entry_date":"2026-08-14","entry_id":"a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"},"related":{"url":"https://smengo.com/api/v1/schedule-entries?from=2026-08-14&to=2026-08-14"}}
Event envelope
{
"id": "3f1c8b52-9a4d-4f7e-b2c1-8e5d0a6b4c39",
"type": "schedule_entry.changed",
"created": "2026-08-01T09:15:23.481Z",
"sequence": 10427,
"org_id": "9c1a2e34-5678-4abc-9def-0123456789ab",
"api_version": "v1",
"data": { "action": "updated", "employee_id": "5e6d9f2a-3b7c-4e2f-9a1d-7c8e2f6b4a10", "entry_date": "2026-08-14", "entry_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d" },
"related": { "url": "https://smengo.com/api/v1/schedule-entries?from=2026-08-14&to=2026-08-14" }
}
| Field | Type | Description |
|---|---|---|
id |
uuid | Same as Smengo-Event-Id. |
type |
string | Event type. |
created |
string | When the event was born, ISO 8601 UTC with a Z suffix and milliseconds. |
sequence |
number | Same as Smengo-Event-Sequence. |
org_id |
uuid | Source organization. Useful when one receiver serves several organizations. |
api_version |
string | The API version the identifiers and the link belong to — always v1 for now. |
data |
object | Contents depend on the event type — see Events. Keys inside data are sorted alphabetically. |
related.url |
string | A ready-made GET request to the v1 API for the details. Optional field: swap.decided, schedule.published and shift deletions don't have it — see Events. |
The envelope key order is fixed (id, type, created, sequence, org_id, api_version, data, related) and identical across retries: the signature is computed over exactly the bytes that go into the body.
Instants inside data arrive the way the database emits them — ISO 8601 with an offset (2026-08-01T09:15:23.481+00:00) — whereas the envelope's created is always normalized to Z. Parse both with an ISO 8601 parser, not by comparing strings.
At-least-once and deduplication
Delivery is guaranteed at least once: an event may arrive again after a timeout, a dropped connection, or the recovery of a stuck delivery. That is normal operation, not a malfunction.
Deduplicate by Smengo-Event-Id. A practical shape: a unique index on that id on your side; a duplicate insert isn't an error but a signal of "already processed, reply 200".
Order is not guaranteed
Events may arrive in any order: one transaction produces a batch, deliveries run in parallel, and a retry can push a single event hours ahead.
To restore order use Smengo-Event-Sequence — an integer that increases monotonically within the organization. created won't do: all events of one transaction share it down to microseconds.
Receiver requirements
- A public
httpsURL on port 443. Addresses in private ranges (10/8,127/8,169.254/16,192.168/16, IPv6 ULA and so on) are rejected — nothing is sent there even with a valid DNS name. - Don't filter by IP. We neither publish nor promise a fixed list of addresses deliveries come from: a request is proven genuine by its signature, not by its source address.
- No
user:pass@in the URL and no#…fragment. The URL may be up to 2000 characters long. - Reply
2xxfast, process asynchronously. Verify the signature, enqueue the event, reply200immediately. If your business logic runs into the 15-second timeout, the delivery counts as failed and will come again. - Be idempotent — see deduplication above.
- Don't rely on redirects. The URL must be served by the final server; we will not follow a
301/302to a new path. - Reply
410 Goneonly when the URL is dead for good: it is the one code that disables the webhook immediately.
The ping test event
The Send test event button sends a service event:
{
"id": "…",
"type": "ping",
"created": "2026-08-01T09:15:23.481Z",
"sequence": 10428,
"org_id": "9c1a2e34-5678-4abc-9def-0123456789ab",
"api_version": "v1",
"data": { "test": true }
}
It is signed with the same secrets and carries the same headers as a real delivery — verify it with the same code. You cannot subscribe to ping: it is sent to one specific endpoint, including a paused one, and is never retried (exactly one attempt).
v1 limitations
- Endpoints are managed in the UI only: there are no
/api/v1/webhook-*REST endpoints, and the v1 API stays read-only. - A subscription covers the whole organization: there is no per-department filter.
- There is no manual redelivery in the UI — read what you missed through the API.
Next
- Events — the catalog,
datacontents, permissions. - Verifying the signature — ready-made Node and Python code.
- Retries and failures — the retry schedule, auto-disabling, the log.