Sync History
curl --request POST \
--url https://win.oneshotagent.com/v1/tools/linkedin/sync \
--header 'Content-Type: application/json' \
--data '
{
"account_id": "<string>",
"mode": "<string>",
"max_pages": 123
}
'import requests
url = "https://win.oneshotagent.com/v1/tools/linkedin/sync"
payload = {
"account_id": "<string>",
"mode": "<string>",
"max_pages": 123
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({account_id: '<string>', mode: '<string>', max_pages: 123})
};
fetch('https://win.oneshotagent.com/v1/tools/linkedin/sync', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://win.oneshotagent.com/v1/tools/linkedin/sync",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'account_id' => '<string>',
'mode' => '<string>',
'max_pages' => 123
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://win.oneshotagent.com/v1/tools/linkedin/sync"
payload := strings.NewReader("{\n \"account_id\": \"<string>\",\n \"mode\": \"<string>\",\n \"max_pages\": 123\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://win.oneshotagent.com/v1/tools/linkedin/sync")
.header("Content-Type", "application/json")
.body("{\n \"account_id\": \"<string>\",\n \"mode\": \"<string>\",\n \"max_pages\": 123\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://win.oneshotagent.com/v1/tools/linkedin/sync")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"account_id\": \"<string>\",\n \"mode\": \"<string>\",\n \"max_pages\": 123\n}"
response = http.request(request)
puts response.read_bodyLinkedIn
Sync History
Ingest a connected account’s LinkedIn message history in bounded, resumable runs.
POST
/
v1
/
tools
/
linkedin
/
sync
Sync History
curl --request POST \
--url https://win.oneshotagent.com/v1/tools/linkedin/sync \
--header 'Content-Type: application/json' \
--data '
{
"account_id": "<string>",
"mode": "<string>",
"max_pages": 123
}
'import requests
url = "https://win.oneshotagent.com/v1/tools/linkedin/sync"
payload = {
"account_id": "<string>",
"mode": "<string>",
"max_pages": 123
}
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({account_id: '<string>', mode: '<string>', max_pages: 123})
};
fetch('https://win.oneshotagent.com/v1/tools/linkedin/sync', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://win.oneshotagent.com/v1/tools/linkedin/sync",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'account_id' => '<string>',
'mode' => '<string>',
'max_pages' => 123
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://win.oneshotagent.com/v1/tools/linkedin/sync"
payload := strings.NewReader("{\n \"account_id\": \"<string>\",\n \"mode\": \"<string>\",\n \"max_pages\": 123\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://win.oneshotagent.com/v1/tools/linkedin/sync")
.header("Content-Type", "application/json")
.body("{\n \"account_id\": \"<string>\",\n \"mode\": \"<string>\",\n \"max_pages\": 123\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://win.oneshotagent.com/v1/tools/linkedin/sync")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"account_id\": \"<string>\",\n \"mode\": \"<string>\",\n \"max_pages\": 123\n}"
response = http.request(request)
puts response.read_bodyImports a connected account’s message history, one bounded run at a time, and returns a
The job result (from
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
request_id to poll. Paid: one flat price per run (see 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.
Request
string
required
Connected LinkedIn account id.
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).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.
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
{
"request_id": "…", "receipt_id": "rcpt_…", "run_id": "…",
"status": "processing", "tool": "linkedin",
"kind": "initial", "resumed_from_cursor": false, "max_pages": 10,
"coverage": { "…": "…" }, "sync_state": "syncing"
}
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:
{
"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": [ "…" ]
}
complete only when:
provider_historyisdone(the provider finished, or itsSYNC_SUCCESSwebhook arrived), and- a windowless enumeration started after that and reached cursor exhaustion, and
- no paused cursor remains.
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 callingsync. 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
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
}