Skip to main content
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-views returns 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 with is_anonymous true, 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-up returns 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 no page_size here: LinkedIn’s own request carries no count knob and the page is server-fixed at 10. Key a card by card_urn, which identifies the prompt rather than the person.
  • POST /api/linkedin-accounts/{sid}/get-my-sales-nav-notifications returns 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 by published_at yourself. Alerts arriving without an id are dropped, which means a page can be shorter than page_size while 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.