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

# Inbox and message sync

> How conversations and messages reach the platform: what the initial backfill imports, how often each surface re-syncs, what realtime push covers, and why a message may not be there yet.

Your LinkedIn inbox is not read live. The platform keeps its own copy of every thread and
message, built by a sync through the account's own browser and topped up by a realtime signal.

## Three inboxes, synced separately

The regular messenger, the Sales Navigator inbox and the LinkedIn Recruiter inbox are separate
surfaces with their own threads and their own clocks. Every thread carries a `messenger_type`.

| | Regular inbox | Sales Navigator | Recruiter |
| - | - | - | - |
| `messenger_type` | `linkedin` | `sales_navigator` | `recruiter` |
| Row on the **Account Sync** tab | **Conversations** | **Sales Nav Conversations** | **Recruiter Conversations** |
| Available when | always | only if the account has Sales Navigator | only if the account holds a Recruiter seat |
| Muting and marking read | supported | not supported, `is_muted` is always `null` | not supported |
| Verbs | `get-my-latest`, `sync-my-linkedin-conversations` | `get-my-latest-sales-nav`, `sync-my-sales-navigator-conversations` | `get-my-latest-recruiter`, `sync-my-recruiter-conversations` |

SN and Recruiter threads sync only once the platform detects the subscription (`premium_check`,
its own surface, every 360 minutes), which has to run before the **Sales Nav Conversations** or
**Recruiter Conversations** row appears. A basic-messenger verb on an SN or Recruiter thread
answers `422 messenger_type_mismatch`.

### The Recruiter inbox

The Recruiter inbox belongs to a seat, not to the profile. The premium check stamps the seat
number and its contract on the account (`recruiter_seat_id`, `recruiter_contract_id`), and every
Recruiter call needs them: without a seat the verbs answer `422 recruiter_required`, without the
number `422 recruiter_seat_unresolvable`. A candidate is identified by `talent_id` (the `AEMAA…`
id the Recruiter UI shows), which the platform decodes to the same `ln_member_id` as the
profile's `ln_id` and `sn_id`, so one candidate is one thread whichever id you send.

One sync run walks the INBOX tab and then UNRESOLVED: a thread you opened with an InMail sits
under UNRESOLVED until the candidate replies, and INBOX alone would never show it. ARCHIVED and
SCHEDULED are not walked. Pages carry 15 threads, the Recruiter client's own size, and there is
no realtime push for this inbox: a reply lands on the next sweep (120 minutes by default) or on a
head refresh.

LinkedIn keeps a separate Recruiter session of about 30 days, and the platform records its
expiry as `recruiter_session_expires_at`. Past that clock every Recruiter call answers
`409 recruiter_reauth_required` and the recurring run is cancelled with the same reason; the
**Recruiter Conversations** row and the Premium block on the account say so. The fix is a
sign-in to Recruiter in the account's own browser. The platform never stores the Recruiter
password.

Sending goes through `POST /api/linkedin-messages/send-recruiter`: a new thread by `talent_id`,
`ln_id` or `sn_id`, or a reply into an existing thread by its sid. LinkedIn allows **one InMail
per candidate per 24 hours** unless they reply sooner, so a second message inside that window
answers `429 rate_limited` with `reason: "recruiter_inmail_cooldown"` and a `retry_after`; a
reply the candidate has not answered yet is refused by LinkedIn under the same rule and surfaces
the same way. `get-my-recruiter-seat` and `get-my-hiring-projects` read the seat's own
entitlements and hiring projects.

## What the initial backfill imports

Onboarding walks a fixed ordered set of surfaces (connections, connection requests, connection
invitations, the regular inbox, the premium check, the Sales Navigator and Recruiter inboxes when
the account holds those seats, then the profile snapshot) and the account unlocks when it finishes. Every thread is imported, since the run
pages to an empty page. Not every message is.

<Warning>
  The message drain runs inline inside the conversations sync itself, while the account is still
  onboarding, and fetches exactly **one page of the 50 newest messages per thread**, then stops;
  it does not page backwards. A 400-message thread arrives with its 50 most recent ones, and
  older history stays outside the platform: `POST /api/linkedin-messages/get-my-latest` refreshes
  the newest page of up to **100 messages** and stops there.
</Warning>

During onboarding the `self_account_sync` daily budget is raised from **1000 to 3000 calls per
day**, and it drops back to 1000 the moment onboarding completes. A first-time backfill runs to
completion whatever hours are configured; only recurring syncs wait for the window: an inbox
sync spends that budget and nothing else.

## How often each surface re-syncs

| Surface | Default interval | Editable per account |
| - | - | - |
| `conversations` | 180 minutes | yes |
| `sales_navigator_conversations` | 120 minutes | yes |
| `recruiter_conversations` | 120 minutes | yes |
| `connections` | 180 minutes | yes |
| `connection_requests` | 720 minutes | yes |
| `connection_invitations` | 720 minutes | yes |
| `premium_check` | 360 minutes | yes |
| `snapshot` | 1320 minutes | no |
| `followers` | 1440 minutes | no |

Three things change the effective interval: your per-account setting on the **Account Sync**
tab, the [sync window](/kb/sync-windows-and-auto-suspend) (a run due outside the configured
hours parks with `schedule:closed` until it reopens), and the free plan floor.

<Warning>
  On the free plan every surface is floored to **once per 1440 minutes (daily)**: the effective
  interval is the larger of the two. Free plan accounts also get **no realtime push**, so a
  reply can sit for up to a day. Initial backfills are exempt from the floor.
</Warning>

A recurring run is not a full re-read: it walks the directory newest first and stops once a page
holds **10 threads it already knows**, and regular-inbox pages are capped at **25 threads** by
the browser plugin (the Sales Navigator sweep pages at 100, the Recruiter sweep at 15). Only threads whose event count or
last activity moved ahead of the stored copy are read.

## Realtime push, and what it does not do

While the account's browser runs, the plugin listens to LinkedIn's own realtime stream and
tells the platform when a message arrives: end to end, a received message reaches the
`linkedin-messages.received` webhook in about **8 seconds**, against the 180 minute sweep.

The push carries identifiers only and never writes a message row. For a thread the platform
already knows it schedules a refresh of that one thread, read through the account's own
browser, and the rows land tagged `source='push'`. For a thread it has never seen it falls back
to an account-wide conversations refresh, whose rows are tagged `source='sync'` like the
sweep's: the webhook still fires, but the live Inbox screen does not update until the sweep.
Signals it drops:

| Reason | Dropped when |
| - | - |
| `self_sender` | Your own send, echoed back by LinkedIn |
| `duplicate_message` | The same message id arrived again within **600 seconds** |
| `coalesced` | A refresh for this thread is in flight, within a **180 second** latch |
| `rate_capped` | More than **10 refreshes in one minute** for this account, or the push path has already spent its half of the daily budget (**100 of 200** calls) |
| `initial_sync_pending` | Onboarding is about to read the thread anyway |
| `free_plan` | Free plan, where realtime push is off by design |

The rest are environmental: `disabled` (off platform-wide), `unhandled_type` (typing indicator
or read receipt), `no_linkedin_account`, `account_not_found` and `dispatch_failed`.

A dropped signal is never a lost message: the worst case is that it appears on the next sweep
instead of within seconds. Your own reply typed in the LinkedIn UI is always in that class, and
lands on the sweep as `type='outbox'` with `automation='synced'`: discovered, not sent.

## Force a refresh now

A **head refresh** reads the newest page of one thread or account synchronously and returns the
rows themselves; use it in almost every case. A **full sync** reconciles the head of the
directory in the background and returns a sync run to poll; like every recurring run it stops
once a page holds 10 threads it already knows, so it will not reach an old thread that is
missing locally. Only a sync reset re-walks the directory end to end.

```bash curl theme={null}
curl -X POST "https://app.gtm-api.com/linkedin/v4/api/linkedin-messages/get-my-latest" \
  -H "Authorization: Bearer gtm_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"linkedin_conversation_sid": "ln_cv_YOUR_THREAD", "page_size": 50}'
```

The response carries a `refresh` block with counters and a `stop_reason` of `overlap`,
`covered`, `page_cap` or `exhausted`. A head refresh never stamps the account's sync clock and
never emits `linkedin-conversations.sync-completed`, so it does not postpone the next drain.

<Note>
  Every head refresh page and every background message pull spends one call from the account's
  `self_account_sync` budget: **1000 calls per day**, paced about **one call a second**. When it
  is spent, reads answer `429 rate_limited` with `limit_type:
    "self_account_sync"` and a `retry_after`, and parked runs resume at the next daily reset or
  when the limit is raised. See [smart limits and warmup](/kb/smart-limits-and-warmup).
</Note>

## Why a message may not appear yet

Three timestamps answer most of it: `last_activity_at` (newest activity LinkedIn reports),
`last_stored_message_at` (newest message stored) and `last_message_sync_at` (last pull). Activity
newer than stored means the thread is already flagged and the next run drains it. Otherwise:

1. **The account is still onboarding.** The drawer shows **Initial sync in progress** and head
   refreshes answer `429 rate_limited` with `context.reason: "initial_sync_pending"`.
2. **A sync is already running for that surface.** A second one does not queue: it answers
   `429 rate_limited` with `reason: "sync_in_progress"` and the working `sync_run_sid` to poll.
   Only a run that is reading counts: one waiting for its window, a browser or a limit does not,
   and a head refresh of one thread never waits for the inbox sync.
3. **The sync budget is spent or held.** The run shows a `wait_reason` starting with `limit:`
   (`limit:daily_saturation`, `limit:held`, `limit:linkedin_quota_hit`, `limit:delay_not_elapsed`).
   Raise the Self account sync limit on **Smart Limits** with reset hold to resume them all now.
4. **The browser is down or logged out.** Sync verbs still enqueue and hand back a
   `sync_run_sid`; the run then parks with a `browser:` `wait_reason` and retries roughly 30
   minutes out, while a head refresh answers `503 service_unavailable`. Budget parks wake on a
   raised limit, [browser parks](/kb/browser-troubleshooting) wait for their retry.
5. **It is outside the sync window.** The run carries `wait_reason: "schedule:closed"` and a
   `next_attempt_at` set to the next opening. Widen the window or head-refresh now.
6. **The message is older than the newest 50 in its thread.** No sweep backfills older history:
   call `get-my-latest` on that thread to pull the newest page of up to 100 messages.
7. **It was a push drop and the sweep has not run.** Your own reply typed on LinkedIn, a
   duplicate signal, a burst above 10 refreshes a minute and free plan accounts all skip the fast
   path: the wait is then up to 180 minutes (regular), 120 (Sales Navigator and Recruiter), 1440 (free plan).
8. **Still nothing?** Send support the account sid (`ln_ac_…`), the **Account Sync** row with
   its status and next due time, and the sync run sid (`ln_sy_…`) with its `wait_reason`.

A sync run never has a "failed" status: runs go `pending`, then `in_progress`, then `completed`
or `cancelled`. One stuck at `in_progress` is almost always waiting, so read its `wait_reason`
and `next_attempt_at`.

<Tip>
  Copyable prompt for your AI agent:

  Diagnose why a LinkedIn message is missing from my GTM API inbox copy, for the `ln_ac_` sid I
  give you. Search `linkedin-account-sync-runs` for it, `sync_type` `conversations`, newest first,
  and report the latest run's `status`, `wait_reason` and `next_attempt_at`. Then search
  `linkedin-conversations` for the thread and compare `last_activity_at` with
  `last_stored_message_at`; if activity is newer, call `get_my_latest_linkedin_messages` on that
  sid with `page_size` 50 and report the `refresh` block. Only full-sync if threads are missing.
</Tip>

## Reading the stored copy

A message carries `type` (`inbox` or `outbox`), `automation` (`auto`, `manual` or `synced`) and
`linkedin_type` (`message`, `inmail` or `connection_note`). A `connection_note` is the invitation
note: LinkedIn puts it into the thread when the invitation is accepted, so it arrives through the
sync like any other message (`automation: synced`) and carries that label. Messages are
append-only on both surfaces: no edit, no delete, no unsend, no recall. There are no read
receipts and no open tracking, and marking a thread read changes the state on LinkedIn only.

There is no export button and no CSV. `linkedin-messages/search` and
`linkedin-conversations/search` page by cursor and touch neither the browser nor the budget
(filters and sorts are in the [API reference](/api-reference/overview)), and a conversation
from another workspace answers `403 wrong_team`, not `404`. Webhooks deliver
`linkedin-messages.received`, `.sent` and `.send-failed`, plus `linkedin-conversations.created`
and `.sync-completed`.

<Note>
  A send that fails never creates a message row: the failure lands on the activity log and is
  emitted as `linkedin-messages.send-failed` carrying the `linkedin_account_sid`, the
  `linkedin_conversation_sid` and an `error_code` (the activity-log sid is not in the payload;
  find the row by account and time). If a send times out and you are unsure whether it went
  through, search messages on that conversation with `type: "outbox"` and `sent_at` after the
  attempt. A blind retry can double-send.
</Note>

<Warning>
  A sync reset with `conversations`, `sales_navigator_conversations` or `recruiter_conversations`
  selected **soft-deletes
  every stored conversation for that account and surface, and all of their messages**, in one
  transaction. The next sync rebuilds the directory, resurrecting the same rows (same sid) as it
  re-observes them, so resurrected threads fire no `linkedin-conversations.created` and only
  genuinely new ones do, and it re-drains the newest 50 messages per thread; anything you fetched
  deeper stays soft-deleted and out of results. Nothing else removes a thread: one that
  disappears on LinkedIn keeps its stored copy.
</Warning>

### What happens when a send fails

A failed send is not retried automatically. Nothing picks it up on a timer, and the resend is something you or your team trigger.

The failure never lands in the conversation: a message row is only written on terminal success, so a failed attempt leaves no half-sent message.

If the failure was a timeout, the message may still have gone through on LinkedIn's side. Check the thread before resending, because a blind retry can double-send.

## Related

* [Sync windows and auto-suspend](/kb/sync-windows-and-auto-suspend)
* [Smart limits and warmup](/kb/smart-limits-and-warmup)
* [Connect a LinkedIn account](/kb/connect-a-linkedin-account)
* [Workspaces and team members](/kb/workspaces-and-team-members)


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