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-Id header 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) and estimated_wait_seconds
  • Your metadata blob is returned verbatim
position_changed.json
{
  "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_by ISO-8601 timestamp
confirmation_required.json
{
  "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.

  • reason is one of ttl, heartbeat, confirmation_timeout
expired.json
{
  "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.

dequeued.json
{
  "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"]}'