API Reference
Webhooks
Linie fires an HTTPS POST to your endpoint on every meaningful state change. No polling needed.
Delivery
How delivery works
- HTTPS POST,
Content-Type: application/json - 5-second connect + read timeout
- Best-effort delivery — no retries in v0.1; webhook reliability coming in v0.2
X-Linie-Delivery-Idheader for idempotent deduplication- HMAC-SHA256 signature on every delivery — verify with
X-Linie-Signature
Verifying signatures
When you set webhook_secret on a queue, every delivery includes an X-Linie-Signature: sha256=<hex> header. Compute HMAC-SHA256(raw_body, secret) and compare — reject the request if it doesn't match.
import crypto from 'node:crypto';
function verifySignature(body, secret, header) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(header)
);
}import hashlib, hmac
def verify_signature(body: bytes, secret: str, header: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode(), body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, header)Configure a webhook URL with PATCH /queues/{id}, setting webhook_url and webhook_secret.
Events
position_changed
Fires whenever an entry moves forward in the queue (another entry was dequeued ahead of it). Used to send position-update messages.
- Fires on every position change
- Includes the new
position(1-indexed) andestimated_wait_seconds - Your metadata blob is returned verbatim
{
"event": "position_changed",
"queue_id": "q_abc123",
"entry_id": "e_xyz789",
"position": 2,
"estimated_wait_seconds": 240,
"metadata": {
"phone": "+6591234567",
"name": "Ahmad R."
},
"occurred_at": "2026-10-15T10:23:41Z"
}confirmation_required
Fires when an entry reaches position 1 and the queue has a confirmation_window set. The customer must confirm within the window or they are expired.
- Fires once, when entering position 1
- Includes
confirm_byISO-8601 timestamp
{
"event": "confirmation_required",
"queue_id": "q_abc123",
"entry_id": "e_xyz789",
"confirm_by": "2026-10-15T10:28:00Z",
"metadata": { "phone": "+6591234567" },
"occurred_at": "2026-10-15T10:23:00Z"
}expired
Fires when an entry is removed by TTL expiry, heartbeat timeout, or confirmation timeout. Use this to send a "your spot expired" message.
reasonis one ofttl,heartbeat,confirmation_timeout
{
"event": "expired",
"queue_id": "q_abc123",
"entry_id": "e_xyz789",
"reason": "ttl",
"metadata": { "phone": "+6591234567" },
"occurred_at": "2026-10-15T10:30:00Z"
}dequeued
Fires when an entry is explicitly dequeued by a staff member (via the dashboard "Call next" or the API). This is the signal to tell the customer it is their turn.
{
"event": "dequeued",
"queue_id": "q_abc123",
"entry_id": "e_xyz789",
"metadata": { "phone": "+6591234567" },
"occurred_at": "2026-10-15T10:31:00Z"
}Testing locally
Use Cloudflare Tunnel or ngrok to expose a local port and receive webhook events during development.
# expose localhost:8080 to the internet
cloudflared tunnel --url http://localhost:8080
# then set your webhook URL to the tunnel URL
curl -X PATCH https://api.linie.io/v1/queues/$QUEUE_ID \
-H "Authorization: Bearer $LINIE_KEY" \
-d '{"webhook_url":"https://your-tunnel.trycloudflare.com/hook","on_events":["position_changed","dequeued"]}'