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

# Data management

> Status and activity per user, reading conversations, and how syncing paces itself.

## Status and recent activity

`GET /api/connections?userRef=` answers the two questions you have about a user: are they connected, and how active have they been.

```json theme={null}
{
  "userRef": "user_123",
  "connected": true,
  "needsReauth": false,
  "recentConversationCount": 12,
  "recentWindowDays": 30,
  "connections": [ { "id": "…", "status": "active", "conversationCount": 142, "recentConversationCount": 12, "…": "…" } ]
}
```

* `connected` is true when at least one connection is `active`.
* `needsReauth` is true when at least one connection is `needs_reauth`. This is the only status that should trigger a "please reconnect" message. A `revoked` connection was either replaced after a reconnect or deleted by the user on purpose.
* `recentConversationCount` counts conversations whose **first message** is dated within the last 30 days, evaluated at request time. It measures what the user did, not what Verso imported: an old history imported yesterday counts for nothing, and continuing an old conversation does not move it into the window.

Always query by `userRef`. Connection ids change when a user reconnects, and one user may hold several connections.

An expired session is detected at the next sync, so `connected` can stay true for a few hours after the provider invalidated the session. `lastSyncAt` tells you when it was last verified.

## Reading conversations

`GET /api/conversations?connectionId=` returns pages of conversations, newest `update_time` first.

```typescript theme={null}
async function fetchAll(connectionId: string) {
  const all = [];
  for (let offset = 0; ; offset += 100) {
    const res = await fetch(
      `https://connect.tryverso.ai/api/conversations?connectionId=${connectionId}&limit=100&offset=${offset}`,
      { headers: { Authorization: `Bearer ${process.env.VERSO_API_KEY}` } },
    );
    const page = await res.json();
    all.push(...page.conversations);
    if (all.length >= page.total) return all;
  }
}
```

Because the list is ordered by `update_time`, a conversation the user continues moves back to the top. To detect changes cheaply, compare `update_time` with what you stored.

### Shape of a conversation

```json theme={null}
{
  "id": "6a1f…",
  "external_id": "68d9c2a1-3f0e-8005-b2c1-7e2f0d1a9b3c",
  "title": "Help me build a REST API",
  "update_time": "2026-09-20T10:05:00Z",
  "payload": {
    "title": "Help me build a REST API",
    "messages": [
      {
        "role": "user",
        "content": "I need a REST API for managing tasks…",
        "timestamp": "2026-09-20T10:00:00.000Z",
        "id": "aaa-…",
        "contentType": "text"
      },
      {
        "role": "assistant",
        "content": "I'll help you build a task management API…",
        "timestamp": "2026-09-20T10:00:05.000Z",
        "id": "bbb-…",
        "contentType": "text",
        "model": "gpt-4o",
        "status": "finished_successfully",
        "endTurn": true
      }
    ]
  }
}
```

`messages` is the conversation's current branch in chronological order. System messages and empty nodes are dropped; messages the user regenerated or branched away from are not included.

| Field | Always present | Description |
| - | - | - |
| `role` | Yes | `user`, `assistant` or `tool` |
| `content` | Yes | Text. Empty when the message only carries attachments. |
| `timestamp` | Yes | ISO 8601, or `null` when the provider gave none |
| `id` | Usually | The provider's message id |
| `contentType` | Usually | `text`, `multimodal_text`, `code`… |
| `model` | Assistant messages | Model that produced the message, e.g. `gpt-4o` |
| `status`, `endTurn` | Usually | Provider status flags |
| `attachments` | When present | Files and images: `type`, `asset_pointer`, `name`, `size_bytes`, `width`, `height` |
| `contentReferences` | When present | Citations embedded in an assistant answer |

Attachments are metadata only for now. The files are stored by Verso but cannot be retrieved through the API yet.

## Sync cadence

Every connection is synced by a background worker that lists the account's conversations, fetches the ones that changed, and stores them. The pace adapts to the account and to the provider's limits.

| Situation | Next sync |
| - | - |
| Right after the connection | Within a minute |
| History still being backfilled | Every 15 minutes, until the whole history is imported |
| Last sync found something new | 4 hours |
| Last sync found nothing | Twice the previous interval, up to 48 hours |
| The provider rate-limited the account | 1 hour |

The initial backfill fetches the most recent conversations first, then works backwards; a large history takes hours because the provider limits how fast one account can be read. Each sync that inserts or updates at least one conversation sends `data.imported`.

## Re-authentication

When the provider rejects the stored session, the connection becomes `needs_reauth` and `reauth.needed` is sent. Sign a new connect link:

```typescript theme={null}
const { url } = await signLink(
  { appId: "app_yourapp", userRef, scopes: ["conversations:read"], purpose: "connect" },
  process.env.VERSO_APP_SECRET!,
);
// send url to the user
```

After the user logs in, the new connection replaces the old one and sync resumes where it stopped. The conversations already imported stay readable in the meantime.
