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 amessenger_type.
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. During onboarding theself_account_sync daily budget is raised from 200 to 3000 calls per
day; the first profile snapshot taken after onboarding completes recomputes it back down to
200. 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
Three things change the effective interval: your per-account setting on the Account Sync
tab, the sync window (a run due outside the configured
hours parks with
schedule:closed until it reopens), and the free plan floor.
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 thelinkedin-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:
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.curl
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.
Every head refresh page and every background message pull spends one call from the account’s
self_account_sync budget: 200 calls per day, with a 6 second pacing delay and bursts
of 5. 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.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:
- The account is still onboarding. The drawer shows Initial sync in progress and head
refreshes answer
429 rate_limitedwithcontext.reason: "initial_sync_pending". - A sync is already running for that surface. A second one does not queue: it answers
429 rate_limitedwithreason: "sync_in_progress"and the workingsync_run_sidto poll. - The sync budget is spent or held. The run shows a
wait_reasonstarting withlimit:(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. - The browser is down or logged out. Sync verbs still enqueue and hand back a
sync_run_sid; the run then parks with abrowser:wait_reasonand retries roughly 30 minutes out, while a head refresh answers503 service_unavailable. Budget parks wake on a raised limit, browser parks wait for their retry. - It is outside the sync window. The run carries
wait_reason: "schedule:closed"and anext_attempt_atset to the next opening. Widen the window or head-refresh now. - The message is older than the newest 50 in its thread. No sweep backfills older history:
call
get-my-lateston that thread to pull the newest page of up to 100 messages. - 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).
- 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 itswait_reason.
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.
Reading the stored copy
A message carriestype (inbox or outbox), automation (auto, manual, connect or
synced) and linkedin_type (message, inmail or connection_note). 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), 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.
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.