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

# Sync History

> Ingest a connected account's LinkedIn message history in bounded, resumable runs.

Imports a connected account's message history, one bounded run at a time, and returns a `request_id` to poll. Paid: one flat price per **run** (see [Pricing](/pricing)). Requires the `read` grant.

Runs are resumable. A run enumerates messages newest-first, up to `max_pages` pages of 250, and commits **each page together with its next cursor** in one transaction. If the run hits its page bound, it is `paused` with a durable cursor. The next call with `mode: "continue"` resumes right after the last committed page. A crash between pages loses nothing, and replaying a page inserts nothing twice (messages are unique per account + provider id).

The call is async. `GET /v1/requests/{request_id}` shows live progress (pages, messages seen/inserted) while the run executes.

<Snippet file="audit-context-params.mdx" />

## Request

<ParamField body="account_id" type="string" required>
  Connected LinkedIn account id.
</ParamField>

<ParamField body="mode" type="string" default="continue">
  `continue` resumes the newest paused cursor (or enumerates from the top when nothing is paused). `restart` discards paused cursors and starts from the top. `history` also re-requests the provider's own history back-fill (see below).
</ParamField>

<ParamField body="max_pages" type="number" default="10">
  Pages of 250 messages this run may fetch, 1–10 (the flat price covers up to 10). A 10,000-message inbox is four runs.
</ParamField>

## Before payment

A rejected sync is never charged, because these checks run on the unpaid request. The account must be yours (`404 account_not_found`), `connected` (`409 reconnect_required`), have the `read` grant (`403 grant_denied`), and have no run already queued or running (`409 sync_in_progress` with `run_id`).

## Response

```json theme={null}
{
  "request_id": "…", "receipt_id": "rcpt_…", "run_id": "…",
  "status": "processing", "tool": "linkedin",
  "kind": "initial", "resumed_from_cursor": false, "max_pages": 10,
  "coverage": { "…": "…" }, "sync_state": "syncing"
}
```

The job result (from `GET /v1/requests/{request_id}`) reports `outcome` ∈ `exhausted` (cursor ran out) · `bounded` (page limit hit, `needs_continuation: true`) · `partial` (data landed, then a hard error, cursor preserved), plus counters: `pages`, `requests`, `messages_seen`, `messages_inserted`, `messages_updated`, `conversations_inserted`, `earliest_seen_at`, `latest_seen_at`, `upstream_ms`, `duration_ms`, and `provider_history`.

## Coverage

`GET /v1/tools/linkedin/accounts/{id}/sync` (free) returns:

```json theme={null}
{
  "account_id": "…",
  "sync_state": "partial",
  "coverage": {
    "provider_history": "running",
    "provider_history_completed_at": null,
    "enumerated_as_of": "2026-09-15T12:10:00.000Z",
    "enumerated_back_to": "2025-02-01T09:12:00.000Z",
    "latest_message_at": "2026-09-15T11:58:00.000Z",
    "pending_cursor": true,
    "message_count": 2500,
    "conversation_count": 113,
    "last_incremental_at": null,
    "last_webhook_at": "2026-09-15T12:21:00.000Z",
    "complete": false
  },
  "active_run": null,
  "last_run": { "run_id": "…", "kind": "initial", "status": "paused", "has_next_cursor": true, "pages": 10, "…": "…" },
  "runs": [ "…" ]
}
```

**Why two notions.** Cursor exhaustion does not mean the history is complete. The connection provider back-fills an account's history in the background after login. Its listing cursor can run out while the back-fill is still running. The live test behind this feature saw 174 messages at cursor exhaustion and 393 on the same account a few minutes later. OneShot therefore requests the provider back-fill on the first run, polls it on later runs, and reports `complete` only when:

1. `provider_history` is `done` (the provider finished, or its `SYNC_SUCCESS` webhook arrived), **and**
2. a windowless enumeration started *after* that and reached cursor exhaustion, **and**
3. no paused cursor remains.

Until then `sync_state` is `partial` (or `syncing` while a run is active). `mode: "history"` forces a new provider back-fill request when a previous one ended in `error`.

If your paid run exhausted the available messages while provider history is still `requested` or `running`, you do not need another paid sync. Reconciliation checks pending history every ten minutes and runs free internal imports, including a full enumeration after the provider finishes, even if the provider's completion webhook never arrives. A paid run paused at its page limit still requires an explicit `continue`.

## Incremental updates

You do not need to keep calling `sync`. New, edited, deleted and read messages arrive through the provider's messaging webhook, and a scheduled reconciliation sweep enqueues small **internal** runs (an overlap window behind the last incremental) every few hours, plus a nightly conversation-metadata refresh. Internal runs are never charged and never continue a *paid* paused cursor. Only the owner can continue one. Cursors are opaque and never returned. `has_next_cursor` / `pending_cursor` tell you one exists.

## Charge semantics

| Situation | Charged? |
| - | - |
| Run completes with zero new messages | Yes, because the enumeration ran. To avoid it, check `pending_cursor` / `complete` first. |
| Rejected before payment (ownership, grant, status, run active) | No. |
| Could not be dispatched (`502 enqueue_failed`) | No (not settled). |
| Run fails at execution with 0 pages (e.g. account disconnected) | Job fails and is eligible for the failed-job refund sweep. |
| Run lands pages then hits a hard error (`partial`) | Yes. The cursor is preserved and the next `continue` resumes. |
| Internal reconciliation runs | Never. |

## Example

```typescript theme={null}
let status = await agent.getLinkedInSync(accountId);
while (!status.coverage.complete) {
  const run = await agent.linkedinSync({ accountId, memo: 'initial import' }); // waits for the run
  status = await agent.getLinkedInSync(accountId);
  if (!run.needs_continuation && status.coverage.provider_history !== 'done') break; // provider still back-filling; come back later
}
```


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