Call sync for app devices: never miss a call you were offline for
An enrolled device can now stream the calls that ended on its number, and catch up on everything it missed while offline — no matter how long it was gone.
An enrolled device can now hold an open stream and be told, as it happens, about calls that ended on its number — completed, missed, busy, or arriving while the person was already on another line.
curl -N "https://api.sautikit.com/v1/devices/calls/stream" \
-H "Authorization: Device $DEVICE_SECRET"Each event is a pointer, not a record:
id: 1756300800482913-9f1c8e2a-0000-4000-8000-000000000001
event: call.ended
data: {"call_id":"9f1c8e2a-...","status":"no_answer","missed":true,"ended_at":"2026-08-29T09:14:02Z"}
Read the full record from GET /v1/calls when you need it.
A push notification is deliver-now-or-lose-it. If the phone is off, out of coverage, or the app has been killed, the message evaporates and nothing remembers it was owed.
This works the other way round. The device holds a cursor and reads forward; the server keeps no per-device state and never sends anything. An offline phone has not lost a message — it simply has not read yet. Reconnect after five minutes or five weeks and the stream replays everything since the cursor.
Two rules make that work, and clients must follow both:
Persist the id: of the last event you processed and send it back as Last-Event-ID on every connect. That single header is also your acknowledgement — there is nothing else to call.
Deduplicate on call_id. A call is re-sent when its row changes after hangup, and re-reading a few seconds of recent history on each poll is deliberate: it is what guarantees a call committed by a slow transaction is never stepped over. Duplicates are free; a miss would not be.
truncated means more calls are waiting than one page holds. Reconnect immediately with your advanced cursor and you will get the next page; repeat until a connect finishes without it.
reconnect means the connection reached its fifteen-minute lifetime cap. Reconnect the same way. The cap exists so that revoking a device takes effect promptly rather than whenever it happens to disconnect.
A device connecting with no Last-Event-ID starts live from the moment it arrives. It does not replay its whole history, because a line with twenty thousand calls would spend a long time walking them before showing anything current.
Load the recents list and page backwards through GET /v1/calls instead, which is cursor-paginated for exactly that. The stream is a change-feed over that list, not a way to walk it.
Both surfaces are scoped to the device's own number, and the scope is taken from the device credential — never from a query parameter, so a device cannot widen its own view. Campaign dials and API-originated calls are excluded from both, so what appears in scroll-back is the same set that arrived live.