Your calls no longer wait on the account's own sync
Your calls have a budget of their own. Each LinkedIn account takes up to 30 of your calls
a minute and 5 at once. The platform’s own work on the account (its syncs, checks and jobs)
runs on a separate budget and no longer counts against yours. Both used to share 10 a minute
and 3 at once, so a sync catching up could refuse a send with
429 rate_limited and
reason: "account_rate_exceeded". That refusal now carries retry_after, the moment a place
frees, so wait for it rather than guess.Inbox reads have room. The daily self_account_sync budget that every head refresh and
every background message pull spends is 1000 calls, up from 200. A busy integration polling
its inbox no longer spends it for the day and stops the account’s sync with it.A waiting sync no longer refuses a head refresh. get-my-latest used to answer
sync_in_progress for as long as a run of the same kind existed, even one that only waited for
its sync window or its browser, for hours. Now only a run that is reading counts, and a head
refresh of one thread never waits for the inbox sync.
Inbox and message sync has the details.Only accounts signed out for 30 days lose their browser profile
A Sandbox account that stays signed in to LinkedIn is never parked. The 30-day sweep used to
park an account with no user-driven activity, even one that was signed in and synced every day.
Now it parks only an account LinkedIn has logged out or restricted, and only after 30 days in
that state. The browser profile of such an account is deleted, so coming back means connecting
it and logging into LinkedIn again.Held profiles follow the same rule. After a downgrade, an account parked over the plan’s
limit used to lose its browser profile on day 28 of the hold. Now the profile is deleted only
once LinkedIn has logged the account out or restricted it for more than 30 days, with the
warning email a week before. A parked account that stays signed in waits for the upgrade.
Billing and plans has the details.
Invitations, comments, posts and reposts are sent once
Every LinkedIn send is sent once now. Connection requests, comments, replies, posts and
reposts follow the rule the message sends got earlier today. Give each one a
client_reference: the key and its place are one send, so a repeat is answered with what the
first send came to and never goes out twice. The place of an invitation is the person, of a
comment the post (named by any of its urns), of a reply the comment it answers, of a post where
it lands (your feed, a company page, a group), and of a repost the reposted post. A lost answer
is a 409 send_outcome_unknown with send_decisive_at, the moment it can no longer land, where
it used to be a 503 that said to retry shortly, and the platform reads LinkedIn to settle it.Ask without sending. POST /api/linkedin-connection-requests/check-sent and
POST /api/linkedin-posting/check-sent (check_linkedin_connection_request_sent and
check_linkedin_posting_sent over MCP) answer what an invitation, a comment, a post or a
repost came to, as the message check-sent does. All three name send_decisive_at for a send in
doubt. Every one of these send endpoints takes a person’s word, confirmed_not_sent or
confirmed_sent.What changes for a caller. A comment repeated after unknown no longer posts a second one.
409 post_not_created used to mean that nothing was published. It now means the post may be
out, so repeat the same request after retry_after, never as a new post. A post into a group
cannot be read back, so one in doubt waits for a person. A reaction stays outside the rule:
LinkedIn keeps one per account and post, so sending it again changes nothing. In a mass action a
comment step goes out under a key of its own, a connection request or a comment whose answer
was lost is asked about before anything goes again, and the step log no longer carries
send_decisive_at.A webhook for a call that failed. linkedin-account-activity-log.task-failed fires once for
each call an account dispatched that ended failed, with failure_source and, on a send,
send_outcome: unknown means the send may be out, so ask check-sent before sending it again.
Sending once and Receive webhooks have the
details.Mass actions: each message gets a key of its own
A run’s
client_reference is now a tag. A mass action sends each message under a key of its
own, so a step that runs again or is retried is answered with what the first send came to and
never goes out twice. A target’s own client_reference is still used as given when the plan sends
with that tool once. A client_reference T in a send step’s args used to go out unchanged on
every message of the run. Now each message carries T:{item sid}:{step id}, and a run with no key
puts {item sid}:{step id} on the messages that carried none.If you matched T exactly. A search for T no longer finds the run’s messages. Find them by each
item’s created_object_sid, or by their exact keys with eq or in. There is no prefix search on
client_reference, and a webhook subscriber matches the T: prefix in its own code. Never create
the run again because T matched nothing: that sends every message twice.Sending once. Every LinkedIn message send endpoint now answers a repeat of the same key to the
same place with what the first send came to, names error.context.send_outcome on every error, and
has check-sent (check_linkedin_message_sent over MCP) to ask without sending. A send step whose
answer was lost waits with wait_reason: send_outcome_unknown and asks before anything goes again,
and a retry by item sid can carry a person’s word with resend_unverified.
Sending once and Run a mass action have the
details.A browser nobody signs in on is deleted after 30 days
An unused browser no longer holds a seat for good. A browser takes a seat of the plan from
the moment it is created. One that no LinkedIn account ever signed in on, a sign-in link nobody
opened or a sign-in left half done, used to keep its seat, its proxy and its profile with no end.
Now the platform deletes such a browser 30 days after the last chance anyone had to sign in on it:
its creation, its last start, the last cloud-browser session opened on it, or the moment its last
sign-in link expired, whichever came last. The seat and the proxy are freed. A profile you brought
from your own GoLogin account stays in your GoLogin account; only the browser on our side goes.Browsers with a connected LinkedIn account are never deleted this way, even while the account is
signed out.
create_antidetect_browser says so in its description, and
Antidetect browsers and proxies has the
details.Recruiter: a seat that was never signed in is not a sign-out
recruiter_status tells the truth about a seat that never signed in. A Recruiter seat whose
browser has never been signed in to Recruiter used to read active and then flip to signed_out
and back on every check, with a “LinkedIn Recruiter signed your account out” email on each flip.
Now the seat read is the verdict: a seat in LinkedIn’s answer is active, a refused talent call
is signed_out, and no seat in the answer on an account that never held one is signed_out with
a new webhook reason, session_unconfirmed. That case gets its own email, Sign in to LinkedIn
Recruiter to switch it on, instead of a sign-out notice. The account’s page shows Sign in to
Recruiter for such a seat, and the row’s Recruiter mark wears the warn tone. Both Recruiter
emails are now sent at most once a day per account.The premium check no longer opens LinkedIn’s contract chooser by itself. From the platform’s
browser the chooser’s list is refused by LinkedIn on every account so far, a live session
included, so each attempt was a failed row on the sender’s activity screen twice a day and the
refusal was read as a lost session. get_linkedin_account_my_recruiter_contracts and
select_linkedin_account_recruiter_contract stay, pass LinkedIn’s answer through verbatim, and
never change recruiter_status on a refusal; a Select that answers a seat marks the account
active. The working way to a bound seat is the sign-in through the cloud browser, where
LinkedIn shows its own chooser.recruiter_session_expires_at is read from the account’s own browser. The clock used to come
from the antidetect vendor’s cookie store, which for a profile you connected from your own
GoLogin account is not the browser’s jar at all (nothing the platform runs writes to it). It is
now read from the running browser first, so the clock is the one the Recruiter calls are served
under.The background premium check paces itself. A full check is up to eight calls on the account
against a cap of ten a minute; it used to fail on the cap and start over from the first call,
which on a busy account filled every following minute with the same calls until the day’s
self-sync budget ran out. It now waits out the cap between calls instead.Post search: page_size stops at 50, and the API says why
scrape_linkedin_search_posts. page_size now takes 1..50 instead of 1..100, and a larger value
is refused with 422 page_size_above_linkedin_max instead of being sent through. The bound is
LinkedIn’s, not ours: its content search opens every query with a cluster of 3 posts and serves at
most 50 more per pagination request. Asked for more, it answers with an empty page, which the
platform read as the end of the feed, so a page_size of 100 came back as 3 rows with
has_more: false and no error attached. Ask for 50 or fewer and follow paging.next_cursor; a
page of 50 fills. The small end is unchanged and worth knowing: page_size 1 or 2 returns the
opening cluster of 3.The end of the feed is LinkedIn’s word. The same tool used to take any page LinkedIn answered
without posts as the end of the feed, which is how an answer the platform could not read (a page
that rendered differently, an interstitial) came back as a completed empty result. Now a page ends
the feed only when LinkedIn’s own answer says the window is empty; anything else without posts is
refused with 503 linkedin_page_unreadable and a retry_after a minute out, never returned as
rows.A signed-out sender is checked before you are told: lost session or LinkedIn restriction
Two red statuses, two emails. When LinkedIn signs a connected account out, the platform now
reads that member’s public profile from another LinkedIn account of the fleet before sending
anything. A profile that answers means a lost session: the browser stays
login_issue and the
Your LinkedIn account was signed out mail carries a Profile check line saying so. No
profile for the member’s stable id means LinkedIn restricted the account (or the holder closed
it): the browser moves to the new restricted status and the mail is LinkedIn restricted your
account, with LinkedIn’s verification and appeal steps instead of a re-login. Both statuses
are sticky, powered off and never auto-restarted; Start re-verifies the session for both.What changes on the wire. antidetect_browsers.status gains restricted; a dispatch on such
a sender is refused with 409 browser_account_restricted (next to browser_logged_out); the
browser event log gains the account_restricted row with the identities asked and LinkedIn’s
answer; the account search counts gain restricted_count, and signed_out_count now counts both
red statuses; two webhook events, antidetect-browsers.restricted and
linkedin-accounts.restricted, fire once per episode after the logged-out pair. See
browser troubleshooting.The services page as one read and one write, and reposts through one tool
Your “Providing services” page.
get_linkedin_account_my_services reads the connected account’s
own services page as its edit form (status, description, the offered services with their ids, the
location and messaging flags, review visibility, pricing, the currencies and the 16 top-level
service categories), or null when there is no page. set_linkedin_account_my_services is the one
write: with no page yet it creates one (description and services required, LinkedIn’s onboarding
defaults for the rest), with a page it changes only what you send, republish: true puts an
unpublished page back without touching it, and unpublish: true takes it down. Each write spends
one edit_profile slot; the response carries the page’s form as it was before the change.repost_linkedin_post. One tool for both kinds of repost: without text it is the plain repost
(the feed’s Repost button), with text it is a repost with your thoughts, which also takes
visibility, allowed_commenters_scope and mentions the way create_linkedin_post does (sending
those without a text is refused, not dropped). post_urn is the post being reposted as an activity,
share or ugcPost urn; a group post cannot be reposted. The answer has one shape for both:
activity_urn (what delete_linkedin_post takes to retract it), post_urn, url, created_at,
text, parent_post_urn. Spends the posting bucket.Edit the whole profile in one call: About, top skills, photo, background, and a tool for positions
edit_linkedin_account_my_profile edits the whole profile. Next to the intro card it now takes
about (the About text, 2600 characters; an empty string clears it), top_skills (up to five, as
names or the ProfileSkill:<n> ids a previous save reported; an empty list removes them all),
photo and background (one image each, as base64 or an https URL we download; PNG, JPEG, GIF or
WEBP, typed by their bytes). Send only what changes: the intro card is read first and submitted
whole, the About save is partial, each image replaces one picture. Every section is its own
LinkedIn save, run in order (intro card, About, photo, background) and reported under
result.sections with its own activity-log row; a section LinkedIn refuses stops the call there and
the error names failed_section and applied_sections, so what landed before it stays. One call
spends one edit_profile slot however many sections it saves.set_linkedin_account_my_position adds, edits or removes one position. No position_id adds
(title, company and start date required, an end date unless is_current); a position_id edits,
and only the fields you send change, because the platform reads the position’s form first and
submits it whole (LinkedIn replaces the form; is_current: true drops the end date, an end date
ends a current position); position_id with delete: true removes it. The id is the one each
entry now carries on the account’s own experience list (enrich_linkedin_person_experience on
the account’s own profile), or the add’s answer. The response carries the position’s form as it
was before an edit or a delete, and LinkedIn’s own verdict. Spends one edit_profile slot per call.The Recruiter InMail balance is a third pool, and it guards Recruiter sends
recruiter_inmail_credits on LinkedIn accounts. LinkedIn grants Recruiter InMails per contract and
moves nothing between its products, so a Recruiter seat holder has a third balance next to
inmail_credits (the Sales Navigator seat’s grant) and premium_inmail_credits (the Premium plan’s
own balance). The premium check’s inmail_credits step now reads it for an account whose
recruiter_status is active, next to whichever of the other two the plan has, and stores what
Recruiter’s Usage Overview shows as InMail credits. The field filters (eq, gte, is_null and the
rest). Null means not read, never zero; a signed-out seat keeps its last number until it signs in
again, and the number goes with the seat when the seat goes. 422 premium_required on a named
inmail_credits check now fires only when none of the three pools can be read.A Recruiter send is guarded by that pool. Opening a new Recruiter thread with
send_linkedin_recruiter_message on a balance the platform knows to be zero is refused before
anything reaches LinkedIn, with the rule no_inmail_credits, the way a linkedin.com InMail is refused
on an empty plan pool. A reply into an existing thread is never guarded. A balance never read does not
block, and LinkedIn’s own out-of-credits refusal zeroes the stored balance so the next send is refused
early.Three smaller things the node release carries. The connected account’s own experience entries
(enrich_linkedin_person_experience on the account’s own profile) carry position_id, the key the
coming profile-editing verbs take. enrich_linkedin_person_services answers page_id as the page’s
vanity slug for a published page, and null for a page its owner has unpublished, which looks the same
as never having had one. In get_linkedin_account_my_analytics a daily value can be negative:
engagements is a daily delta, and a retracted reaction shows as a below-zero day, so do not clamp it,
the period total is the sum.The invitation note is the message LinkedIn delivers, and the accept webhooks name it
connection_note is a label on a real message, not a copy. When an invitation is accepted,
LinkedIn puts the note into the 1:1 thread as an ordinary message. The platform recognizes it by its
text and labels that row linkedin_type: "connection_note"; it no longer writes a second, synthetic
copy of the note. So a note is one message, with LinkedIn’s own id, and it arrives through the
message sync like any other (automation: "synced").automation no longer takes connect. That value marked the synthetic copy and nothing else, so
no message ever carried it. The field is now auto, manual or synced. A filter that passed
connect matched nothing before and is rejected now.Both accept webhooks name the note. linkedin-connection-requests.accepted and
linkedin-connection-invitations.accepted carry note_message_sid, the ln_ms_ sid of that thread
message, or null when the invitation had no note or the thread has not synced yet. On invitations it
replaces seeded_message_sid, which was always null. linkedin-connection-requests.accepted also
carries resolved_connection_sid, the ln_cn_ sid of the connection the acceptance produced, so
reaching the new connection no longer needs a search.A custom-domain certificate stays active while it renews
A renewal no longer moves the certificate out of
active. renew_ssl_certificate and the daily
renewal job used to move the row to challenge for the length of the attempt, and to failed when the
attempt failed, while the certificate on the domain went on serving. status now says what the domain
serves: an active certificate stays active while a renewal runs and after one fails, until it expires
or is deleted. The attempt is the challenge object. It is set while an attempt is in flight, on any
status, and null otherwise.After a renew, poll challenge, not status. Read get_ssl_certificate until challenge is null.
last_error null means the new certificate is live, and issued_at and expires_at have moved.
last_error set means the attempt failed and the current certificate keeps serving. A client that waits
for status: "active" after a renew sees it on the first read and learns nothing from it. A row that
serves nothing (pending, failed, expired) moves through challenge to active or failed as
before, on issue_ssl_certificate and on renew_ssl_certificate.An active certificate with last_error set is still serving. It means the last renewal failed. The
platform retries once a day while the certificate is within 30 days of expires_at. The failure email is
sent when a renewal starts failing, not on every retry, and the notice about an approaching expiry is sent
once per certificate. Fix what last_error names (usually the CNAME) and call renew_ssl_certificate, or
leave it to the next daily attempt. An issue or renew call while an attempt is in flight answers
409 issuance_already_in_progress.One live certificate per domain. More than one workspace can register the same domain while nobody
holds it, and the first certificate to go active holds the domain until it expires or is deleted. From
then on create_ssl_certificate, issue_ssl_certificate and renew_ssl_certificate answer
409 domain_active_in_another_team for that domain in every other workspace, and retrying does not help
while that certificate is live. An attempt that was already in flight there ends as a failed attempt, with
last_error opening with the same reason. Before, only create checked, so a workspace that had
registered the domain earlier could still issue a second live certificate for it.Registering a failed, expired or deleted domain again answers as a new registration.
create_ssl_certificate on a domain the workspace already has a row for keeps that row and its sid. When
the row is pending, active or has an attempt in flight it comes back untouched, with
already_exists: true and a 200. When the row is failed or expired with nothing in flight, or was
deleted, the call resets it to pending and clears challenge and last_error. That answer is now
already_exists: false with a 201. It used to say true and 200, which read as “nothing changed” on a
row that had just lost its last_error. Read last_error with get_ssl_certificate before you register
a failed domain again if you still need the reason.The burst leaves the smart-limit row: one flat delay per bucket
delay_in_seconds is the whole pacing rule. Since 2026-09-18 calls of one action type leave one
at a time, and the row’s “burst N, then hold X” pair only divided the number the gate used. The pair
is gone: batch_size and last_counted_at left the smart-limit row, the update verb and the
linkedin-account-smart-limits.update mass-action step, and delay_in_seconds is the flat gap
between any two calls of the bucket (1..3600, varied by up to 20% per call; an undo still waits
2 seconds). Existing rows were carried onto the number they were already running: a row at the
platform pair took the platform’s flat default (10 s for messages, 30 s for connection requests,
5 s for searches and profile reads, 1 s for own-account reads), a hand-set pair kept its own
average rate. A body that still sends batch_size is refused with validation_failed.One person is one identity, whichever LinkedIn id you hold
Every LinkedIn id of a person decodes to the same member. A profile URN (
ACoAA...), a Sales
Navigator id (ACwAA...) and a Recruiter id (AEMAA...) carry the same member number, and the platform
now reads them in one place. A URN is filed by what it is, not by the field it arrived in (ln_id: "ACwAA..." lands on sn_id), ln_member_id is derived from any of them, and the same person named twice
is one row: in the data corpus, in a mass action (two targets that decode to one member are one item,
never two sends), in the synced connections, invitations and threads.A bad identifier is refused where you sent it. ln_member_id is the decoded member number, digits
only: a vanity slug there is 422 ln_member_id_not_numeric on that field, with the field it belongs in
named. An id that does not decode is urn_undecodable; two ids that name two different people are
identity_conflict; a Recruiter id given to a verb that dispatches on the member profile (connect,
message, visit, follow, endorse, the person getters) is profile_id_not_dispatchable with a pointer to
the profile URN. Mass-action previews refuse these on the target that carries them
(scope.targets.3.ln_member_id), so a run no longer previews green and dies on its canary.A message’s ln_member_id can be null. Only on a thread with an organization (a Message Ads page
names nobody); the literal unknown that used to stand in for it is gone from the column.Every refusal has one shape: a field, a reason, a sentence, a context
The reason is the rule, the sentence is the message. A
validation_failed names the field, and each entry
under it carries rule (the machine reason: ln_member_id_not_numeric, resend_not_available,
no_connected_account) and message (the sentence for it). A conflict, rate_limited or
service_unavailable carries the reason in context.reason. Until now the shape depended on who wrote the
refusal: some answered the bare reason as the whole message, some wrote the reason inside the message under
the rule name validation, and the platform’s own field checks (The x field is required.) named no reason
at all. Now a refusal from the platform’s field checks names the rule that failed (required, max, in),
and no reason is answered without its sentence: every one is listed on the new
Reasons page, generated from the same table the backends answer from.Pacing is one call at a time, and every answer names the clock
Calls of one action type leave one at a time. A bucket no longer fires a burst and then holds: two
calls are spaced
delay_in_seconds / batch_size apart (10 seconds for messages, 30 for connection
requests, 5 for searches and profile reads), varied by up to 20% per call. The slot is reserved before the
call leaves, so two calls fired in the same second never reach LinkedIn together, which is what LinkedIn
answered with 429 FUSE_LIMIT_EXCEEDED on 2026-09-17. An undo (unreact, delete a comment or a post,
unendorse, unfollow, withdraw, recall a message) waits 2 seconds, so an action and its undo fit together.A wait is either slept or named. Up to 20 seconds the server sleeps it and the call just takes
longer. A longer wait answers 429 rate_limited with context.reason bucket_saturated, cause
delay_not_elapsed and retry_after: the same call goes through at that moment, and an earlier retry is
refused again with the same timestamp. Parallel calls on one account queue behind each other, so run them
one after another or spread them over accounts.A success names the clock too. Every response whose call reached LinkedIn carries
pacing: { linkedin_account_sid, limit_type, next_call_after, remaining_today }. next_call_after is the
next slot while the day has budget left and the daily reset when it has none. MCP tools that are paced say
so in their description (Paced: send_messages.), a toolset listing states the rule once, and the OpenAPI
carries x-gtm-paced-bucket on the same 110 operations.One refusal no longer collapses a limit. A LinkedIn refusal lowers a bucket’s learned ceiling only to
what the account has proven: the most it did in a day over the last 10 days, counted from sent actions and
from the counts LinkedIn refused at. A refusal with no volume behind it changes nothing, a lowered ceiling
expires when 10 days pass without another refusal, and rows the old rule had lowered re-derive at the
next snapshot.A preview validates the arguments before it hands out a token
The preview of a protected action asks the backend first. A preview used to check the arguments
against the tool schema only, so a call the platform would refuse with
422 was previewed, confirmed, and
refused at commit. The preview now runs the real route’s own checks (authentication, permissions, request
validation) without running the action. A refusal comes back at the preview, as the same error the commit
would give, and no commit_token is issued for it. A preview that passed says validated: true. That is
a statement about the arguments: the daily budget, a Sales Navigator seat or whether the person is
connected are only known when the action runs. LinkedIn tools today; identity and mass-action tools
preview as before.select_linkedin_account_recruiter_contract is a protected action. Switching the contract changes
which seat and inbox the session acts under, so it previews like the other outward actions.API request log: why a call was refused, on which account, uploads included
A refused call says why.
search_api_requests rows carry error_code, error_reason and
error_cause, read off the envelope the client received, and account_sid, the account the call named.
rate_limited is four different refusals and the three together tell them apart: a saturated bucket, a
hold a person set, LinkedIn’s own lock, the per-minute guard. All four filter with
eq / ne / in / nin / is_null, so “everything refused as held on this sender” is one call. The
dashboard has chips for All / Errors / 4xx / 5xx, prints the reason in words in the row and opens the
whole row on click.A split by client is named. get_api_request_metrics with group_by: "client_sid" returns label
(the API key’s name, revoked keys included, or the OAuth client’s) and credential_kind on every group.An upload through a one-time link is in the log. The PUT to an upload link is recorded as a call of
the client that requested the slot (PUT external/uploads/{token}), refusals included, so a 422 on the
file has a row to point at.Sales Navigator calls re-arm a stale session by themselves
403 SALES_SEAT_REQUIRED is a stale session, and the platform now handles it for every call.
LinkedIn’s Sales Navigator API refuses a seated browser that has not opened a Sales Navigator page for a
while. The background sync already re-opened it; direct calls (get_linkedin_account_my_credits, the
Sales Navigator searches and typeahead, the Sales Navigator inbox reads and sends) failed on the first
refusal. They now re-arm the session and retry once. A seat that turns out to be gone answers
422 sales_nav_required; a second refusal right after the re-arm answers 409 sales_session_stale.The Sales Navigator inbox sync no longer depends on the account’s own sn_id. Who in a thread is the
account owner is decided by the member id the participant decodes to. Accounts that had no sn_id stored
skipped every page of that sync; they sync now.Searching by a person’s identifier finds the person. A filter on ln_id, sn_id or recruiter_id
matches the member on every surface for every operator (ne and nin included, which some searches
used to ignore), on conversations, messages, connections, invitations, requests, followers and the
activity and block logs. search_linkedin_messages also filters by client_reference.Upload slots for post media, media by URL on create-post, image checks before dispatch
A place to put a file the caller cannot send inline.
request_media_upload (the id platform mount)
reserves an upload slot and answers with a one-time upload link on our domain (a person opens it and drops
the file, a shell runs curl -T <file> <link>), a public file_url to pass on, and, when file_type is
given, a pre-signed S3 form. A slot takes one file of up to 35 MB within 30 minutes, the object is kept for
7 days, and a workspace may request 120 slots an hour. Purpose post_media (PNG, JPEG, GIF, WEBP, MP4,
MOV, WEBM) is the only purpose today.create_linkedin_post takes media by URL. Each images[] member and video carries exactly one of
file_base64 or url; a URL, an upload slot’s file_url included, is downloaded by the platform over
https from a public host and checked before the post is dispatched. Typing a file out as base64 damages
it: an agent that re-typed a PNG produced a corrupt file, and LinkedIn answered with an empty 400 after the
upload. A damaged image is now refused up front, before any browser or LinkedIn call: 422 on
images[i].file_base64 (or images[i].url) with rule image_not_base64, image_format_unrecognized or
image_corrupt.Every URL the platform downloads for you is https and public. attachments[].file_url on
send_linkedin_message, send_linkedin_inmail, send_linkedin_sales_nav_message and
send_linkedin_recruiter_message, and audio.url on send_linkedin_voice_message, must be https URLs of
public hosts. A private or reserved address, credentials in the URL or a port other than 443 answer 422
(attachment_url_invalid / attachment_url_host_forbidden, audio_url_invalid /
audio_url_host_forbidden); the codes for a failed or oversized download are unchanged.Reactions and comments land on the post's social thread
The tools address a post’s social thread for you. LinkedIn files reactions and comments under the
post’s own
urn:li:ugcPost: or urn:li:groupPost: for those families, and under the activity urn for a
urn:li:share: post. A reaction sent to the wrong key answered success and landed nowhere, and a comment
was refused with linkedin_400. react_linkedin_post, unreact_linkedin_post,
create_linkedin_comment, scrape_linkedin_get_post_reactors and scrape_linkedin_get_post_comments now
take a post urn of any family or a post link (a -ugcPost-<id>- share link and a
/feed/update/urn:li:ugcPost:<id>/ permalink included) and name the thread before the wire. An activity
or share urn costs one post read on the acting account, cached for 7 days; a post that cannot be read
answers 422 post_not_resolvable. post.post_ln_id in the reactor and comment lists is the urn the page
was read by.Mass actions: the sender is a step arg, a target's own extras win, preview says more
A send step names its sender in
args. preview_mass_action refuses a
linkedin-connection-requests.send, linkedin-messages.send or email-messages.send step without
args.linkedin_account_sid / args.email_account_sid (422, rule sender_required on
plan.steps.{i}.args.<key>). Such a run used to pass preview and fail on its first item.A target’s own client_reference, note and allow_no_note_fallback win over the step args. The
args stay the run-level default. This reverses the 2026-09-16 note below: client_reference is the key of
one row, and a run that names one per target keeps them.preview_mass_action warns and estimates. warnings[] lists every {{placeholder}} in the step args
(the platform renders no merge fields; the braces go out as typed), and eta.estimated_completion_at is
filled for a scheduled run: the first item starts at once, each further gap is the mean of the schedule’s
interval.append_mass_action_items takes payload leads only. A generate run (step 1 is a .create verb) and
an objects run (items addressed by an existing row) answer 422 run_not_payload_addressed; create a new
run instead.Runs authored with an API key work. The identity behind a gtm_live_ key now carries the workspace’s
home cluster, so a mass action created through the REST API no longer fails its first channel step.get_webhook_logs_metrics rates are ratios of terminal outcomes. success_rate and failure_rate
divide by succeeded plus failed deliveries and are null while nothing terminal exists in the period;
deliveries still retrying no longer read as a zero failure rate.Stricter tool arguments, live reads marked, smart limits and InMail
Unknown keys in tool arguments are refused. The MCP tools and the REST edge parse strictly: a key the
schema does not know answers
422 validation_failed with rule unknown_key and a hint, for example the
nested filter shape or filter.q for free text. A dotted filter key that used to fall through as an
empty filter now fails loudly.The ten get_my_latest_* tools are live reads. They start the account’s browser and read LinkedIn, so
they carry readOnlyHint: false and openWorldHint: true; an MCP client that auto-approves read-only
tools will ask first.set_linkedin_account_smart_limits remembers nothing. A real flip in either direction puts every
limit row of the account back on the platform defaults first (cap, hold, burst, target), so off never
inherits the ramp’s last cap and on never inherits a hand-typed one.Smart-limit rows of a deleted account are gone from the public surface.
search_linkedin_account_smart_limits no longer lists them, and update_linkedin_account_smart_limit
and reset_linkedin_account_smart_limit_hold answer 404.InMail credits guard the send. send_linkedin_inmail, and a send_linkedin_sales_nav_message that
opens a new thread outside the network, refuse before the wire when the account’s pool is known to be
empty (422 no_inmail_credits). The balance LinkedIn reports after a send is written back to the account,
a NOT_ENOUGH_INMAIL_CREDIT refusal zeroes it, and a stale Sales Navigator session is re-armed once before
409 sales_session_stale.Cloud-browser access keys are shown only to callers who may mint them. A read with a token that lacks
can_manage_cloud_browser_external_links gets the cloud_browser_access entries with key: null; the
link, its expiry and its use stay visible. generate_cloud_browser_access_key takes send_to_email: the
platform mails the link to that address, and the result carries sent_to_email.Group conversation verbs are tracked. add_linkedin_conversation_participants,
remove_linkedin_conversation_participants and rename_linkedin_conversation write an activity-log row
(activity_log_sid in the result), a LinkedIn refusal is the typed 409 linkedin_refused, and
participant_ln_ids take bare profile ids (a urn is 422).Voice messages: our failure is not your file. A transcoder missing on the platform answers
503 audio_transcoder_unavailable; audio that cannot be decoded answers 422 audio_invalid.Proxy location checks no longer report a false mismatch. A check that could not conclude
(no_ip_port, exit_unreachable, no_corroboration) is recorded as inconclusive.Workspace deletion in 30 days with two notices, deleted workspaces cleaned up everywhere, smaller fixes
The data-retention clock is 30 days, with two emails and no restore. After a terminal loss of coverage
a workspace is scheduled for deletion 30 days out (it was 8 weeks): the owner gets one email when the date
is set and a final notice 72 hours before, and the deletion runs only after the final notice went out.
delete_team answers recovery_available_until: null: a deleted workspace is not restored.A deleted workspace is cleaned up on every service. On deletion its webhooks stop and its mass-action
runs are stopped; its LinkedIn accounts are disconnected and the platform’s own browser profiles released.
Thirty days later every row the workspace owned is deleted for good, LinkedIn data, browsers, logs and
photos included.linkedin-connection-requests.resend-available is sent. The event was in the webhook catalog but
nothing emitted it; an hourly sweep now announces each withdrawn request once its 21-day cooldown has
passed, once per row.An auto scrape run waits out a pacing refusal instead of failing. A page refused by the scraping
bucket’s batch pace (delay_not_elapsed) parks the run at its cursor and resumes it; only budget
exhaustion (daily_saturation, a hold, a LinkedIn quota) fails a run. Runs used to fail on page 3 and
restart from page 1.A browser parked by a platform maintenance window comes back on its own. It is released to idle
and wakes on its next due sync or dispatch; it used to be released to stopped, which nothing restarted.The card a first checkout paid with shows up. The first transaction of a subscription is attached to
it even though Paddle delivers it before the subscription event, so the payment-methods list derives the
billing card from it right after checkout.Data request reads no longer hit MySQL’s sort-memory limit. A workspace with one large cached result
(over 256 KB) got 500 on cache reuse, on a nickname resolve and on search_data_requests; those reads
were reworked and the lookups are index-served.Removing a card a subscription still uses answers 409
A card a subscription still uses is a conflict, not a server error.
delete_billing_payment_method (DELETE /api/billing-payment-methods/{sid}) only takes a method
off the paying user’s saved list; it never changes the card a subscription is billed to. Paddle
refuses to delete a saved method that an active, paused or past due subscription uses, and that
refusal used to come back as 500 internal_error. It is now 409 conflict with
context.reason: payment_method_in_use, the sid, a message and a suggestion: move the
subscription to another card with the link from get_billing_payment_method_add_link first, then
remove the old card. A card that is not is_default can get the same answer when it pays for
another subscription of the same payer, in another workspace or one not applied to any.The card the plan is billed to is refused up front. The method listed with
is_default: true is usually not a saved method at all, so deleting it used to answer
already_deleted: true while the card kept billing. While the plan is active, past due or paused
it now gets the same 409 payment_method_in_use, with a suggestion to switch the plan to another
card first. Once the plan is canceled, its last card is removed like any other.get_billing_payment_method_add_link is no longer annotated read-only. Every call opens a
new portal session, so MCP clients that approve read-only tools automatically will ask before
calling it.client_reference on every send verb, and on mass-action sends
Every send verb takes
client_reference. The field (your own key for a send: a task id, a
campaign, an idempotency token; max 255) used to be accepted by the basic message, the InMail and
the connection request only. It is now accepted by send_linkedin_sales_nav_message,
send_linkedin_recruiter_message, send_linkedin_voice_message and
start_linkedin_group_conversation too, with the same contract: stored as given on the message
row, searchable through filter.client_reference on search_linkedin_messages, and echoed on
linkedin-messages.sent. A Sales Navigator or Recruiter send that lost its response can now be
checked before it is repeated, exactly like a basic one.Mass-action sends carry the plan’s key. A linkedin-connection-requests.send or
linkedin-messages.send plan step with client_reference in its args tags every row the run
creates and every event about those rows, the way email-messages.send already did. One value per
run: it is the run’s tag, not a per-item token.A Recruiter status on the account, a sign-out notification, and the contract chooser as tools
recruiter_status replaces has_recruiter on LinkedIn accounts. The flag said that a seat
existed and nothing about whether its Recruiter session still answered, so a seat LinkedIn had
signed out looked exactly like a working one until a call ran into the 401. The new field carries
both facts: null = no Recruiter seat, active = the seat answers, signed_out = LinkedIn
Recruiter asks the seat holder for the password again (it does so about every 30 days). It
filters (eq, in, is_null), and linkedin-accounts.premium-changed carries it in place of the
flag. Existing seat holders start as active; the platform sorts the signed-out ones within
minutes. This is a breaking rename: read recruiter_status where you read has_recruiter
(recruiter_status !== null is the old true).You hear about a sign-out once. The status flips to signed_out on the first refused
Recruiter call, and a 15-minute check of the session clock catches it before a sync does. The
crossing sends one webhook, linkedin-accounts.recruiter-signed-out (with reason:
session_expired or wire_401), and one email to the person who connected the account; the way
back sends linkedin-accounts.recruiter-signed-in. While signed out, Recruiter calls answer
409 recruiter_reauth_required with context.account_url (the account’s page) and a suggestion
an agent can relay; the recruiter inbox sync waits.Sign in to Recruiter from the account’s page. The button opens the account’s cloud browser
straight on Recruiter’s sign-in; after you confirm, the platform restarts the browser, re-checks
the seat and flips the status back to active. Only the seat holder can do it, and the platform
never stores the password.The contract chooser as tools. get_linkedin_account_my_recruiter_contracts lists the
Recruiter contracts the account’s member can act under, with current_contract_id;
select_linkedin_account_recruiter_contract binds the browser to one of them (contract_id exactly
as listed; 422 contract_not_offered names the rows on offer). Both live on the recruiter mount and
neither is gated on the session clock. The premium check still binds an unambiguous choice by
itself; these are for the case where it could not.Acceptance detection, sending by profile URL, widening a webhook
A connection request is accepted the moment we can prove it.
linkedin-connection-requests.accepted
now fires from three places: the connection-request sync, which refreshes the head of the
connections list before it judges an invitation that left the sent list; the connections sync
itself, which resolves a pending request the moment the connection row appears (.accepted lands
before that row’s .added, and the row carries linkedin_connection_request_sid); and the accept
of an invitation whose inviter is an account on this platform. Until now the sync judged by the
connection rows it already had, so an acceptance the connections sync had not read yet was recorded
as withdrawn with a 21-day resend cooldown, and a request past the first page of the sent list was
recorded as withdrawn every day (LinkedIn’s sent-invitations view reports a zero total). Both are
fixed, the rows this produced were repaired, and a request whose invitation is seen again on the sent
list goes back to pending on its own.Send by profile URL. send_linkedin_connection_request and send_linkedin_message take
public_identifier (a vanity slug or a linkedin.com/in/<slug> URL) next to the URN. The slug is
resolved server-side: your own rows first, then the corpus, then a lite-profile read on the sender,
which spends the enrichment bucket like any enrichment. One addressing form per call.A narrowed webhook can be widened. update_webhook with filters: null or filters: {} clears
the account narrowing. Before, neither spelling worked and the only way was to delete the webhook and
create it again.The Premium InMail balance, the Recruiter contract chooser and a wider own-account burst
The Premium plan’s InMail balance is read. Accounts carry two InMail fields now, because
LinkedIn keeps one pool per product and moves nothing between them:
inmail_credits is the Sales
Navigator seat’s grant, read only while the account holds a seat, and premium_inmail_credits is
the Premium Career or Business balance, read for a Premium account without a seat. The premium
check’s inmail_credits step reads whichever pool the plan has; naming it for an account with no
Premium answers 422 premium_required (it used to answer 422 sales_nav_required for a seatless
Premium account, whose balance was never read at all). Both fields filter. Null means not read,
never zero.Several Recruiter contracts. A seat holder on several contracts holds no seat until the browser
is bound to one. The premium check now binds it when the choice is unambiguous (the contract the
account is already on, the only contract, or the only corporate one) and stamps the seat from the
answer; when several corporate contracts are on offer the seat holder picks in the account’s
browser and the next check stamps it. A missing Recruiter session cookie no longer reads as an
expired session: recruiter_session_expires_at is null until the cookie is seen, and the Recruiter
wire’s own refusal is what answers 409 recruiter_reauth_required.Own-account reads burst 8. The self-account bucket paces 8 calls per 6-second hold (was 5),
so one premium check fits a single burst; the daily cap is unchanged.The LinkedIn Recruiter inbox is the third messenger, with its seat, its session clock and the 24-hour rule
273 operations: LinkedIn 179, ID and Teams 72, Orchestration 22.Recruiter conversations are their own surface. Threads and messages carry
messenger_type: "recruiter" and the candidate’s talent_id; sync-my-recruiter-conversations
runs the background sync (one run walks the INBOX tab and then UNRESOLVED, where a thread you
opened sits until the candidate replies), get-my-latest-recruiter on conversations and on
messages is the head refresh, and recruiter_conversations joins the sync, webhook and
reset-sync vocabularies with the same 120-minute default cadence as Sales Navigator. The account
carries recruiter_seat_id, recruiter_contract_id, recruiter_session_expires_at and
last_recruiter_conversations_sync_at, all filterable; the two clocks sort.The seat and the session are facts on the account, not inputs. The premium check
(checks: ["recruiter"]) stamps the seat number and its contract and reads the expiry of
LinkedIn’s 30-day Recruiter session. Recruiter calls without a seat answer
422 recruiter_required or 422 recruiter_seat_unresolvable; past the session clock they answer
409 recruiter_reauth_required until the seat holder signs in to Recruiter in the account’s
browser. No Recruiter password is ever stored. get-my-recruiter-seat and
get-my-hiring-projects read the seat’s own entitlements and hiring projects.The Recruiter people search and its typeahead. search-recruiter-people runs the LinkedIn
Recruiter search from a filters object (chips with exclude, required and a per-facet scope,
the closed code sets, the three year sliders) or from a pasted Recruiter url, 25 rows a page, 40
pages at most, LinkedIn’s real total, and rows that carry the headline, current position, location,
connection degree, talent_id, can_send_inmail and open_to_work. A Recruiter URL carries no
filters, only the searchHistoryId of the search LinkedIn keeps on the seat, so the url half replays
that stored search; every answer returns search_history_id and search_url, the handle to page by
without re-sending the filters. recruiter-param-id-lookup resolves the eleven
chip kinds into their ids. LinkedIn’s per-seat search throttle answers 429 recruiter_search_usage_limit with LinkedIn’s own sentence and a one-minute retry_after. On MCP
both ride on /mcp/linkedin/scraping with every other live list.Sending is send-recruiter. One verb over two LinkedIn wires: a new thread by talent_id,
ln_id or sn_id, or a reply by thread sid. LinkedIn’s one InMail per candidate per 24 hours
is enforced before dispatch and read back from the wire: 429 rate_limited with
reason: "recruiter_inmail_cooldown" and a retry_after, on a fresh InMail and on a reply the
candidate has not answered yet.The first sync now tells you when it is done. The person who connected a LinkedIn account gets
Your LinkedIn account is ready to use when its onboarding sync latches, with the connections and
conversations it brought in and how long it took. A large account can spend its first-day read
budget part-way: it then carries initial_sync_held_at and initial_sync_hold_reason (filterable),
emits linkedin-accounts.initial-sync-held, resumes on its own after the daily reset, and the mail
arrives a day later saying so. Sent once per account; a reset-sync re-latch sends nothing.In the app, the Recruiter Conversations row appears on the Account Sync tab once a seat is
detected, with its own interval, Sync now and Reset sync; the initial-sync gate lists the
surface; the Premium block shows the seat and the session’s expiry, and says so once it has
expired.Browser ownership is derived, deletes confirm the stop, and a key is not a person
262 operations: LinkedIn 168, ID and Teams 72, Orchestration 22.
browser_owner is no longer an input. POST /api/antidetect-browsers derives it from the one
fact that decides it: send vendor_profile_id and the profile is yours (customer), omit it and we
mint one in our own vendor account (platform). The field stays on the row and in filters, but its
vocabulary is now exactly those two values; mirror_profiles is gone from the enum. Sending
browser_owner on create is a validation error naming the field.vendor_profile_id is validated as what it is: a GoLogin profile id, 24 hex characters, not a
profile URL or a share link. A profile that is not shared with our vendor account answers
422 vendor_profile_not_found; an owning account over its GoLogin plan answers
402 vendor_profile_plan_limit, and retrying will not help until the plan changes.Deleting a running browser now stops it first and waits for the confirm. The call can take up to
~20 s; if the node does not confirm the teardown, the delete is refused
409 with context.reason: browser_stop_unconfirmed and nothing is deleted - retry in a minute
rather than assuming success.A key is not a person. GET /api/users/current (get_current_user) on an API key answers 403
with context.reason: not_a_user_actor instead of a misleading validation error about a sid field
you never sent. No permission on the key changes that: the endpoint returns a signed-in person’s
profile, and a key has none. The same applies to PATCH /api/users/current.A freshly connected sender starts on business hours. Its sync_config.window is seeded
Monday-Friday 09:00-18:00 in the workspace timezone (UTC when the workspace has none). An empty
window still means round the clock, as it always has - there is no “off” through this field.Person languages goes GA, short links resolve, and hand-off links carry a purpose
Person languages is live.
POST /api/linkedin-enrichment/person-languages answers with the
profile’s Languages card (raw name plus optional proficiency strings) instead of 501. Same
shape and paging as certifications and recommendations.lnkd.in short links are a first-class input. LinkedIn’s own “Copy link to post” mints
https://lnkd.in/p/..., so every endpoint that takes a post link now accepts one and expands it
server-side: enrichment reads, get-activity-urn-by-url, reactions, reshares. A post page URL also
resolves its backend share / ugcPost urn where only the activity urn resolved before.Cloud-browser links say what they are for. generate-cloud-browser-access-key takes purpose:
relogin (the default, and what every key minted before the field carries) walks the visitor
through a LinkedIn sign-in whose confirm re-binds the account; share just hands the browser over,
with a Done button instead of a sign-in flow. The mass-action step forwards the same argument, so a
batch of links minted to be handed out must say share explicitly.used_connects is gone from access-key entries. Nothing ever incremented it, so every reader
saw 0 forever. max_connects caps CONCURRENT sessions and is enforced against live
cloud-browser-sessions rows at connect time. On session rows, access_key is now masked to
cb_ak_******** plus the last 4 characters - enough to attribute a session to a key you hold, and
nothing anyone else could redeem.Workspace capacity in one read. POST /api/antidetect-browsers/seat-usage answers the seats and
5G proxy slots the whole workspace occupies against its plan ceilings - what the plan card prints,
independent of any member’s account scope.Held profiles get a schedule. A workspace holding more browser profiles than its plan allows
gets a warning at day 21 of the hold and a teardown of the over-cap profiles at day 28, oldest
first. Growing the plan back at any point before day 28 keeps everything.Message attachments deliver again. send-message with attachments produced
handler_failed: Invalid File URL on the wire since the wire expects a full data: URL; fixed
server-side, no request change needed.One operator field on a sender, and it is now searchable
display_name is gone from the LinkedIn account. It is off the account object in every response,
off PATCH /api/linkedin-accounts/{sid}, and its column is dropped. The field was meant to override
the LinkedIn name wherever an account was listed, and it never did: a few screens honoured it and the
rest read the synced name, so one account could answer to two names depending on which page asked.
The name a sender carries is LinkedIn’s own full_name, refreshed by the account snapshot, and no
endpoint edits it.The update verb now sets label, and only label. In MCP the tool update_linkedin_account is
renamed set_linkedin_account_label. label is a required key rather than an optional one: send the
field on every call, with a string to set it or null to clear it. An empty body is now a validation
error that names the field, instead of a nothing_to_update whose cause you had to guess.label is searchable and filterable. filter.label takes eq, ne, in, nin and is_null
(is_null: true finds untagged senders), and the reserved q now runs its LIKE over label
alongside full_name and nickname, so “find the account of Jane Roe” and “find the one we call
burner #3” are the same search. It is deliberately not a sort axis: the tag is sparse, so a page
ordered by it is a page of nulls.The label is private to the workspace that wrote it. LinkedIn never sees it and no recipient ever
sees it, and sharing or transferring an account hands over the account, not your note about it, so
the borrowing team’s copy starts empty.Scheduled posts, media posts, mentions, and explicit browser states
263 operations: LinkedIn 167, ID and Teams 74, Orchestration 22.Scheduled posts.
POST /api/linkedin-posting/create-post takes scheduled_at (ISO 8601, in the
future) and LinkedIn queues the post instead of publishing it. A scheduled share answers with
post_urn only - activity_urn and url stay null until it publishes - and that post_urn is the
handle the new pair takes: POST /api/linkedin-posting/get-scheduled-posts lists the account’s queue
(or a company page’s, with author_organization_id), and
POST /api/linkedin-posting/delete-scheduled-post removes a draft by that backend urn. A refused
delete is 409 scheduled_post_not_deleted, never a success body. All of it spends the posting
bucket, which grew to fit: 20 a day in series of 3 before the 1200 s pause (4 a day on the free plan).Media posts. images takes up to 20 (array order is carousel order; with two or more the service
supplies the author id itself, there is no profile_id to send), or one video instead. LinkedIn
does not mix the two in a share, so sending both is a 422, and the decoded bytes across all media
must stay under 35 MB. These calls are synchronous: a heavy upload can outlive your client timeout
while the post still publishes, so do not retry a timeout blindly - read the queue or your own posts
first.Posting as a page, into a group, as a partnership. author_organization_id (the bare numeric
company id) publishes as a page the account administers; group_id posts into a group and is
mutually exclusive with visibility; brand_partnership: true adds LinkedIn’s label.Mentions. create-comment takes mentions as {profile_id, name} pairs, name being an exact
substring of text, matched left to right in list order. create-post takes ready positions
instead, {profile_id, start, length} in UTF-16 code units - the way JavaScript counts string
length, so an emoji is two. Both are validated before anything is dispatched.Reactions on comments, and a new reaction. entity_urn on react and unreact now takes a post
urn in any family (activity, share, ugcPost, groupPost) or a comment urn in either form, so a
reaction can land on a comment, and react returns reaction_urn. reaction_type gains
interested, LinkedIn’s reaction on event posts; on an ordinary post LinkedIn silently ignores it and
reaction_urn comes back null, which is the tell. POST /api/linkedin-scraping/get-post-reactors
accepts a comment urn in post to read a comment’s reactors.Reactor rows changed vocabulary. reaction_type on get-post-reactors rows now uses the values
you write with (like, celebrate, support, love, insightful, funny, interested). Until now
the rows leaked LinkedIn’s internal names (interest, praise, empathy, appreciation,
entertainment) while the schema promised ours; a caller matching on the old strings must switch.Resharers. post on get-post-resharers takes a post URL, an activity urn, or a backend
share / ugcPost urn. A groupPost urn is refused 422 reshare_target_unsupported: group posts
have no reshare feed.Undo verbs, and recalling a message (shipped 2026-08-20). delete-post, delete-comment and
unreact retract what create-post, comment and react did, spending the same bucket as the
action they undo. POST /api/linkedin-messages/{sid}/delete-on-linkedin recalls one of your own
messages for every participant. LinkedIn allows that only within an hour of sending, so an older
message answers 409 message_too_old_to_recall with nothing dispatched; the row stays in the thread
marked is_deleted with an empty body.is_deleted on message rows. Every message row carries it, and the sync keeps it true to
LinkedIn: a message recalled by either side is marked on the next read of its thread, with the body
emptied. Sales Navigator threads carry no recall marker on LinkedIn’s side and are left alone.Post readers. person-posts, company-posts, post-details and the post-by-url read carry
group, the group a post was published in (null elsewhere). On a group post LinkedIn shows the
group as the card actor, so author there is a bare name with no profile url or ids; do not read
that as an organization author.Auto-scrapes. A get-post-comments source accepts sort_order in source_input (RELEVANCE,
CHRONOLOGICAL, REVERSE_CHRONOLOGICAL). It is frozen at create and used on every page of every
run, because LinkedIn’s pagination cursor belongs to the order it was issued under.Browser states are explicit. A call that needs a browser which is down no longer times out or
hides behind a generic conflict. 503 browser_unavailable says the session is gone;
503 browser_unreachable says the service tried once to start the browser inside the call and
carries the error that attempt hit plus a retry_after; 503 browser_starting says a cold start is
still warming up; 409 browser_logged_out says the account itself is signed out;
409 no_live_browsers says an enrichment or scrape with no pinned account found no live browser to
run on; 503 infrastructure_maintenance says the browser fleet is being updated.Sender health. POST /api/linkedin-accounts/search counters carry signed_out_count, so a
logged-out sender shows without opening each row.Billing. POST /api/billing-subscriptions/{sid}/undo-cancel reverses a scheduled cancellation
before the period ends.Response meta. Every response meta block now carries team_sid and actor_type, across all
three services.5G Proxy reaches the antidetect-browser surface
256 operations: LinkedIn 159, ID and Teams 73, Orchestration 22, Support 2.
proxy_5g on antidetect browsers. The 5G Proxy add-on is now on every surface of the
resource: the browser row carries proxy_5g, both POST /api/antidetect-browsers and
POST /api/antidetect-browsers/update-proxy accept it, and POST /api/antidetect-browsers/search
filters on it (proxy_5g: {eq: true} is how slot usage is counted). Arming a browser with no
add-on headroom left is refused 402 insufficient_proxy_5g_slots.Scraping and enrichment pick an executor for you
Automatic executor selection. Every scraping and enrichment method runs on your own connected
accounts. Pin
linkedin_account_sid and the call runs on that account only, refusing
429 bucket_saturated when its daily budget is spent. Leave it out and the service picks a
connected account with remaining budget for you, skipping accounts on hold or out of capacity;
Sales Navigator methods narrow the pick to accounts holding a live seat. A call with no ready
account answers 422 with a message starting no_connected_accounts: (or sales_nav_required:),
and a team whose accounts are all at capacity gets 429 with retry_after.The three own-dashboard reads
259 operations: LinkedIn 159, ID and Teams 77, Orchestration 21, Support 2.Three feeds off your own LinkedIn dashboard. All three are one-shot, cursor-paginated reads on
an account you connected, and none of them writes anything.
POST /api/linkedin-accounts/{sid}/get-my-profile-viewsreturns who viewed the profile over the last 90 days, newest first. The window and the sort are LinkedIn’s and there is no filter. Rows carry rendered text (“Viewed 1w ago”), never a parseable date, and an anonymized viewer arrives withis_anonymoustrue, no member ids and a people-search url where the profile would be, so branch on that flag before you key a row.POST /api/linkedin-accounts/{sid}/get-my-catch-upreturns the nurture cards on My Network (birthdays, job changes and work anniversaries among the account’s connections), each with the one-click message LinkedIn printed on the button. It lists prompts and sends nothing. There is nopage_sizehere: LinkedIn’s own request carries no count knob and the page is server-fixed at 10. Key a card bycard_urn, which identifies the prompt rather than the person.POST /api/linkedin-accounts/{sid}/get-my-sales-nav-notificationsreturns the Sales Navigator alert bell, and needs a Sales Navigator seat on the account. The feed is ordered by LinkedIn’s relevance score rather than reverse-chronologically, so sort bypublished_atyourself. Alerts arriving without an id are dropped, which means a page can be shorter thanpage_sizewhile the feed continues: do not read a short page as the end here.
LinkedIn product and school search
260 operations: LinkedIn 160, ID and Teams 77, Orchestration 21, Support 2.LinkedIn products search.
POST /api/linkedin-scraping/search-products searches LinkedIn’s
product catalogue and returns product rows (slug, url, name, category line, vendor name, tagline,
top features, a connections counter and artwork). Send filters with a required keywords plus
any of free_version, product_category and product_company, or send a
https://www.linkedin.com/search/results/products/ url the search screen produced. Exactly one of
the two.The two id filters take digit strings from two different places: product_category ids come from
POST /api/linkedin-scraping/param-id-lookup with type=product_category, while
product_company takes ordinary LinkedIn organization ids. Nothing checks one against the other,
so a category id used as a company id returns an empty page rather than an error. The identity
field on a row is product_slug, a slug rather than an id, and it cannot be fed back into either
filter.LinkedIn schools search. POST /api/linkedin-scraping/search-schools searches LinkedIn school
pages and returns school rows (slug, url, name, location line, a students-and-alumni counter and a
blurb). keywords is the whole filter vocabulary here, so every other facet the schools screen
offers is reachable only by pasting a https://www.linkedin.com/search/results/schools/ url into
the same endpoint. There is no school-id filter: the ids param-id-lookup returns for
type=school do not fit this endpoint, and sending one is refused rather than quietly ignored.The identity field is school_slug, a slug rather than an id, and it can arrive URL encoded, so
pass it through as you got it. students_alumni_count is a rounded figure parsed from the text
LinkedIn printed, counting students and alumni, not employees.With these two the LinkedIn search surface is complete: people, companies, both Sales Navigator
searches, service providers, posts, jobs, events, groups, courses, products and schools.LinkedIn course and group search
259 operations: LinkedIn 159, ID and Teams 77, Orchestration 21, Support 2.LinkedIn courses search.
POST /api/linkedin-scraping/search-courses searches LinkedIn
Learning and returns course rows (slug, url, title, author, duration, release and viewer text,
thumbnail). Two filters beyond keywords: difficulty (beginner / intermediate /
advanced) and time_to_complete (under_10_mins through 3_plus_hours). Send those readable
values, not LinkedIn’s own labels: the translation happens server-side. The software and subject
facets are not in the filter vocabulary and are reachable by pasting a
https://www.linkedin.com/search/results/learning/ url into the same endpoint.LinkedIn groups search. POST /api/linkedin-scraping/search-groups returns group rows (id,
url, name, privacy, member count, description, logo). Its filter vocabulary is keywords and
nothing else, which is LinkedIn’s whole surface for that vertical rather than a subset we chose;
every other groups facet is reachable by pasting the UI search url into the same endpoint.Five new LinkedIn endpoints, seven more come out of stub
253 operations, up from 248: LinkedIn 155, ID and Teams 77, Orchestration 21.Company reads that stay cheap.
POST /api/linkedin-enrichment/company-lite-profile returns
id, vanity, name and logo, cached 7 days. POST /api/linkedin-enrichment/company-public-identifier
resolves a numeric company id to its vanity slug, cached 30 days. The pattern the two are built
for: resolve the id once, then address every later company read by vanity. A /company/{slug}/
url is one request on LinkedIn’s side, while a Sales Navigator /sales/company/{id} url costs
two, because the vanity has to be resolved first (measured at 433 ms against 2479 ms).A middle person read. POST /api/linkedin-enrichment/person-basic-profile sits between the
lite identity stub and the full dossier: first and last name as separate fields, headline, country
and display location, profile imagery, and the premium, influencer, creator and verified flags.
One request, cached 24 hours. Addressed by public_identifier only, so resolve the slug first if
all you hold is a URN.Services search by pasted url. The service-providers search gained a url form, so the
marketplace facets the filter vocabulary does not carry are reachable by pasting a
/search/results/services/ url. A url from any other search screen is refused rather than
silently scraping something else.Editable account display fields. PATCH /api/linkedin-accounts/{sid} sets display_name and
label. They are team-authored, never synced from LinkedIn, and an explicit null clears one.Seven more endpoints answer for real, no longer placeholders: my analytics, my SSI, edit my
profile, person certifications, person comment activity, person recommendations, and
service-provider search by filters.Mass actions take nine more step verbs. The browser lifecycle (run, stop, create, delete, and
minting or revoking a cloud-browser access key) plus sync-config and smart-limit edits can now be
plan steps.Initial public release of the reference
The public API contract is published. 248 operations across three services: LinkedIn (150), ID
and Teams (77), Orchestration (21). The reference is generated from the MCP tool registry, one
endpoint per tool, and regenerates with it.The MCP server is live at
mcp.gtm-api.com/mcp, exposing the same contract as 160+ typed
tools across 10 toolsets. Connect guides for Claude, Cursor and Docker-based clients are in the
MCP tab.Webhooks are platform-wide. Subscriptions, signed deliveries (X-Webhook-Signature,
HMAC-SHA256 with a timestamp), a queryable delivery log, and a test endpoint live on the
Orchestration service.Mass actions ship with a two-phase consent flow. Preview validates the whole plan and mints a
15-minute commit token; commit consumes it. Canary mode, randomized pacing and per-item retry are
part of the same surface.