> ## Documentation Index
> Fetch the complete documentation index at: https://developers.goparlay.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Sync calls into your database

> The recommended pattern for a daily (or hourly) sync that never misses a late, edited or deleted call.

Most teams want every Parlay call in their own warehouse or CRM. The reliable way is two steps: **one initial load**, then **a change feed** you read on a schedule.

> [!WARNING]
>
> Do not sync by asking for "calls recorded since yesterday". Calls can finish processing hours after they were recorded, and scores or outcomes can change later. The change feed catches all of that. A date window does not.

## The change feed

`GET /calls/changes` returns every change to your organization's calls in the order it happened.

```json theme={null}
{
  "data": [
    { "id": "e0a1…", "sequence": "48211", "type": "call.completed", "call_id": "6f1c…", "created_at": "2026-10-23T14:02:10Z" },
    { "id": "e0a2…", "sequence": "48212", "type": "call.transcript.ready", "call_id": "6f1c…", "created_at": "2026-10-23T14:02:41Z" },
    { "id": "e0a3…", "sequence": "48219", "type": "call.updated", "call_id": "91b0…", "created_at": "2026-10-23T15:10:02Z" },
    { "id": "e0a4…", "sequence": "48230", "type": "call.deleted", "call_id": "77d4…", "created_at": "2026-10-23T16:44:59Z" }
  ],
  "has_more": false,
  "checkpoint": "eyJ…"
}
```

| Event | Means | Your sync should |
| - | - | - |
| `call.completed` | A call finished processing and is ready | Fetch the call **and its transcript**, and store both |
| `call.updated` | The call, its scores or its transcript changed | Fetch the call **and its transcript** again and overwrite your copy |
| `call.transcript.ready` | A transcript was added to a finished call | Fetch the call and its transcript |
| `call.deleted` | The call was deleted or is no longer visible | Delete your copy |

Events carry ids, not call data, and one event can stand for several changes. So for **every** event except `call.deleted`, re-read both the call and its transcript. A transcript can arrive before the call finishes (you'll only see `call.completed`), and an edited or removed transcript arrives as `call.updated`. Re-reading both is simple and always ends with your copy matching Parlay.

## Step 1: initial load

Page through `GET /calls` (100 per page) and fetch `GET /calls/{call_id}/transcript` for each call you want text for. Your full history back to when your organization joined Parlay is available.

If you load with `GET /calls`, then on your first feed run call `GET /calls/changes` with **no cursor**. The feed starts from the beginning of its history and replays events you may already have. Applying them again is harmless, and it guarantees nothing that changed during your load is missed.

## Step 2: read the feed on a schedule

**1. Load your saved checkpoint**

Use the checkpoint from your last successful run. On the first run, send no cursor.

**2. Read every page**

Call `GET /calls/changes?cursor=<checkpoint>&limit=100`. Repeat with the returned `checkpoint` while `has_more` is `true`.

**3. Apply each event**

For `call.deleted`, delete your copy. For anything else, re-read the call and its transcript and overwrite your copy. If either returns 404, delete it locally. Applying the same event twice is harmless.

**4. Save the checkpoint last**

Only after your database commit succeeds. If the run fails, the next run repeats from the old checkpoint and nothing is lost.

```python theme={null}
import os, requests

BASE = "https://app.goparlay.io/api/v1"
s = requests.Session()
s.headers["Authorization"] = f"Bearer {os.environ['PARLAY_KEY']}"

class Stale(Exception):
    pass

def get(path, **params):
    r = s.get(BASE + path, params={k: v for k, v in params.items() if v is not None})
    if r.status_code == 404:
        return None                             # gone, not visible, or no transcript
    if r.status_code == 409:
        raise Stale()                           # transcript changed while paging
    r.raise_for_status()
    return r.json()

def transcript_text(call_id):
    """The full transcript, or None if the call has none (or it was removed)."""
    for _attempt in range(3):
        parts, cursor = [], None
        try:
            while True:
                page = get(f"/calls/{call_id}/transcript", limit=10, cursor=cursor)
                if page is None:
                    return None
                parts.append(page["data"]["text"])
                if not page["has_more"]:
                    return "".join(parts)
                cursor = page["next_cursor"]
        except Stale:
            continue                            # start again from the first page
    raise RuntimeError(f"transcript for {call_id} kept changing; retry next run")

def sync(db):
    checkpoint = db.load_checkpoint()          # None on the very first run
    while True:
        page = get("/calls/changes", cursor=checkpoint, limit=100)
        for event in page["data"]:
            call_id = event["call_id"]
            if event["type"] == "call.deleted":
                db.delete_call(call_id)
                continue
            call = get(f"/calls/{call_id}")
            if call is None:                    # no longer visible
                db.delete_call(call_id)
                continue
            db.upsert_call(call["data"])
            text = transcript_text(call_id)     # always re-read: it may be new, edited or removed
            if text is None:
                db.delete_transcript(call_id)
            else:
                db.upsert_transcript(call_id, text)
        db.commit()
        checkpoint = page["checkpoint"]
        db.save_checkpoint(checkpoint)          # only after the commit
        if not page["has_more"]:
            break
```

## Good to know

* **Run it as often as you like.** Hourly or every 15 minutes works well. An empty run costs one request.
* **A checkpoint belongs to your organization, not to one key.** After you roll or replace a key, keep using the same checkpoint with the new key. (Ordinary list cursors, like `next_cursor` on `/calls`, do belong to the key that made them.)
* **A 404 on a call means remove it.** Calls can become invisible to your key, for example when they are deleted.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.