Status and recent activity
GET /api/connections?userRef= answers the three questions you have about a user: are they connected, how active have they been, and is their history whole.
connectedis true when at least one connection isactive.needsReauthis true when at least one connection isneeds_reauth. This is the only status that should trigger a “please reconnect” message. Arevokedconnection was either replaced after a reconnect or deleted by the user on purpose.recentConversationCountcounts 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.historyCompleteis true once the whole history of the connection has been imported, andoldestConversationAt/newestConversationAtgive its span (first message of the oldest and of the newest conversation). While the initial import is still running,historyCompleteis false and the span only reflects what has arrived so far; the moment it turns true is announced by thehistory.completedwebhook.
userRef. Connection ids change when a user reconnects, and one user may hold several connections. Each connection carries an accountId, stable for one ChatGPT account within your app: two connections with the same accountId are the same account, for example an old revoked one and its replacement. A ChatGPT account can be connected by only one user of your app.
Each connection also carries what the provider says about the account: plan (free, go, plus, pro, team, enterprise…) and country (ISO 3166-1 alpha-2). Both are null until the first sync. Ads are shown to free and go accounts only.
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.
Your whole user base
For a periodic run over every user,GET /api/users returns pages of users in the same shape, oldest first.
limit goes up to 500, so ten thousand users take twenty calls, well within the rate limit of one minute. Users who opened a connect link but never finished logging in are listed with connected: false and no connections.
Reading conversations
GET /api/conversations?connectionId= returns pages of conversations, newest update_time first.
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
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.
Attachments are metadata only for now. The files are stored by Verso but cannot be retrieved through the API yet.
Ads shown in a conversation
ChatGPT shows sponsored results to Free and Go accounts under some answers. Each conversation carries them inads, keyed to the user message they answered:
messageId is the id of the user message in payload.messages; visibility is allowed for an ad that was displayed. ads is read within two days of the conversation’s last update, the time the provider keeps it: it is empty for paid accounts (see plan on the connection), for conversations without any, and for conversations that were already older when the user connected. For those, and for every ad regardless of age, use the impressions below.
Every ad the account saw
The provider also keeps, for months, the history of every ad the account saw (viewed) or clicked (opened). Verso reads it back to its start over the first syncs, then picks up the new impressions at each sync, and matches each one to the conversation that was open at that moment. GET /api/ad-impressions?connectionId= lists them newest first:
conversationId is exact when Verso read the ad in the conversation (messageId set, the same id as in ads), the conversation active at that moment otherwise, and null when none of the imported conversations covers it. Each conversation also carries its own impressions as adImpressions, oldest first, so a conversation from before the user connected still shows the ads it had. A click is its own row, next to the viewed row of the same adId; adId is the ad’s request id, which ads[].adId extends with :<n>.
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.
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. When the backfill reaches the end of the history, history.completed is sent once, with the connection’s figures, and historyComplete turns true on GET /api/connections.
Re-authentication
When the provider rejects the stored session, the connection becomesneeds_reauth and reauth.needed is sent. Sign a new connect link: