---
title: 'Call sync for app devices: never miss a call you were offline for'
description: >-
  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.
summary: >-
  A Server-Sent Events change-feed for enrolled devices, with cursor-based
  catch-up that survives any offline period.
date: 2026-08-28T00:00:00.000Z
type: changelog
---


## What's new

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.

```bash
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.

## Why it is a stream and not a push

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.

## Two control events

`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.

## History is not on the stream

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.

## Scoping

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.
