# Lend an account to another workspace Source: https://docs.gtm-api.com/api-reference/id/account_shares/lend-an-account-to-another-workspace /api-reference/id/openapi.yaml post /api/account-shares Lend ONE of your connected accounts to another workspace, temporarily and revocably. This is a loan, not a handover: permanent ownership transfer has no MCP tool at all. Target exactly one of to_email (resolved to that user's working team) or to_team_sid; neither, both, a bad email or a self-target is 422. account_sid is prefix agnostic (ln_ac_, em_ac_); `channel` says which service owns it. ALWAYS ASYNC: returns 201 with status pending, and the loan has NOT started. Poll search_account_shares with filter { sid: { eq } } until status is active (the holder can work) or failed (read failure_reason); rolling_back means it is being undone, so do not re-issue. holder_over_slot true means the copy landed in the holder's subscription hold and they cannot work until they upgrade; a share never fails on slots. One row is one account with no bulk arm, so N accounts is N calls. planned_return_at only emails the owner a recall link; there is no auto return. Needs can_manage_account_shares. Contract: - MCP tool `create_account_share`, registry package `mcp.id/account_shares`, mount `id.access`. - Operation `create`, response envelope `create`. - Flags: none. # List received account shares Source: https://docs.gtm-api.com/api-reference/id/account_shares/list-received-account-shares /api-reference/id/openapi.yaml post /api/account-shares/list-received List the account shares your team RECEIVED: accounts on loan TO you. Reach for this whenever the question is about a BORROWED account (what did we borrow, from whom, until when, is it still active), and for search_account_shares when it is about an account your own team owns and lent out. Registered as an action route but it is a plain read, with the same envelope, filters and sorts as search. It is also the holder's only programmatic arrival signal, because the domain events fan out to the owner team alone: poll filter { status: { eq: "active" } } sorted by created_at asc. Rows belong to the owner tenant, so created_by and ended_by arrive with permissions emptied and the trace id nulled. include:["counterparty_team"] names the other workspace; no counts block, and page_size 0 returns pagination.total_count alone. Needs can_view_account_shares. Contract: - MCP tool `list_received_account_shares`, registry package `mcp.id/account_shares`, mount `id.access`. - Operation `action`, response envelope `search`. - Flags: read only. # Recall a lent-out account Source: https://docs.gtm-api.com/api-reference/id/account_shares/recall-a-lent-out-account /api-reference/id/openapi.yaml post /api/account-shares/recall Take a lent-out account back. Owner side, no holder consent. Target EXACTLY ONE of sid (ac_sh_) or filter; neither or both is 422 on sid_or_filter. FILTER MODE fans out over the loans you ISSUED, the server FORCES status to active so your filter can only narrow the set, and it drains INLINE at most 100 shares per call, minting no mass-action row. It also requires confirmation_count equal to the active-pinned pagination.total_count search_account_shares reports for the same filter; anything else is 409 confirmation_count_mismatch. Single-sid mode ignores it. ASYNC: the call stamps status closing and a driver does the teardown, so closing_count means shares that STARTED closing, never shares recalled; poll status to recalled. NOT atomic: a mid-batch error returns no counters and earlier shares stay closing, so re-query search rather than retrying the same body. closing_count below total_matched leaves a remainder; every next call needs a FRESH count. Needs can_manage_account_shares. Contract: - MCP tool `recall_account_share`, registry package `mcp.id/account_shares`, mount `id.access`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; mass action: this verb's own request accepts a filter or targets[] set, which the owning service drains. # Return a borrowed account Source: https://docs.gtm-api.com/api-reference/id/account_shares/return-a-borrowed-account /api-reference/id/openapi.yaml post /api/account-shares/return Give a borrowed account back early. Holder side. Same shape as recall_account_share: EXACTLY ONE of sid (ac_sh_) or filter, filter mode server-pinned to status active, drained INLINE at most 100 shares per call with no mass-action row minted, and confirmation_count required with filter and matched against the active-pinned pagination.total_count of list_received_account_shares (409 confirmation_count_mismatch otherwise). The difference is the side: the filter resolves over the loans you RECEIVED, and a share you do not hold is 403 not_share_holder. ASYNC: it stamps status closing, then the driver tears the holder copy down and un-parks the owner, so closing_count counts shares that STARTED closing; poll status to returned. Not atomic, and the same remainder and recount rule as recall. Deleting the borrowed copy on the channel has the same effect, ending the loan with end_reason holder_deleted_copy. Needs can_manage_account_shares. Contract: - MCP tool `return_account_share`, registry package `mcp.id/account_shares`, mount `id.access`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; mass action: this verb's own request accepts a filter or targets[] set, which the owning service drains. # Search issued account shares Source: https://docs.gtm-api.com/api-reference/id/account_shares/search-issued-account-shares /api-reference/id/openapi.yaml post /api/account-shares/search List the account shares your team ISSUED: accounts you lent OUT to another workspace. Sharing is TEMPORARY and revocable; permanent ownership transfer is a web-app-only flow with no MCP tool, so never offer this as a way to give an account away. This is also the polling surface, since there is no get tool and no /{sid} route: read one loan with filter { sid: { eq } } and watch status until active (the holder can work), failed (it never started, read failure_reason), or terminal recalled / returned. filter.owner_account_sid answers 'is this account lent out and to whom'; holder_over_slot finds loans that landed with no free slot. The number recall_account_share has to confirm is pagination.total_count here, NOT total_matched; page_size 0 returns it with no rows. No include[], no counts block, no free-text q. Needs can_view_account_shares. Contract: - MCP tool `search_account_shares`, registry package `mcp.id/account_shares`, mount `id.access`. - Operation `search`, response envelope `search`. - Flags: read only. # API key metrics Source: https://docs.gtm-api.com/api-reference/id/api_keys/api-key-metrics /api-reference/id/openapi.yaml post /api/api-keys/metrics Period-bound counts of the team's keys by status, plus issuance / expiry tallies inside the window (created_in_period, expired_in_period). Requires period {from, to}. Optional filter. Counts key rows, not key traffic. Contract: - MCP tool `get_api_keys_metrics`, registry package `mcp.id/api_keys`, mount `id.access`. - Operation `metrics`, response envelope `metrics`. - Flags: read only. # Create API key Source: https://docs.gtm-api.com/api-reference/id/api_keys/create-api-key /api-reference/id/openapi.yaml post /api/api-keys Issue a new API key and return the full secret in the create envelope's plaintext_token EXACTLY ONCE. It is unrecoverable, so hand it to the user immediately and never log or echo it; if lost, rotate. BOTH grant axes are clamped to the ISSUER's own current authority (403, reason grant_exceeds_ceiling): permissions must sit inside the caller's token set ('*' only for an unrestricted issuer; on delegation, inside the parent key's set), and account_sids inside the caller's account slice - a sliced caller cannot pass null ("all accounts") and must send an explicit list within their slice. Verify re-intersects both axes with the issuer's CURRENT authority, so narrowing the issuer narrows every key they minted. Default expires_at = now + 3 years; pass null for a perpetual key. Natural key (team_sid, name) among non-revoked keys → a duplicate returns already_exists with the existing key (and NO secret). Contract: - MCP tool `create_api_key`, registry package `mcp.id/api_keys`, mount `id.access`. - Operation `create`, response envelope `create`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Get API key Source: https://docs.gtm-api.com/api-reference/id/api_keys/get-api-key /api-reference/id/openapi.yaml get /api/api-keys/{sid} Fetch a single API key by sid, with optional metrics include. Returns token_prefix + token_last4 for display; the secret is unrecoverable (use rotate_api_key to mint a new one). Contract: - MCP tool `get_api_key`, registry package `mcp.id/api_keys`, mount `id.access`. - Operation `get`, response envelope `get`. - Flags: read only. # Revoke API key Source: https://docs.gtm-api.com/api-reference/id/api_keys/revoke-api-key /api-reference/id/openapi.yaml delete /api/api-keys/{sid} Revoke a key (simple soft-delete): status=revoked, deleted_at set. The key stops verifying at once: id tells every channel service to forget its cached verdict (a peer the fan-out cannot reach falls back to its 5-minute cache). Irreversible: a revoked key never re-activates; issue a fresh one. Idempotent: re-deleting → already_deleted. Contract: - MCP tool `delete_api_key`, registry package `mcp.id/api_keys`, mount `id.access`. - Operation `delete`, response envelope `delete_simple`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Rotate API key Source: https://docs.gtm-api.com/api-reference/id/api_keys/rotate-api-key /api-reference/id/openapi.yaml post /api/api-keys/{sid}/rotate Swap the key's secret IN PLACE and return the new plaintext in result.plaintext_token EXACTLY ONCE: hand it to the user, never log it, unrecoverable. The key's sid / name / permissions / expires_at are unchanged. The previous secret stays valid until result.previous_token_expires_at (now + grace_seconds, default 24h) so the agent can update config without downtime. Use when a secret leaks or before offboarding. NOT idempotent: each call mints a brand-new secret. Rotating a revoked/expired key → 409. Contract: - MCP tool `rotate_api_key`, registry package `mcp.id/api_keys`, mount `id.access`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Search API keys Source: https://docs.gtm-api.com/api-reference/id/api_keys/search-api-keys /api-reference/id/openapi.yaml post /api/api-keys/search List the team's API keys for an access audit: which keys exist, what each can do (permissions), their status, when each was last_used_at and when each expires. Filter by status for live vs dead keys, by last_used_at.lt for stale keys to revoke at offboarding, or by expires_at for keys expiring soon. The secret is never returned (only token_prefix + token_last4). include:["metrics"] attaches per-key counts; page_size:0 returns counts only. Contract: - MCP tool `search_api_keys`, registry package `mcp.id/api_keys`, mount `id.access`. - Operation `search`, response envelope `search`. - Flags: read only. # Update API key Source: https://docs.gtm-api.com/api-reference/id/api_keys/update-api-key /api-reference/id/openapi.yaml patch /api/api-keys/{sid} Partial update of name / permissions / account_sids (the key's identity and secret are untouched; use rotate_api_key to change the secret). Each list is a FULL replacement, clamped to the STORED issuer's current authority on its axis (403, reason grant_exceeds_ceiling): permissions inside their token set, account_sids inside their slice (null = all accounts, grantable only under an unrestricted issuer slice). At least one field is required. A change takes effect at once: id tells every channel service to forget the key's cached verdict (a peer the fan-out cannot reach falls back to its 5-minute cache). Contract: - MCP tool `update_api_key`, registry package `mcp.id/api_keys`, mount `id.access`. - Operation `update`, response envelope `update`. - Flags: none. # API request metrics Source: https://docs.gtm-api.com/api-reference/id/api_requests/api-request-metrics /api-reference/id/openapi.yaml post /api/api-requests/metrics Period-bound totals of the team's external API requests, merged across the answering services: total_count, error_count and the 2xx/3xx/4xx/5xx split in aggregated.counts; error_rate, avg_duration_ms, first/last_occurred_at in aggregated.metrics. Requires period {from, to} (ISO 8601 UTC, half-open, at most 92 days). Optional group_by splits the same numbers per key: client_sid ("how much does each agent call", groups carry the client name), route or entity ("which tools"), status_family, service, surface (occurred_hour / occurred_day exist too but bucket is the better time axis). Optional bucket (hour | day) cuts the window into consecutive UTC buckets, series.points[] one per bucket over the whole window with zeros filled, each carrying its own counts and its own group_by split: "requests per day by type" is group_by entity + bucket day in one call. Use to answer "how many calls this week and how many failed" in one round-trip instead of paging the log. aggregated.counts.sources says which services answered. Contract: - MCP tool `get_api_request_metrics`, registry package `mcp.id/api_requests`, mount `id.platform`. - Operation `metrics`, response envelope `metrics`. - Flags: read only. # Search API requests Source: https://docs.gtm-api.com/api-reference/id/api_requests/search-api-requests /api-reference/id/openapi.yaml post /api/api-requests/search List the team's external API requests newest first: every call made with an api key or an OAuth client (your own calls included), as logged by the service that answered it, merged across id, linkedin and orchestration. One row per request: the client (key or OAuth client, with its name), the route and entity, the HTTP status and its family, the typed error it failed with (error_code, error_reason, error_cause), the account it named, the duration, the trace id. Use for "what did our agents do", "show the failing requests since noon" (filter status_family in [4xx,5xx]), "why are these all 429" (filter error_cause: a paused account is not a spent daily limit), "what did the n8n client call" (filter client_sid), or one MCP turn (filter trace_id). page_size 0 counts only; the cursor is a keyset merged across services. counts.sources says which services answered; a service marked error contributed nothing. Not LinkedIn activity (that is the account activity log) and not a quota: the log counts, it never limits. Contract: - MCP tool `search_api_requests`, registry package `mcp.id/api_requests`, mount `id.platform`. - Operation `search`, response envelope `search`. - Flags: read only. # Delete payment method Source: https://docs.gtm-api.com/api-reference/id/billing_payment_methods/delete-payment-method /api-reference/id/openapi.yaml delete /api/billing-payment-methods/{sid} Remove a saved payment method: this only takes it off the paying user's saved list in Paddle. A method a live subscription uses is refused (409 conflict, payment_method_in_use) until that subscription is moved to another card via get_billing_payment_method_add_link: always the case for is_default:true while the plan bills it. A non-default card can be refused too, when it pays another subscription of the same payer (another workspace or a floating one). Idempotent. Requires billing.manage. Contract: - MCP tool `delete_billing_payment_method`, registry package `mcp.id/billing_payment_methods`, mount `id.billing`. - Operation `delete`, response envelope `delete_simple`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Get add-card link Source: https://docs.gtm-api.com/api-reference/id/billing_payment_methods/get-add-card-link /api-reference/id/openapi.yaml post /api/billing-payment-methods/get-add-link Return a temporary Paddle Customer Portal deep link to add or replace a card for the workspace's applied subscription. This is the ONLY way to add a card: Paddle has no add-card API; the user enters card details on Paddle's hosted page. Never cached. Returns no_billing_account if the workspace has no applied subscription. Requires billing.manage. Contract: - MCP tool `get_billing_payment_method_add_link`, registry package `mcp.id/billing_payment_methods`, mount `id.billing`. - Operation `action`, response envelope `action`. - Flags: none. # List payment methods Source: https://docs.gtm-api.com/api-reference/id/billing_payment_methods/list-payment-methods /api-reference/id/openapi.yaml get /api/billing-payment-methods List the saved cards / PayPal / wallets for the active workspace's paying user (proxied live from Paddle). The list is re-sorted so the billing card is items[0]; is_default:true is the card that last billed the workspace, so answer 'which card am I paying with' by reading it. Cards may be listed even with no applied subscription; empty if the user has no Paddle customer. Brand + last4 + expiry only (PCI). Requires billing.view. Contract: - MCP tool `list_billing_payment_methods`, registry package `mcp.id/billing_payment_methods`, mount `id.billing`. - Operation `search`, response envelope `search`. - Flags: read only. # Search billing products Source: https://docs.gtm-api.com/api-reference/id/billing_products/search-billing-products /api-reference/id/openapi.yaml post /api/billing-products/search List the Paddle-synced product catalog (the base plan + add-ons) with filtering, sorting and cursor pagination. Filter by type to split plan vs add-on, is_active for the live catalog, or q for an infix match over name/slug. Each row carries the per-unit limits grant. Read-only: the catalog is authored in Paddle, never here. page_size:0 returns counts only. Contract: - MCP tool `search_billing_products`, registry package `mcp.id/billing_products`, mount `id.billing`. - Operation `search`, response envelope `search`. - Flags: read only. # Add subscription add-on Source: https://docs.gtm-api.com/api-reference/id/billing_subscriptions/add-subscription-add-on /api-reference/id/openapi.yaml post /api/billing-subscriptions/{sid}/add-addon Add a recurring add-on item to the SAME subscription (one Paddle PATCH, one prorated charge). DANGEROUS: money moves; server-side confirm gate applies. The proration rides the subscription's own next invoice (prorated_next_billing_period) so the buyer sees one charge on one date, except while a cancel or pause is scheduled, where Paddle refuses a next-period mode and the prorated amount is charged immediately instead. result.proration_mode reports which of the two was used. Not idempotent: every call buys quantity more units. When Paddle's answer to the change is lost, or it answers 5xx, the service reads the subscription back: the call succeeds when Paddle carries the change, and anything else is 409 billing_change_unconfirmed, which is not a retry: read the subscription again before buying more. Requires billing.manage. Contract: - MCP tool `add_billing_subscription_addon`, registry package `mcp.id/billing_subscriptions`, mount `id.billing`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Apply subscription to team Source: https://docs.gtm-api.com/api-reference/id/billing_subscriptions/apply-subscription-to-team /api-reference/id/openapi.yaml post /api/billing-subscriptions/{sid}/apply Apply a floating subscription (owned by the caller) to a team, activating its slots + limits there. Owner-only. DANGEROUS state change (server-side confirm gate); rejects with 409 if the team already has a live applied subscription or is soft-deleted. No charge. Requires billing.manage. Contract: - MCP tool `apply_billing_subscription`, registry package `mcp.id/billing_subscriptions`, mount `id.billing`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Cancel subscription Source: https://docs.gtm-api.com/api-reference/id/billing_subscriptions/cancel-subscription /api-reference/id/openapi.yaml post /api/billing-subscriptions/{sid}/cancel Cancel the subscription at the end of the current period (terminal). DANGEROUS: the team loses capacity when the period ends, though data is retained and there is no immediate charge or refund. Server-side confirm gate applies. There is no delete verb; cancel is the canonical termination. Requires billing.manage. Contract: - MCP tool `cancel_billing_subscription`, registry package `mcp.id/billing_subscriptions`, mount `id.billing`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Change subscription Source: https://docs.gtm-api.com/api-reference/id/billing_subscriptions/change-subscription /api-reference/id/openapi.yaml post /api/billing-subscriptions/{sid}/change Commit a quantity / bracket-price change. DANGEROUS: an UPGRADE charges the owner's card immediately (prorated); a downgrade applies next period (no immediate charge). Server-side preview→commit gate applies; call preview_billing_subscription_change first. manual (sales) subscriptions are not re-bracketed (422). The committed state arrives via webhook; the response returns the optimistic post-change row. Provide at least one of quantity / price_sid. Requires billing.manage. Contract: - MCP tool `change_billing_subscription`, registry package `mcp.id/billing_subscriptions`, mount `id.billing`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Create subscription checkout Source: https://docs.gtm-api.com/api-reference/id/billing_subscriptions/create-subscription-checkout /api-reference/id/openapi.yaml post /api/billing-subscriptions/create-checkout Start a self-serve purchase of a subscription. DANGEROUS (charges money): it pre-creates a billed Paddle transaction and returns a hosted checkout_url the buyer pays in the Paddle overlay (server-side preview→commit gate applies). Resolves the bracket price for the requested slot count, optionally applies to team_sid on activation (omit to buy floating), and can bundle add-ons in the same checkout. The real subscription row is born on the billing-subscriptions.activated webhook; poll get/search afterwards. Requires billing.manage. Contract: - MCP tool `create_billing_subscription_checkout`, registry package `mcp.id/billing_subscriptions`, mount `id.billing`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Get billing subscription Source: https://docs.gtm-api.com/api-reference/id/billing_subscriptions/get-billing-subscription /api-reference/id/openapi.yaml get /api/billing-subscriptions/{sid} Fetch a single billing subscription by sid, with optional includes (price, product, team, latest_transactions). Contract: - MCP tool `get_billing_subscription`, registry package `mcp.id/billing_subscriptions`, mount `id.billing`. - Operation `get`, response envelope `get`. - Flags: read only. # Get update-payment-method link Source: https://docs.gtm-api.com/api-reference/id/billing_subscriptions/get-update-payment-method-link /api-reference/id/openapi.yaml post /api/billing-subscriptions/{sid}/get-update-payment-method-link Return a short-lived Paddle portal deep link to update the card for this subscription (the typical past_due recovery path). No charge, mutates nothing on our side. Requires billing.manage. Contract: - MCP tool `get_billing_subscription_update_payment_method_link`, registry package `mcp.id/billing_subscriptions`, mount `id.billing`. - Operation `action`, response envelope `action`. - Flags: read only. # Pause subscription Source: https://docs.gtm-api.com/api-reference/id/billing_subscriptions/pause-subscription /api-reference/id/openapi.yaml post /api/billing-subscriptions/{sid}/pause Pause the subscription effective the next billing period. DANGEROUS state change (server-side confirm gate); while paused the applied team is gated like suspended, slots/data retained, billing stops. No charge. The committed status arrives via webhook. Resume with resume_billing_subscription. Requires billing.manage. Contract: - MCP tool `pause_billing_subscription`, registry package `mcp.id/billing_subscriptions`, mount `id.billing`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Preview subscription change Source: https://docs.gtm-api.com/api-reference/id/billing_subscriptions/preview-subscription-change /api-reference/id/openapi.yaml post /api/billing-subscriptions/{sid}/preview-change Read-only proration preview for a quantity / bracket-price change BEFORE committing. Calls Paddle Pricing Preview and returns the immediate charge (upgrade) or next-period credit (downgrade), the resolved new bracket price, and whether it crosses a bracket boundary. No state change, no charge. This is the confirmation gate to show the user before change_billing_subscription. Provide at least one of quantity / price_sid. Requires billing.view. Contract: - MCP tool `preview_billing_subscription_change`, registry package `mcp.id/billing_subscriptions`, mount `id.billing`. - Operation `action`, response envelope `action`. - Flags: read only. # Remove subscription add-on Source: https://docs.gtm-api.com/api-reference/id/billing_subscriptions/remove-subscription-add-on /api-reference/id/openapi.yaml post /api/billing-subscriptions/{sid}/remove-addon Remove add-on units from the subscription, effective next period (no refund, no immediate charge). Pass quantity to give back part of the line, or omit it to drop the whole line; result.remaining_quantity reports what is left (0 means the line is gone). The price_sid must be a live item on this subscription (422 addon_not_on_subscription otherwise). Works while a cancel or pause is scheduled. A give-back Paddle did not confirm (its answer lost, or a 5xx) is 409 billing_change_unconfirmed: read the subscription again before repeating it. DANGEROUS state change (server-side confirm gate). Requires billing.manage. Contract: - MCP tool `remove_billing_subscription_addon`, registry package `mcp.id/billing_subscriptions`, mount `id.billing`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Resume subscription Source: https://docs.gtm-api.com/api-reference/id/billing_subscriptions/resume-subscription /api-reference/id/openapi.yaml post /api/billing-subscriptions/{sid}/resume Resume a paused subscription; billing continues from the next period and the applied team re-derives active. DANGEROUS state change (server-side confirm gate). No charge. The committed status arrives via webhook. Requires billing.manage. Contract: - MCP tool `resume_billing_subscription`, registry package `mcp.id/billing_subscriptions`, mount `id.billing`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Search billing subscriptions Source: https://docs.gtm-api.com/api-reference/id/billing_subscriptions/search-billing-subscriptions /api-reference/id/openapi.yaml post /api/billing-subscriptions/search List the caller's billing subscriptions: the ones they bought (owner = caller, including floating subscriptions applied to no team) plus the one applied to a team they can read. Filter by status, team_sid:{is_null:true} for floating, paddle_subscription_id:{is_null:true} for the internal sentinels (free/partner), or current_period_ends_at for expiry windows. include price / product / team / latest_transactions to hydrate context. page_size:0 returns counts only. Contract: - MCP tool `search_billing_subscriptions`, registry package `mcp.id/billing_subscriptions`, mount `id.billing`. - Operation `search`, response envelope `search`. - Flags: read only. # Unapply subscription Source: https://docs.gtm-api.com/api-reference/id/billing_subscriptions/unapply-subscription /api-reference/id/openapi.yaml post /api/billing-subscriptions/{sid}/unapply Detach a subscription from its team, returning it to floating on the buyer's balance. DANGEROUS: removes the team's capacity (the team goes suspended if this was its only applied subscription); does NOT cancel billing or refund. Used to move a subscription to another team. Requires billing.manage. Contract: - MCP tool `unapply_billing_subscription`, registry package `mcp.id/billing_subscriptions`, mount `id.billing`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Undo scheduled cancel Source: https://docs.gtm-api.com/api-reference/id/billing_subscriptions/undo-scheduled-cancel /api-reference/id/openapi.yaml post /api/billing-subscriptions/{sid}/undo-cancel Drop a scheduled cancel before it takes effect; billing continues as if cancel was never asked. Decided on the live Paddle state: nothing pending is a success no-op (cleared:false), a scheduled pause or resume is refused with 409 scheduled_change_is_not_cancel naming the verb that clears it, and an already canceled subscription is too late to undo. Synchronous, no charge, no pending block. Also the unlock a 409 subscription_locked_pending_changes points at. Requires billing.manage. Contract: - MCP tool `undo_cancel_billing_subscription`, registry package `mcp.id/billing_subscriptions`, mount `id.billing`. - Operation `action`, response envelope `action`. - Flags: none. # Billing transaction metrics Source: https://docs.gtm-api.com/api-reference/id/billing_transactions/billing-transaction-metrics /api-reference/id/openapi.yaml post /api/billing-transactions/metrics Period-bound money sums over the caller's transactions. Requires period {from,to}. Money never crosses currencies; every sum lives inside a per-currency by_currency entry keyed by ISO 4217. Answers 'how much did I pay / tax this quarter'. Sums are GROSS (refunds not modeled in v1). Filter is the same object as search and every axis is applied; billed_at / created_at operators are dropped from the period-bound sums (period is the one window there) but they DO scope the counts block, which carries no period of its own. Requires billing.view. Contract: - MCP tool `get_billing_transactions_metrics`, registry package `mcp.id/billing_transactions`, mount `id.billing`. - Operation `metrics`, response envelope `metrics`. - Flags: read only. # Get billing transaction Source: https://docs.gtm-api.com/api-reference/id/billing_transactions/get-billing-transaction /api-reference/id/openapi.yaml get /api/billing-transactions/{sid} Fetch a single billing transaction by sid, with optional subscription / owner / team includes. Returns the frozen items[] charge snapshot inline. Contract: - MCP tool `get_billing_transaction`, registry package `mcp.id/billing_transactions`, mount `id.billing`. - Operation `get`, response envelope `get`. - Flags: read only. # Get transaction invoice Source: https://docs.gtm-api.com/api-reference/id/billing_transactions/get-transaction-invoice /api-reference/id/openapi.yaml post /api/billing-transactions/{sid}/get-invoice Fetch a temporary, time-limited URL for this transaction's invoice PDF from Paddle (never stored here; expires ~1h, opens inline). Works for self-serve card charges and sales-issued manual invoices. Returns invoice_not_ready before the transaction is billed, and invoice_not_available when Paddle issued no invoice for it (a $0 payment-method-change charge, a canceled checkout): the rows with a PDF are the ones carrying invoice_number. Read-only, mutates nothing. Requires billing.view. Contract: - MCP tool `get_billing_transaction_invoice`, registry package `mcp.id/billing_transactions`, mount `id.billing`. - Operation `action`, response envelope `action`. - Flags: read only. # Search billing transactions Source: https://docs.gtm-api.com/api-reference/id/billing_transactions/search-billing-transactions /api-reference/id/openapi.yaml post /api/billing-transactions/search List billing transactions (invoices / charges) for the caller: one row per Paddle transaction, newest first. Filter by status / origin / collection_mode / currency / billed_at window, or locate one by invoice_number. include subscription / owner / team to hydrate context. Read-only, append-only ledger; page_size:0 returns counts only. Requires billing.view. Contract: - MCP tool `search_billing_transactions`, registry package `mcp.id/billing_transactions`, mount `id.billing`. - Operation `search`, response envelope `search`. - Flags: read only. # Request media upload Source: https://docs.gtm-api.com/api-reference/id/media_uploads/request-media-upload /api-reference/id/openapi.yaml post /api/media-uploads/request-upload Get a place for ONE image or video file, then pass its file_url to create_linkedin_post (images[].url or video.url) instead of base64. In a chat without HTTP (the user pasted or has the picture): give the user upload_link, ask them to open it and drop the file, and call create_linkedin_post with file_url once they confirm. With a shell: run the curl line (upload_link, replace ). Pass file_type to also get a pre-signed S3 POST form (upload). Never type base64 of a pasted image: the model sees pixels, not the file, and the bytes come out wrong. The link works once for 30 minutes, up to 35 MB, images PNG/JPEG/GIF/WEBP or video MP4/MOV/WEBM (checked by content). The file is public by URL and deleted after 7 days. 120 per workspace per hour. Contract: - MCP tool `request_media_upload`, registry package `mcp.id/media_uploads`, mount `id.platform`. - Operation `action`, response envelope `action`. - Flags: none. # Dismiss notification Source: https://docs.gtm-api.com/api-reference/id/notifications/dismiss-notification /api-reference/id/openapi.yaml delete /api/notifications/{sid} Dismiss a notification from the inbox: a soft-delete that sets deleted_at. DESTRUCTIVE (removes the row from inbox lists). It does NOT cancel in-flight delivery: if the delivery worker already picked the row up, the email still goes out. The audit row is retained (recoverable via deleted_at filters). Contract: - MCP tool `delete_notification`, registry package `mcp.id/notifications`, mount `id.platform`. - Operation `delete`, response envelope `delete_simple`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Get notification Source: https://docs.gtm-api.com/api-reference/id/notifications/get-notification /api-reference/id/openapi.yaml get /api/notifications/{sid} Fetch a single notification by sid: the rendered subject / body, delivery state, and failure reason if any. Contract: - MCP tool `get_notification`, registry package `mcp.id/notifications`, mount `id.platform`. - Operation `get`, response envelope `get`. - Flags: read only. # Notifications metrics Source: https://docs.gtm-api.com/api-reference/id/notifications/notifications-metrics /api-reference/id/openapi.yaml post /api/notifications/metrics Period-bound delivery-health aggregates over a filtered notification set: how many notifications were delivered, failed, and rate-limited in the window. Requires period {from,to}. Optional filter and a single group_by axis (event_name / status / channel). Contract: - MCP tool `get_notifications_metrics`, registry package `mcp.id/notifications`, mount `id.platform`. - Operation `metrics`, response envelope `metrics`. - Flags: read only. # Search notifications Source: https://docs.gtm-api.com/api-reference/id/notifications/search-notifications /api-reference/id/openapi.yaml post /api/notifications/search List notifications (email and in_app channels) for the team/user with filtering, sorting and cursor pagination: the user inbox ("what was sent to me?"), admin audit ("what did the system email Jane about?"), or failure triage (status:failed in the last 24h). No full-text search. Returns a counts block of predicate tallies (by status / event_name / channel). Bodies are omitted from list rows; call get_notification for the rendered body. On gtm-api.com every row is channel email: the in_app channel, read_at and the mark-read verbs are a shared-contract surface the GetSales side writes, so read_at is always null here and there is nothing to mark read. Contract: - MCP tool `search_notifications`, registry package `mcp.id/notifications`, mount `id.platform`. - Operation `search`, response envelope `search`. - Flags: read only. # Disconnect app (revoke grant) Source: https://docs.gtm-api.com/api-reference/id/oauth_authorizations/disconnect-app-revoke-grant /api-reference/id/openapi.yaml post /api/oauth-authorizations/{sid}/revoke Disconnect an app: flip the grant to status=revoked, clear its refresh credential, and cascade-revoke every installation token (all teams) in one transaction, so the agent is cut off immediately. The envelope item is the post-action grant (it persists, revoked); result.revoked_sessions is the count cut. Idempotent (already-revoked → 0). 404 if the sid is not the caller's own grant. Contract: - MCP tool `revoke_oauth_authorization`, registry package `mcp.id/oauth_authorizations`, mount `id.access`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Edit connected app Source: https://docs.gtm-api.com/api-reference/id/oauth_authorizations/edit-connected-app /api-reference/id/openapi.yaml patch /api/oauth-authorizations/{sid} Partial self-service edit of the grant's mutable fields WITHOUT re-running the browser consent: team_scope_mode, consented_team_sids (full-replace, non-empty when mode=selected), and permissions (full-replace, must stay ⊆ the client ceiling). Each submitted team is re-checked against live membership (403 if not a member). Narrowing is security-positive; you can never exceed the ceiling via update. Changes are frozen-at-mint, so live tokens keep their claims until their short TTL; use revoke for an immediate cut. At least one field required. 404 if not the caller's own grant. Contract: - MCP tool `update_oauth_authorization`, registry package `mcp.id/oauth_authorizations`, mount `id.access`. - Operation `update`, response envelope `update`. - Flags: none. # Search connected apps Source: https://docs.gtm-api.com/api-reference/id/oauth_authorizations/search-connected-apps /api-reference/id/openapi.yaml post /api/oauth-authorizations/search List the caller's connected apps (standing OAuth consent grants). Always scoped to the caller's own grants, never cross-user. Filter by status (active vs revoked history), oauth_client_sid, or team_scope_mode; include client (name + ceiling), teams (in-scope team names), and active_sessions (live installation tokens). This is the user's view of who has access to their workspaces. page_size:0 returns counts only. Contract: - MCP tool `search_oauth_authorizations`, registry package `mcp.id/oauth_authorizations`, mount `id.access`. - Operation `search`, response envelope `search`. - Flags: read only. # Create OAuth client Source: https://docs.gtm-api.com/api-reference/id/oauth_clients/create-oauth-client /api-reference/id/openapi.yaml post /api/oauth-clients Register a new OAuth application; the server generates client_id. v1 clients are public/PKCE (no secret). A confidential client (fast-follow) returns client_secret in the create envelope ONCE. Tell the user to store it; it is unrecoverable and must never be logged. permissions is the ceiling the client may request; redirect_uris is an exact-match https allow-list (closes open-redirect). Dedup (team_sid, name) → already_exists. Set platform:true (admin only) to register a platform client with no owning team. Contract: - MCP tool `create_oauth_client`, registry package `mcp.id/oauth_clients`, mount `id.access`. - Operation `create`, response envelope `create`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Delete OAuth client Source: https://docs.gtm-api.com/api-reference/id/oauth_clients/delete-oauth-client /api-reference/id/openapi.yaml delete /api/oauth-clients/{sid} Destructive. Soft-delete the client (sets deleted_at) and cascade-revoke every issued oauth session and grant, disconnecting all agents connected through this app immediately. The public OAuth flow rejects a soft-deleted client. Idempotent: re-deleting → already_deleted. The client's sessions survive as revoked-not-deleted (retained). Contract: - MCP tool `delete_oauth_client`, registry package `mcp.id/oauth_clients`, mount `id.access`. - Operation `delete`, response envelope `delete_simple`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Get OAuth client Source: https://docs.gtm-api.com/api-reference/id/oauth_clients/get-oauth-client /api-reference/id/openapi.yaml get /api/oauth-clients/{sid} Fetch one OAuth client by sid. include:["active_sessions"] lists agents connected through it (requires sessions.view). client_secret is never returned on a read. Contract: - MCP tool `get_oauth_client`, registry package `mcp.id/oauth_clients`, mount `id.access`. - Operation `get`, response envelope `get`. - Flags: read only. # Search OAuth clients Source: https://docs.gtm-api.com/api-reference/id/oauth_clients/search-oauth-clients /api-reference/id/openapi.yaml post /api/oauth-clients/search List registered OAuth applications for the caller's team (plus platform clients when team_sid.is_null). Filter by status, client_id, registration_kind, is_confidential; include:["active_sessions"] to see the agents connected through each app (requires sessions.view). No full-text: filter by name / client_id. page_size:0 returns counts only. Contract: - MCP tool `search_oauth_clients`, registry package `mcp.id/oauth_clients`, mount `id.access`. - Operation `search`, response envelope `search`. - Flags: read only. # Update OAuth client Source: https://docs.gtm-api.com/api-reference/id/oauth_clients/update-oauth-client /api-reference/id/openapi.yaml patch /api/oauth-clients/{sid} Partial update. Editable: name, redirect_uris, permissions, grant_types, status (redirect_uris / permissions full-replace). status:"disabled" cascade-revokes the client's oauth grants and issued tokens. Tightening permissions affects only future mints: live tokens keep their frozen set; use disabled / delete for an immediate cut. client_id / is_confidential / registration_kind are not editable. At least one field required. Contract: - MCP tool `update_oauth_client`, registry package `mcp.id/oauth_clients`, mount `id.access`. - Operation `update`, response envelope `update`. - Flags: none. # Revoke session Source: https://docs.gtm-api.com/api-reference/id/sessions/revoke-session /api-reference/id/openapi.yaml post /api/sessions/{sid}/revoke End a session immediately: status active → revoked. The token is rejected from the next request (verifier cache flushed within 30-60s). This is the canonical "delete" for this entity; there is no delete tool. Idempotent: revoking an already revoked/expired session returns the row with result.already_revoked:true. Contract: - MCP tool `revoke_session`, registry package `mcp.id/sessions`, mount `id.identity`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Search sessions Source: https://docs.gtm-api.com/api-reference/id/sessions/search-sessions /api-reference/id/openapi.yaml post /api/sessions/search List sessions. Default scope is the caller's own active sessions ("Active sessions"); pass kind:{eq:"oauth"} for "Connected agents", or status to see revoked/expired history. Each row carries the derived is_current flag (true for the caller's own request) so "log out everywhere except this device" can skip it. Returns a counts block. page_size:0 returns counts only. Contract: - MCP tool `search_sessions`, registry package `mcp.id/sessions`, mount `id.identity`. - Operation `search`, response envelope `search`. - Flags: read only. # Delete SSL certificate Source: https://docs.gtm-api.com/api-reference/id/ssl_certificates/delete-ssl-certificate /api-reference/id/openapi.yaml delete /api/ssl-certificates/{sid} Remove a custom domain's certificate. DESTRUCTIVE. Revokes the certificate at the CA (best-effort), removes the on-disk serving files, and soft-deletes the row. The CA-revoke and file-removal are external side effects, not a DB cascade, so this is a simple delete. No recovery: re-create + re-issue to restore. Contract: - MCP tool `delete_ssl_certificate`, registry package `mcp.id/ssl_certificates`, mount `id.platform`. - Operation `delete`, response envelope `delete_simple`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Get SSL certificate Source: https://docs.gtm-api.com/api-reference/id/ssl_certificates/get-ssl-certificate /api-reference/id/openapi.yaml get /api/ssl-certificates/{sid} Fetch a single certificate by sid: status, issuer, validity window, redacted last_error, and the HTTP-01 challenge data while an attempt is in flight (status='challenge' on a first issuance, 'active' during a renewal). Poll this after issue/renew until challenge is null: a first issuance ends 'active' or 'failed' with last_error; a renewal stays 'active', with last_error null when the new certificate is live and set when it failed (the current one keeps serving). The cert body and key are never returned. Contract: - MCP tool `get_ssl_certificate`, registry package `mcp.id/ssl_certificates`, mount `id.platform`. - Operation `get`, response envelope `get`. - Flags: read only. # Issue SSL certificate Source: https://docs.gtm-api.com/api-reference/id/ssl_certificates/issue-ssl-certificate /api-reference/id/openapi.yaml post /api/ssl-certificates/{sid}/issue Start the ACME HTTP-01 flow for a registered domain. ASYNC: synchronously requests authorization from Let's Encrypt, stores the token + key-authorization, flips status→challenge, and returns the challenge data + a pending ref; the platform then self-checks, validates, finalizes and flips status→active in the background (poll get_ssl_certificate, or await the ssl-certificates.issued / .failed webhook). Use after create. Not idempotent: each call requests a fresh authorization. 409 conflict if the domain is already active (use renew), while an attempt is in flight (issuance_already_in_progress), or while another team's certificate for the domain is active (domain_active_in_another_team: nothing was started and retrying cannot help). An attempt already in flight when that happens ends failed, with last_error opening with the same reason. Contract: - MCP tool `issue_ssl_certificate`, registry package `mcp.id/ssl_certificates`, mount `id.platform`. - Operation `action`, response envelope `action_async`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Register SSL certificate Source: https://docs.gtm-api.com/api-reference/id/ssl_certificates/register-ssl-certificate /api-reference/id/openapi.yaml post /api/ssl-certificates Register a custom domain as a certificate row in status='pending'. Does NOT start ACME; call issue_ssl_certificate next to begin the HTTP-01 flow. Idempotent on the domain natural key, and already_exists says whether the row was left as found. true: the team's row is returned as it stands (pending, active, or an attempt in flight whatever the status), nothing was written. false: a new row, or a failed/expired row with nothing in flight or a soft-deleted row recreated in place (same sid, reset to pending, challenge and last_error cleared; read last_error with get_ssl_certificate first if the reason matters). A domain whose certificate is active in another team returns 409 conflict (domain_active_in_another_team). A domain another team has only registered is not refused: the first certificate to go active holds it. Contract: - MCP tool `create_ssl_certificate`, registry package `mcp.id/ssl_certificates`, mount `id.platform`. - Operation `create`, response envelope `create`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Renew SSL certificate Source: https://docs.gtm-api.com/api-reference/id/ssl_certificates/renew-ssl-certificate /api-reference/id/openapi.yaml post /api/ssl-certificates/{sid}/renew Force a re-issue of an existing certificate, ignoring the 30-day expiry window (operator-forced). ASYNC: same HTTP-01 path as issue and the auto-renew job, on the same row. On an active certificate the row STAYS active (it keeps serving and routing) while challenge carries the attempt: on success the persisted leaf / full chain / key are replaced and issued_at / expires_at re-stamped, on failure last_error is set and the current certificate keeps serving. On a pending/failed/expired row nothing is serving, so it flips status→challenge and ends active or failed like issue. Poll get_ssl_certificate until challenge is null, or await ssl-certificates.renewed / .failed. Use after a CA chain fix, to retry a failing renewal once its cause is fixed, or to recover a failed/expired certificate. 409 conflict while an attempt is in flight (issuance_already_in_progress, e.g. the renew job's) or while another team's certificate for the domain is active (domain_active_in_another_team, retrying cannot help). Contract: - MCP tool `renew_ssl_certificate`, registry package `mcp.id/ssl_certificates`, mount `id.platform`. - Operation `action`, response envelope `action_async`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Search SSL certificates Source: https://docs.gtm-api.com/api-reference/id/ssl_certificates/search-ssl-certificates /api-reference/id/openapi.yaml post /api/ssl-certificates/search List the team's TLS certificates for custom email domains: which domains are registered, each status, issuer, issued_at, expires_at, and the redacted last_error for failures. Filter by status for live vs failed, by expires_at:{lte} for certificates expiring soon, or by domain for a specific FQDN. The certificate body and private key are never returned. Returns a counts block of predicate tallies; page_size:0 returns counts only. Contract: - MCP tool `search_ssl_certificates`, registry package `mcp.id/ssl_certificates`, mount `id.platform`. - Operation `search`, response envelope `search`. - Flags: read only. # Update SSL certificate Source: https://docs.gtm-api.com/api-reference/id/ssl_certificates/update-ssl-certificate /api-reference/id/openapi.yaml patch /api/ssl-certificates/{sid} Metadata-only partial update. No certificate-affecting field is mutable here: domain, status, challenge, issuer and validity are driven by the ACME actions and jobs, never by update. v1 has no client-mutable field, so any body returns 422 nothing_to_update; the endpoint exists for surface symmetry. Contract: - MCP tool `update_ssl_certificate`, registry package `mcp.id/ssl_certificates`, mount `id.platform`. - Operation `update`, response envelope `update`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Escalate to a human Source: https://docs.gtm-api.com/api-reference/id/support_requests/escalate-to-a-human /api-reference/id/openapi.yaml post /api/support-requests Hand the current issue to the human support team. Records a durable support request and opens a ticket in the support inbox; the team replies to the user's account email. Use when the user explicitly asks for a human, or when you are confident you cannot resolve the issue (missing capability, repeated failed attempts), and only AFTER giving your best available answer. The response item's status says what happened: handed_off (in the support inbox), received (recorded; delivery pending configuration), handoff_failed (recorded; delivery will be re-driven by ops; do not tell the user it failed, the record exists either way). Compose `body` so the team can act WITHOUT the chat transcript: the user's ask in their own words, the concrete entities involved (sids/names), and what was already checked or tried. Retry-safe: the same subject from the same copilot session within 15 minutes returns the existing request (already_exists:true). Contract: - MCP tool `escalate_to_human`, registry package `mcp.id/support_requests`, mount `id.platform`. - Operation `create`, response envelope `create`. - Flags: none. # Get support request Source: https://docs.gtm-api.com/api-reference/id/support_requests/get-support-request /api-reference/id/openapi.yaml get /api/support-requests/{sid} Fetch a single support escalation by sid: the full body the team received, handoff state (intercom_ticket_id, handed_off_at) and the redacted delivery error if the handoff failed. Contract: - MCP tool `get_support_request`, registry package `mcp.id/support_requests`, mount `id.platform`. - Operation `get`, response envelope `get`. - Flags: read only. # Search support requests Source: https://docs.gtm-api.com/api-reference/id/support_requests/search-support-requests /api-reference/id/openapi.yaml post /api/support-requests/search List the team's support escalations with filtering, sorting and cursor pagination: the ops view ("what escalations are open?", "did this session already escalate?"). Returns a counts block of predicate tallies (by status / reason). Bodies are omitted from list rows; call get_support_request for the full text. No full-text search. Contract: - MCP tool `search_support_requests`, registry package `mcp.id/support_requests`, mount `id.platform`. - Operation `search`, response envelope `search`. - Flags: read only. # Accept team-member invitation Source: https://docs.gtm-api.com/api-reference/id/team_members/accept-team-member-invitation /api-reference/id/openapi.yaml post /api/team-members/accept-invitation Accept a pending invite by its one-time code. Public REST: code-bearer, no team scope. Hashes the code, matches the row it belongs to and branches on that row: invited → flips invited → active, binds the authenticated user_sid, and sets users.default_team_sid if the user had none; already active for the SAME user → 200 already_accepted:true naming that same invite; already redeemed by another account → 409 invitation_already_redeemed; no row (unknown, cancelled, or the membership was removed) → 404. An expired code is 409 invitation_expired, a seatless plan 402. Contract: - MCP tool `accept_team_member_invitation`, registry package `mcp.id/team_members`, mount `id.identity`. - Operation `action`, response envelope `action`. - Flags: none. # Invite team member Source: https://docs.gtm-api.com/api-reference/id/team_members/invite-team-member /api-reference/id/openapi.yaml post /api/team-members Invite a person by email. Born status=invited, user_sid=null, with a one-time code (+7d expiry); dispatches the invite email. permissions is the explicit token list validated against the catalog (403 grant_exceeds_ceiling when it exceeds the CALLER's own grant; '*' is grantable only by the owner or a wildcard holder; can_view_teams is always added - the membership floor). account_sids is the optional profile slice (null/omitted = all team profiles; an explicit empty list is refused, and a sliced caller cannot grant beyond their own slice). Natural key (team, email) over live rows; re-inviting a live member returns already_exists:true. Contract: - MCP tool `create_team_member`, registry package `mcp.id/team_members`, mount `id.identity`. - Operation `create`, response envelope `create`. - Flags: none. # Remove team member Source: https://docs.gtm-api.com/api-reference/id/team_members/remove-team-member /api-reference/id/openapi.yaml delete /api/team-members/{sid} Remove a pending invite OR kick an active member: one tool, soft-delete. DESTRUCTIVE. The structural owner is protected (409 conflict, owner_cannot_be_removed); move ownership via transfer_team_ownership first. Kicking a member revokes their sessions in this team only. Contract: - MCP tool `delete_team_member`, registry package `mcp.id/team_members`, mount `id.identity`. - Operation `delete`, response envelope `delete_simple`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Resend team-member invitation Source: https://docs.gtm-api.com/api-reference/id/team_members/resend-team-member-invitation /api-reference/id/openapi.yaml post /api/team-members/{sid}/resend-invitation Re-roll the one-time code on a pending invite, push its expiry (+7d), and re-send the invite email. Only valid on a status=invited row (else 409 invalid_transition). Contract: - MCP tool `resend_team_member_invitation`, registry package `mcp.id/team_members`, mount `id.identity`. - Operation `action`, response envelope `action`. - Flags: none. # Search team members Source: https://docs.gtm-api.com/api-reference/id/team_members/search-team-members /api-reference/id/openapi.yaml post /api/team-members/search List members and pending invites in the caller's team: render the Members tab, find pending invites (filter.user_sid.is_null or filter.status.eq="invited"), or look up a member by email (q). include:["user"] hydrates the resolved profile (absent for pending invites). Returns a counts block. page_size:0 returns counts only. Contract: - MCP tool `search_team_members`, registry package `mcp.id/team_members`, mount `id.identity`. - Operation `search`, response envelope `search`. - Flags: read only. # Update team member Source: https://docs.gtm-api.com/api-reference/id/team_members/update-team-member /api-reference/id/openapi.yaml patch /api/team-members/{sid} Re-scope an existing member: replace permissions and/or account_sids. email / status / user_sid / team_sid are read-only (rejected silently). At least one field with an ACTUAL change required (a value-identical PATCH answers 422 nothing_to_update). The change applies immediately: the member's live tokens are invalidated and re-minted with the new grant on their next request (agents re-exchange). Bounded by the caller's own grant (403 grant_exceeds_ceiling); the structural owner's seat is not re-scopable (409 owner_cannot_be_rescoped - move ownership via transfer_team_ownership). Contract: - MCP tool `update_team_member`, registry package `mcp.id/team_members`, mount `id.identity`. - Operation `update`, response envelope `update`. - Flags: none. # Create team Source: https://docs.gtm-api.com/api-reference/id/teams/create-team /api-reference/id/openapi.yaml post /api/teams Create a new workspace. The caller becomes the owner AND the first member; the free "Sandbox" plan (forever free, 1 account slot, no card) is applied automatically where the owner does not already run a free workspace. Names are not unique: every call creates a new team (no dedup). Contract: - MCP tool `create_team`, registry package `mcp.id/teams`, mount `id.identity`. - Operation `create`, response envelope `create`. - Flags: none. # Delete team Source: https://docs.gtm-api.com/api-reference/id/teams/delete-team /api-reference/id/openapi.yaml delete /api/teams/{sid} Cascade soft-delete a workspace and its owned rows (members, keys, sessions, oauth clients, certs). Owner-only. DESTRUCTIVE. Blocked (409 delete_blocked) while a paid applied subscription is live. Unapply/cancel it first; clear soft blockers (active keys / members) with acknowledge[]. Deleting the owner's LAST workspace is allowed and leaves their ACCOUNT untouched: they are left with no workspace until their next sign-in provisions a replacement. Erasing an account is a separate deliberate act, never a side effect of this tool. There is no restore: the deleted workspace cannot be brought back, cascade.recovery_available_until is null, and its data is erased 30 days later. Contract: - MCP tool `delete_team`, registry package `mcp.id/teams`, mount `id.identity`. - Operation `delete`, response envelope `delete_cascade`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Get team Source: https://docs.gtm-api.com/api-reference/id/teams/get-team /api-reference/id/openapi.yaml get /api/teams/{sid} Fetch a single team by sid, with optional includes (applied subscription, owner, member counts). Contract: - MCP tool `get_team`, registry package `mcp.id/teams`, mount `id.identity`. - Operation `get`, response envelope `get`. - Flags: read only. # Search teams Source: https://docs.gtm-api.com/api-reference/id/teams/search-teams /api-reference/id/openapi.yaml post /api/teams/search List the caller's teams ("my workspaces"): teams they are a member of, plus teams they own. Filter by status to find suspended workspaces, or by subscription_sid.is_null for teams with no capacity. q does an infix match on name. include[] hydrates subscription / owner / member counts. Returns a counts block. page_size:0 returns counts only. Contract: - MCP tool `search_teams`, registry package `mcp.id/teams`, mount `id.identity`. - Operation `search`, response envelope `search`. - Flags: read only. # Transfer team ownership Source: https://docs.gtm-api.com/api-reference/id/teams/transfer-team-ownership /api-reference/id/openapi.yaml post /api/teams/{sid}/transfer-ownership Move structural ownership of the team to another active member. Owner-only (an admin cannot transfer). Transferring to the current owner is a no-op. This is the only way to change owner_user_sid, and the prerequisite for the current owner to leave the team. Contract: - MCP tool `transfer_team_ownership`, registry package `mcp.id/teams`, mount `id.identity`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Update team Source: https://docs.gtm-api.com/api-reference/id/teams/update-team /api-reference/id/openapi.yaml patch /api/teams/{sid} Rename a team or edit its config. Only name / config are mutable; owner_user_sid (transfer-ownership), status (derived), subscription_sid (apply/unapply) and limits (system) are read-only and silently rejected. At least one field required. Contract: - MCP tool `update_team`, registry package `mcp.id/teams`, mount `id.identity`. - Operation `update`, response envelope `update`. - Flags: none. # Get current user Source: https://docs.gtm-api.com/api-reference/id/users/get-current-user /api-reference/id/openapi.yaml get /api/users/current Return the authenticated user's own profile: the JWT subject, self only (no {sid} form). include[]=default_team hydrates the working team; include[]=accessible_teams returns the owner + active-membership union (workspace switcher / OAuth consent multiselect). Reads email, name, timezone, default_team_sid. Needs a credential with a person behind it (an app session or an OAuth connector). An API key authenticates as itself, not as a user, so on a key this answers 403 with context.reason=not_a_user_actor and no permission changes that. Contract: - MCP tool `get_current_user`, registry package `mcp.id/users`, mount `id.identity`. - Operation `get`, response envelope `get`. - Flags: read only. # Update current user Source: https://docs.gtm-api.com/api-reference/id/users/update-current-user /api-reference/id/openapi.yaml patch /api/users/current Patch the authenticated user's own profile: first_name / last_name / avatar_url / phone / timezone / country / config only. email (change-email flow), password (change-password flow), auth_provider, email_status and all sids are rejected if sent. At least one field required. config merges key-by-key. Same subject as get_current_user: an API key has no user to patch and answers 403 with context.reason=not_a_user_actor. Contract: - MCP tool `update_current_user`, registry package `mcp.id/users`, mount `id.identity`. - Operation `update`, response envelope `update`. - Flags: none. # Search antidetect browser logs Source: https://docs.gtm-api.com/api-reference/linkedin/antidetect_browser_logs/search-antidetect-browser-logs /api-reference/linkedin/openapi.yaml post /api/antidetect-browser-logs/search List antidetect-browser runtime log rows for the team with filters, sorting and cursor pagination. This is the operational trail behind a browser's status (start/stop, runtime errors, proxy issues, logouts). Returns a counts block of predicate tallies; include[] can eager-load the antidetect_browser. Scope to one browser with antidetect_browser_sid. Contract: - MCP tool `search_antidetect_browser_logs`, registry package `mcp.linkedin/antidetect_browser_logs`, mount `linkedin.browsers`. - Operation `search`, response envelope `search`. - Flags: read only. # Check proxy connectivity Source: https://docs.gtm-api.com/api-reference/linkedin/antidetect_browser_proxies/check-proxy-connectivity /api-reference/linkedin/openapi.yaml post /api/antidetect-browser-proxies/check-proxy-connectivity Probe connectivity + latency for one or many proxies. Target EXACTLY ONE of sid (ab_px_) or filter; filter mode checks up to an inline cap and reports total_matched (truncated:true when the cap was hit). Read-only probe, no row mutation. Contract: - MCP tool `check_antidetect_browser_proxy_connectivity`, registry package `mcp.linkedin/antidetect_browser_proxies`, mount `linkedin.browsers`. - Operation `action`, response envelope `action`. - Flags: mass action: this verb's own request accepts a filter or targets[] set, which the owning service drains. # Check proxy location Source: https://docs.gtm-api.com/api-reference/linkedin/antidetect_browser_proxies/check-proxy-location /api-reference/linkedin/openapi.yaml post /api/antidetect-browser-proxies/check-proxy-location Run a multi-vendor geo + fraud check on one or many proxies. Target EXACTLY ONE of sid (ab_px_) or filter; filter mode checks up to an inline cap and reports total_matched (truncated:true when the cap was hit). A verdict moves the row: an exit country that disagrees with country_code flips it to country_mismatch, a fraud score over the threshold to fraud_flagged (each emits its event). Rows the vendors could not settle are listed in inconclusive with a reason (no_ip_port / exit_unreachable / no_corroboration) and left untouched, so total_checked counts only rows that got a verdict. Contract: - MCP tool `check_antidetect_browser_proxy_location`, registry package `mcp.linkedin/antidetect_browser_proxies`, mount `linkedin.browsers`. - Operation `action`, response envelope `action`. - Flags: mass action: this verb's own request accepts a filter or targets[] set, which the owning service drains. # Search antidetect browser proxies Source: https://docs.gtm-api.com/api-reference/linkedin/antidetect_browser_proxies/search-antidetect-browser-proxies /api-reference/linkedin/openapi.yaml post /api/antidetect-browser-proxies/search List antidetect browser proxies (platform-wide pool) with filters, sorting and cursor pagination. Live proxies by default (filter {"deleted_at": {"is_null": true}}). Credentials (ip / port / username / password) are masked to null on this public read surface. Contract: - MCP tool `search_antidetect_browser_proxies`, registry package `mcp.linkedin/antidetect_browser_proxies`, mount `linkedin.browsers`. - Operation `search`, response envelope `search`. - Flags: read only. # Change antidetect browser proxy Source: https://docs.gtm-api.com/api-reference/linkedin/antidetect_browsers/change-antidetect-browser-proxy /api-reference/linkedin/openapi.yaml post /api/antidetect-browsers/update-proxy Change the proxy of an existing antidetect browser, country included. Supply EXACTLY ONE source: antidetect_browser_proxy_sid, proxy_country_code or custom_proxy_config. EVERY arm needs browser_owner=platform: a profile bound by id keeps its proxy in the vendor (422 managed_proxy_forbidden_for_owner / 422 custom_proxy_forbidden_for_owner). custom_proxy_config is probed first (422 custom_proxy_unreachable_* changes nothing), takes the exit country measured there, and RELEASES any armed 5G slot. Zero sources 422 proxy_assignment_missing, more than one 422 proxy_assignment_conflict, empty pool 422 proxy_pool_empty, 5G on a BYO proxy 422 proxy_5g_requires_managed_proxy. proxy_5g alone is a valid body: it moves the add-on binding only. DANGEROUS twice over: a location flip mid-campaign can trip a LinkedIn risk check, and a running browser is restarted, since a live session keeps the old proxy until it respawns. Read result.restarted. To rotate the IP in place, use replace_antidetect_browser_proxy. Contract: - MCP tool `update_antidetect_browser_proxy`, registry package `mcp.linkedin/antidetect_browsers`, mount `linkedin.browsers`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Check custom proxy Source: https://docs.gtm-api.com/api-reference/linkedin/antidetect_browsers/check-custom-proxy /api-reference/linkedin/openapi.yaml post /api/antidetect-browsers/check-proxy Probe a customer-supplied proxy tuple and report the exit it reaches: country, IP and round-trip latency. Nothing is written, and no browser is named: call it for a proxy you are about to pass as custom_proxy_config, on create or on update-proxy. A proxy that refuses is a SUCCESSFUL call with ok:false plus error_kind (timeout | tls | network | http_error); only a malformed tuple is a 422. The write paths run the same probe themselves, so this verb is for showing the operator what a tuple resolves to BEFORE committing to it. Contract: - MCP tool `check_antidetect_browser_proxy`, registry package `mcp.linkedin/antidetect_browsers`, mount `linkedin.browsers`. - Operation `action`, response envelope `action`. - Flags: read only. # Create antidetect browser Source: https://docs.gtm-api.com/api-reference/linkedin/antidetect_browsers/create-antidetect-browser /api-reference/linkedin/openapi.yaml post /api/antidetect-browsers Provision ONE antidetect browser for the team. The main flow mints a fresh vendor (GoLogin) profile and pushes the resolved proxy into it, so supply exactly one proxy source: proxy_country_code is the default, custom_proxy_config routes that minted profile through a proxy the caller supplies (probed first, 422 custom_proxy_unreachable_* when dead, never with proxy_5g). Pick the branch yourself, never ask an end user for a sid. Bind an existing vendor_profile_id for the BYO-PROFILE path instead, with no proxy source: that profile carries its own. If no LinkedIn account signs in on it for 30 days, it is deleted and its seat freed. DANGEROUS: creates real vendor + proxy infrastructure. For SEVERAL browsers under ONE approval, do not call this per browser: author a mass action on /mcp/orchestration/mass-actions with scope {kind:"generate", count:N} and a plan of antidetect-browsers.create (+ antidetect-browsers.generate-cloud-browser-access-key), then read the connect links off each row when it finishes. Contract: - MCP tool `create_antidetect_browser`, registry package `mcp.linkedin/antidetect_browsers`, mount `linkedin.browsers`. - Operation `create`, response envelope `create`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Delete antidetect browser Source: https://docs.gtm-api.com/api-reference/linkedin/antidetect_browsers/delete-antidetect-browser /api-reference/linkedin/openapi.yaml delete /api/antidetect-browsers/{sid} Decommission (soft-delete) a single antidetect browser by sid: outward, destructive, one-way through MCP. A browser in an active-run status is stopped FIRST and the node must confirm: the call can take up to ~20s waiting for that confirm, and if it does not arrive the delete is refused with 409 conflict (context.reason=browser_stop_unconfirmed) and NOTHING is deleted - retry in a minute rather than assuming success. Variant B cascade: the bound linkedin-account is soft-deleted and pending plugin tasks transition to failed; the cascade block reports the counts. A platform-minted GoLogin profile is reclaimed (deleted) at the vendor unless another live browser row still holds it; customer BYO vendor profiles are left intact. Contract: - MCP tool `delete_antidetect_browser`, registry package `mcp.linkedin/antidetect_browsers`, mount `linkedin.browsers`. - Operation `delete`, response envelope `delete_cascade`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Generate cloud-browser access key Source: https://docs.gtm-api.com/api-reference/linkedin/antidetect_browsers/generate-cloud-browser-access-key /api-reference/linkedin/openapi.yaml post /api/antidetect-browsers/generate-cloud-browser-access-key Mint a cloud-browser access key (smart-link) on a browser. Returns the whole minted entry in result.access_key, whose `key` field is the raw cb_ak_ token (shown once), AND result.public_connect_url, the ready-to-share link to hand to whoever opens it (no platform account needed on their side). Pass result.access_key.key, never result.access_key, to revoke_cloud_browser_access_key. `purpose` decides what the page behind the link does: `relogin` walks them through signing the LinkedIn session back in and re-binds the browser once they confirm, `share` just hands them the browser to drive. Optional ttl_hours / max_connects / allowed_ips / allowed_countries scope the key; send_to_email also mails the link to whoever holds the LinkedIn password (a fixed mail, no text to supply). DANGEROUS: both the key and the link are bearer secrets granting remote browser access. Contract: - MCP tool `generate_cloud_browser_access_key`, registry package `mcp.linkedin/antidetect_browsers`, mount `linkedin.browsers`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Get antidetect browser Source: https://docs.gtm-api.com/api-reference/linkedin/antidetect_browsers/get-antidetect-browser /api-reference/linkedin/openapi.yaml get /api/antidetect-browsers/{sid} Fetch a single antidetect browser by sid, with optional eager-loaded relations. Contract: - MCP tool `get_antidetect_browser`, registry package `mcp.linkedin/antidetect_browsers`, mount `linkedin.browsers`. - Operation `get`, response envelope `get`. - Flags: read only. # Get seat usage Source: https://docs.gtm-api.com/api-reference/linkedin/antidetect_browsers/get-seat-usage /api-reference/linkedin/openapi.yaml post /api/antidetect-browsers/seat-usage Seats and 5G proxy slots the workspace occupies: every live browser row, whatever its status and whether a LinkedIn account is bound. Team-wide; compare against limits.accounts and limits.proxy_5g. Contract: - MCP tool `get_antidetect_browser_seat_usage`, registry package `mcp.linkedin/antidetect_browsers`, mount `linkedin.browsers`. - Operation `action`, response envelope `action`. - Flags: read only. # List proxy countries Source: https://docs.gtm-api.com/api-reference/linkedin/antidetect_browsers/list-proxy-countries /api-reference/linkedin/openapi.yaml post /api/antidetect-browsers/proxy-countries Proxy countries a new browser can be given right now, with how many active proxies back each. Empty list = the pool is dry; create refuses a country with no stock (422 proxy_pool_empty). Contract: - MCP tool `list_antidetect_browser_proxy_countries`, registry package `mcp.linkedin/antidetect_browsers`, mount `linkedin.browsers`. - Operation `action`, response envelope `action`. - Flags: read only. # Revoke cloud-browser access key Source: https://docs.gtm-api.com/api-reference/linkedin/antidetect_browsers/revoke-cloud-browser-access-key /api-reference/linkedin/openapi.yaml post /api/antidetect-browsers/revoke-cloud-browser-access-key Revoke a cloud-browser access key on a browser. Pass the cb_ak_ key to revoke one; omit key to revoke EVERY key on the row. Only removes the key; live cloud-browser sessions are not force-disconnected. DANGEROUS: cuts off remote access. Contract: - MCP tool `revoke_cloud_browser_access_key`, registry package `mcp.linkedin/antidetect_browsers`, mount `linkedin.browsers`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Rotate antidetect browser proxy Source: https://docs.gtm-api.com/api-reference/linkedin/antidetect_browsers/rotate-antidetect-browser-proxy /api-reference/linkedin/openapi.yaml post /api/antidetect-browsers/replace-proxy Rotate an antidetect browser onto another MANAGED proxy of the SAME country. Geo-binding is preserved, so this is the safe way to drop a flagged IP. Omit antidetect_browser_proxy_sid to take the least-loaded active proxy of that country, or pin one (a pin from another country is refused, 422 proxy_country_mismatch). Platform-owned browsers only, 422 managed_proxy_forbidden_for_owner; a platform row that never got a proxy is 422 proxy_not_managed; a country with no other active proxy is 422 proxy_replacement_unavailable. A running browser is stopped and started again so the new IP takes effect; result.restarted reports whether it came back. Contract: - MCP tool `replace_antidetect_browser_proxy`, registry package `mcp.linkedin/antidetect_browsers`, mount `linkedin.browsers`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Run antidetect browser Source: https://docs.gtm-api.com/api-reference/linkedin/antidetect_browsers/run-antidetect-browser /api-reference/linkedin/openapi.yaml post /api/antidetect-browsers/run Start an antidetect browser session by sid. Two-phase: transitions to queued_to_start, dispatches to an automation server, then settles into initializing / running / start_issue. Identify the browser by its ab_br_ sid in the body. Contract: - MCP tool `run_antidetect_browser`, registry package `mcp.linkedin/antidetect_browsers`, mount `linkedin.browsers`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Search antidetect browsers Source: https://docs.gtm-api.com/api-reference/linkedin/antidetect_browsers/search-antidetect-browsers /api-reference/linkedin/openapi.yaml post /api/antidetect-browsers/search List antidetect browsers on the team with filters (name lookup via the vendor_name filter, eq/in), sorting and cursor pagination. Live browsers by default (filter {"deleted_at": {"is_null": true}}). Returns a counts block of predicate tallies; include[] can eager-load linkedin_account, antidetect_browser_proxy and antidetect_browser_logs. Use this to find a browser sid before run / stop / delete. Contract: - MCP tool `search_antidetect_browsers`, registry package `mcp.linkedin/antidetect_browsers`, mount `linkedin.browsers`. - Operation `search`, response envelope `search`. - Flags: read only. # Stop antidetect browser Source: https://docs.gtm-api.com/api-reference/linkedin/antidetect_browsers/stop-antidetect-browser /api-reference/linkedin/openapi.yaml post /api/antidetect-browsers/stop Stop an antidetect browser session by sid. Default is graceful (queued_to_stop → stopped on the node teardown confirmation); now:true commits stopped immediately and dispatches a best-effort teardown. Identify the browser by its ab_br_ sid in the body. Contract: - MCP tool `stop_antidetect_browser`, registry package `mcp.linkedin/antidetect_browsers`, mount `linkedin.browsers`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Cloud browser sessions metrics Source: https://docs.gtm-api.com/api-reference/linkedin/cloud_browser_sessions/cloud-browser-sessions-metrics /api-reference/linkedin/openapi.yaml post /api/cloud-browser-sessions/metrics Period-bound aggregates over a filtered cloud-browser-session set. Requires period {from,to}. Returns total_count, active_count, avg_session_duration_seconds, login_success_rate, unique_devices_count and unique_ips_count. Optional filter and a single group_by axis (access_type / login_result / ip_country / antidetect_browser_sid / …). Contract: - MCP tool `get_cloud_browser_sessions_metrics`, registry package `mcp.linkedin/cloud_browser_sessions`, mount `linkedin.browsers`. - Operation `metrics`, response envelope `metrics`. - Flags: read only. # Search cloud browser sessions Source: https://docs.gtm-api.com/api-reference/linkedin/cloud_browser_sessions/search-cloud-browser-sessions /api-reference/linkedin/openapi.yaml post /api/cloud-browser-sessions/search List cloud-browser sessions on the team with filters, sorting and cursor pagination. This is the audit trail of who connected to which browser, from where, and how each login attempt resolved. The smart-link access_key is redacted to cb_ak_ followed by asterisks and the key's last 4 characters, so a session can be attributed to one of the browser's access keys without exposing a redeemable token; it is null only when there was no key at all (an internal session). It is not a public filter. Contract: - MCP tool `search_cloud_browser_sessions`, registry package `mcp.linkedin/cloud_browser_sessions`, mount `linkedin.browsers`. - Operation `search`, response envelope `search`. - Flags: read only. # List cloud-browser team occupancy Source: https://docs.gtm-api.com/api-reference/linkedin/cloud_browsers/list-cloud-browser-team-occupancy /api-reference/linkedin/openapi.yaml post /api/cloud-browsers/list-team-occupancy List the cloud-browser slots the team currently occupies. Each slot carries the connected antidetect_browser sid, the connected user, and the connect / expiry timestamps. Read-only snapshot returned in result.items. Contract: - MCP tool `list_cloud_browser_team_occupancy`, registry package `mcp.linkedin/cloud_browsers`, mount `linkedin.browsers`. - Operation `action`, response envelope `action`. - Flags: read only. # Get data request Source: https://docs.gtm-api.com/api-reference/linkedin/data_requests/get-data-request /api-reference/linkedin/openapi.yaml get /api/data-requests/{sid} Fetch a single ledger row by sid to inspect one request referenced from a data-requests.* webhook or another row's cached_from_sid pointer. Contract: - MCP tool `get_data_request`, registry package `mcp.linkedin/data_requests`, mount `linkedin.data`. - Operation `get`, response envelope `get`. - Flags: read only. # Search data requests Source: https://docs.gtm-api.com/api-reference/linkedin/data_requests/search-data-requests /api-reference/linkedin/openapi.yaml post /api/data-requests/search List/filter the request history: what was asked, where it ran, what came from cache. The go-to tool for retry forensics (idempotency_key:{eq}), per-person request history (ln_member_id:{eq}) and mid-flight visibility (status:{in:["pending","running"]}). This is a read-only ledger. To RUN scraping/enrichment call the linkedin-scraping / linkedin-enrichment surfaces instead. Contract: - MCP tool `search_data_requests`, registry package `mcp.linkedin/data_requests`, mount `linkedin.data`. - Operation `search`, response envelope `search`. - Flags: read only. # LinkedIn account activity-log metrics Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_account_activity_log/linkedin-account-activity-log-metrics /api-reference/linkedin/openapi.yaml post /api/linkedin-account-activity-log/metrics Period-bound aggregates over a filtered activity-log set (avg plugin duration, avg pending age) plus the counts block and an optional group_by axis (action_type | status | linkedin_account_sid). Use for: "what was done today/this week grouped by action_type", at-a-glance fail rate (group_by status). Guardrail (high-write table): period {from,to} is REQUIRED and filter must include linkedin_account_sid.eq/.in. Unbounded scans are rejected; window may not exceed 90 days. The filter here is NARROWER than search: linkedin_account_sid and status are the only axes the aggregation applies. Slice any other dimension with group_by, or use search. Contract: - MCP tool `get_linkedin_account_activity_log_metrics`, registry package `mcp.linkedin/linkedin_account_activity_log`, mount `linkedin.account-monitor`. - Operation `metrics`, response envelope `metrics`. - Flags: read only. # Search LinkedIn account activity log Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_account_activity_log/search-linkedin-account-activity-log /api-reference/linkedin/openapi.yaml post /api/linkedin-account-activity-log/search List activity-log rows, one per plugin call (Task↔Result cycle): pending on dispatch, then success / failed / skipped. Use for: "why is account X failing" (status:failed + created_at window, read error_message/error_code), inspecting in-flight pending tasks, cross-referencing a Grafana trace_id, and polling an MCP-triggered async action by sid until status flips terminal. NOT for LinkedIn quota hits (linkedin-account-quota-hits), the rolling limit count (linkedin-account-smart-limits), or account state (linkedin-accounts). No q. Sort: created_at (default desc) | duration_ms. include[]: linkedin_account, linkedin_account_smart_limit, sync_run. page_size: 0 is count mode: no rows, and the counts block over the filter (total_count, the per-status scalars, groups by action_type / status / linkedin_account_sid); a page fetch carries no counts and a null pagination.total_count. Contract: - MCP tool `search_linkedin_account_activity_log`, registry package `mcp.linkedin/linkedin_account_activity_log`, mount `linkedin.account-monitor`. - Operation `search`, response envelope `search`. - Flags: read only. # Search LinkedIn account block log Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_account_block_log/search-linkedin-account-block-log /api-reference/linkedin/openapi.yaml post /api/linkedin-account-block-log/search List per-target block observations for the team: one permanently-deduplicated row per (account, target) whenever a target profile blocked one of our accounts (established by the §5.6 check-target-block). Use for: "did target X block any of our accounts" (filter target_ln_member_id, no account filter → crosses the team), churn caused by blocks (include linkedin_connection), a hostile-segment trend (created_at window + total_count), reverse-lookup from a failed activity_log row or a soft-deleted connection. NOT for platform-wide locks (weekly cap / InMail rate-limit → linkedin-account-quota-hits) or raw plugin error text (linkedin-account-activity-log). No q; target_nickname is exact-match. Sort: created_at (default desc). include[]: linkedin_account, linkedin_account_activity_log, linkedin_connection. Contract: - MCP tool `search_linkedin_account_block_log`, registry package `mcp.linkedin/linkedin_account_block_log`, mount `linkedin.account-monitor`. - Operation `search`, response envelope `search`. - Flags: read only. # Search LinkedIn account quota hits Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_account_quota_hits/search-linkedin-account-quota-hits /api-reference/linkedin/openapi.yaml post /api/linkedin-account-quota-hits/search List LinkedIn-side block events (weekly connection-request cap, InMail rate-limit, a 429 on any action bucket, …) for the team: immutable, append-only, one row per (account, reason, bucket) per 12h dedup window. Use for: "is this account currently locked" (linkedin_quota_hit_till: { gt: "" }), block-frequency trend, and "at what action volumes do blocks happen" (read the daily_count distribution). include[] linkedin_account / linkedin_account_snapshot correlate the block with account state at hit time. NOT for setting/clearing a lock (that lives on linkedin-account-smart-limits.linkedin_quota_hit_till) or per-target blocks (linkedin-account-block-log). No q. Sort: created_at (default desc) | linkedin_quota_hit_till. Contract: - MCP tool `search_linkedin_account_quota_hits`, registry package `mcp.linkedin/linkedin_account_quota_hits`, mount `linkedin.account-monitor`. - Operation `search`, response envelope `search`. - Flags: read only. # Reset LinkedIn account smart-limit hold Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_account_smart_limits/reset-linkedin-account-smart-limit-hold /api-reference/linkedin/openapi.yaml post /api/linkedin-account-smart-limits/{sid}/reset-hold Clear OUR platform-side pause (hold_till = null) on one row. On a spent budget (done_today_count >= daily_limit) the saturation latch re-holds at once, so this is a no-op that stays held: the cap has to grow first. On a HAND-MANAGED account (smart_limits_enabled false) prefer update({daily_limit:, reset_hold:true}), which raises the cap and resumes atomically. On a GOVERNED account (the default) daily_limit is the warmup's and cannot be raised by hand: target_limit only bounds the cap, the warmup ceiling grows with account age and sent activity across snapshots, and the ramp lifts the latch by itself once its recomputed cap outgrows done_today_count, so nothing resumes today. This action resumes WITHOUT changing the limit (meaningful only when done_today_count < daily_limit, e.g. a systemic hold). Idempotent. Does NOT touch done_today_count or linkedin_quota_hit_till: a LinkedIn-side hard lock cannot be reset from the public surface (result.new_status stays linkedin_blocked while that clock runs). Contract: - MCP tool `reset_linkedin_account_smart_limit_hold`, registry package `mcp.linkedin/linkedin_account_smart_limits`, mount `linkedin.accounts`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Search LinkedIn account smart limits Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_account_smart_limits/search-linkedin-account-smart-limits /api-reference/linkedin/openapi.yaml post /api/linkedin-account-smart-limits/search List per-limit_type policy rows (one account owns 15 public buckets: send_connection_requests, send_messages, send_inmails, scraping, enrichment, custom_request, …) with filters, sorting, cursor pagination and a counts block (status / limit_type breakdowns). Whether the warmup governs a row is the ACCOUNT's smart_limits_enabled, not a row field. Use for: "which limits are at their daily cap / held / blocked by LinkedIn" (status filter; held means the daily budget is spent), "can account X still send InMails today" (filter linkedin_account_sid + limit_type, read status + daily_limit − done_today_count), fleet capacity, smart-limit adoption rate. Every row also carries recommended_daily_limit / recommended_delay_in_seconds (what the platform would run this bucket at for THIS account) and risk_level (none | elevated | ban_likely) for the values currently set. Single-row case = search(filter:{linkedin_account_sid, limit_type}, page_size:1). No q. include[]: linkedin_account, linkedin_account_quota_hits. Contract: - MCP tool `search_linkedin_account_smart_limits`, registry package `mcp.linkedin/linkedin_account_smart_limits`, mount `linkedin.accounts`. - Operation `search`, response envelope `search`. - Flags: read only. # Update LinkedIn account smart limit Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_account_smart_limits/update-linkedin-account-smart-limit /api-reference/linkedin/openapi.yaml patch /api/linkedin-account-smart-limits/{sid} GATE: on a governed account (smart_limits_enabled true, the default) daily_limit / delay_in_seconds belong to the warmup; moving either is 409 smart_limits_governed: never promise a raised cap there. target_limit only BOUNDS (the row follows min(smart_limit, target_limit)): a target above the warmup ceiling changes nothing today; a young or dormant account sits at 1..2 for weeks (see warmup_breakdown). The other way: set_linkedin_account_smart_limits({enabled:false}), said out loud as removing ban protection. One row (daily_limit / target_limit / delay_in_seconds / learning_enabled; one field OR reset_hold:true). Switch off: nothing refused; read the row's recommended_* first, 2x (or half the delay) is "elevated", beyond that "ban_likely", warn so. A spent budget is "held": raising daily_limit alone does not clear it, pass reset_hold:true too; it resumes once the cap exceeds done_today_count (governed rows re-hold at once). System fields are rejected; a LinkedIn lock is not resettable. Contract: - MCP tool `update_linkedin_account_smart_limit`, registry package `mcp.linkedin/linkedin_account_smart_limits`, mount `linkedin.accounts`. - Operation `update`, response envelope `update`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Search LinkedIn account snapshots Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_account_snapshots/search-linkedin-account-snapshots /api-reference/linkedin/openapi.yaml post /api/linkedin-account-snapshots/search Per-account warmup snapshots over time (immutable, append-only, captured ~every 22h by the scheduler). Use for: a warmup trend over N days (filter linkedin_account_sid + created_at range), before/after compare (paused automation, rotated proxy/nickname), and reading warmup_breakdown + tips to explain WHY a warmup_score is what it is (score = round(100 × ∏(1 − penalty_pct/100)) over 6 compounding drivers). For the LATEST snapshot per account use linkedin-accounts.search with include[]=linkedin_account_snapshot (1:1); searching here for the latest is more expensive. No q (no text-searchable fields). Sort: created_at (default desc) | warmup_score. include[]: linkedin_account. Contract: - MCP tool `search_linkedin_account_snapshots`, registry package `mcp.linkedin/linkedin_account_snapshots`, mount `linkedin.account-monitor`. - Operation `search`, response envelope `search`. - Flags: read only. # Get LinkedIn account sync run Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_account_sync_runs/get-linkedin-account-sync-run /api-reference/linkedin/openapi.yaml get /api/linkedin-account-sync-runs/{sid} Fetch one sync run by sid, with includes (linkedin_account; activity_log_pages for the per-page fetch log; limit for the gating smart-limit bucket, always resolved). Use to drill into a single run's progress, resume-cursor state, and per-page audit. Contract: - MCP tool `get_linkedin_account_sync_run`, registry package `mcp.linkedin/linkedin_account_sync_runs`, mount `linkedin.account-monitor`. - Operation `get`, response envelope `get`. - Flags: read only. # LinkedIn account sync-run metrics Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_account_sync_runs/linkedin-account-sync-run-metrics /api-reference/linkedin/openapi.yaml post /api/linkedin-account-sync-runs/metrics Aggregate sync-run counters for dashboards: a counts block (total + per-status: pending, in_progress, completed, cancelled) plus avg_completed_duration_seconds, with an optional group_by axis (status | sync_type | triggered_by) that repeats the same block per group. Required period {from,to} bounds the window. Contract: - MCP tool `get_linkedin_account_sync_runs_metrics`, registry package `mcp.linkedin/linkedin_account_sync_runs`, mount `linkedin.account-monitor`. - Operation `metrics`, response envelope `metrics`. - Flags: read only. # Search LinkedIn account sync runs Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_account_sync_runs/search-linkedin-account-sync-runs /api-reference/linkedin/openapi.yaml post /api/linkedin-account-sync-runs/search List sync runs: the backend-owned progress tracker for one paginated sync of one data surface (connections, connection_requests, connection_invitations, conversations, sales_navigator_conversations, followers) or a §5.7 composition (snapshot, premium_check). Use for: "is account X fully synced / how far along" (filter linkedin_account_sid + status in [pending, in_progress]), "which accounts are stuck mid-sync" (status in_progress, sort last_progress_at), "how fresh is the connection data" (latest completed connections run → finished_at). Runs are not created here; the write surface is linkedin-accounts.reset-sync / .sync. No q. Sort: created_at (default desc) | started_at | finished_at | last_progress_at | next_attempt_at | items_saved. include[]: linkedin_account, activity_log_pages, limit (default). Contract: - MCP tool `search_linkedin_account_sync_runs`, registry package `mcp.linkedin/linkedin_account_sync_runs`, mount `linkedin.account-monitor`. - Operation `search`, response envelope `search`. - Flags: read only. # Check premium subscription Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_accounts/check-premium-subscription /api-reference/linkedin/openapi.yaml post /api/linkedin-accounts/{sid}/check-premium Refresh the account's subscription state and persist it: Premium, the Sales Navigator and Recruiter seats, and the InMail balances (self-probe, no target, no credits). `checks` picks which of those four to run and omitting it runs all four, which is the call you want when you just need the picture. Ask for a subset when you need one answer cheaply: `recruiter` reads a marker in the already-open tab, while the full run opens a background tab for the profile. Each result is written to the account record, and a probe that fails leaves the previous value alone rather than clearing it, so a failure never reads as "seat lost". On the full run a false Premium settles both seats without probing them, because each includes Premium. A Recruiter seat whose member holds several contracts is bound to the only corporate contract on offer by this check itself; with several on offer the seat stays unstamped until the seat holder picks one in the account's browser. Contract: - MCP tool `check_linkedin_account_premium_subscription`, registry package `mcp.linkedin/linkedin_accounts`, mount `linkedin.accounts`. - Operation `action`, response envelope `action`. - Flags: read only; paced: a call spends the `self_account_sync` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Check target reachability Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_accounts/check-target-reachability /api-reference/linkedin/openapi.yaml post /api/linkedin-accounts/{sid}/check-target-block Check whether a target person can be reached from this account (not blocked / out of network). Probes from the account itself; when the platform cannot obtain an independent second viewpoint the outcome is `inconclusive` rather than a guess. Contract: - MCP tool `check_linkedin_account_target_block`, registry package `mcp.linkedin/linkedin_accounts`, mount `linkedin.accounts`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `self_account_sync` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Edit my profile Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_accounts/edit-my-profile /api-reference/linkedin/openapi.yaml patch /api/linkedin-accounts/{sid}/my-profile Edit the connected account's OWN LinkedIn profile in one call: the intro card (name, headline, additional name, industry, location, the current position/education pins and their visibility, website, pronouns), the About section (`about`, `top_skills`), the profile photo (`photo`) and the background image (`background`). Send only what changes: the intro card is read first and submitted whole (the LinkedIn wire replaces it), the About save is partial (an omitted field keeps its value, about: '' clears the text, top_skills: [] removes them all), each image replaces one picture. Every section is its own LinkedIn save, run in that order and reported under result.sections; a refused section stops the call and the error names failed_section and applied_sections. One call spends one edit_profile slot however many sections it saves. Positions (work experience) have their own tool, set_linkedin_account_my_position. Contract: - MCP tool `edit_linkedin_account_my_profile`, registry package `mcp.linkedin/linkedin_accounts`, mount `linkedin.accounts`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `edit_profile` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Endorse skill by id Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_accounts/endorse-skill-by-id /api-reference/linkedin/openapi.yaml post /api/linkedin-accounts/{sid}/endorse-skill-by-id Endorse ONE explicit skill on a 1st-degree connection as this account (wire endorse-skill, ?action=endorse). Unlike endorse_linkedin_account_skill there is no discovery read and no auto-pick: you supply skill_id, exactly one dispatch. Skill ids exist only on skills that already carry at least one endorsement (a zero-endorsement skill is not addressable), so take them from the enrichment person-skills read. Requires a 1st-degree connection (409 not_connected). Spends the endorse_skills bucket (30/day, paced; the undo waits only 2 s, so an endorse and its undo fit back to back). Returns the echoed skill urn, endorsed_by_viewer (true after endorse) and the global endorsement_count, plus the activity-log row. Re-endorsing is a LinkedIn-side no-op; undo with unendorse_linkedin_account_skill. Contract: - MCP tool `endorse_linkedin_account_skill_by_id`, registry package `mcp.linkedin/linkedin_accounts`, mount `linkedin.content`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `endorse_skills` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Endorse skills Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_accounts/endorse-skills /api-reference/linkedin/openapi.yaml post /api/linkedin-accounts/{sid}/endorse-skill Endorse a target person's skills from this account (warm-up / engagement action). Outward action on LinkedIn. Contract: - MCP tool `endorse_linkedin_account_skill`, registry package `mcp.linkedin/linkedin_accounts`, mount `linkedin.accounts`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `endorse_skills` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Follow LinkedIn member Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_accounts/follow-linkedin-member /api-reference/linkedin/openapi.yaml post /api/linkedin-followings/follow Follow a LinkedIn member as this account, so their posts reach its feed. One-way and needs no relationship: you can follow anyone whose profile allows it, connected or not, which makes it a common warmup step before a connection request. IDEMPOTENT: following someone this account already follows returns 200 with the same terminal state, so a repeat is a success, not an error. The wire cannot tell a fresh follow from a repeat, so read get_linkedin_account_my_following first if you need that answer; either way the call spends one networking_general unit (40 a day, 180 s apart). Pass target.nickname whenever you know it, even alongside ln_id: it saves LinkedIn a member lookup and cuts the action from three wire requests to two. Identity-bound: linkedin_account_sid REQUIRED, 429 on saturation. Writes nothing locally; the follow list catches up on the next get_linkedin_account_my_following refresh. Contract: - MCP tool `follow_linkedin_member`, registry package `mcp.linkedin/linkedin_accounts`, mount `linkedin.accounts`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; step eligible: gtm.service.orchestration can run this verb as a mass-action plan step, once per item; paced: a call spends the `networking_general` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Get LinkedIn account Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_accounts/get-linkedin-account /api-reference/linkedin/openapi.yaml get /api/linkedin-accounts/{sid} Fetch a single LinkedIn account by sid, with optional eager-loaded relations. For login or health state add include antidetect_browser: browser status login_issue means the sender is signed out of LinkedIn, while the item status field is platform lifecycle, not login state. Contract: - MCP tool `get_linkedin_account`, registry package `mcp.linkedin/linkedin_accounts`, mount `linkedin.accounts`. - Operation `get`, response envelope `get`. - Flags: read only. # Get my analytics Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_accounts/get-my-analytics /api-reference/linkedin/openapi.yaml post /api/linkedin-accounts/{sid}/get-my-analytics Read the connected account's creator content analytics over a date window: per metric (impressions, engagements) the daily series, the period total, and the percentage change against the prior period. Omit both dates to get the trailing 28 days, which is what the LinkedIn dashboard itself opens on. Supplying one date without the other is refused rather than half-defaulted, and a window running into the future is refused too (LinkedIn has no data there and would answer with zeroes that read like a collapse in reach). Identity-bound: this is the account's own dashboard, not a competitor read. The metrics map is open: new cards LinkedIn adds show up as extra keys. The 'Discovery' card (in-network vs out-of-network reach) is a separate component and is NOT included. Contract: - MCP tool `get_linkedin_account_my_analytics`, registry package `mcp.linkedin/linkedin_accounts`, mount `linkedin.accounts`. - Operation `action`, response envelope `action`. - Flags: read only; paced: a call spends the `self_account_sync` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Get my catch-up cards Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_accounts/get-my-catch-up-cards /api-reference/linkedin/openapi.yaml post /api/linkedin-accounts/{sid}/get-my-catch-up The nurture cards on this account's My Network catch-up tab (linkedin.com/mynetwork/catch-up/all): birthdays, job changes and work anniversaries among its connections, each with the one-click message LinkedIn printed on the card's button. The "who is worth congratulating today" feed. READ ONLY: it lists prompts and sends nothing. message_sent means this account already answered that card, and null means LinkedIn said nothing, not no. NO page_size, deliberately: LinkedIn's own request carries no count knob, so the page is server-fixed at 10. Page with `cursor` instead, passing paging.next_page_cursor back until it is absent; an empty page is the end. Key a card by card_urn, which identifies the PROMPT rather than the person, since `position` is our own count and shifts whenever a card fails to parse. NOT profile views (get_linkedin_account_my_profile_views) and NOT Sales Navigator alerts (get_linkedin_account_my_sales_nav_notifications). Contract: - MCP tool `get_linkedin_account_my_catch_up`, registry package `mcp.linkedin/linkedin_accounts`, mount `linkedin.accounts`. - Operation `action`, response envelope `action`. - Flags: read only; paced: a call spends the `self_account_sync` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Get my credits Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_accounts/get-my-credits /api-reference/linkedin/openapi.yaml post /api/linkedin-accounts/{sid}/get-my-credits Return the Sales Navigator InMail credit balance (the LSS_INMAIL grant) for the connected account. Requires a Sales Navigator seat: the balance is read from the Sales Navigator credits API, so an account without a seat is refused with 422 `sales_nav_required` instead of being dispatched into a guaranteed LinkedIn 403. Contract: - MCP tool `get_linkedin_account_my_credits`, registry package `mcp.linkedin/linkedin_accounts`, mount `linkedin.accounts`. - Operation `action`, response envelope `action`. - Flags: read only; paced: a call spends the `self_account_sync` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Get my following Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_accounts/get-my-following /api-reference/linkedin/openapi.yaml post /api/linkedin-accounts/{sid}/get-my-following List who the connected account follows, newest-first: the follow-edge twin of get-my-followers (§5.8 latest-*). A null cursor refreshes then returns the head page; a non-null cursor is a read-only continuation over the already-refreshed head (no limit spend). Cursor-paginated. Contract: - MCP tool `get_linkedin_account_my_following`, registry package `mcp.linkedin/linkedin_accounts`, mount `linkedin.accounts`. - Operation `action`, response envelope `action`. - Flags: read only; paced: a call spends the `self_account_sync` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Get my full profile Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_accounts/get-my-full-profile /api-reference/linkedin/openapi.yaml post /api/linkedin-accounts/{sid}/get-my-full-profile Fetch the full self-profile of the connected LinkedIn account. Contract: - MCP tool `get_linkedin_account_my_full_profile`, registry package `mcp.linkedin/linkedin_accounts`, mount `linkedin.accounts`. - Operation `action`, response envelope `action`. - Flags: read only; paced: a call spends the `self_account_sync` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Get my lite profile Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_accounts/get-my-lite-profile /api-reference/linkedin/openapi.yaml post /api/linkedin-accounts/{sid}/get-my-lite-profile Fetch the lite self-profile (name, headline, avatar) of the connected account. Contract: - MCP tool `get_linkedin_account_my_lite_profile`, registry package `mcp.linkedin/linkedin_accounts`, mount `linkedin.accounts`. - Operation `action`, response envelope `action`. - Flags: read only; paced: a call spends the `self_account_sync` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Get my profile views Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_accounts/get-my-profile-views /api-reference/linkedin/openapi.yaml post /api/linkedin-accounts/{sid}/get-my-profile-views Who viewed this account's profile over the LAST 90 DAYS, newest first: the list behind linkedin.com/me/profile-views. Window and sort are fixed by LinkedIn and there is no filter, so "who viewed me today" means reading the top of the feed. Rows carry rendered text only ("Viewed 1w ago"), never a parseable date. An ANONYMIZED viewer arrives with is_anonymous true, a descriptor where the name goes, a people-search url instead of a profile, and no member ids, so branch on that flag before opening or keying a row. Served by the Premium viewer screen, so what a non-premium account gets back is unverified. Cursor-paginated, no total: pass paging.next_page_cursor back as `cursor` until it is absent. A short page is usually the end but can also be one card we could not read, so do not call the list complete. NOT the catch-up prompts (get_linkedin_account_my_catch_up) and NOT Sales Navigator alerts (get_linkedin_account_my_sales_nav_notifications). Contract: - MCP tool `get_linkedin_account_my_profile_views`, registry package `mcp.linkedin/linkedin_accounts`, mount `linkedin.accounts`. - Operation `action`, response envelope `action`. - Flags: read only; paced: a call spends the `self_account_sync` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Get my Recruiter contracts Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_accounts/get-my-recruiter-contracts /api-reference/linkedin/openapi.yaml post /api/linkedin-accounts/{sid}/get-my-recruiter-contracts The Recruiter contracts this account's member can act under (LinkedIn's contract chooser, wire get-recruiter-contract-options), with current_contract_id = the contract the account's seat is stamped under. NEEDS A RECRUITER SEAT on the account (422 recruiter_required otherwise). Read it when recruiter_status is set but recruiter_seat_id is null: a member on several contracts holds no seat until the browser is bound to one. The premium check does not bind one by itself: from the platform's browser this list answers LinkedIn 401 on every account so far, a live session included, and the refusal comes back verbatim as result.status failed without touching recruiter_status. The working way to a bound seat is the seat holder's Sign in to Recruiter: the cloud browser opens on Recruiter, LinkedIn shows its own contract chooser, and the confirmed start re-reads the seat. CORPORATE rows are Recruiter seats; the INDIVIDUAL row is a Job Posting contract on the personal account. Not gated on the Recruiter session clock. Contract: - MCP tool `get_linkedin_account_my_recruiter_contracts`, registry package `mcp.linkedin/linkedin_accounts`, mount `linkedin.recruiter`. - Operation `action`, response envelope `action`. - Flags: read only; paced: a call spends the `self_account_sync` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Get my Recruiter hiring projects Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_accounts/get-my-recruiter-hiring-projects /api-reference/linkedin/openapi.yaml post /api/linkedin-accounts/{sid}/get-my-hiring-projects One page of this account's own LinkedIn Recruiter hiring projects, most recently accessed first, optionally filtered by a name keyword. NEEDS A RECRUITER SEAT (422 recruiter_required otherwise). The one recruiter list with an exact paging.total. Omit page_size to get the wire default of 10; pass paging.next_page_cursor back as `cursor` for the next page. Read-only, persists nothing, one self_account_sync unit. Contract: - MCP tool `get_linkedin_account_my_hiring_projects`, registry package `mcp.linkedin/linkedin_accounts`, mount `linkedin.recruiter`. - Operation `action`, response envelope `action`. - Flags: read only; paced: a call spends the `self_account_sync` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Get my Recruiter seat Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_accounts/get-my-recruiter-seat /api-reference/linkedin/openapi.yaml post /api/linkedin-accounts/{sid}/get-my-recruiter-seat This account's own LinkedIn Recruiter seat, contract, profile and entitlements (the talentMe read). NEEDS A RECRUITER SEAT on the account (422 recruiter_required otherwise). Every field can be null: the wire may omit any of them. owner_seat_id is the seat number the recruiter sync and thread tools dispatch with; the premium check stamps it onto the account itself (recruiter_seat_id), this read persists nothing. Call it to check can_send_paid_inmail and credits before send_linkedin_recruiter_message, or to see using_multiple_contracts. Spends one self_account_sync unit. Contract: - MCP tool `get_linkedin_account_my_recruiter_seat`, registry package `mcp.linkedin/linkedin_accounts`, mount `linkedin.recruiter`. - Operation `action`, response envelope `action`. - Flags: read only; paced: a call spends the `self_account_sync` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Get my Sales Navigator alerts Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_accounts/get-my-sales-navigator-alerts /api-reference/linkedin/openapi.yaml post /api/linkedin-accounts/{sid}/get-my-sales-nav-notifications This account's Sales Navigator alert feed, the bell on linkedin.com/sales/home: saved leads and accounts posting, changing job, accepting an invitation, or turning up in a profile-view signal. NEEDS A SALES NAVIGATOR SEAT on the account; without one we refuse up front rather than spend a run on a likely refusal. Ordered by LinkedIn's own RELEVANCE score, NOT reverse-chronological, so sort by published_at yourself before calling anything new. That field is epoch milliseconds and is the only machine-readable timestamp across the three self feeds. metadata.num_unseen counts unseen alerts feed-wide, not on this page, and is not a row total. Cursor-paginated, no total: pass paging.next_page_cursor back as `cursor` until it is absent. Alerts sent without an id are dropped, so a page can be shorter than page_size while the feed continues; never read a short page as the end here. NOT profile views (get_linkedin_account_my_profile_views) and NOT the catch-up prompts (get_linkedin_account_my_catch_up). Contract: - MCP tool `get_linkedin_account_my_sales_nav_notifications`, registry package `mcp.linkedin/linkedin_accounts`, mount `linkedin.accounts`. - Operation `action`, response envelope `action`. - Flags: read only; paced: a call spends the `self_account_sync` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Get my services page Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_accounts/get-my-services-page /api-reference/linkedin/openapi.yaml post /api/linkedin-accounts/{sid}/get-my-services Read the connected account's OWN LinkedIn services page ("Providing services") as its edit form: status (ACTIVE or UNPUBLISHED), description, the offered services with their standardized-skill ids, the location and messaging flags, review visibility, pricing, the currencies LinkedIn offers and the 16 top-level service categories (valid service ids, though sub-skills exist too: lookup_linkedin_param_id with type service_category finds them by name). result.page is null when the account has no page. The public page of any member is enrich_linkedin_person_services; this is the owner's view, read live. Contract: - MCP tool `get_linkedin_account_my_services`, registry package `mcp.linkedin/linkedin_accounts`, mount `linkedin.accounts`. - Operation `action`, response envelope `action`. - Flags: read only; paced: a call spends the `self_account_sync` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Get my sessions Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_accounts/get-my-sessions /api-reference/linkedin/openapi.yaml post /api/linkedin-accounts/{sid}/get-my-sessions List the active LinkedIn sessions for the connected account. Contract: - MCP tool `get_linkedin_account_my_sessions`, registry package `mcp.linkedin/linkedin_accounts`, mount `linkedin.accounts`. - Operation `action`, response envelope `action`. - Flags: read only; paced: a call spends the `self_account_sync` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Get my SSI Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_accounts/get-my-ssi /api-reference/linkedin/openapi.yaml post /api/linkedin-accounts/{sid}/get-my-ssi Read the connected account's Social Selling Index: the overall score, the four pillars it is the sum of (professional brand, finding the right people, engaging with insights, building relationships), and where the account stands inside its industry and its own network. Answers WITHOUT a Sales Navigator seat, reporting that as active_seat: false with the scores still filled in, so it is a health signal for any account rather than a premium-only read. Identity-bound: the sid names the account whose own dashboard is read; there is no way to read someone else's SSI. Every score is FRACTIONAL (a real reading is 53.496, not 53), so do not round before comparing runs, the week-over-week movement is usually under a point. Contract: - MCP tool `get_linkedin_account_my_ssi`, registry package `mcp.linkedin/linkedin_accounts`, mount `linkedin.accounts`. - Operation `action`, response envelope `action`. - Flags: read only; paced: a call spends the `self_account_sync` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Invite members to event Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_accounts/invite-members-to-event /api-reference/linkedin/openapi.yaml post /api/linkedin-accounts/{sid}/invite-to-event Invite up to 25 members to a LinkedIn event, in ONE call: one invitation batch from the account's open LinkedIn tab, one browser round and one networking_general unit (40 a day, 180 s apart; 429 on saturation). event_ln_id is the digits of the event urn, the event_ln_id of a search_linkedin_events row (the number in /events//). A target is a member id (ln_id, preferred) or a nickname; a nickname alone costs one profile read first, and a Sales Navigator id is not an address here. LinkedIn decides who may invite and who may be invited: a member inviting 1st-degree connections to a public event it does not organize went through live, as LinkedIn's own Invite button allows. The answer is one flag for the batch; a false is 409 event_invite_rejected with the ids sent, without saying which member was refused or whether the others landed. A member already invited counts as success, one who already accepted is skipped. Writes nothing locally. Contract: - MCP tool `invite_linkedin_members_to_event`, registry package `mcp.linkedin/linkedin_accounts`, mount `linkedin.network`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `networking_general` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Reset account sync Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_accounts/reset-account-sync /api-reference/linkedin/openapi.yaml post /api/linkedin-accounts/{sid}/reset-sync Reset one or more sync tracks for the account and re-run them from scratch. ASYNC: returns pending refs to poll; DESTRUCTIVE: discards sync cursors. Contract: - MCP tool `reset_linkedin_account_sync`, registry package `mcp.linkedin/linkedin_accounts`, mount `linkedin.accounts`. - Operation `action`, response envelope `action_async`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Search LinkedIn accounts Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_accounts/search-linkedin-accounts /api-reference/linkedin/openapi.yaml post /api/linkedin-accounts/search List LinkedIn accounts (senders) on the team with operator filters, sort and cursor pagination, plus an always-present counts block (total, premium / SN / Recruiter, active_today, stale, signed_out). Also the way to find an account sid before calling account-scoped tools. For ANY health or availability question, request include antidetect_browser: login state lives on the bound browser row, whose status login_issue means the sender is signed out of LinkedIn. The item status field is platform lifecycle and stays active through a logout, so never read it as health. counts.signed_out_count flags signed-out senders even without the include; per-row attribution needs it. A full sender health summary combines antidetect_browser with linkedin_account_smart_limits, linkedin_account_snapshot (warmup score) and linkedin_account_block_log_counts. Items with includes are large: page a fleet with page_size 10 or less and follow next_cursor instead of asking for one big page. Contract: - MCP tool `search_linkedin_accounts`, registry package `mcp.linkedin/linkedin_accounts`, mount `linkedin.accounts`. - Operation `search`, response envelope `search`. - Flags: read only. # Select my Recruiter contract Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_accounts/select-my-recruiter-contract /api-reference/linkedin/openapi.yaml post /api/linkedin-accounts/{sid}/select-recruiter-contract Bind the account's browser to ONE of the member's Recruiter contracts (the contract chooser's Select, wire select-recruiter-contract) and stamp the seat that comes with it. contract_id is the ts_contract NUMBER from get_linkedin_account_my_recruiter_contracts, never a urn. Pick a CORPORATE row: the wrong row binds the browser to the wrong inbox, and a member on several contracts is exactly who this is for; the premium check never binds one by itself. A seat in the answer marks recruiter_status active. 422 contract_not_offered lists the rows on offer in context.offered; 422 contract_not_selectable means the row lacks an urn the Select needs. On success recruiter_seat_id and recruiter_contract_id are stamped from LinkedIn's answer; when it carried no seat the browser is still bound and the next premium check with checks: ['recruiter'] stamps the pair. Switching contracts switches the Recruiter inbox the sync reads: a reset of recruiter_conversations after a switch is the operator's call. Contract: - MCP tool `select_linkedin_account_recruiter_contract`, registry package `mcp.linkedin/linkedin_accounts`, mount `linkedin.recruiter`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `self_account_sync` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Set account label Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_accounts/set-account-label /api-reference/linkedin/openapi.yaml patch /api/linkedin-accounts/{sid} Set the private operator tag on one sender. `label` is team-authored free text (pod, campaign, owner) stored on the account row and visible only inside the owning workspace: LinkedIn never sees it and no recipient sees it. It is not the sender's name, which is LinkedIn's own (full_name) and is not editable by any tool. Send label to replace the tag, null to clear it; the key is required, so an empty call is a schema error. Filter on it with search_linkedin_accounts filter.label. Contract: - MCP tool `set_linkedin_account_label`, registry package `mcp.linkedin/linkedin_accounts`, mount `linkedin.accounts`. - Operation `update`, response envelope `update`. - Flags: none. # Set account smart limits Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_accounts/set-account-smart-limits /api-reference/linkedin/openapi.yaml post /api/linkedin-accounts/{sid}/set-smart-limits Switch smart-limit governance on or off for the WHOLE account, every bucket at once (no per-bucket toggle). The switch remembers nothing: a real flip in EITHER direction first resets every limit row to the platform defaults (daily_limit = the bucket's platform max, delay_in_seconds = the flat default, target_limit = the platform default, hold cleared). ON: the warmup then sets every row's daily_limit / delay_in_seconds (re-derived from the latest snapshot) and update_linkedin_account_smart_limit refuses those fields with 409 smart_limits_governed; target_limit and learning_enabled stay yours. The seeded target_limit is a target the warmup climbs toward, never a limit somebody set. OFF: every cap runs exactly as typed and the account loses the warmup ban protection: say so first, read the rows' recommended_* before typing caps. Idempotent. SINGLE account: a fleet is a mass action with step linkedin-accounts.set-smart-limits (scope objects, args {enabled}), run BEFORE a limit-row run on the same senders. Contract: - MCP tool `set_linkedin_account_smart_limits`, registry package `mcp.linkedin/linkedin_accounts`, mount `linkedin.accounts`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; step eligible: gtm.service.orchestration can run this verb as a mass-action plan step, once per item. # Set my position Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_accounts/set-my-position /api-reference/linkedin/openapi.yaml post /api/linkedin-accounts/{sid}/my-position Add, edit or remove ONE position (work experience entry) on the connected account's OWN LinkedIn profile. No position_id = add (title, company_name and start_date required, end_date unless is_current). position_id = edit: send only the fields that change, the backend reads the position's form first and submits it whole (the LinkedIn wire replaces the form); is_current: true drops the end date, an end_date ends a current position. position_id + delete: true = remove it (the backend reads the form and mirrors it into the delete, as LinkedIn's own editor does). position_id is the numeric id each entry carries on the account's own experience list (enrich_linkedin_person_experience on its own profile, or the add's answer). Company, geo and employment-type ids are LinkedIn's own numeric ids; without them the company is free text and the location display text only. Spends one edit_profile slot per call. Contract: - MCP tool `set_linkedin_account_my_position`, registry package `mcp.linkedin/linkedin_accounts`, mount `linkedin.accounts`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `edit_profile` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Set my services page Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_accounts/set-my-services-page /api-reference/linkedin/openapi.yaml post /api/linkedin-accounts/{sid}/my-services Create, update, republish or unpublish the connected account's OWN LinkedIn services page ("Providing services"): one tool, the page's state picks the LinkedIn action. No page yet: description and services (1..10 standardized-skill ids; get_linkedin_account_my_services lists the 16 top-level ones) are required and the page is created live with LinkedIn's onboarding defaults for what you omit. A page exists: send only what changes, the rest keeps its stored value; a page that was unpublished comes back on the profile with any save, and republish: true with no field does just that (LinkedIn has no publish action). unpublish: true takes the page down (page_slug optional; the platform resolves it while the page is still published). pricing is {kind: contact} or {kind: starting_at, currency, amount}; at least one of location_profile / location_remote must stay true. Spends one edit_profile slot. Contract: - MCP tool `set_linkedin_account_my_services`, registry package `mcp.linkedin/linkedin_accounts`, mount `linkedin.accounts`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `edit_profile` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Unendorse skill Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_accounts/unendorse-skill /api-reference/linkedin/openapi.yaml post /api/linkedin-accounts/{sid}/unendorse-skill Retract ONE skill endorsement this account holds on a 1st-degree connection (wire unendorse-skill, ?action=unendorse): the undo of endorse_linkedin_account_skill / endorse_linkedin_account_skill_by_id, addressed by the same skill_id. Spends the SAME endorse_skills bucket as the endorse it reverses (verb-pair rule; an undo is paced 2 s, not the bucket delay, so a do/undo pair fits back to back). Returns the echoed skill urn, endorsed_by_viewer (false after unendorse) and the global endorsement_count, plus the activity-log row. Retracting the LAST endorsement of a skill collapses its person-skills row to name only (no skill_id, no count). Retracting one this account never made is unverified on the wire. Contract: - MCP tool `unendorse_linkedin_account_skill`, registry package `mcp.linkedin/linkedin_accounts`, mount `linkedin.content`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `endorse_skills` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Unfollow LinkedIn member Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_accounts/unfollow-linkedin-member /api-reference/linkedin/openapi.yaml post /api/linkedin-followings/unfollow Stop following a LinkedIn member as this account, the inverse of follow_linkedin_member. NOT a disconnect: it leaves a 1st-degree connection intact and only stops that member's posts reaching the feed, so it is how you mute someone you still want to stay connected to. Severing the relationship is remove_linkedin_connection. IDEMPOTENT: unfollowing someone this account does not follow returns 200 with the same terminal state, so a repeat is a success, not an error, and it still spends one networking_general unit (40 a day, 180 s apart). Pass target.nickname whenever you know it, even alongside ln_id: it saves LinkedIn a member lookup and cuts the action from three wire requests to two. Identity-bound: linkedin_account_sid REQUIRED, 429 on saturation. Deletes nothing locally; the follow list catches up on the next get_linkedin_account_my_following refresh. Contract: - MCP tool `unfollow_linkedin_member`, registry package `mcp.linkedin/linkedin_accounts`, mount `linkedin.accounts`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `networking_general` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Update account sync config Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_accounts/update-account-sync-config /api-reference/linkedin/openapi.yaml post /api/linkedin-accounts/{sid}/update-sync-config Set the account's weekly sync schedule: the timezone, the allowed per-weekday time windows, and per-track cadence entries. Replaces the stored sync_config wholesale (send the full desired config, not a delta). Returns the updated account. SINGLE account: to reschedule a fleet, author a mass action on /mcp/orchestration/mass-actions with the step `linkedin-accounts.update-sync-config` (scope objects) so the whole change rides ONE approval. Contract: - MCP tool `update_linkedin_account_sync_config`, registry package `mcp.linkedin/linkedin_accounts`, mount `linkedin.accounts`. - Operation `action`, response envelope `action`. - Flags: step eligible: gtm.service.orchestration can run this verb as a mass-action plan step, once per item. # Visit profile Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_accounts/visit-profile /api-reference/linkedin/openapi.yaml post /api/linkedin-accounts/{sid}/visit-profile Visit a target person's profile from this account (warm-up / signal action). Outward action on LinkedIn. Contract: - MCP tool `visit_linkedin_account_profile`, registry package `mcp.linkedin/linkedin_accounts`, mount `linkedin.accounts`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `visit_profiles` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Search LinkedIn auto-scrape results Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_auto_scrape_results/search-linkedin-auto-scrape-results /api-reference/linkedin/openapi.yaml post /api/linkedin-auto-scrape-results/search Read the leads a collector has captured: one row per unique person or company, deduped across every run, each carrying flat identifiers plus the latest payload (name, headline, location, industry). Use for: the whole registry of a job (filter linkedin_auto_scrape_sid), the fresh delta of one run (first_seen_run_sid, the ingest path once a run completes), is this person already collected (parent sid plus ln_member_id or company_ln_id), and which leads keep re-appearing (seen_runs_count gte 3). page_size 0 returns counts.total_count, which IS the unique-lead count because dedup is built into the row. NOT for how a run went (search_linkedin_auto_scrape_runs), NOT for managing the job (search_linkedin_auto_scrapes), and NOT for outreach state: this registry records who was collected, never what was done to them. No q; include first_seen_run / last_seen_run for the run number behind the sid; nickname is exact match; dedup_key needs its parent partner. Sort: created_at (default desc) | updated_at | seen_runs_count. Contract: - MCP tool `search_linkedin_auto_scrape_results`, registry package `mcp.linkedin/linkedin_auto_scrape_results`, mount `linkedin.auto-scrapes`. - Operation `search`, response envelope `search`. - Flags: read only. # Get LinkedIn auto-scrape run Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_auto_scrape_runs/get-linkedin-auto-scrape-run /api-reference/linkedin/openapi.yaml get /api/linkedin-auto-scrape-runs/{sid} Fetch one run by sid: the poll handle after run_linkedin_auto_scrape_now. Read status, page_cursor and pages_applied for progress, new_count against enrolled_count for how much of the delta reached the linked mass-action, and error_message for why a failed run failed. The leads it discovered come from search_linkedin_auto_scrape_results filtered on first_seen_run_sid, which is the authoritative delta (new_count is a best-effort rollup under crash retry). Contract: - MCP tool `get_linkedin_auto_scrape_run`, registry package `mcp.linkedin/linkedin_auto_scrape_runs`, mount `linkedin.auto-scrapes`. - Operation `get`, response envelope `get`. - Flags: read only. # Search LinkedIn auto-scrape runs Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_auto_scrape_runs/search-linkedin-auto-scrape-runs /api-reference/linkedin/openapi.yaml post /api/linkedin-auto-scrape-runs/search List executions of the collectors: one row per pass of one auto-scrape page loop, with its page progress and its delta (new_count, seen_count, enrolled_count). Use for: the history of one job (filter linkedin_auto_scrape_sid, sort run_number desc, page_size 1 for the latest), why collection stopped (status failed, then read error_message), which runs produced (sort new_count desc), and which fed a mass-action (mass_action_sid). NOT for the leads themselves (search_linkedin_auto_scrape_results on first_seen_run_sid) and NOT for starting a run (run_linkedin_auto_scrape_now): runs are server-minted and immutable. No q, no include[]. run_number needs its parent partner (422 bounded_scan_required). Sort: created_at (default desc) | run_number | completed_at | new_count. Contract: - MCP tool `search_linkedin_auto_scrape_runs`, registry package `mcp.linkedin/linkedin_auto_scrape_runs`, mount `linkedin.auto-scrapes`. - Operation `search`, response envelope `search`. - Flags: read only. # Create LinkedIn auto-scrape Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_auto_scrapes/create-linkedin-auto-scrape /api-reference/linkedin/openapi.yaml post /api/linkedin-auto-scrapes Save a scraping source as a collector and start it. Give the source EXACTLY one way: a bare url (people and company searches only) or the pair source_method plus source_input. Anything else is 422. Use for the standing ask, "keep pulling everyone behind this link". To wire outreach, author the plan FIRST on the mass-actions mount (preview_mass_action, then create_mass_action with scope kind none) and pass its ma_ac_ sid here: every new lead is appended to that run as one item. Omit mass_action_sid to collect only, omit replay_frequency for a one-shot. Run 1 dispatches AT ONCE: real LinkedIn automation starts, and a linked run begins enrolling. Watch it with search_linkedin_auto_scrape_runs, read the leads with search_linkedin_auto_scrape_results. Rejections are all pre-flight 422: invalid_url, invalid_source_method, method_not_available, source_input_incomplete, account_not_in_team, sales_nav_required (sales-nav sources and company-decision-makers need Sales Navigator), mass_action_incompatible. Contract: - MCP tool `create_linkedin_auto_scrape`, registry package `mcp.linkedin/linkedin_auto_scrapes`, mount `linkedin.auto-scrapes`. - Operation `create`, response envelope `create`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Delete LinkedIn auto-scrape Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_auto_scrapes/delete-linkedin-auto-scrape /api-reference/linkedin/openapi.yaml delete /api/linkedin-auto-scrapes/{sid} Retire a collector for good: soft-delete, so no further run is ever minted and a run in flight abandons at its next page boundary. Runs and collected leads are retained for audit and stay readable by sid. The linked mass-action is INDEPENDENT and keeps draining the leads already enrolled in it: stop that separately with delete_mass_action. To stop collecting for a while without losing the job, use pause_linkedin_auto_scrape instead. No undo through MCP. Contract: - MCP tool `delete_linkedin_auto_scrape`, registry package `mcp.linkedin/linkedin_auto_scrapes`, mount `linkedin.auto-scrapes`. - Operation `delete`, response envelope `delete_simple`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Get LinkedIn auto-scrape Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_auto_scrapes/get-linkedin-auto-scrape /api-reference/linkedin/openapi.yaml get /api/linkedin-auto-scrapes/{sid} Fetch one collector by sid: status and paused_reason, next_run_at and last_run_at, runs_count and total_new_results, and mass_action_sid (null = collect-only). include: linkedin_account (the executor row), last_run (the newest run, null when the job has never run). Executions and leads are separate reads: search_linkedin_auto_scrape_runs and search_linkedin_auto_scrape_results. Contract: - MCP tool `get_linkedin_auto_scrape`, registry package `mcp.linkedin/linkedin_auto_scrapes`, mount `linkedin.auto-scrapes`. - Operation `get`, response envelope `get`. - Flags: read only. # Link LinkedIn auto-scrape to a mass-action Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_auto_scrapes/link-linkedin-auto-scrape-to-a-mass-action /api-reference/linkedin/openapi.yaml post /api/linkedin-auto-scrapes/{sid}/set-mass-action Wire this collector to a mass-action: from the next run on, every newly discovered lead is appended to that run as one item, which is how collection turns into outreach. Author the run FIRST on the mass-actions mount (preview_mass_action, then create_mass_action, normally with scope kind none) and pass its ma_ac_ sid here. Slot semantics: an existing link is replaced and returned as previous_mass_action_sid, and that older run keeps draining what it already holds. A run in flight is unaffected, it keeps the link it was minted with. 422 when the mass-action is missing, foreign, deleted, or its target family cannot accept this collector result_kind. Contract: - MCP tool `set_linkedin_auto_scrape_mass_action`, registry package `mcp.linkedin/linkedin_auto_scrapes`, mount `linkedin.auto-scrapes`. - Operation `action`, response envelope `action`. - Flags: none. # Pause LinkedIn auto-scrape Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_auto_scrapes/pause-linkedin-auto-scrape /api-reference/linkedin/openapi.yaml post /api/linkedin-auto-scrapes/{sid}/pause Freeze recurrence: status becomes paused with paused_reason 'manual' and next_run_at is cleared, so no new run is minted. A run already in flight FINISHES (pausing never abandons it). Idempotent. Use to stop collecting over a holiday or while an outreach plan is being reworked; use delete_linkedin_auto_scrape to retire the job outright. Contract: - MCP tool `pause_linkedin_auto_scrape`, registry package `mcp.linkedin/linkedin_auto_scrapes`, mount `linkedin.auto-scrapes`. - Operation `action`, response envelope `action`. - Flags: none. # Resume LinkedIn auto-scrape Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_auto_scrapes/resume-linkedin-auto-scrape /api-reference/linkedin/openapi.yaml post /api/linkedin-auto-scrapes/{sid}/resume Re-arm a paused collector: status becomes active, paused_reason clears, consecutive_failures resets to 0 and next_run_at is set to now, so the next scheduler tick mints a run. Works on any paused job, including a paused one-shot (a repeat pass is cheap because identity dedup makes it near zero new). 409 invalid_status_transition when the job is already active: an exhausted one-shot stays active with a null clock, and resuming it is exactly how you run it again. Contract: - MCP tool `resume_linkedin_auto_scrape`, registry package `mcp.linkedin/linkedin_auto_scrapes`, mount `linkedin.auto-scrapes`. - Operation `action`, response envelope `action`. - Flags: none. # Run LinkedIn auto-scrape now Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_auto_scrapes/run-linkedin-auto-scrape-now /api-reference/linkedin/openapi.yaml post /api/linkedin-auto-scrapes/{sid}/run-now Mint a manual run immediately, off schedule, and return its run_sid in result. Asynchronous: a worker drives the page loop, so poll get_linkedin_auto_scrape_run for status, new_count and enrolled_count. Use when the operator will not wait for the next slot. Only one run may be in flight per collector: 409 run_in_progress while one is pending or in_progress, and 409 auto_scrape_not_active on a paused job (resume it first). Contract: - MCP tool `run_linkedin_auto_scrape_now`, registry package `mcp.linkedin/linkedin_auto_scrapes`, mount `linkedin.auto-scrapes`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Search LinkedIn auto-scrapes Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_auto_scrapes/search-linkedin-auto-scrapes /api-reference/linkedin/openapi.yaml post /api/linkedin-auto-scrapes/search List the team's collectors: saved jobs that paginate one LinkedIn or Sales Navigator list surface on a cadence and file deduped leads. Use for: which collectors are live or paused and why (status, paused_reason), what a given account collects (linkedin_account_sid), which ones feed outreach (mass_action_sid, is_null:true = collect-only), what runs next (sort next_run_at asc), and which produce (total_new_results). NOT for the leads themselves (search_linkedin_auto_scrape_results) or for one execution (search_linkedin_auto_scrape_runs). q: substring of the title, the ln_as_ sid or the pasted source URL (not the executor name or the mass-action title). include: linkedin_account (the executor row) and last_run (the newest run, the only place an in-flight run shows up). Sort: created_at (default desc) | updated_at | next_run_at | last_run_at | total_new_results. page_size:0 returns the count alone. Contract: - MCP tool `search_linkedin_auto_scrapes`, registry package `mcp.linkedin/linkedin_auto_scrapes`, mount `linkedin.auto-scrapes`. - Operation `search`, response envelope `search`. - Flags: read only. # Unlink LinkedIn auto-scrape from its mass-action Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_auto_scrapes/unlink-linkedin-auto-scrape-from-its-mass-action /api-reference/linkedin/openapi.yaml post /api/linkedin-auto-scrapes/{sid}/unset-mass-action Cut the outreach wire: clear mass_action_sid so the collector goes back to collect-only from the next run. This does NOT stop the mass-action. Leads already enrolled keep running, and stopping them is delete_mass_action on the mass-actions mount. A run in flight keeps enrolling into the link it was minted with. 409 no_mass_action_set when nothing is linked. To point the collector at a different plan, call set_linkedin_auto_scrape_mass_action instead: it replaces the slot in one call. Contract: - MCP tool `unset_linkedin_auto_scrape_mass_action`, registry package `mcp.linkedin/linkedin_auto_scrapes`, mount `linkedin.auto-scrapes`. - Operation `action`, response envelope `action`. - Flags: none. # Update LinkedIn auto-scrape Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_auto_scrapes/update-linkedin-auto-scrape /api-reference/linkedin/openapi.yaml patch /api/linkedin-auto-scrapes/{sid} Patch a collector. Only title and replay_frequency are writable, and at least one must be sent (else 422 nothing_to_update). The source is immutable by design: a different source is a different collector, so create a new one. The outreach link moves through set_linkedin_auto_scrape_mass_action / unset_linkedin_auto_scrape_mass_action, and status through pause_linkedin_auto_scrape / resume_linkedin_auto_scrape. A cadence change re-plans next_run_at only when the clock is already armed; a run in flight keeps its claim and picks the new cadence up when it finalizes. Contract: - MCP tool `update_linkedin_auto_scrape`, registry package `mcp.linkedin/linkedin_auto_scrapes`, mount `linkedin.auto-scrapes`. - Operation `update`, response envelope `update`. - Flags: none. # Get latest LinkedIn benchmark Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_benchmarks/get-latest-linkedin-benchmark /api-reference/linkedin/openapi.yaml post /api/linkedin-benchmarks/get-latest-benchmark Return the most recent platform benchmark row (largest period_start < today). Platform-GLOBAL: every team reads the same weekly row. No parameters. Use to compare an account or fleet against the market (acceptance rate, reply rates) and to calibrate safe daily ceilings by cohort. Pair it with linkedin-account-snapshots / linkedin-accounts / *.metrics for the per-account numbers to compare against. Every metric lives in the group_1 / group_2 / group_3 JSON blobs; dereference the specific path per MARKET_BENCHMARKS.md. NOT for a single account's current numbers (linkedin-accounts / -snapshots), live limit utilisation (linkedin-account-smart-limits), a specific quota hit (linkedin-account-quota-hits), or historical trends (internal-only). Cold start returns 404 no_benchmark_published. Contract: - MCP tool `get_latest_linkedin_benchmark`, registry package `mcp.linkedin/linkedin_benchmarks`, mount `linkedin.account-monitor`. - Operation `action`, response envelope `action`. - Flags: read only. # Accept connection invitation Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_connection_invitations/accept-connection-invitation /api-reference/linkedin/openapi.yaml post /api/linkedin-connection-invitations/{sid}/accept Accept one inbound connection invitation (outward action). On terminal success the invitation row is soft-deleted (removal_kind=accepted) and a linkedin-connections row is created for the new 1st-degree edge. Identify the invitation by its ln_ci_ sid. Contract: - MCP tool `accept_linkedin_connection_invitation`, registry package `mcp.linkedin/linkedin_connection_invitations`, mount `linkedin.network`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `networking_general` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Connection invitations metrics Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_connection_invitations/connection-invitations-metrics /api-reference/linkedin/openapi.yaml post /api/linkedin-connection-invitations/metrics Period-bound aggregates over one account's invitations. Requires period {from,to} and filter.linkedin_account_sid (.eq or .in) - that is the ONLY filter axis the aggregation applies, unlike search. Returns avg_time_to_triage_seconds; acceptance / ignore rates are derived client-side from the counts block. Contract: - MCP tool `get_linkedin_connection_invitations_metrics`, registry package `mcp.linkedin/linkedin_connection_invitations`, mount `linkedin.network`. - Operation `metrics`, response envelope `metrics`. - Flags: read only. # Get my latest connection invitations Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_connection_invitations/get-my-latest-connection-invitations /api-reference/linkedin/openapi.yaml post /api/linkedin-connection-invitations/get-my-latest Always-fresh head read of pending inbound invitations: refresh the newest from LinkedIn in-request (§5.8), then return the last N (received_at DESC). The first page (cursor null) triggers the refresh; continuation pages read the already-refreshed DB. Account-scoped. Runs through the account's browser: a stopped or idle one is cold-started first (about 50 s, or 503 browser_starting), which also releases that account's overdue syncs. Contract: - MCP tool `get_my_latest_linkedin_connection_invitations`, registry package `mcp.linkedin/linkedin_connection_invitations`, mount `linkedin.network`. - Operation `action`, response envelope `search`. - Flags: paced: a call spends the `self_account_sync` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Ignore connection invitation Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_connection_invitations/ignore-connection-invitation /api-reference/linkedin/openapi.yaml post /api/linkedin-connection-invitations/{sid}/ignore Ignore one inbound connection invitation (outward action). On terminal success the invitation row is soft-deleted (removal_kind=ignored); no connection is created. Identify the invitation by its ln_ci_ sid. Contract: - MCP tool `ignore_linkedin_connection_invitation`, registry package `mcp.linkedin/linkedin_connection_invitations`, mount `linkedin.network`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `networking_general` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Search connection invitations Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_connection_invitations/search-connection-invitations /api-reference/linkedin/openapi.yaml post /api/linkedin-connection-invitations/search List inbound LinkedIn connection invitations we received, with filters (including an exact-match note filter; there is no free-text q on this entity), sorting and cursor pagination. Pending rows have removal_kind IS NULL; terminal rows are accepted / ignored / expired (deleted_at set). include[] can eager-load linkedin_account. Contract: - MCP tool `search_linkedin_connection_invitations`, registry package `mcp.linkedin/linkedin_connection_invitations`, mount `linkedin.network`. - Operation `search`, response envelope `search`. - Flags: read only. # Sync connection invitations Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_connection_invitations/sync-connection-invitations /api-reference/linkedin/openapi.yaml post /api/linkedin-connection-invitations/sync-my-linkedin-connection-invitations Kick off a fresh inbound-invitations sync for one account by inserting a sync_run. ASYNC: returns pending refs to poll or await the linkedin-connection-invitations.sync-completed webhook. Contract: - MCP tool `sync_my_linkedin_connection_invitations`, registry package `mcp.linkedin/linkedin_connection_invitations`, mount `linkedin.network`. - Operation `action`, response envelope `action_async`. - Flags: none. # Check whether a LinkedIn connection request went out Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_connection_requests/check-whether-a-linkedin-connection-request-went-out /api-reference/linkedin/openapi.yaml post /api/linkedin-connection-requests/check-sent Ask what a LinkedIn connection request came to, without sending: by the client_reference it went out under (optionally for one person: profile_id or ln_member_id), or by the activity_log_sid a 409 named. result.outcome: sent (item is the request, or null with reason request_row_pending or caller_confirmed), in_flight, unknown (the answer was lost and the sent invitations have not shown it yet; retry_after, send_decisive_at), or not_sent (no_send_under_key, refused, not_in_invitations, caller_confirmed). Send the same request again only on not_sent, under the same key. result.activity_log_sid is the attempt a person's word names (confirmed_not_sent / confirmed_sent on the send). An invitation in doubt has the sent invitations read now when the last look is old enough, so a call can take as long as a head read. A key sent to several persons is 422 place_required (context.places). Contract: - MCP tool `check_linkedin_connection_request_sent`, registry package `mcp.linkedin/linkedin_connection_requests`, mount `linkedin.network`. - Operation `action`, response envelope `action`. - Flags: none. # Connection requests metrics Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_connection_requests/connection-requests-metrics /api-reference/linkedin/openapi.yaml post /api/linkedin-connection-requests/metrics Period-bound aggregates over one account's sent requests. Requires period {from,to} and filter.linkedin_account_sid (.eq or .in) - that is the ONLY filter axis the aggregation applies, unlike search. Returns avg_note_length_of_accepted and avg_time_to_accept_seconds; acceptance / withdraw / expiry rates are derived client-side from the counts block. Contract: - MCP tool `get_linkedin_connection_requests_metrics`, registry package `mcp.linkedin/linkedin_connection_requests`, mount `linkedin.network`. - Operation `metrics`, response envelope `metrics`. - Flags: read only. # Get my latest connection requests Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_connection_requests/get-my-latest-connection-requests /api-reference/linkedin/openapi.yaml post /api/linkedin-connection-requests/get-my-latest Always-fresh head read of pending outbound requests: refresh the newest from LinkedIn in-request (§5.8), then return the last N (sent_at DESC). The first page (cursor null) triggers the refresh; continuation pages read the already-refreshed DB. Account-scoped. Runs through the account's browser: a stopped or idle one is cold-started first (about 50 s, or 503 browser_starting), which also releases that account's overdue syncs. Contract: - MCP tool `get_my_latest_linkedin_connection_requests`, registry package `mcp.linkedin/linkedin_connection_requests`, mount `linkedin.network`. - Operation `action`, response envelope `search`. - Flags: paced: a call spends the `self_account_sync` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Search connection requests Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_connection_requests/search-connection-requests /api-reference/linkedin/openapi.yaml post /api/linkedin-connection-requests/search List outbound LinkedIn connection requests (invitations we sent) with filters, sorting, cursor pagination and filter.q, a LIKE (substring) over the note. Pending rows have removal_kind IS NULL; terminal rows are accepted / withdrawn / expired (deleted_at set). include[] can eager-load linkedin_account. Contract: - MCP tool `search_linkedin_connection_requests`, registry package `mcp.linkedin/linkedin_connection_requests`, mount `linkedin.network`. - Operation `search`, response envelope `search`. - Flags: read only. # Send connection request Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_connection_requests/send-connection-request /api-reference/linkedin/openapi.yaml post /api/linkedin-connection-requests/send Send one outbound LinkedIn connection request (outward action). Address the person by profile_id (the URN, ln_id OR sn_id) or by public_identifier (a vanity slug or linkedin.com/in/ URL, resolved server-side). Server-side checks run first: the daily send limit, the premium-aware note cap (200 free / 300 premium), and the 21-day resend cooldown. Fire-on-success: a row is created only when LinkedIn confirms the send. Sends once per client_reference and person: a repeat is answered, never sent twice; on a 409 read error.context.send_outcome, never resend under a new key. A person with a request on record under another key or none is refused 422 resend_not_available, context.cause pending, accepted, cooldown (resend_available_at) or sync_reset; LinkedIn refusing a second invitation while one of the account's is out is answered by that one, never as a refusal of this send. check_linkedin_connection_request_sent asks what an invitation came to without sending. Contract: - MCP tool `send_linkedin_connection_request`, registry package `mcp.linkedin/linkedin_connection_requests`, mount `linkedin.network`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; step eligible: gtm.service.orchestration can run this verb as a mass-action plan step, once per item; paced: a call spends the `send_connection_requests` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Sync connection requests Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_connection_requests/sync-connection-requests /api-reference/linkedin/openapi.yaml post /api/linkedin-connection-requests/sync-linkedin-connection-requests Refresh the outbound sent-invitations snapshot for one account by inserting a sync_run (upsert-only). ASYNC: returns pending refs to poll or await the linkedin-connection-requests.sync-completed webhook. Contract: - MCP tool `sync_linkedin_connection_requests`, registry package `mcp.linkedin/linkedin_connection_requests`, mount `linkedin.network`. - Operation `action`, response envelope `action_async`. - Flags: none. # Withdraw connection request Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_connection_requests/withdraw-connection-request /api-reference/linkedin/openapi.yaml post /api/linkedin-connection-requests/{sid}/withdraw Withdraw a pending outbound connection request (outward, destructive). Dispatches the withdraw-invitation browser verb (networking_general bucket); on terminal success the row is soft-deleted (removal_kind=withdrawn) and a 21-day resend cooldown begins. Identify the request by its ln_cr_ sid. Contract: - MCP tool `withdraw_linkedin_connection_request`, registry package `mcp.linkedin/linkedin_connection_requests`, mount `linkedin.network`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `networking_general` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Check connection degree Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_connections/check-connection-degree /api-reference/linkedin/openapi.yaml post /api/linkedin-connections/check-degree Probe one contact's current connection degree (1st / 2nd / 3rd / 4_plus) from a managed account, and actualize a stale stored row: a live connection whose degree is no longer 1st is soft-deleted (removal_kind=removed) in the same call. Reuses the get-lite-profile getter (owner-only: it runs on the account you name). Supply exactly one target identifier. Contract: - MCP tool `check_linkedin_connection_degree`, registry package `mcp.linkedin/linkedin_connections`, mount `linkedin.network`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `networking_general` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Get my latest connections Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_connections/get-my-latest-connections /api-reference/linkedin/openapi.yaml post /api/linkedin-connections/get-my-latest Always-fresh head read: refresh the newest connections from LinkedIn in-request (§5.8), then return the last N (connected_at DESC) with a counts block. The first page (cursor null) triggers the refresh; continuation pages read the already-refreshed DB. Account-scoped. For richer slicing call search_linkedin_connections right after. Runs through the account's browser: a stopped or idle one is cold-started first (about 50 s, or 503 browser_starting), which also releases that account's overdue syncs. Contract: - MCP tool `get_my_latest_linkedin_connections`, registry package `mcp.linkedin/linkedin_connections`, mount `linkedin.network`. - Operation `action`, response envelope `search`. - Flags: paced: a call spends the `self_account_sync` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # LinkedIn connections metrics Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_connections/linkedin-connections-metrics /api-reference/linkedin/openapi.yaml post /api/linkedin-connections/metrics Period-bound aggregates over a filtered connection set. Requires period {from,to} (≤ 90 days). Returns connections_growth, connections_removed, net_growth, reply_rate_within_connections, dormant_connections and avg_time_to_first_message. Optional filter and a single group_by axis. Contract: - MCP tool `get_linkedin_connections_metrics`, registry package `mcp.linkedin/linkedin_connections`, mount `linkedin.network`. - Operation `metrics`, response envelope `metrics`. - Flags: read only. # Remove connection Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_connections/remove-connection /api-reference/linkedin/openapi.yaml post /api/linkedin-connections/{sid}/remove Disconnect a 1st-degree connection on LinkedIn (outward, destructive). Dispatches the remove-connection browser verb (networking_general bucket); on terminal success the local row is soft-deleted (removal_kind=removed) and linkedin-connections.removed is emitted. Identify the connection by its ln_cn_ sid. Contract: - MCP tool `remove_linkedin_connection`, registry package `mcp.linkedin/linkedin_connections`, mount `linkedin.network`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `networking_general` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Search LinkedIn connections Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_connections/search-linkedin-connections /api-reference/linkedin/openapi.yaml post /api/linkedin-connections/search List an account's 1st-degree LinkedIn connections with filters, sorting, cursor pagination and filter.q, a LIKE (substring) over the contact name. Live rows by default (filter {"deleted_at": {"is_null": true}}); pass {"deleted_at": {"is_null": false}} for churn analysis. Returns a counts block of predicate tallies; include[] can eager-load linkedin_account, the source request/invitation, conversations and last_messages. Contract: - MCP tool `search_linkedin_connections`, registry package `mcp.linkedin/linkedin_connections`, mount `linkedin.network`. - Operation `search`, response envelope `search`. - Flags: read only. # Sync my connections Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_connections/sync-my-connections /api-reference/linkedin/openapi.yaml post /api/linkedin-connections/sync-my-linkedin-connections Kick off a full background connections sync for one account by inserting a sync_run (upsert-only: new/updated rows only, no removal detection). ASYNC: returns pending refs to poll or await the linkedin-connections.sync-completed webhook. For an in-request head refresh use get_my_latest_linkedin_connections instead. Contract: - MCP tool `sync_my_linkedin_connections`, registry package `mcp.linkedin/linkedin_connections`, mount `linkedin.network`. - Operation `action`, response envelope `action_async`. - Flags: none. # Add conversation participants Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_conversations/add-conversation-participants /api-reference/linkedin/openapi.yaml post /api/linkedin-conversations/{sid}/add-participants Add one or more members to an existing thread on LinkedIn (outward action). Pass the conversation sid + participant_ln_ids (1..20 bare LinkedIn profile ids, the ACoA... ln_id of a connection or conversation participant; never a urn: string, the SDK wraps the id itself and a URN is wrapped twice and refused); a 1:1 thread is promoted to a group. item is the stored thread as it was before the call (the participant list follows on the next inbox sync); result carries status success and the activity_log_sid of the call. A LinkedIn refusal is a typed error (409 linkedin_refused, or the account-level 409 / 422 codes), never a success with a failed status. Contract: - MCP tool `add_linkedin_conversation_participants`, registry package `mcp.linkedin/linkedin_conversations`, mount `linkedin.messaging`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `messaging_general` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Delete conversation on LinkedIn Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_conversations/delete-conversation-on-linkedin /api-reference/linkedin/openapi.yaml post /api/linkedin-conversations/{sid}/delete-on-linkedin Delete one thread from this account's OWN LinkedIn mailbox (wire delete-conversation, matrix row 110). Counterpart mailboxes keep their full copies and no message is recalled: the opposite blast radius of delete_linkedin_message_on_linkedin, which recalls ONE message for everyone. Irreversible on the wire (LinkedIn resurrects the thread only when a counterpart writes again), so not a mass action. Pass the conversation sid; the LinkedIn-side delete runs first and the stored row is soft-deleted only on a confirmed success (the next sync converges to the same state). Voyager basic messenger only: a Sales Navigator thread is refused 422 messenger_type_mismatch, a thread not yet synced to its real 2-... hash is refused 422 conversation_not_deletable, an already deleted one 422 conversation_deleted. Spends the messaging_general bucket. Contract: - MCP tool `delete_linkedin_conversation_on_linkedin`, registry package `mcp.linkedin/linkedin_conversations`, mount `linkedin.messaging`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `messaging_general` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Get LinkedIn conversation Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_conversations/get-linkedin-conversation /api-reference/linkedin/openapi.yaml get /api/linkedin-conversations/{sid} Fetch one conversation by sid: the stored row including the denormalized participants list. Stored-DB read: no plugin call, no limit spend. Contract: - MCP tool `get_linkedin_conversation`, registry package `mcp.linkedin/linkedin_conversations`, mount `linkedin.messaging`. - Operation `get`, response envelope `get`. - Flags: read only. # Get LinkedIn conversations metrics Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_conversations/get-linkedin-conversations-metrics /api-reference/linkedin/openapi.yaml post /api/linkedin-conversations/metrics Period-bound conversation aggregates for one account (new_conversations_count, active_count) with an optional group_by axis. Requires period {from,to} (≤ 90 days) and filter.linkedin_account_sid - that is the ONLY filter axis the aggregation applies, unlike search. Returns the counts block alongside. Contract: - MCP tool `get_linkedin_conversations_metrics`, registry package `mcp.linkedin/linkedin_conversations`, mount `linkedin.messaging`. - Operation `metrics`, response envelope `metrics`. - Flags: read only. # Get my latest LinkedIn conversations Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_conversations/get-my-latest-linkedin-conversations /api-reference/linkedin/openapi.yaml post /api/linkedin-conversations/get-my-latest Always-fresh head read of the basic-LinkedIn-messenger thread list for one account (§5.8 foreground refresh, then the last N threads from the refreshed DB, last_activity_at DESC). Never serves stale data: it 429s with bucket_saturated / sync_in_progress instead. Use before an inbox decision; use sync_my_linkedin_conversations for full backfill. Runs through the account's browser: a stopped or idle one is cold-started first (about 50 s, or 503 browser_starting), which also releases that account's overdue syncs. Contract: - MCP tool `get_my_latest_linkedin_conversations`, registry package `mcp.linkedin/linkedin_conversations`, mount `linkedin.messaging`. - Operation `action`, response envelope `search`. - Flags: paced: a call spends the `self_account_sync` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Get my latest Recruiter conversations Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_conversations/get-my-latest-recruiter-conversations /api-reference/linkedin/openapi.yaml post /api/linkedin-conversations/get-my-latest-recruiter LinkedIn Recruiter variant of get_my_latest_linkedin_conversations: always-fresh head read of the recruiter inbox for one account (§5.8 refresh-then-return over the talent wire, page size fixed at 15 by that wire). Same hard-429 guards; returns messenger_type='recruiter' rows whose recruiter_id is the candidate. Needs a Recruiter seat and its stamped seat number (422 recruiter_required / recruiter_seat_unresolvable). Runs through the account's browser: a stopped or idle one is cold-started first (about 50 s, or 503 browser_starting), which also releases that account's overdue syncs. Contract: - MCP tool `get_my_latest_linkedin_conversations_recruiter`, registry package `mcp.linkedin/linkedin_conversations`, mount `linkedin.recruiter`. - Operation `action`, response envelope `search`. - Flags: paced: a call spends the `self_account_sync` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Get my latest Sales Navigator conversations Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_conversations/get-my-latest-sales-navigator-conversations /api-reference/linkedin/openapi.yaml post /api/linkedin-conversations/get-my-latest-sales-nav Sales Navigator variant of get_my_latest_linkedin_conversations: always-fresh head read of the SN thread list for one account (§5.8 refresh-then-return). Same hard-429 guards; returns messenger_type='sales_navigator' rows. Runs through the account's browser: a stopped or idle one is cold-started first (about 50 s, or 503 browser_starting), which also releases that account's overdue syncs. Contract: - MCP tool `get_my_latest_linkedin_conversations_sales_nav`, registry package `mcp.linkedin/linkedin_conversations`, mount `linkedin.messaging`. - Operation `action`, response envelope `search`. - Flags: paced: a call spends the `self_account_sync` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Mark conversation read Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_conversations/mark-conversation-read /api-reference/linkedin/openapi.yaml post /api/linkedin-conversations/{sid}/mark-read Mark a conversation thread as read on LinkedIn's side (outward action). LinkedIn-side effect only, no stored read-state changes; the audit rides an activity-log row. Idempotent; safe to retry. Contract: - MCP tool `mark_linkedin_conversation_read`, registry package `mcp.linkedin/linkedin_conversations`, mount `linkedin.messaging`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `messaging_general` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Mark conversation unread Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_conversations/mark-conversation-unread /api-reference/linkedin/openapi.yaml post /api/linkedin-conversations/{sid}/mark-unread Mark a conversation thread as unread on LinkedIn's side (outward action), the hand-back-to-human signal after an agent triaged a thread. LinkedIn-side effect only; no stored read-state changes. Contract: - MCP tool `mark_linkedin_conversation_unread`, registry package `mcp.linkedin/linkedin_conversations`, mount `linkedin.messaging`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `messaging_general` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Mute conversation Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_conversations/mute-conversation /api-reference/linkedin/openapi.yaml post /api/linkedin-conversations/{sid}/mute Mute or unmute one thread's notifications on LinkedIn (outward action). Pass the conversation sid + mute (true = mute, default; false = unmute); the change is dispatched to LinkedIn and mirrored on the stored row. Contract: - MCP tool `mute_linkedin_conversation`, registry package `mcp.linkedin/linkedin_conversations`, mount `linkedin.messaging`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `messaging_general` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Remove conversation participants Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_conversations/remove-conversation-participants /api-reference/linkedin/openapi.yaml post /api/linkedin-conversations/{sid}/remove-participants Remove one or more members from a group thread on LinkedIn (outward action). Pass the conversation sid + participant_ln_ids (1..20 bare LinkedIn profile ids, the ACoA... ln_id; never a urn: string, the SDK wraps the id itself). item is the stored thread as it was before the call (the participant list follows on the next inbox sync); result carries status success and the activity_log_sid of the call. A LinkedIn refusal is a typed error (409 linkedin_refused, or the account-level 409 / 422 codes), never a success with a failed status. Contract: - MCP tool `remove_linkedin_conversation_participants`, registry package `mcp.linkedin/linkedin_conversations`, mount `linkedin.messaging`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `messaging_general` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Rename conversation Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_conversations/rename-conversation /api-reference/linkedin/openapi.yaml post /api/linkedin-conversations/{sid}/rename Rename a group thread on LinkedIn (outward action). Pass the conversation sid + the new title (1..256 chars). Group threads only: LinkedIn refuses a 1:1 thread, and that refusal is a typed error (409 linkedin_refused), never a success with a failed status. item is the stored thread as it was before the call (the title follows on the next inbox sync); result carries status success and the activity_log_sid of the call. Contract: - MCP tool `rename_linkedin_conversation`, registry package `mcp.linkedin/linkedin_conversations`, mount `linkedin.messaging`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `messaging_general` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Search LinkedIn conversations Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_conversations/search-linkedin-conversations /api-reference/linkedin/openapi.yaml post /api/linkedin-conversations/search List LinkedIn message threads for the team across both messenger surfaces (basic LinkedIn + Sales Navigator) with operator-object filters (account, contact ids, conversation_hash = LinkedIn's own 2-…== thread id, messenger_type, event-count, activity / message-sync clock ranges), sort, cursor pagination, and include[] for the parent account and the last 50 messages. Live rows by default; page_size:0 for count-only. Given a thread id by the user, look it up with filter.filter {"conversation_hash": {"eq": ""}}: q is a nickname LIKE and will not find it. Contract: - MCP tool `search_linkedin_conversations`, registry package `mcp.linkedin/linkedin_conversations`, mount `linkedin.messaging`. - Operation `search`, response envelope `search`. - Flags: read only. # Sync my LinkedIn conversations Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_conversations/sync-my-linkedin-conversations /api-reference/linkedin/openapi.yaml post /api/linkedin-conversations/sync-my-linkedin-conversations Start a background sync run that reconciles the full basic-LinkedIn-messenger thread directory for one account. ASYNC: returns a pending ref (sync_run_sid) to poll; changed threads have their messages pulled inline. Use for initial backfill / full reconciliation; for a fresh inbox head use get_my_latest_linkedin_conversations. Contract: - MCP tool `sync_my_linkedin_conversations`, registry package `mcp.linkedin/linkedin_conversations`, mount `linkedin.messaging`. - Operation `action`, response envelope `action_async`. - Flags: none. # Sync my Recruiter conversations Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_conversations/sync-my-recruiter-conversations /api-reference/linkedin/openapi.yaml post /api/linkedin-conversations/sync-my-recruiter-conversations Start a background sync run that reconciles the LinkedIn Recruiter inbox (the talent mailbox a recruiter seat owns) for one account: messenger_type='recruiter' threads with the candidate's recruiter_id, their messages drained inline. ASYNC: returns a pending ref to poll. Needs a Recruiter seat on the account AND its stamped seat number (422 recruiter_required / recruiter_seat_unresolvable otherwise; check_linkedin_account_premium_subscription with checks: ['recruiter'] refreshes both). Walks the INBOX tab and then UNRESOLVED (a thread this seat opened sits there until the candidate replies), never ARCHIVED or SCHEDULED. Separate sync_type and cadence from the two other messengers. Contract: - MCP tool `sync_my_recruiter_conversations`, registry package `mcp.linkedin/linkedin_conversations`, mount `linkedin.recruiter`. - Operation `action`, response envelope `action_async`. - Flags: none. # Sync my Sales Navigator conversations Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_conversations/sync-my-sales-navigator-conversations /api-reference/linkedin/openapi.yaml post /api/linkedin-conversations/sync-my-sales-navigator-conversations Start a background sync run that reconciles the full Sales Navigator thread directory for one account. ASYNC: returns a pending ref to poll. Separate sync_type and cadence from the basic-LinkedIn sync so SN-only lanes never touch basic-messenger rate limits. Contract: - MCP tool `sync_my_sales_navigator_conversations`, registry package `mcp.linkedin/linkedin_conversations`, mount `linkedin.messaging`. - Operation `action`, response envelope `action_async`. - Flags: none. # Execute custom LinkedIn request (escape hatch) Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_custom_requests/execute-custom-linkedin-request-escape-hatch /api-reference/linkedin/openapi.yaml post /api/linkedin-custom/execute ESCAPE HATCH: issue one arbitrary LinkedIn HTTP call (url + GET/POST + headers/body) under a chosen OWN account, for endpoints the typed methods do not cover. May be a read or an action (apply to a job, attend an event), so you own the semantics. High-risk: admin-gated + feature-flagged; the url must match the server host allowlist (else 403 forbidden_endpoint). OWN-ACCOUNT ONLY: linkedin_account_sid is REQUIRED. It spends the account's custom_request bucket, which returns 429 when saturated. A non-2xx response is still a successful dispatch (ok=false, status_code carries it; never throws). The response comes back inline, NOT stored; the audit is the linkedin-account-activity-log row (action_type=custom_request). PREFER TYPED METHODS FIRST: searches/lists → linkedin-scraping; one profile/company/post's own data → linkedin-enrichment; sends/invites/reactions/comments → messaging & networking. Never use it to route around a rate limit or a missing permission. Contract: - MCP tool `execute_linkedin_custom_request`, registry package `mcp.linkedin/linkedin_custom_requests`, mount `linkedin.platform`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `custom_request` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Enrich activity URN by URL Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_enrichment/enrich-activity-urn-by-url /api-reference/linkedin/openapi.yaml post /api/linkedin-enrichment/get-activity-urn-by-url Resolve any linkedin.com post URL (feed permalink, /posts/ slug) or lnkd.in shortlink to its stable activity URN (urn:li:activity:…). A shortlink is expanded server-side before the page is opened. The URN never changes, so this is a cacheable enrichment lookup at cached 7d. It is the resolver the get-post-* / tracked-post flows build on. Returns activity_urn (null if the URL could not be resolved - notably a shortlink of a ugcPost-threaded post, whose page exposes no activity urn at all; address such posts by post_urn via enrich_linkedin_post_details instead, which takes the shortlink directly). Contract: - MCP tool `enrich_linkedin_get_activity_urn_by_url`, registry package `mcp.linkedin/linkedin_enrichment`, mount `linkedin.enrichment`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `enrichment` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Enrich company lite profile Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_enrichment/enrich-company-lite-profile /api-reference/linkedin/openapi.yaml post /api/linkedin-enrichment/company-lite-profile The cheap company read: id, vanity, name and logo, nothing else. Deliberately sparse, the company twin of the lite person profile. Use it to confirm a company exists and to pin its identifiers before joining data; use enrich_linkedin_company_profile when you actually need industry, size, HQ or the about text. cached 7d. COST NOTE: a /company// URL is ONE small request on LinkedIn's side, while a Sales Navigator /sales/company/ URL costs TWO, because the vanity has to be resolved first (measured: 433 ms vs 2479 ms). Prefer the vanity URL, or resolve the id once with enrich_linkedin_company_public_identifier and reuse it. Contract: - MCP tool `enrich_linkedin_company_lite_profile`, registry package `mcp.linkedin/linkedin_enrichment`, mount `linkedin.enrichment`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `enrichment` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Enrich company posts Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_enrichment/enrich-company-posts /api-reference/linkedin/openapi.yaml post /api/linkedin-enrichment/company-posts The company page's own feed, newest-first: the company-side twin of person-posts (author = the company page). Addressed by NUMERIC company id, nested: {company:{company_ln_id}}. If all you have is a company URL or slug, call enrich_linkedin_company_profile first and take company_id off its result; passing {company:{company_url}} here validates and is then refused not_dispatchable, having still opened a ledger row. cached 7d, paginated. Contract: - MCP tool `enrich_linkedin_company_posts`, registry package `mcp.linkedin/linkedin_enrichment`, mount `linkedin.enrichment`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `enrichment` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Enrich company profile Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_enrichment/enrich-company-profile /api-reference/linkedin/openapi.yaml post /api/linkedin-enrichment/company-profile One company page's own data: name, industry, size, HQ, about, website, followers. URL-addressed (ledgered input_kind='url'). cached 7d. To LIST a company's people use scraping (company-employees / company-decision-makers). Contract: - MCP tool `enrich_linkedin_company_profile`, registry package `mcp.linkedin/linkedin_enrichment`, mount `linkedin.enrichment`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `enrichment` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Enrich person basic profile Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_enrichment/enrich-person-basic-profile /api-reference/linkedin/openapi.yaml post /api/linkedin-enrichment/person-basic-profile The middle person read, between the lite identity stub and the full dossier: name PARTS (first / last, which no other read splits out), headline, country and display location, profile and background imagery, and the premium / influencer / creator / verified flags. One request, cached 24h. Reach for this when you want to know WHO someone is without pulling the experience and education cards you will not read. Addressed by public_identifier ONLY: the underlying query keys on the vanity slug, so a urn-only call is refused before any browser work (resolve the slug with enrich_linkedin_person_lite_profile first). NOTE primary_language and supported_languages are the profile's LOCALE settings, i.e. which language the profile is written in. They are NOT the Languages section and carry no proficiency; do not read them as 'this person speaks these languages'. Contract: - MCP tool `enrich_linkedin_person_basic_profile`, registry package `mcp.linkedin/linkedin_enrichment`, mount `linkedin.enrichment`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `enrichment` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Enrich person certifications Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_enrichment/enrich-person-certifications /api-reference/linkedin/openapi.yaml post /api/linkedin-enrichment/person-certifications Licenses and certifications from the target profile: issuing authority, license number, validity window. A qualification and compliance signal for recruiting and technical ICP fit. cached 7d. Most profiles have NONE, so an empty list is a real answer about the person rather than a failed read. license_number and url are frequently null even on real entries (LinkedIn own certifications carry neither). Address by profile_id XOR public_identifier, the same exactly-one-of rule as every other person read: passing both is a 422, they prohibit each other. The screen behind this one is the exception that needs the vanity slug AND the member urn at once, so the backend resolves whichever half you did not send. Addressing by public_identifier skips even that resolve once this team has enriched the person before, because the urn is then already on an earlier ledger row. Contract: - MCP tool `enrich_linkedin_person_certifications`, registry package `mcp.linkedin/linkedin_enrichment`, mount `linkedin.enrichment`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `enrichment` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Enrich person comment activity Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_enrichment/enrich-person-comment-activity /api-reference/linkedin/openapi.yaml post /api/linkedin-enrichment/person-comment-activity The target's recent comments across OTHER people's posts (activity feed, Comments tab): where the target spends attention, and warm entry points for an opener. Each item carries the PARENT POST too, so you can read what they were reacting to without a second call. cached 7d, paginated. Addressed by profile_id alone. PAGING: the end of the feed is a MISSING paging.next_page_cursor, never an empty page, because member filtering can empty a page mid-feed; there is no total, LinkedIn reports 0 for this feed whatever is in it. The target's OWN posts are person-posts; the commenters OF a post are scraping. Contract: - MCP tool `enrich_linkedin_person_comment_activity`, registry package `mcp.linkedin/linkedin_enrichment`, mount `linkedin.enrichment`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `enrichment` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Enrich person contact info Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_enrichment/enrich-person-contact-info /api-reference/linkedin/openapi.yaml post /api/linkedin-enrichment/person-contact-info Contact details for ONE person: email, phones (typed, so HOME/WORK is kept, unlike the lossy flat `phone` on person-full-profile), websites and twitter handles. Cached 24h. IMPORTANT: LinkedIn reveals contacts to 1st-degree connections ONLY, so this runs from whichever of YOUR accounts is connected to the target (picked automatically, respecting limits; linkedin_account_sid is only a preference). If none of your accounts is connected it refuses 422 no_connected_account, so connect first (linkedin_connection_requests send), then retry after the invite is accepted. An empty block means the connected account still cannot see them. Contract: - MCP tool `enrich_linkedin_person_contact_info`, registry package `mcp.linkedin/linkedin_enrichment`, mount `linkedin.enrichment`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `enrichment` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Enrich person education Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_enrichment/enrich-person-education /api-reference/linkedin/openapi.yaml post /api/linkedin-enrichment/person-education Education history only, a subset projection of the full-profile wire call (NOT a stub: shares get-full-profile, no dedicated verb). cached 7d under its own method key (a cached full profile does NOT satisfy it). Not paginated. public_identifier passes straight through (native slug support). Contract: - MCP tool `enrich_linkedin_person_education`, registry package `mcp.linkedin/linkedin_enrichment`, mount `linkedin.enrichment`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `enrichment` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Enrich person experience Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_enrichment/enrich-person-experience /api-reference/linkedin/openapi.yaml post /api/linkedin-enrichment/person-experience Complete, paginated work history ("Show all experiences"), unlike the truncated head on full-profile. One call = one page; loop cursor until paging.next_page_cursor is absent. cached 7d. A public_identifier-only call needs a stored URN (run person-lite-profile first) or is refused not_dispatchable. On the connected account's OWN profile each entry also carries position_id, the key of the position editing verbs. Contract: - MCP tool `enrich_linkedin_person_experience`, registry package `mcp.linkedin/linkedin_enrichment`, mount `linkedin.enrichment`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `enrichment` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Enrich person featured Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_enrichment/enrich-person-featured /api-reference/linkedin/openapi.yaml post /api/linkedin-enrichment/person-featured The Featured section: what the target chose to pin (posts, links, media). Small, cheap, high-signal for personalization: pinned content is what the person WANTS noticed. cached 7d. Not paginated. A public_identifier-only call needs a stored URN. Contract: - MCP tool `enrich_linkedin_person_featured`, registry package `mcp.linkedin/linkedin_enrichment`, mount `linkedin.enrichment`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `enrichment` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Enrich person full profile Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_enrichment/enrich-person-full-profile /api-reference/linkedin/openapi.yaml post /api/linkedin-enrichment/person-full-profile The complete person dossier: identity, headline/position/company, location, about, follower/connection counts, premium/verification flags, plus truncated heads of experience/education/skills/recent posts. cached 24h. One full profile beats separate subset calls when you need profile + 2 or more sections. Full sub-record lists still need the dedicated paginated methods. picture_url is normalized to avatar_url. Contract: - MCP tool `enrich_linkedin_person_full_profile`, registry package `mcp.linkedin/linkedin_enrichment`, mount `linkedin.enrichment`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `enrichment` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Enrich person interests Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_enrichment/enrich-person-interests /api-reference/linkedin/openapi.yaml post /api/linkedin-enrichment/person-interests ONE tab of the Interests section per call: top_voices (the default), companies, groups, newsletters or schools. Cheap ICP-fit and conversation-hook signal. cached 7d, paginated. The wire pages a single tab, so reading all five spends the method five times; the requested tab is echoed at result.tab so a caller looping the vocabulary can tell the responses apart. Addressed by public_identifier ONLY: a urn-only call is ledgered and then refused not_dispatchable, so run person-lite-profile first if all you have is the urn. Contract: - MCP tool `enrich_linkedin_person_interests`, registry package `mcp.linkedin/linkedin_enrichment`, mount `linkedin.enrichment`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `enrichment` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Enrich person languages Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_enrichment/enrich-person-languages /api-reference/linkedin/openapi.yaml post /api/linkedin-enrichment/person-languages The Languages section: spoken languages with self-declared proficiency. Cheap localization signal: write the opener in the prospect's native language. cached 7d. Most profiles declare NONE, so an empty list is a real answer about the person rather than a failed read. proficiency is the RAW string LinkedIn renders, suffix included ("Native or bilingual proficiency", "Full professional proficiency", ...), localized on non-English sessions and null when the member declared the language without a level. Address by profile_id XOR public_identifier; like certifications, the screen behind this one needs the vanity slug AND the member urn at once, so the backend resolves whichever half you did not send. Contract: - MCP tool `enrich_linkedin_person_languages`, registry package `mcp.linkedin/linkedin_enrichment`, mount `linkedin.enrichment`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `enrichment` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Enrich person lite profile Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_enrichment/enrich-person-lite-profile /api-reference/linkedin/openapi.yaml post /api/linkedin-enrichment/person-lite-profile Cheapest person read: existence check + full identifier resolution. THE tool for turning a vanity slug (/in/…) or a bare URN into the canonical identifier quad (ln_member_id + both URNs + nickname) that every deeper enrich and stored-domain join keys on. cached 24h. Pin linkedin_account_sid and the relationship degree comes back with it in network_distance, which saves the second dispatch to linkedin-connections.check-degree; without a pinned account that field is null, because a degree relative to an account you did not name is not an answer. Address by profile_id XOR public_identifier. Contract: - MCP tool `enrich_linkedin_person_lite_profile`, registry package `mcp.linkedin/linkedin_enrichment`, mount `linkedin.enrichment`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `enrichment` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Enrich person posts Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_enrichment/enrich-person-posts /api-reference/linkedin/openapi.yaml post /api/linkedin-enrichment/person-posts Posts authored or reshared by the target, newest-first: the personalization goldmine before outreach. Reshares carry the original in reshared_post; created_at is ISO 8601. cached 7d. This is the target's OWN feed; to harvest who ENGAGED with a post use the scraping surface. A public_identifier-only call needs a stored URN. Contract: - MCP tool `enrich_linkedin_person_posts`, registry package `mcp.linkedin/linkedin_enrichment`, mount `linkedin.enrichment`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `enrichment` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Enrich person reaction activity Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_enrichment/enrich-person-reaction-activity /api-reference/linkedin/openapi.yaml post /api/linkedin-enrichment/person-reaction-activity The target's recent reactions on OTHER people's posts (activity feed, Reactions tab): topical interest signal, cheaper to act on than comments. Each item carries the reaction type, when they reacted, and the PARENT POST whole. cached 7d, paginated. Addressed by profile_id alone. PAGING: the end of the feed is a MISSING paging.next_page_cursor, never an empty page; there is no total, LinkedIn reports 0 for this feed whatever is in it. reaction_type is null when the header phrase did not map, which is normal. The reactors OF a post are scraping (get-post-reactors). Contract: - MCP tool `enrich_linkedin_person_reaction_activity`, registry package `mcp.linkedin/linkedin_enrichment`, mount `linkedin.enrichment`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `enrichment` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Enrich person recommendations Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_enrichment/enrich-person-recommendations /api-reference/linkedin/openapi.yaml post /api/linkedin-enrichment/person-recommendations Recommendations on the target profile: who vouches for them and in what words. `direction` picks received (the default, what LinkedIn opens on) or given; these are two SEPARATE reads of the same screen, so asking for both runs the method twice. cached 7d. The counterpart carries a nickname, name, headline and network distance but NO ln_id: LinkedIn does not put the member urn in this payload, so enrich the nickname separately if you need the urn. created_at is parsed out of the free-text context line and is null when that line has no date. Address by profile_id XOR public_identifier, the same exactly-one-of rule as every other person read: passing both is a 422, they prohibit each other. The screen behind this one is the exception that needs the vanity slug AND the member urn at once, so the backend resolves whichever half you did not send. Contract: - MCP tool `enrich_linkedin_person_recommendations`, registry package `mcp.linkedin/linkedin_enrichment`, mount `linkedin.enrichment`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `enrichment` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Enrich person services Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_enrichment/enrich-person-services /api-reference/linkedin/openapi.yaml post /api/linkedin-enrichment/person-services The "Providing services" marketplace page of one service-provider profile: business name, pricing line, page URL and the standardized services they list. Complements scraping-side search-service-providers (that discovers providers, this reads one provider's own page). cached 7d, no pagination. result.services is NULL when the member sells no services at all, which is a different fact from a page whose services_provided is empty, and both are a normal completed read. A page its owner has UNPUBLISHED reads as null too (LinkedIn drops the link the read follows), which looks the same as never having had one. Contract: - MCP tool `enrich_linkedin_person_services`, registry package `mcp.linkedin/linkedin_enrichment`, mount `linkedin.enrichment`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `enrichment` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Enrich person skills Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_enrichment/enrich-person-skills /api-reference/linkedin/openapi.yaml post /api/linkedin-enrichment/person-skills Complete, paginated skills list ("Show all skills") with endorsement counts (skill_id is the endorse handle). cached 7d. Use to see/reason over skills; to simply ENDORSE, use linkedin-accounts.endorse-skill (auto-discovers skills, no enrich call needed). A public_identifier-only call needs a stored URN. Contract: - MCP tool `enrich_linkedin_person_skills`, registry package `mcp.linkedin/linkedin_enrichment`, mount `linkedin.enrichment`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `enrichment` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Enrich post details Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_enrichment/enrich-post-details /api-reference/linkedin/openapi.yaml post /api/linkedin-enrichment/post-details One post's own data by its urn: full text, author, media, engagement counters, per-reaction breakdown, is_reshare with the original post embedded. LIVE since 2026-08-14 (node get-post). Accepts post_urn XOR url. result.post is null when the post is deleted or unavailable - a delivered answer, not an error, so do not retry it. Cached 7d. WHO engaged is scraping (get-post-comments / get-post-reactors / get-post-resharers); impressions exist only on the executor's own posts. Contract: - MCP tool `enrich_linkedin_post_details`, registry package `mcp.linkedin/linkedin_enrichment`, mount `linkedin.enrichment`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `enrichment` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Resolve company vanity Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_enrichment/resolve-company-vanity /api-reference/linkedin/openapi.yaml post /api/linkedin-enrichment/company-public-identifier Resolve a numeric LinkedIn company id to its vanity slug. Its own method because this resolution is the single most expensive company request LinkedIn serves: a ~164KB organizational-page download, the only id-native request that reveals the vanity. Cached 30d, since the mapping does not change. The pattern that keeps company work cheap: resolve the id ONCE here, then call every other company read by vanity. Contract: - MCP tool `enrich_linkedin_company_public_identifier`, registry package `mcp.linkedin/linkedin_enrichment`, mount `linkedin.enrichment`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `enrichment` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Get my latest followers Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_followers/get-my-latest-followers /api-reference/linkedin/openapi.yaml post /api/linkedin-followers/get-my-latest Always-fresh head read of the followers list: refresh the newest from LinkedIn in-request (§5.8), then return the last N (created_at DESC) with a counts block. The first page (cursor null) triggers the refresh; continuation pages read the already-refreshed DB. Account-scoped. Runs through the account's browser: a stopped or idle one is cold-started first (about 50 s, or 503 browser_starting), which also releases that account's overdue syncs. Contract: - MCP tool `get_my_latest_linkedin_followers`, registry package `mcp.linkedin/linkedin_followers`, mount `linkedin.network`. - Operation `action`, response envelope `search`. - Flags: paced: a call spends the `self_account_sync` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # LinkedIn followers metrics Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_followers/linkedin-followers-metrics /api-reference/linkedin/openapi.yaml post /api/linkedin-followers/metrics Period-bound audience aggregates over a filtered follower set. Requires period {from,to}. Returns followers_growth, follower_to_connection_conversions and connected_follower_share. Optional filter and a single group_by axis (linkedin_account_sid). Contract: - MCP tool `get_linkedin_followers_metrics`, registry package `mcp.linkedin/linkedin_followers`, mount `linkedin.network`. - Operation `metrics`, response envelope `metrics`. - Flags: read only. # Search LinkedIn followers Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_followers/search-linkedin-followers /api-reference/linkedin/openapi.yaml post /api/linkedin-followers/search List stored followers of team accounts with filters, sorting, cursor pagination and filter.q, a LIKE (substring) over the follower name. Returns a counts block. include[] can eager-load linkedin_account plus the funnel probes linkedin_connection / linkedin_connection_request; both absent means a warm, untouched prospect. Contract: - MCP tool `search_linkedin_followers`, registry package `mcp.linkedin/linkedin_followers`, mount `linkedin.network`. - Operation `search`, response envelope `search`. - Flags: read only. # Check whether a LinkedIn message went out Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_messages/check-whether-a-linkedin-message-went-out /api-reference/linkedin/openapi.yaml post /api/linkedin-messages/check-sent Ask what a LinkedIn message send came to, without sending: by the client_reference it went out under (optionally at one place: linkedin_conversation_sid, ln_member_id or profile_urn), or by the activity_log_sid a 409 named. result.outcome: sent (item is the message, or null with reason message_row_pending or caller_confirmed), in_flight, unknown (the thread has not shown it yet; retry_after, send_decisive_at), or not_sent (no_send_under_key, refused, not_in_thread, caller_confirmed). Send the same request again only on not_sent, under the same key. result.activity_log_sid is the attempt a person's word names (confirmed_not_sent / confirmed_sent on the send tools). A send in doubt has its thread read now when a look is due, so a call can be slow. A key sent to several places is 422 place_required, a place in another form than the sends recorded 422 place_unmatched (context.places). Invitations: check_linkedin_connection_request_sent; comments, posts, reposts: check_linkedin_posting_sent. Contract: - MCP tool `check_linkedin_message_sent`, registry package `mcp.linkedin/linkedin_messages`, mount `linkedin.messaging`. - Operation `action`, response envelope `action`. - Flags: none. # Download message attachment Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_messages/download-message-attachment /api-reference/linkedin/openapi.yaml post /api/linkedin-messages/download-attachment Download one inbound attachment of a basic-messenger message, returned inline as a base64 data_url (nothing is persisted, no row changes). This verb is addressed by RAW LinkedIn ids, not by our message sid, so assemble it from a message row you already hold (search_linkedin_messages): linkedin_account_sid = that row's linkedin_account_sid; message_id = that row's message_hash (must be the 2-… form); attachment_urn = attachments[i].urn on the same row. profile_id is the ACTING account's own LinkedIn URN: read ln_id off that linkedin_account_sid via search_linkedin_accounts, NOT the other party's URN. 120 s budget, up to ~50 MB; spends the messaging_general bucket. Contract: - MCP tool `download_linkedin_message_attachment`, registry package `mcp.linkedin/linkedin_messages`, mount `linkedin.messaging`. - Operation `action`, response envelope `action`. - Flags: read only; paced: a call spends the `messaging_general` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Download Sales Navigator attachment Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_messages/download-sales-navigator-attachment /api-reference/linkedin/openapi.yaml post /api/linkedin-messages/download-sales-nav-attachment Sales Navigator variant of download_linkedin_message_attachment: one inbound attachment of an SN message, inline as a base64 data_url. Same raw-id addressing, one field fewer (the SN wire verb takes no profile_id): linkedin_account_sid must be an account with Sales Navigator, message_id is the message row's message_hash, attachment_urn is attachments[i].urn on that row. The row must be messenger_type='sales_navigator' (an account without SN is refused before dispatch). Same 120 s budget and messaging_general bucket. Contract: - MCP tool `download_linkedin_sales_nav_message_attachment`, registry package `mcp.linkedin/linkedin_messages`, mount `linkedin.messaging`. - Operation `action`, response envelope `action`. - Flags: read only; paced: a call spends the `messaging_general` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Get LinkedIn messages metrics Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_messages/get-linkedin-messages-metrics /api-reference/linkedin/openapi.yaml post /api/linkedin-messages/metrics Period-bound message aggregates for one account (reply_rate_pct, avg_time_to_reply_seconds, avg_thread_depth) with an optional group_by axis. Requires period {from,to} (≤ 90 days) and filter.linkedin_account_sid - that is the ONLY filter axis the aggregation applies, unlike search. Returns the counts block alongside. Contract: - MCP tool `get_linkedin_messages_metrics`, registry package `mcp.linkedin/linkedin_messages`, mount `linkedin.messaging`. - Operation `metrics`, response envelope `metrics`. - Flags: read only. # Get my latest LinkedIn messages Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_messages/get-my-latest-linkedin-messages /api-reference/linkedin/openapi.yaml post /api/linkedin-messages/get-my-latest Always-fresh head read of ONE basic-messenger thread (§5.8 refresh of that conversation from LinkedIn, then the last N messages, sent_at DESC). Never stale: it 429s with bucket_saturated / sync_in_progress. Use before drafting a reply; use search_linkedin_messages for stored history. Runs through the account's browser: a stopped or idle one is cold-started first (about 50 s, or 503 browser_starting), which also releases that account's overdue syncs. Contract: - MCP tool `get_my_latest_linkedin_messages`, registry package `mcp.linkedin/linkedin_messages`, mount `linkedin.messaging`. - Operation `action`, response envelope `search`. - Flags: paced: a call spends the `self_account_sync` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Get my latest Recruiter messages Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_messages/get-my-latest-recruiter-messages /api-reference/linkedin/openapi.yaml post /api/linkedin-messages/get-my-latest-recruiter LinkedIn Recruiter variant of get_my_latest_linkedin_messages: always-fresh head read of ONE recruiter thread's messages (§5.8 refresh-then-return over the talent wire, dispatched with the account's seat). Requires a messenger_type='recruiter' conversation (a 422 messenger_type_mismatch names the right tool otherwise) and a Recruiter seat with its stamped seat number; same hard-429 guards. Rows: linkedin_type='inmail', type outbox for the seat's own messages and inbox for the candidate's. Runs through the account's browser: a stopped or idle one is cold-started first (about 50 s, or 503 browser_starting), which also releases that account's overdue syncs. Contract: - MCP tool `get_my_latest_linkedin_messages_recruiter`, registry package `mcp.linkedin/linkedin_messages`, mount `linkedin.recruiter`. - Operation `action`, response envelope `search`. - Flags: paced: a call spends the `self_account_sync` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Get my latest Sales Navigator messages Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_messages/get-my-latest-sales-navigator-messages /api-reference/linkedin/openapi.yaml post /api/linkedin-messages/get-my-latest-sales-nav Sales Navigator variant of get_my_latest_linkedin_messages: always-fresh head read of ONE SN thread's messages (§5.8 refresh-then-return). Requires a messenger_type='sales_navigator' conversation; same hard-429 guards. Runs through the account's browser: a stopped or idle one is cold-started first (about 50 s, or 503 browser_starting), which also releases that account's overdue syncs. Contract: - MCP tool `get_my_latest_linkedin_messages_sales_nav`, registry package `mcp.linkedin/linkedin_messages`, mount `linkedin.messaging`. - Operation `action`, response envelope `search`. - Flags: paced: a call spends the `self_account_sync` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # React to message Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_messages/react-to-message /api-reference/linkedin/openapi.yaml post /api/linkedin-messages/{sid}/react Add or remove an emoji reaction on one stored message on LinkedIn (outward action). Pass the message sid + a reaction_type; the reaction is dispatched to LinkedIn and mirrored onto the stored row. Set remove:true to take YOUR reaction back (pass the same reaction_type you reacted with). Basic messenger only: an SN message or a non-2-… message_hash is refused before dispatch. Spends the messaging_general bucket. Contract: - MCP tool `react_linkedin_message`, registry package `mcp.linkedin/linkedin_messages`, mount `linkedin.messaging`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `messaging_general` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Recall message on LinkedIn Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_messages/recall-message-on-linkedin /api-reference/linkedin/openapi.yaml post /api/linkedin-messages/{sid}/delete-on-linkedin Recall one of OUR OWN sent messages: it disappears for EVERY participant, not just for us. Addressed by the message sid, like react_linkedin_message. Irreversible - a wrong sid destroys a real conversation, so confirm the message first. LinkedIn allows a recall only WITHIN AN HOUR of sending; past that it refuses, so an older message comes back as 409 message_too_old_to_recall with nothing dispatched and no limit spent. The stored row is NOT removed: it stays in the thread marked is_deleted with an emptied body, which is what LinkedIn serves afterwards, so the operator still sees that something was withdrawn. Basic messenger only; an SN message, a non-2-… message_hash, or a row already marked is_deleted is refused before dispatch. Recallability is otherwise not pre-validated, so 'not yours' arrives as a wire error. Spends messaging_general, the reaction verbs' bucket. Contract: - MCP tool `delete_linkedin_message_on_linkedin`, registry package `mcp.linkedin/linkedin_messages`, mount `linkedin.messaging`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `messaging_general` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Search LinkedIn messages Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_messages/search-linkedin-messages /api-reference/linkedin/openapi.yaml post /api/linkedin-messages/search List LinkedIn messages (basic DM + Sales Navigator + InMail) with operator-object filters (account, conversation, channel, direction, contact ids, date ranges) and filter.q, a LIKE (substring) over body + subject (message text, not contact names). Supports include[], sort, cursor pagination. page_size:0 for count-only. To read a single message, filter by {sid:{eq:"ln_ms_…"}}. Contract: - MCP tool `search_linkedin_messages`, registry package `mcp.linkedin/linkedin_messages`, mount `linkedin.messaging`. - Operation `search`, response envelope `search`. - Flags: read only. # Send LinkedIn InMail Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_messages/send-linkedin-inmail /api-reference/linkedin/openapi.yaml post /api/linkedin-messages/send-inmail Send one premium InMail to a person (outward action). Person-addressed only via ln_id / sn_id, subject required (≤ 200), body ≤ 1900. Sends once per client_reference and person: a repeat is answered, never sent twice; on a 409 read error.context.send_outcome, never resend under a new key. Guards: send_inmails daily cap, Premium guard, InMail-credits guard (422 no_inmail_credits when the account's pool is known empty: the seat's grant inmail_credits with a Sales Navigator seat, premium_inmail_credits without; a balance never read does not block). Fire-on-success; the plugin's inmail_credits_remaining is written back onto that pool, and LinkedIn's own NOT_ENOUGH_INMAIL_CREDIT answers 422 no_inmail_credits and zeroes it (it is not a block of the target). When NOT: 1st-degree connection → send_linkedin_message (free); no Premium / zero credits → the guards 422; SN thread continuation → send_linkedin_sales_nav_message. Contract: - MCP tool `send_linkedin_inmail`, registry package `mcp.linkedin/linkedin_messages`, mount `linkedin.messaging`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `send_inmails` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Send LinkedIn message Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_messages/send-linkedin-message /api-reference/linkedin/openapi.yaml post /api/linkedin-messages/send Send one outbound regular LinkedIn DM on the basic messenger (outward action). Reply to an existing thread via linkedin_conversation_sid, or open a new thread to a 1st-degree connection via ln_id / sn_id or by public_identifier (slug or linkedin.com/in/ URL, resolved server-side). Guards run first: send_messages daily cap, 8000-char body cap, connection guard, attachment https:// reachability, basic-messenger surface guard. Fire-on-success: a row is inserted only on terminal success. Sends once per client_reference and place (the thread, else the person): a repeat is answered, never sent twice; on a 409 read error.context.send_outcome, never resend under a new key. When NOT: InMail to a non-connection → send_linkedin_inmail; voice note → send_linkedin_voice_message; Sales Navigator thread → send_linkedin_sales_nav_message; the connection-request note lives on linkedin-connection-requests. Bulk send: loop client-side and respect the daily cap. Contract: - MCP tool `send_linkedin_message`, registry package `mcp.linkedin/linkedin_messages`, mount `linkedin.messaging`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; step eligible: gtm.service.orchestration can run this verb as a mass-action plan step, once per item; paced: a call spends the `send_messages` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Send LinkedIn voice message Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_messages/send-linkedin-voice-message /api-reference/linkedin/openapi.yaml post /api/linkedin-messages/send-voice Send one voice note on the basic LinkedIn messenger (outward action). audio arrives as a https:// url or base64 (any common format) and is normalized server-side to AAC/m4a, hard-capped at 60 s. Same guard chain as send_linkedin_message plus audio validation. Fire-on-success: a row is inserted only on terminal success; no text body. Sends once per client_reference and place (the thread, else the person): a repeat is answered, never sent twice; on a 409 read error.context.send_outcome, never resend under a new key. A keyed repeat is answered before its audio is read. When NOT: text DM → send_linkedin_message; SN threads → send_linkedin_sales_nav_message; audio longer than 60 s is rejected, so trim client-side first. Contract: - MCP tool `send_linkedin_voice_message`, registry package `mcp.linkedin/linkedin_messages`, mount `linkedin.messaging`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `send_messages` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Send Recruiter InMail Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_messages/send-recruiter-inmail /api-reference/linkedin/openapi.yaml post /api/linkedin-messages/send-recruiter Send one LinkedIn Recruiter InMail (outward action): open a NEW recruiter thread via recruiter_id (AEMAA… id; ln_id / sn_id work too), or reply INTO an existing thread via linkedin_conversation_sid (messenger_type='recruiter'). Guards, in order: Recruiter seat (422 recruiter_required); live Recruiter session (409 recruiter_reauth_required: only the seat holder renews Recruiter's 30-day session, by signing in again in the account's browser; tell the user, do not retry); stamped seat on a reply (422 recruiter_seat_unresolvable); LinkedIn's one InMail per candidate per 24 h (429 recruiter_inmail_cooldown, cooldown_ends_at: refresh the thread with get_my_latest_linkedin_messages_recruiter if a reply may have landed, else wait). Spends send_inmails and an InMail credit. Sends once per client_reference and place: a repeat is answered, never sent twice; on a 409 read error.context.send_outcome, never resend under a new key. When NOT: basic threads → send_linkedin_message; SN → send_linkedin_sales_nav_message. Contract: - MCP tool `send_linkedin_recruiter_message`, registry package `mcp.linkedin/linkedin_messages`, mount `linkedin.recruiter`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `send_inmails` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Send Sales Navigator message Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_messages/send-sales-navigator-message /api-reference/linkedin/openapi.yaml post /api/linkedin-messages/send-sales-nav Send one Sales Navigator message (outward action): continue an SN thread via linkedin_conversation_sid (messenger_type=sales_navigator), or open a new one via ln_id / sn_id. Guards: send_inmails daily cap, SN seat and surface, and on a NEW thread outside the network the InMail-credits guard (422 no_inmail_credits when the seat's inmail_credits are known to be 0). Fire-on-success. Sends once per client_reference and place: a repeat is answered, never sent twice; on a 409 read error.context.send_outcome, never resend under a new key. LinkedIn's 403 SALES_SEAT_REQUIRED is a stale SN session, not a lost seat: the send re-arms it once and retries; a second refusal is 409 sales_session_stale (run check_linkedin_account_premium_subscription with checks ["sales_nav"], retry), a seat the probe finds gone 422 sales_nav_required. When NOT: basic threads → send_linkedin_message / send_linkedin_voice_message; cold InMail outside SN → send_linkedin_inmail; no SN seat is 422, so check linkedin-accounts.has_sn first. Contract: - MCP tool `send_linkedin_sales_nav_message`, registry package `mcp.linkedin/linkedin_messages`, mount `linkedin.messaging`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `send_inmails` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Start group conversation Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_messages/start-group-conversation /api-reference/linkedin/openapi.yaml post /api/linkedin-messages/start-group Start a new multi-attendee (group) chat on the basic messenger and send an opening message (outward action). Provide the executor account, 2..20 attendee URNs and the opening body (1..8000 chars, optional attachments, optional conversation_title to name the group); mints the group thread and dispatches the first message. Spends the send_messages bucket. Sends once per client_reference and attendee set: a repeat is answered, never sent twice; on a 409 read error.context.send_outcome, never resend under a new key. A group has no place check_linkedin_message_sent takes: ask it by the key alone or by activity_log_sid. Contract: - MCP tool `start_linkedin_group_conversation`, registry package `mcp.linkedin/linkedin_messages`, mount `linkedin.messaging`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `send_messages` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Check whether a LinkedIn comment, post or repost went out Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_posting/check-whether-a-linkedin-comment-post-or-repost-went-out /api-reference/linkedin/openapi.yaml post /api/linkedin-posting/check-sent Ask what a LinkedIn comment, post or repost came to, without sending: by the client_reference it went out under and its verb (comment, create-post, repost), optionally at one place (entity_urn: the post a comment or a repost went to, in any of its forms, or the comment a reply answers; parent_comment_urn for a reply; post_place for a post), or by the activity_log_sid a 409 named. result.outcome: sent (item is what it made, or null with reason result_pending or caller_confirmed), in_flight, unknown (LinkedIn has not shown it yet; retry_after, send_decisive_at), or not_sent (no_send_under_key, refused, not_on_linkedin, caller_confirmed). Send the same request again only on not_sent, under the same key. result.activity_log_sid is the attempt a person's word names (confirmed_not_sent / confirmed_sent on the send). A send in doubt has LinkedIn read now when the last look is old enough. A key sent to several places is 422 place_required, a place LinkedIn could not be read for 422 place_unmatched (context.places). Contract: - MCP tool `check_linkedin_posting_sent`, registry package `mcp.linkedin/linkedin_posting`, mount `linkedin.content`. - Operation `action`, response envelope `action`. - Flags: none. # Create LinkedIn comment Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_posting/create-linkedin-comment /api-reference/linkedin/openapi.yaml post /api/linkedin-posting/comment Leave ONE outbound comment on any LinkedIn post (wire create-comment): entity_urn takes a post urn in any family or a post URL carrying one, and the comment is written on the post's social thread (see entity_urn). Outward and fire-on-success: the post does NOT need to be tracked or owned by us. Identity-bound: linkedin_account_sid REQUIRED, spends the comment_posts bucket (30/day at a 360 s floor), saturation returns 429. Reply to an existing comment via parent_comment_urn. mentions turns exact substrings of text into clickable profile links ({profile_id, name} pairs; a name missing or out of order is a 422 with no dispatch spent). Returns the created comment ref plus the activity-log row. Sends once per client_reference and place (the post as LinkedIn files it, or the comment a reply answers): a repeat is answered, never sent twice; on a 409 read error.context.send_outcome, never resend under a new key. To react use react_linkedin_post; to resolve a post URL use get_activity_urn_by_url. Contract: - MCP tool `create_linkedin_comment`, registry package `mcp.linkedin/linkedin_posting`, mount `linkedin.content`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; mass action: this verb's own request accepts a filter or targets[] set, which the owning service drains; step eligible: gtm.service.orchestration can run this verb as a mass-action plan step, once per item; paced: a call spends the `comment_posts` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Create LinkedIn post Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_posting/create-linkedin-post /api-reference/linkedin/openapi.yaml post /api/linkedin-posting/create-post Publish ONE feed post - as the member, AS a company page it administers (author_organization_id), or INTO a group (group_id) - now or scheduled (scheduled_at) - wire create-post. Public; retract with delete_linkedin_post, or delete a scheduled draft with delete_linkedin_scheduled_post. Identity-bound: linkedin_account_sid REQUIRED, spends the posting bucket (20/day in series of 3 at a 1200 s pause; free plan 4), saturation returns 429. text is REQUIRED as a key but may be an EMPTY string when media is attached; no text, images or video at all is a 422. Media: up to 20 images XOR one video, each base64 or an https url, 35 MB total. Answers the published post (urn, url, time, body) and the activity-log row. Sends once per client_reference and place (your feed, the page or the group; a new scheduled_at is the same post): a repeat is answered, never sent twice; on a 409 read error.context.send_outcome, never resend under a new key. 409 post_not_created is send_outcome unknown: repeat after retry_after. Contract: - MCP tool `create_linkedin_post`, registry package `mcp.linkedin/linkedin_posting`, mount `linkedin.content`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `posting` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Delete LinkedIn comment Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_posting/delete-linkedin-comment /api-reference/linkedin/openapi.yaml post /api/linkedin-posting/delete-comment Delete one of OUR OWN LinkedIn comments, addressed by comment_urn (wire delete-comment). The counterpart of create_linkedin_comment. You already hold the handle: create_linkedin_comment returns comment_urn, and the linkedin-scraping get-post-comments rows carry the same compound urn, so a comment can be removed without any extra read. LinkedIn only deletes the account's own comments and we do not pre-validate that. Identity-bound: linkedin_account_sid REQUIRED, spends the SAME comment_posts bucket as commenting (30/day), but an undo is paced 2 s, not the bucket delay, so a comment and its delete fit back to back; saturation returns 429. Nothing is stored on this service. Contract: - MCP tool `delete_linkedin_comment`, registry package `mcp.linkedin/linkedin_posting`, mount `linkedin.content`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `comment_posts` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Delete LinkedIn post Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_posting/delete-linkedin-post /api-reference/linkedin/openapi.yaml post /api/linkedin-posting/delete-post Delete one of OUR OWN LinkedIn posts, addressed by activity_urn (wire delete-post). The counterpart of create_linkedin_post and the way to retract a post an agent published: pass the activity_urn that create_linkedin_post returned, or the post URL. LinkedIn only deletes the account's own posts and we do not pre-validate that. Identity-bound: linkedin_account_sid REQUIRED, spends the SAME posting bucket as publishing (20/day in series of 3 at a 1200 s pause; free plan 4 - the series is what lets a publish and its delete fit back-to-back), saturation returns 429. Backend urns (urn:li:share:, urn:li:ugcPost:) are NOT accepted here even though create_linkedin_post returns one: the wire builds its payload from the numeric activity id. A refusal - not your post, already gone - comes back as 409 post_not_deleted with LinkedIn's own toast in error.context, never as a success. Nothing is stored on this service, so nothing local is deleted either. Contract: - MCP tool `delete_linkedin_post`, registry package `mcp.linkedin/linkedin_posting`, mount `linkedin.content`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `posting` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Delete scheduled LinkedIn post Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_posting/delete-scheduled-linkedin-post /api-reference/linkedin/openapi.yaml post /api/linkedin-posting/delete-scheduled-post Delete one SCHEDULED (never published) post from the account's queue, addressed by the BACKEND urn get_linkedin_scheduled_posts returns as rows[].post_urn (wire delete-scheduled-post). NOT a variant of delete_linkedin_post: an unpublished share has no activity urn, and this verb runs a graphql mutation against the queue while delete-post drives an SDUI action on a live feed post - the two handles are not interchangeable. Spends the one Content bucket, posting (20/day in series of 3 with a 1200 s pause; free plan 4/day) - the same bucket publishing spends, sized so queue cleanup and publishing fit together. A refusal - not this account's draft, or already gone - comes back as 409 scheduled_post_not_deleted, never as a success. Contract: - MCP tool `delete_linkedin_scheduled_post`, registry package `mcp.linkedin/linkedin_posting`, mount `linkedin.content`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `posting` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Get scheduled LinkedIn posts Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_posting/get-scheduled-linkedin-posts /api-reference/linkedin/openapi.yaml post /api/linkedin-posting/get-scheduled-posts One page of the account's SCHEDULED-posts queue: drafts waiting for their publication time, each with post_urn, scheduled_at, text and LinkedIn's own error_message (wire get-scheduled-posts). A PRIVATE read - nobody on LinkedIn sees it - but it spends the one Content bucket, posting (20/day in series of 3 with a 1200 s pause; free plan 4/day), shared with publishing. Pass author_organization_id (the bare numeric company id) to read a COMPANY PAGE's queue the account administers instead of the member's own. Offset-paged: page_size 1-100 (default 20), pass paging.next_cursor back as cursor. rows[].post_urn is the exact handle delete_linkedin_scheduled_post takes. Scheduling itself is not exposed through this API yet - posts are scheduled in LinkedIn's own UI; this pair reads and cleans the queue. Contract: - MCP tool `get_linkedin_scheduled_posts`, registry package `mcp.linkedin/linkedin_posting`, mount `linkedin.content`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `posting` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # React to LinkedIn post Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_posting/react-to-linkedin-post /api-reference/linkedin/openapi.yaml post /api/linkedin-posting/react Leave OUR reaction on any LinkedIn post OR comment (wire create-reaction): entity_urn takes a post urn in any family, a post URL carrying one, or a comment urn; a post is reacted on at its social thread (see entity_urn). The social-selling counterpart of create_linkedin_comment. Outward and fire-on-success: the post does NOT need to be tracked, nothing is persisted here. Identity-bound: linkedin_account_sid REQUIRED, spends the react_posts bucket (30/day at a 360 s floor), saturation returns 429. Returns reaction_urn (the reaction's key, naming the entity it landed on; null on drift or when LinkedIn ignored it) plus the activity-log row. interested is the EVENT-post reaction: ordinary posts silently ignore it (success envelope, null reaction_urn, nothing lands - verified live). Takes no key: LinkedIn keeps one reaction per account and post, so repeating the same reaction changes nothing; every refusal still names error.context.send_outcome. To comment use create_linkedin_comment. Contract: - MCP tool `react_linkedin_post`, registry package `mcp.linkedin/linkedin_posting`, mount `linkedin.content`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; mass action: this verb's own request accepts a filter or targets[] set, which the owning service drains; step eligible: gtm.service.orchestration can run this verb as a mass-action plan step, once per item; paced: a call spends the `react_posts` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Remove LinkedIn reaction Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_posting/remove-linkedin-reaction /api-reference/linkedin/openapi.yaml post /api/linkedin-posting/unreact Remove OUR reaction from a LinkedIn post or comment, addressed by the same entity_urn react_linkedin_post took (wire delete-reaction). Undo a reaction left by mistake or by a play that has been retargeted. Takes no reaction_type: LinkedIn holds at most one reaction per account per entity, so removal is unambiguous. Identity-bound: linkedin_account_sid REQUIRED, spends the SAME react_posts bucket as reacting (30/day), but an undo is paced 2 s, not the bucket delay, so a reaction and its removal fit back to back; saturation returns 429. A post is addressed at its social thread exactly like react_linkedin_post, so undoing with the value you reacted with removes the reaction that landed. removed: true means LinkedIn accepted the delete, not that a reaction existed (a delete with nothing to remove answered true live, 2026-09-16); a refused removal comes back 409 reaction_not_removed. Contract: - MCP tool `unreact_linkedin_post`, registry package `mcp.linkedin/linkedin_posting`, mount `linkedin.content`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `react_posts` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Repost LinkedIn post Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_posting/repost-linkedin-post /api-reference/linkedin/openapi.yaml post /api/linkedin-posting/repost Repost a LinkedIn post from one of the team accounts (wire create-instant-repost / create-repost-with-thoughts): ONE tool, the body picks which. Without text it is the plain repost (the feed's Repost button) and nothing else may be sent. With text it is a repost with your thoughts, a share of your own above the reposted post, which takes visibility, allowed_commenters_scope and mentions exactly as create_linkedin_post does. post_urn is the post being reposted as urn:li:activity, urn:li:share or urn:li:ugcPost (a group post cannot be reposted). Public and outward; retract with delete_linkedin_post on the answered activity_urn. Spends the posting bucket. Sends once per client_reference and reposted post (as LinkedIn files it, whichever of its urns you name): a repeat is answered, never sent twice; on a 409 read error.context.send_outcome, never resend under a new key. A plain repost has no deadline on LinkedIn's side, so one whose answer was lost stays in doubt for an hour. Contract: - MCP tool `repost_linkedin_post`, registry package `mcp.linkedin/linkedin_posting`, mount `linkedin.content`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; paced: a call spends the `posting` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Build LinkedIn search URL Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_scraping/build-linkedin-search-url /api-reference/linkedin/openapi.yaml post /api/linkedin-scraping/build-search-url Build the LinkedIn URL of a people or company search, regular or Sales Navigator, from that search's filters: send exactly ONE of people / sales_nav_people / companies / sales_nav_companies. Facet values take an id or plain text; each text is resolved by the search's own typeahead (the option labelled exactly so, else the first; a multi-word miss is retried by its first word), at most 10 lookups per call, each a paced scraping read (Sales Navigator ones need an SN seat). A text nothing matches is a 422 naming the closest options, except on Sales Navigator titles and companies, where it stays free text. Returns url (Sales Navigator in its ?query= form, the one imports read), filters in the exact shape the matching search tool takes, and resolved: per text, the id picked and the alternatives. All ids given: no LinkedIn call at all. Recruiter has no build: its URL comes from running scrape_linkedin_search_recruiter_people. Contract: - MCP tool `scrape_linkedin_build_search_url`, registry package `mcp.linkedin/linkedin_scraping`, mount `linkedin.scraping`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `scraping` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Look up LinkedIn facet id Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_scraping/look-up-linkedin-facet-id /api-reference/linkedin/openapi.yaml post /api/linkedin-scraping/param-id-lookup Facet-id typeahead for the REGULAR (non Sales Navigator) search: turn a human term ("United States", "SaaS", "Acme") into the id a filter member needs. One type per call, and the type picks the member: people → followers_of, connections → connections_of, location → locations, company → current_companies, industry → industries, service_category → service_categories. Rows are the dropdown itself, best match first, about ten of them, with no paging and no size knob. An empty rows list means the dropdown offered nothing for that term, which is an answer, not an error. It types into the account's already-open tab, so no page is loaded, no search is spent and the people-search commercial-use limit is untouched. Cheapest method here. Sales Navigator ids are a different space: use scrape_linkedin_sales_nav_param_id_lookup. Contract: - MCP tool `scrape_linkedin_param_id_lookup`, registry package `mcp.linkedin/linkedin_scraping`, mount `linkedin.scraping`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `scraping` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Look up LinkedIn Recruiter facet id Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_scraping/look-up-linkedin-recruiter-facet-id /api-reference/linkedin/openapi.yaml post /api/linkedin-scraping/recruiter-param-id-lookup LinkedIn Recruiter facet-id typeahead: resolve one Recruiter search facet (type) for a typed query into the ids the scrape_linkedin_search_recruiter_people filters take (urn:li:ts_*:N; the lowercase language name for spoken_languages). A THIRD id space: neither the flagship nor the Sales Navigator lookup ids fit here. Eleven kinds, all text facets (query required); LinkedIn answers a fixed 10. Needs a Recruiter-seat executor. Costs a scraping-bucket slot per call. Contract: - MCP tool `scrape_linkedin_recruiter_param_id_lookup`, registry package `mcp.linkedin/linkedin_scraping`, mount `linkedin.scraping`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `scraping` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Look up Sales Navigator facet id Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_scraping/look-up-sales-navigator-facet-id /api-reference/linkedin/openapi.yaml post /api/linkedin-scraping/sales-nav-param-id-lookup Sales Navigator facet-id typeahead: resolve one SN facet (type) into the opaque ids the SN search filters need. type is a node salesApiFacetTypeahead kind passed through verbatim; the seven text facets resolve query, the rest return their fixed / account-scoped list. Type → target filter member: TITLE → current_titles/past_titles; BING_GEO → locations/company_headquarters; INDUSTRY → industries; COMPANY_WITH_LIST → current_companies/past_companies; GROUP → groups; SCHOOL → schools; CONNECTION_OF → connections_of; ACCOUNT_LIST → account_lists (company search). Skip it for the static kinds (COMPANY_SIZE/FUNCTION/SENIORITY_V2/RELATIONSHIP/COMPANY_TYPE/TENURE/PROFILE_LANGUAGE): their full id sets are inlined in the search filter schemas. PERSONA/LEAD_LIST/LEAD_INTERACTIONS/SAVED_LEADS_AND_ACCOUNTS have no filters member yet. SN executor required. Contract: - MCP tool `scrape_linkedin_sales_nav_param_id_lookup`, registry package `mcp.linkedin/linkedin_scraping`, mount `linkedin.scraping`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `scraping` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Scrape company decision makers Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_scraping/scrape-company-decision-makers /api-reference/linkedin/openapi.yaml post /api/linkedin-scraping/company-decision-makers Sales Navigator’s decision-makers panel for ONE company: the bounded, seniority-weighted leadership/buying-committee list SN computes. SN-capable executor required. Contract: - MCP tool `scrape_linkedin_company_decision_makers`, registry package `mcp.linkedin/linkedin_scraping`, mount `linkedin.scraping`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `scraping` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Scrape company employees Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_scraping/scrape-company-employees /api-reference/linkedin/openapi.yaml post /api/linkedin-scraping/company-employees List the people currently working at ONE company (the company page’s "employees" people-search, page-numbered). Heavy, page-numbered list. Contract: - MCP tool `scrape_linkedin_company_employees`, registry package `mcp.linkedin/linkedin_scraping`, mount `linkedin.scraping`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `scraping` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Scrape event search Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_scraping/scrape-event-search /api-reference/linkedin/openapi.yaml post /api/linkedin-scraping/search-events One page of a LinkedIn EVENT search. `filters` carries keywords and NOTHING else (this engine has no location, date, type or organizer facet, and any other key is refused by name), so a pasted /search/results/events/ `url` is the only way to any facet the events screen itself offers: send exactly one of the two. Rows are events keyed by event_ln_id, with a tracking-free /events/ URL, title, the date and location lines as printed, the organizer when the card names one, the blurb, the attendee counter (0 is real, null is unread) and a cover image. The English dependency is PARTIAL: rows still come back on another interface language, but date_text and location_text can swap and organizer and attendee_count go null, silently. Page on with has_more, never on paging.total. Contract: - MCP tool `scrape_linkedin_search_events`, registry package `mcp.linkedin/linkedin_scraping`, mount `linkedin.scraping`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `scraping` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Scrape group search Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_scraping/scrape-group-search /api-reference/linkedin/openapi.yaml post /api/linkedin-scraping/search-groups One page of a LinkedIn GROUP search, the fifth row kind on this surface. `filters` carries keywords and nothing else, exactly like the events search, so a pasted /search/results/groups/ `url` is the only way to anything the groups screen offers beyond a keyword: send exactly one of the two. Rows are groups keyed by group_ln_id, with a /groups/ URL, the name, Public or Private, the member counter both as printed and parsed (0 is real, null is unread), a best-effort blurb and a logo. WARNING, the English dependency is TOTAL, worse than the jobs search: the name is read out of English UI copy and a card whose name cannot be read is dropped, so a non-English executor returns ZERO rows and still reports success. Page on with has_more, never on paging.total. Contract: - MCP tool `scrape_linkedin_search_groups`, registry package `mcp.linkedin/linkedin_scraping`, mount `linkedin.scraping`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `scraping` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Scrape job-posting search Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_scraping/scrape-job-posting-search /api-reference/linkedin/openapi.yaml post /api/linkedin-scraping/search-jobs One page of a LinkedIn JOB-POSTING search, addressed EITHER by `filters` (keywords is REQUIRED here, which no other engine on this surface demands) OR by a pasted /jobs/search-results/ `url`, which is the only way to hybrid or on-site work, several locations, a radius around one, verified jobs and the other facet segments LinkedIn ships: exactly one of the two. Rows are postings keyed by job_ln_id, with a tracking-free /jobs/view/ URL, title, hiring company, location plus workplace_type, listing age, the salary line as printed, and the Easy Apply / early-applicant / verified flags. Card text is read off an ENGLISH LinkedIn UI: an executor whose interface is another language returns zero rows and still reports success. Page on with has_more, never on paging.total. Contract: - MCP tool `scrape_linkedin_search_jobs`, registry package `mcp.linkedin/linkedin_scraping`, mount `linkedin.scraping`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `scraping` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Scrape LinkedIn company search Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_scraping/scrape-linkedin-company-search /api-reference/linkedin/openapi.yaml post /api/linkedin-scraping/search-companies One page of a regular LinkedIn company search, addressed EITHER by `filters` (keywords, geo ids, industry ids, headcount buckets) OR by a pasted /search/results/companies/ `url`: exactly one of the two, never both, never neither. Rows are company previews, not people, carrying the card's industry, followers (LinkedIn's rounded count) and location next to the identity. Same engine and the same company rows either way; page on with paging.has_more. For revenue, growth, follower, Fortune and buying-signal facets use scrape_linkedin_search_sales_nav_companies instead: this engine has none of them. Contract: - MCP tool `scrape_linkedin_search_companies`, registry package `mcp.linkedin/linkedin_scraping`, mount `linkedin.scraping`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `scraping` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Scrape LinkedIn Learning course search Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_scraping/scrape-linkedin-learning-course-search /api-reference/linkedin/openapi.yaml post /api/linkedin-scraping/search-courses One page of a LinkedIn LEARNING course search, the sixth row kind here: `filters` OR a pasted /search/results/learning/ `url` (that screen, NOT /courses/), exactly one of the two. filters is a REQUIRED keywords plus two closed vocabularies, difficulty and time_to_complete, whose snake_case values the NODE maps onto LinkedIn's own labels, so never send a label yourself; the screen's software and subject facets need the url form. Rows are courses keyed by course_slug (this wire has no numeric id), with a tracking-free /learning/ URL, title, instructor, a normalised duration, the release and viewer lines as printed, and artwork. WARNING, the English dependency is TOTAL, as on groups: the title is read out of the "Save the course X" button label and a card without a readable title is DROPPED, so a non-English executor returns ZERO rows at success. Page on has_more, never on paging.total. Contract: - MCP tool `scrape_linkedin_search_courses`, registry package `mcp.linkedin/linkedin_scraping`, mount `linkedin.scraping`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `scraping` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Scrape LinkedIn people search Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_scraping/scrape-linkedin-people-search /api-reference/linkedin/openapi.yaml post /api/linkedin-scraping/search-people One page of a regular LinkedIn people search, addressed EITHER by `filters` OR by a pasted search `url`: exactly one of the two, never both, never neither. Use filters when the agent composes the search itself, url for a search already built in the LinkedIn UI or for anything the filter vocabulary cannot express. Same engine and the same person rows either way; page on with paging.has_more. Rows carry the card's headline, location and degree badge next to the identity (position / company_name are Recruiter-only). commercial_use_limit_hit true means LinkedIn's monthly search paywall fired and rows may be truncated: switch to the Sales Navigator people tool or wait for the reset. Contract: - MCP tool `scrape_linkedin_search_people`, registry package `mcp.linkedin/linkedin_scraping`, mount `linkedin.scraping`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `scraping` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Scrape LinkedIn product search Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_scraping/scrape-linkedin-product-search /api-reference/linkedin/openapi.yaml post /api/linkedin-scraping/search-products One page of a LinkedIn PRODUCT search, the seventh row kind here: `filters` OR a pasted /search/results/products/ `url`, exactly one of the two. filters is a REQUIRED keywords plus a one-way free_version toggle and two arrays of digit-string ids that come from DIFFERENT places: product_category from param-id-lookup type=product_category, product_company from the ordinary organization id space. Nothing reconciles them, so a swapped id returns an empty page, not an error. Rows are product pages keyed by product_slug (the wire calls it product_id, but it is a slug and must never be sent back as an id), with a tracking-free /products/ URL, name, vendor, tagline, top features and a connections counter. Unlike courses, a non-English executor still returns named rows here, but category_text and tagline can silently carry the WRONG text. Page on has_more, never on paging.total. Contract: - MCP tool `scrape_linkedin_search_products`, registry package `mcp.linkedin/linkedin_scraping`, mount `linkedin.scraping`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `scraping` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Scrape LinkedIn Recruiter people search Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_scraping/scrape-linkedin-recruiter-people-search /api-reference/linkedin/openapi.yaml post /api/linkedin-scraping/search-recruiter-people One page of a LinkedIn Recruiter (talent) people search, by a structured filters object (the Recruiter facets: titles, skills, companies, locations, postal codes, industries, education, languages, seniority, function, company size and type, year sliders, network, open-to-work) OR by a pasted Recruiter search url. 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 on your page; every answer returns search_history_id and search_url, so page 2 onwards is addressed by the URL alone. 25 hits a page, page 1..40, a real total; rows carry recruiter_id (for send_linkedin_recruiter_message), headline, location, current position, can_send_inmail. Needs a Recruiter-seat executor with a live session (422 recruiter_required, 409 recruiter_reauth_required); 429 recruiter_search_usage_limit is LinkedIn's per-seat throttle, wait a minute. Ids via scrape_linkedin_recruiter_param_id_lookup. One scraping-bucket slot per page. Contract: - MCP tool `scrape_linkedin_search_recruiter_people`, registry package `mcp.linkedin/linkedin_scraping`, mount `linkedin.scraping`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `scraping` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Scrape LinkedIn school search Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_scraping/scrape-linkedin-school-search /api-reference/linkedin/openapi.yaml post /api/linkedin-scraping/search-schools One page of a LinkedIn SCHOOL search, the eighth row kind here: `filters` OR a pasted /search/results/schools/ `url`, exactly one of the two. filters is a REQUIRED keywords and NOTHING else, the same one-member shape the events search has, so every facet the schools screen offers lives only in the url form. There is NO school-id filter: the ids param_id_lookup returns for type=school fit nothing here. Rows are school pages keyed by school_slug (the wire calls it school_id, it is a slug, it can be URL-encoded, and it is not that lookup id), with a tracking-free /school/ URL, name, location line, a students-and-alumni counter and a blurb. Names and slugs survive a non-English executor, but description can silently carry the students line. Page on has_more, never on paging.total. Contract: - MCP tool `scrape_linkedin_search_schools`, registry package `mcp.linkedin/linkedin_scraping`, mount `linkedin.scraping`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `scraping` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Scrape post comments Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_scraping/scrape-post-comments /api-reference/linkedin/openapi.yaml post /api/linkedin-scraping/get-post-comments Direct LinkedIn read (bypasses our DB): one page of the COMMENTS on ONE post, live. This is the only comments read there is - a row carries the comment AND the person who wrote it (content, posted_at, comment_permalink, reactions_count, replies_count, is_pinned, is_edited + member id, nickname, name, headline), annotated with is_own, so reading "what was written" and "who commented" is a single call and never two. Target any post by URL, activity URN, or the backend urn a company-page post / newsletter issue / group thread carries (share, ugcPost, groupPost) in post. Top-level comments only: replies are not on this wire at all, replies_count is a count. Order with sort_order; one wire page per call, page on with cursor. Contract: - MCP tool `scrape_linkedin_get_post_comments`, registry package `mcp.linkedin/linkedin_scraping`, mount `linkedin.scraping`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `scraping` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Scrape post reactors Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_scraping/scrape-post-reactors /api-reference/linkedin/openapi.yaml post /api/linkedin-scraping/get-post-reactors Direct LinkedIn read (bypasses our DB): live pull of who is reacting to ONE post - or ONE COMMENT - right now, as transient reactor objects with reaction_type in OUR write vocabulary (interested = the event-post reaction), annotated with is_own. Target a post by URL or urn in any family (activity, share, ugcPost, groupPost) in post, or target a comment by its urn (either form: urn:li:comment:(...) or the fsd urn:li:fsd_comment:(...)) to read the comment's reactors. LinkedIn files a post's reactions under its social thread, which is the ugcPost or groupPost urn for those posts and the activity urn for a share post; this tool names the thread for you and result.post.post_ln_id is the urn the feed was read by. One wire page per call; page on with cursor. Contract: - MCP tool `scrape_linkedin_get_post_reactors`, registry package `mcp.linkedin/linkedin_scraping`, mount `linkedin.scraping`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `scraping` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Scrape post resharers Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_scraping/scrape-post-resharers /api-reference/linkedin/openapi.yaml post /api/linkedin-scraping/get-post-resharers Direct LinkedIn read (bypasses our DB): live pull of the profiles that RESHARED one post, the third and typically highest-intent leg of the engagement trio. Targeting mirrors the reshare feed's own grammar (2026-08-21): a post URL, an activity URN, or a backend urn:li:share: / urn:li:ugcPost: urn verbatim - NOT urn:li:groupPost:, which is refused 422 reshare_target_unsupported (a group post has no reshare feed). is_own annotated. When exposed, resharer_commentary carries the added text and reshare_urn is the reshare’s own activity URN (feed it back into get-post-comments / -reactors). One wire page per call; page on with cursor. Contract: - MCP tool `scrape_linkedin_get_post_resharers`, registry package `mcp.linkedin/linkedin_scraping`, mount `linkedin.scraping`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `scraping` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Scrape post search Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_scraping/scrape-post-search /api-reference/linkedin/openapi.yaml post /api/linkedin-scraping/search-posts LinkedIn content search: find POSTS by keywords, recency or author. The discovery front door for post-engagement plays (feed the returned post_ln_id into the commenters / reactors / resharers tools). Addressed EITHER by `filters` (keywords REQUIRED) OR by a pasted /search/results/content/ `url`, which the node parses back into the same filter object: exactly one of the two, never both, never neither. CURSOR paginated, unlike every other search here: feed paging.next_cursor back verbatim and size the page with page_size. A page is the end only when LinkedIn itself answers that the window is empty; an answer the platform cannot read is refused (503 linkedin_page_unreadable, retry in a minute), never returned as an empty page. Contract: - MCP tool `scrape_linkedin_search_posts`, registry package `mcp.linkedin/linkedin_scraping`, mount `linkedin.scraping`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `scraping` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Scrape Sales Navigator company search Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_scraping/scrape-sales-navigator-company-search /api-reference/linkedin/openapi.yaml post /api/linkedin-scraping/search-sales-nav-companies One page of a Sales Navigator ACCOUNT search, the one engine here that filters on annual revenue, company and per-department headcount growth, department headcount, follower buckets, Fortune tier and buying signals (leadership changes, funding events). Addressed EITHER by `filters`, whose typeahead members take ids from scrape_linkedin_sales_nav_param_id_lookup while the static enums are inline, OR by a pasted /sales/search/company `url`: exactly one of the two. SN-capable executor required. Contract: - MCP tool `scrape_linkedin_search_sales_nav_companies`, registry package `mcp.linkedin/linkedin_scraping`, mount `linkedin.scraping`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `scraping` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Scrape Sales Navigator people search Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_scraping/scrape-sales-navigator-people-search /api-reference/linkedin/openapi.yaml post /api/linkedin-scraping/search-sales-nav-people One page of a Sales Navigator people search: richer facets than the regular engine (seniority, function, tenure, headcount, company type), no commercial-use limit, SN URNs (sn_id) on the rows. Addressed EITHER by `filters`, which carry the COMPLETE SN facet vocabulary, OR by a pasted /sales/search/people `url`: exactly one of the two. Typeahead members take [{id, text, exclude}] values from scrape_linkedin_sales_nav_param_id_lookup; the static-enum members list their ids inline and need no lookup. Needs an SN-capable executor (a non-SN own account gets 422 sales_nav_required; the auto-pick filters to SN seats). Contract: - MCP tool `scrape_linkedin_search_sales_nav_people`, registry package `mcp.linkedin/linkedin_scraping`, mount `linkedin.scraping`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `scraping` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Scrape service-provider search Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_scraping/scrape-service-provider-search /api-reference/linkedin/openapi.yaml post /api/linkedin-scraping/search-service-providers One page of LinkedIn's service-provider marketplace (freelancers and agencies), addressed EITHER by `filters` (keywords, LinkedIn's own numeric service-category and geo ids, connection degree, profile language) OR by a pasted /search/results/services/ `url`, which is how to reach anything those five cannot express: exactly one of the two. Rows are the same person previews the people search returns, because the services screen carries no ratings and no service labels in its results, only provider profiles. Contract: - MCP tool `scrape_linkedin_search_service_providers`, registry package `mcp.linkedin/linkedin_scraping`, mount `linkedin.scraping`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `scraping` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Scrape similar companies Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_scraping/scrape-similar-companies /api-reference/linkedin/openapi.yaml post /api/linkedin-scraping/similar-companies LinkedIn’s "similar companies / pages people also viewed" list for ONE target company: lookalike account expansion. One-shot list. Contract: - MCP tool `scrape_linkedin_similar_companies`, registry package `mcp.linkedin/linkedin_scraping`, mount `linkedin.scraping`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `scraping` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Scrape similar profiles Source: https://docs.gtm-api.com/api-reference/linkedin/linkedin_scraping/scrape-similar-profiles /api-reference/linkedin/openapi.yaml post /api/linkedin-scraping/similar-profiles LinkedIn’s "people also viewed" browsemap for ONE seed profile: lookalike expansion from a known-good persona. IMPORTANT: address by profile.nickname (the vanity slug) ONLY, because the wire navigates by vanityName, so a ln_id/sn_id URN is refused not_dispatchable (resolve it via enrich_linkedin_person_lite_profile first, then pass the nickname). Returns ~20 cards; rows carry ln_member_id + nickname + full_name + headline (no ACoAA id, no avatar, no degree). One-shot list (no pagination); limit caps the cards. Contract: - MCP tool `scrape_linkedin_similar_profiles`, registry package `mcp.linkedin/linkedin_scraping`, mount `linkedin.scraping`. - Operation `action`, response envelope `action`. - Flags: paced: a call spends the `scraping` smart-limit bucket of the account it runs on, and calls of one bucket are spaced per account. A short wait is slept by the server; a longer one answers 429 `rate_limited` with `context.retry_after`, and the same call succeeds from that moment. Parallel calls on one account queue behind each other. # Retry mass-action items Source: https://docs.gtm-api.com/api-reference/orchestration/mass_action_items/retry-mass-action-items /api-reference/orchestration/openapi.yaml post /api/mass-action-items/retry Re-enter FAILED items of a mass action AT their current_step: completed steps never run again, their step_log entries and minted objects stay. Target EXACTLY ONE of sid (ma_im_…) or filter (both or neither: 422 sid_xor_filter_required); the filter arm is not parent-scoped, so pass mass_action_sid unless you mean every run. Per item: status pending, retry_count++, the error, timings and wait_reason cleared; parents settle again when drained. Only failed items match (retried_count 0 is not an error), 10 retries each at most, never an item of a deleted run or a cancelled one: by sid 422 retry_limit_exceeded and 409 cancelled_cannot_retry, by filter left alone. A send is never sent again blind: one failed send_unconfirmed or item_timeout is asked about under its key first; one with no key stays failed (left_unconfirmed_count) until a person checks the channel and retries it by sid with resend_unverified. Contract: - MCP tool `retry_mass_action_items`, registry package `mcp.orchestration/mass_action_items`, mount `orchestration.mass_actions`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API; mass action: this verb's own request accepts a filter or targets[] set, which the owning service drains. # Search mass-action items Source: https://docs.gtm-api.com/api-reference/orchestration/mass_action_items/search-mass-action-items /api-reference/orchestration/openapi.yaml post /api/mass-action-items/search Per-target drill-down of a mass action: one row per target with current_step, the object cursor (object_type / object_sid, on a generative run what the row created) and step_log[], one entry per plan step with status (running / deferred / succeeded / failed / skipped), executor_ref (the key a send step went out under, next to its doubt fields), created_object_sid for steps that minted one, and error_message. The last step_log entry says what failed and why, earlier ones what completed: a failed item keeps the effects of its finished steps (fail-forward), created_object_sid is the cleanup surface. Filter by mass_action_sid, then narrow by status / current_step / wait_reason; object_sid is parent-scoped, so always pair it with mass_action_sid (unpaired it degrades to a full team scan). Sort defaults to position asc (insertion order; position 1 is the canary); page_size 0 returns pagination.total_count alone. No include[] and no counts block; run-level aggregation lives on the parent, mass-actions metrics. Contract: - MCP tool `search_mass_action_items`, registry package `mcp.orchestration/mass_action_items`, mount `orchestration.mass_actions`. - Operation `search`, response envelope `search`. - Flags: read only. # Append mass-action items Source: https://docs.gtm-api.com/api-reference/orchestration/mass_actions/append-mass-action-items /api-reference/orchestration/openapi.yaml post /api/mass-actions/{sid}/append-items Feed more leads into a run that is already going, without a second preview or consent token: the plan they execute is the one already approved, and appended items join the pacing chain BEHIND the tail. Identities are payload-kind leads, the shape a targets-scope create takes: at least one of ln_member_id (the decoded member NUMBER), ln_id / sn_id / profile_id (a LinkedIn URN), company_ln_id or nickname (a slug). A lead already enrolled comes back in skipped_duplicate_count, not run twice: the dedup is by member id, whichever URN each source used. Up to 100 per call; repeat to keep a standing run fed. The run has to be active (a paused one is 409 run_not_active, a stopped one too) and payload-addressed: created with scope targets or none. A generate run (step 1 is a creates: verb) is 422 run_not_payload_addressed, each appended lead would provision one more paid browser beyond the count consented to; an objects run is 422 too, its items address existing rows. The canary gate still applies. Contract: - MCP tool `append_mass_action_items`, registry package `mcp.orchestration/mass_actions`, mount `orchestration.mass_actions`. - Operation `action`, response envelope `action`. - Flags: none. # Create mass-action Source: https://docs.gtm-api.com/api-reference/orchestration/mass_actions/create-mass-action /api-reference/orchestration/openapi.yaml post /api/mass-actions Commit a previewed plan into a running bulk dispatch. Requires the commit_token from preview_mass_action over EXACTLY these inputs: the server re-derives the HMAC, so an edited plan, a changed scope or a different caller is 422 invalid_commit_token, and a token older than 15 minutes is 422 commit_token_expired. Re-preview in either case. Re-runs the full preview validation, then inserts the run plus its items and starts dispatching in one transaction. Always asynchronous, even for one item: the returned sid IS the monitoring handle. Poll get_mass_action with include=metrics or get_mass_actions_metrics, or subscribe to the mass-actions.settled / .paused webhooks. With canary_mode=first_item only item 1 runs until it succeeds; a canary failure pauses the run with paused_reason=canary_failed. A none-scope run is created empty (total_count 0) and dispatches nothing until an auto-scrape appends leads into it. Replaying a still-valid token creates a SECOND identical run, so discard the token after use. Contract: - MCP tool `create_mass_action`, registry package `mcp.orchestration/mass_actions`, mount `orchestration.mass_actions`. - Operation `create`, response envelope `create`. - Flags: none. # Get mass-action Source: https://docs.gtm-api.com/api-reference/orchestration/mass_actions/get-mass-action /api-reference/orchestration/openapi.yaml get /api/mass-actions/{sid} Fetch one bulk run by sid: the plan that runs, status / paused_reason / hold_till, the canary gate and settled_at. include=metrics adds the item breakdown, which is where outcome lives. Contract: - MCP tool `get_mass_action`, registry package `mcp.orchestration/mass_actions`, mount `orchestration.mass_actions`. - Operation `get`, response envelope `get`. - Flags: read only. # Mass-action metrics Source: https://docs.gtm-api.com/api-reference/orchestration/mass_actions/mass-action-metrics /api-reference/orchestration/openapi.yaml post /api/mass-actions/metrics Where a run is right now, in one round-trip, without paging items. Typical call: filter {sid:{eq}}. Returns items_by_status (the outcome split), items_by_current_step, items_by_wait_reason, in_flight (0 means the run is caught up), the next / last scheduled item, created_objects and the top error prefixes. No group_by: the shape is fixed. Use this instead of listing a run's items to answer 'how is it going'. Contract: - MCP tool `get_mass_actions_metrics`, registry package `mcp.orchestration/mass_actions`, mount `orchestration.mass_actions`. - Operation `metrics`, response envelope `metrics`. - Flags: read only. # Pause mass-action Source: https://docs.gtm-api.com/api-reference/orchestration/mass_actions/pause-mass-action /api-reference/orchestration/openapi.yaml post /api/mass-actions/{sid}/pause Freeze dispatch: status becomes paused with paused_reason='manual'. Queued jobs roll back at pre-flight and in-flight items finish their current step, then stop at the boundary; vendor calls already on the wire are never aborted. Pass hold_till to arm auto-resume at that moment, omit it to wait for an explicit resume_mass_action. Idempotent: pausing a paused run refreshes it. Contract: - MCP tool `pause_mass_action`, registry package `mcp.orchestration/mass_actions`, mount `orchestration.mass_actions`. - Operation `action`, response envelope `action`. - Flags: none. # Preview mass-action plan Source: https://docs.gtm-api.com/api-reference/orchestration/mass_actions/preview-mass-action-plan /api-reference/orchestration/openapi.yaml post /api/mass-actions/preview Validate a bulk plan without running anything and mint the consent token create_mass_action consumes. ALWAYS the first call of a bulk dispatch. One pass, all findings at once: plan shape (1..3 steps), step-eligibility of each tool, scope shape and size (1..100), the generate-scope rule (step 1 must mint an object), the send-class schedule mandate. A tool outside the step vocabulary is 422 with field_errors["plan.steps.{i}.tool"][0].rule = "not_step_eligible". Nothing is persisted, charged or created. Result: preview (items_count, steps_per_item, dangerous_steps, eta, warnings), commit_token, expires_at. eta.starts is when item 1 starts (ISO when scheduled, else asap); eta.estimated_completion_at is starts plus (items_count - 1) mean gaps, null for ASAP or an empty scope. warnings are non-blocking, today one per step arg with an unresolved {{placeholder}} (args go out verbatim). Show the preview, then pass the token to create_mass_action UNCHANGED with the same inputs (an HMAC over them). Tokens live 15 min. Contract: - MCP tool `preview_mass_action`, registry package `mcp.orchestration/mass_actions`, mount `orchestration.mass_actions`. - Operation `action`, response envelope `action`. - Flags: read only. # Release mass-action canary Source: https://docs.gtm-api.com/api-reference/orchestration/mass_actions/release-mass-action-canary /api-reference/orchestration/openapi.yaml post /api/mass-actions/{sid}/release Open the canary gate by hand: stamps canary_satisfied_at and releases every remaining item, the same sweep the canary would have triggered by succeeding. Use it when the first item failed for a reason you accept, or when you do not want to wait for it. Valid only while the run is live, canary_mode=first_item and the gate is still closed, else 409 invalid_transition. Releasing a PAUSED run stamps the gate but dispatches nothing until resume_mass_action: the pause dominates. Contract: - MCP tool `release_mass_action_canary`, registry package `mcp.orchestration/mass_actions`, mount `orchestration.mass_actions`. - Operation `action`, response envelope `action`. - Flags: none. # Resume mass-action Source: https://docs.gtm-api.com/api-reference/orchestration/mass_actions/resume-mass-action /api-reference/orchestration/openapi.yaml post /api/mass-actions/{sid}/resume Unfreeze dispatch: status becomes active, paused_reason and hold_till are cleared, and pending items are re-planned onto a fresh jitter chain from now. Items that deferred mid-cascade restart at their current step, so completed steps are not re-run. Does NOT touch the canary gate: after a canary_failed pause only the retried canary dispatches, and release_mass_action_canary is what opens the gate. 409 invalid_transition if the run is not paused. Contract: - MCP tool `resume_mass_action`, registry package `mcp.orchestration/mass_actions`, mount `orchestration.mass_actions`. - Operation `action`, response envelope `action`. - Flags: none. # Search mass-actions Source: https://docs.gtm-api.com/api-reference/orchestration/mass_actions/search-mass-actions /api-reference/orchestration/openapi.yaml post /api/mass-actions/search List the team's bulk runs with filter, sort and cursor pagination. Filter by status (active / paused), paused_reason, target_entity, canary_satisfied_at (is_null:true = the canary gate is still closed), or the created / updated / started / settled time ranges. status is a CONTROL axis, not an outcome: to learn how a run went call get_mass_actions_metrics or get_mass_action with include=metrics. Stopped runs are excluded unless deleted_at is filtered explicitly. page_size:0 returns the count alone. Contract: - MCP tool `search_mass_actions`, registry package `mcp.orchestration/mass_actions`, mount `orchestration.mass_actions`. - Operation `search`, response envelope `search`. - Flags: read only. # Stop and delete mass-action Source: https://docs.gtm-api.com/api-reference/orchestration/mass_actions/stop-and-delete-mass-action /api-reference/orchestration/openapi.yaml delete /api/mass-actions/{sid} STOP a bulk run. This is the cancel path and it works at any status: pending and queued items are swept to cancelled, running items finish their current step and stop at the boundary, an auto-scrape feeding a standing run stops being able to append, and the run is soft-deleted. The cascade block reports how many were cancelled and how many were still in flight. Items stay queryable; there is no undo through MCP, and a plan cannot be edited, so restarting means a fresh preview and create. Contract: - MCP tool `delete_mass_action`, registry package `mcp.orchestration/mass_actions`, mount `orchestration.mass_actions`. - Operation `delete`, response envelope `delete_cascade`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Cancel webhook delivery Source: https://docs.gtm-api.com/api-reference/orchestration/webhook_logs/cancel-webhook-delivery /api-reference/orchestration/openapi.yaml post /api/webhook-logs/{sid}/cancel Cancel a webhook delivery (status → cancelled); no further retries. Allowed from pending / retrying / in_progress; terminal rows → 422 cannot_cancel_terminal. Cancelling an in_progress row may still flip back to success if the worker in-flight attempt returns 2xx (re-read the row; result.in_flight_attempt_pending signals this). Pass expected_status for a CAS write (409 on mismatch). Aborts a pending delivery; state-changing. Contract: - MCP tool `cancel_webhook_log`, registry package `mcp.orchestration/webhook_logs`, mount `orchestration.webhooks`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Retry webhook delivery Source: https://docs.gtm-api.com/api-reference/orchestration/webhook_logs/retry-webhook-delivery /api-reference/orchestration/openapi.yaml post /api/webhook-logs/{sid}/retry Manually re-arm a webhook delivery row: flips status to pending, sets scheduled_at=now(), clears cancel_reason. retry_count stays (it counts executed attempts, a re-arm is not one). error_message is preserved (cleared only on the next successful terminal outcome). Allowed from every state but in_progress (409 lease_in_progress), cancelled included whatever the reason; rejected on a soft-deleted / disabled parent webhook (422). Many rows at once: replay_webhook on the parent, paced. Pass expected_status for a CAS write (409 on mismatch). Re-delivers to the integrator; state-changing. Contract: - MCP tool `retry_webhook_log`, registry package `mcp.orchestration/webhook_logs`, mount `orchestration.webhooks`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Search webhook logs Source: https://docs.gtm-api.com/api-reference/orchestration/webhook_logs/search-webhook-logs /api-reference/orchestration/openapi.yaml post /api/webhook-logs/search List webhook delivery rows for the caller's team with filter, sort, include and group-counts. Deliveries from every producer service (LinkedIn, email, …) land in this one log; filter per webhook, per account_sid, per source_service, per event type, per HTTP code or status; include=webhook. Returns a counts block (groups by status / event_type / response_code / webhook_sid / account_sid). Use for delivery auditing and integrator-outage debugging. For period-bound success rates use get_webhook_logs_metrics; single-row lookup is a filter on sid. Contract: - MCP tool `search_webhook_logs`, registry package `mcp.orchestration/webhook_logs`, mount `orchestration.webhooks`. - Operation `search`, response envelope `search`. - Flags: read only. # Webhook logs metrics Source: https://docs.gtm-api.com/api-reference/orchestration/webhook_logs/webhook-logs-metrics /api-reference/orchestration/openapi.yaml post /api/webhook-logs/metrics Period-bound aggregates over webhook deliveries: success_rate, failure_rate, p50/p95/p99 latency, total_retries, last_success_at / last_failure_at. The rates and the two timestamps count TERMINAL outcomes only (rows in status success / failed): a delivery still in its retry loop is neither, so it shows in counts.groups.status.retrying and total_retries here and in the webhook row's consecutive_failed_attempts / last_failure_at (which count every attempt), not in these rates; a period with nothing terminal reads null, not 0. Requires period {from,to} (≤ 90 days). Optional filter scopes the row set (created_at operators inside filter are ignored; period is the only time window) and an optional single group_by axis (status, event_type, response_code, webhook_sid, account_sid). Contract: - MCP tool `get_webhook_logs_metrics`, registry package `mcp.orchestration/webhook_logs`, mount `orchestration.webhooks`. - Operation `metrics`, response envelope `metrics`. - Flags: read only. # Create webhook Source: https://docs.gtm-api.com/api-reference/orchestration/webhooks/create-webhook /api-reference/orchestration/openapi.yaml post /api/webhooks Create a webhook subscription endpoint. The server generates a 32-char hex secret and returns it ONCE in this response (masked on every subsequent read). Store it client-side for HMAC verification. target_url must be https:// and public (non-RFC1918); each events[] value is validated against WebhookEventTypeEnum; the same target_url cannot be registered twice per team. State-changing. Contract: - MCP tool `create_webhook`, registry package `mcp.orchestration/webhooks`, mount `orchestration.webhooks`. - Operation `create`, response envelope `create`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Delete webhook Source: https://docs.gtm-api.com/api-reference/orchestration/webhooks/delete-webhook /api-reference/orchestration/openapi.yaml delete /api/webhooks/{sid} Soft-delete a webhook (deleted_at set; webhook stops matching new events). Existing webhook_logs remain queryable but un-retryable; pending / retrying deliveries are cancelled. Destructive and one-way (no API undo). Contract: - MCP tool `delete_webhook`, registry package `mcp.orchestration/webhooks`, mount `orchestration.webhooks`. - Operation `delete`, response envelope `delete_simple`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Get webhook Source: https://docs.gtm-api.com/api-reference/orchestration/webhooks/get-webhook /api-reference/orchestration/openapi.yaml get /api/webhooks/{sid} Fetch a single webhook by sid. Use include=latest_webhook_logs for the recent delivery view. secret is masked (null) on reads. Contract: - MCP tool `get_webhook`, registry package `mcp.orchestration/webhooks`, mount `orchestration.webhooks`. - Operation `get`, response envelope `get`. - Flags: read only. # Replay webhook deliveries Source: https://docs.gtm-api.com/api-reference/orchestration/webhooks/replay-webhook-deliveries /api-reference/orchestration/openapi.yaml post /api/webhooks/{sid}/replay Put the deliveries a broken endpoint left behind back on the wire: every webhook_logs row of this webhook created inside [from, to] whose status is in statuses (default failed + cancelled; success re-fires delivered events, opt-in) goes back to pending, oldest first, one row per second, and the normal scheduler delivers it under the same webhook_log_sid. Cancelled rows come back only when the platform cancelled them (webhook_failed / webhook_disabled / webhook_deleted), never an operator's manual cancel; rows in flight are never touched. from is required; the window spans at most 30 days and one call re-arms at most 5000 rows, refused with the cap named beyond either (narrow the window or statuses). Requires status=on: re-enable a failed webhook first (update_webhook status:'on'). Rate-limited to 10 calls/min per webhook. Re-delivers real events; state-changing. Contract: - MCP tool `replay_webhook`, registry package `mcp.orchestration/webhooks`, mount `orchestration.webhooks`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Search webhooks Source: https://docs.gtm-api.com/api-reference/orchestration/webhooks/search-webhooks /api-reference/orchestration/openapi.yaml post /api/webhooks/search List webhook subscriptions owned by the caller's team with filter, sort and cursor pagination. Filter by status, subscribed event types (events.contains / contains_any), account narrowing or name; include=latest_webhook_logs for the recent-delivery view. secret is masked (null) on every read. Use this to find a webhook sid before calling webhook-scoped tools. Contract: - MCP tool `search_webhooks`, registry package `mcp.orchestration/webhooks`, mount `orchestration.webhooks`. - Operation `search`, response envelope `search`. - Flags: read only. # Test webhook Source: https://docs.gtm-api.com/api-reference/orchestration/webhooks/test-webhook /api-reference/orchestration/openapi.yaml post /api/webhooks/{sid}/test Synchronously POST a synthetic payload to the webhook target_url and return the receiver response (HTTP status, duration, error kind). Verifies endpoint health / signature handling without waiting for a real event; does NOT write to webhook_logs and does NOT affect the consecutive-failure counter. Allowed while status=failed (revival flow); rejected while status=off. Rate-limited to 10 calls/min per webhook. Outward live HTTP probe. Contract: - MCP tool `test_webhook`, registry package `mcp.orchestration/webhooks`, mount `orchestration.webhooks`. - Operation `action`, response envelope `action`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # Update webhook Source: https://docs.gtm-api.com/api-reference/orchestration/webhooks/update-webhook /api-reference/orchestration/openapi.yaml patch /api/webhooks/{sid} Patch a webhook. Editable fields: name, target_url, events, filters, status. status:'on' on a failed webhook resets the consecutive-failure counter and resumes deliveries, but re-issues nothing: the rows the disable cancelled come back with replay_webhook. status:'failed' cannot be set manually. events and filters are FULL replacements, not merges. secret is NOT writable here (rotation is admin-internal). State-changing. Contract: - MCP tool `update_webhook`, registry package `mcp.orchestration/webhooks`, mount `orchestration.webhooks`. - Operation `update`, response envelope `update`. - Flags: dangerous: the MCP layer gates it behind a preview/commit token, and the effect cannot be undone through this API. # API reference overview Source: https://docs.gtm-api.com/api-reference/overview How to read the reference: base URLs, auth, the envelope, and the x-gtm operation flags. The reference documents the public contract of three services. Every page is generated from the same registry that defines the MCP tools (one endpoint per tool), so it matches what the server validates, not what someone remembered to write down. ## Base URLs | Service | Base URL | | - | - | | LinkedIn | `https://app.gtm-api.com/linkedin/v4` | | ID and Teams | `https://app.gtm-api.com/id/v4` | | Orchestration | `https://app.gtm-api.com/orchestration/v4` | Auth is the same everywhere: `Authorization: Bearer ` with an [API key or JWT](/authentication). The playground on each page can send real requests; point it at production and use a test team. ## Reading an operation Beyond the request and response schemas, each operation carries flags from the registry. They render in the description and as `x-gtm-*` extensions in the raw spec, so both a person and a client generator can act on them: | Flag | Meaning | | - | - | | `dangerous` | The MCP layer gates this call behind a preview and confirm step, and the effect cannot be undone through the API | | `mass action` | The verb's own request accepts a `filter` or `targets[]` set, which the owning service drains | | `step eligible` | Orchestration can run the verb as a step of a [mass action plan](/guides/run-a-mass-action) | | `schedule required` | Bulk use of the verb must be paced with a schedule | | `availability` | `ga`, or `stub_501` for endpoints whose contract is final but not yet live | ## Deprecated means "not yet", not "going away" Operations marked deprecated are `stub_501` endpoints: they validate the full request and then answer `501 not_implemented`. The request contract is final, so you can build against it today; the marker exists so a generated client tells you not to ship against it yet. The reason is in the operation's description. ## Two serialization details * On `GET` and `DELETE`, object parameters (`filter`, `sort`) travel as JSON text in the query string, and array parameters repeat as `name[]=value`. Details in [Pagination and filtering](/concepts/pagination-and-filtering). * Success and error bodies are envelopes shared by every endpoint. Details in [Envelopes and errors](/concepts/envelopes-and-errors). # Authentication Source: https://docs.gtm-api.com/authentication API keys, session JWTs and team scoping. Every request to every service authenticates with one header: ``` Authorization: Bearer ``` Two token kinds are accepted, and the server tells them apart by shape. ## API keys (recommended for integrations) An API key is an opaque bearer token of the form `gtm_live_` plus 40 characters. There is no OAuth handshake: you paste it into your runtime and send it as the bearer token on every call. * **The secret is shown once.** `POST /api/api-keys` returns the full token in `plaintext_token` in that one response. Afterwards the API returns only `token_prefix` and `token_last4` for display. The server stores a SHA-256 hash, not the key. * **Rotation replaces the secret.** `POST /api/api-keys/{sid}/rotate` mints a new `gtm_live_` secret for the same key record and invalidates the old one. Rotate on any suspicion of a leak, or on a schedule. * **Keys expire.** Default expiry is 3 years from creation; pass `expires_at: null` on create for a perpetual key. * **Keys carry permissions.** A key created by another key can only hold a subset of the parent's permissions. Downscoping is allowed, escalation is a `forbidden` error. * **Keys are team-scoped.** A key belongs to the team it was created in and acts on that team's data. * **A key is not a person.** It authenticates as itself, with its own permissions, so no user account sits behind it. `GET /api/users/current` (`get_current_user`) therefore answers `403` with `context.reason: not_a_user_actor` on a key, and no permission you add to the key changes that: the profile that endpoint returns belongs to a signed-in person. Sign in, or connect through OAuth, when you actually need it. Manage keys in the app or over the [ID and Teams API](/api-reference/id): search them for an access audit (`last_used_at` shows stale keys worth revoking), check `expires_at` for keys about to lapse, and revoke what you no longer use. ## Session JWTs Signing in to [app.gtm-api.com](https://app.gtm-api.com/login) issues a short-lived JWT. It goes in the same `Authorization: Bearer` header. This is what the app itself uses; for server-to-server integrations prefer an API key, which does not expire mid-job. ## The Team-SID header A token is normally already scoped to one team, and that scope is authoritative. The optional `Team-SID` header exists for tokens that do not carry a team: it selects which of your teams the call acts on. ``` Team-SID: ts_tm_Hx7kQ3mN2pL4 ``` When the token already names a team, `Team-SID` is ignored. With an API key you can leave it out entirely. ## Auth failures | Code | HTTP | Meaning | | - | - | - | | `unauthorized` | 401 | Missing, expired, revoked or malformed token | | `forbidden` | 403 | Valid token, but no rights on this team or resource | Both arrive as the standard [error envelope](/concepts/envelopes-and-errors), so they are machine-readable like every other failure. ## Handling the secret Treat `gtm_live_` tokens like passwords: keep them in a secret manager or environment variable, never in client-side code or a repository, and never in logs. If a key leaks, rotate it; the old secret stops working immediately. # Changelog Source: https://docs.gtm-api.com/changelog What's new in the GTM API platform and this documentation. **Sandbox, the forever free plan, now includes one webhook.** A free workspace can register one endpoint with `POST /api/webhooks`, and that endpoint may subscribe to every event type with `events: ["*"]`. A second registration answers `402 webhook_limit_reached` with `used` and `limit`, the same refusal a paid workspace gets past its 100. A trigger in n8n, Make or Zapier registers a webhook of its own, so on Sandbox one trigger runs at a time. Before this release every one of them answered `402` on a free workspace. Realtime push stays off on Sandbox, so a received message reaches the webhook when a sync reads it. See [billing and plans](/kb/billing-and-plans) and [receive webhooks](/guides/receive-webhooks). **`invite-to-event` invites up to 25 members to a LinkedIn event, in one call.** Send the event's numeric id (`event_ln_id`, the one a `search-events` row carries and the number in the event's `/events//` URL) and a list of people, each by member id (`ln_id`) or by nickname; a nickname alone costs one profile read first, and a Sales Navigator id is not an address here. The whole list goes out as one invitation batch from the account's open LinkedIn tab and spends one `networking_general` unit. LinkedIn decides who may invite and who may be invited (1st-degree connections, by the organizer or by a member LinkedIn offers its Invite button to): the answer is one flag for the batch, and a refused batch is `409 event_invite_rejected` with the ids that were sent. A member already invited counts as success; one who already accepted is skipped. On MCP the tool is `invite_linkedin_members_to_event` on the `/mcp/linkedin/network` mount, next to the other invitation verbs. **`build-search-url` returns the LinkedIn URL of a search.** Send the filters of a people or company search, regular or Sales Navigator, with each value as an id or as plain text, and get back the search URL, the same filters with every text turned into its id, and what each text matched along with the other options LinkedIn offered. A text is resolved by that search's own typeahead, one lookup per distinct text; when every value already carries an id, nothing runs on LinkedIn. Sales Navigator URLs come in their `?query=` form. See [people and company search](/kb/people-and-company-search#getting-the-search-url). **`search-companies` takes `network` and `has_jobs`.** `network: ["1st"]` keeps companies where you have 1st-degree connections, and `has_jobs: true` keeps companies hiring on LinkedIn right now. An auto scrape over the company search takes them too. **Sales Navigator facets need their id outside titles and companies.** LinkedIn matches a value without an id as free text on job titles and companies only; on a location, a headquarters region, an industry or a school it ignores it, and the search ran as if that filter had not been sent. `search-sales-nav-people` and `search-sales-nav-companies` now refuse such a value with `422 facet_id_required`, and an auto scrape saved with one stops with the same error. Resolve the text with `sales-nav-param-id-lookup`, or send it to `build-search-url`. **A toggle sent as `1` counts.** `open_to_volunteer`, the Sales Navigator account toggles (`hiring_on_linkedin`, `first_degree_connection`, `saved_accounts_only`) and the `exclude` and `required` flags on filter values were accepted as `1` or `"1"` and then left out of the search. They now count as `true`. **A parked browser is tried again when its cause can clear.** A browser parked in `support_error_investigation` is started again every three hours until it runs. One parked in `client_error_investigation` on its proxy is started again after three hours, then six, then twelve, then once a day for two weeks, so a proxy you fix in your GoLogin account comes back without a Restart. A parking on any other cause of yours still waits for you. **A Restart of a parked browser no longer alerts twice.** When a parked browser is started again and fails on the same cause, it goes back to its parking quietly: no second email, no second escalation. When it fails on a cause the other side owns, it is parked again under that verdict, once. **Retrying under the same `idempotency_key` works.** A scraping or enrichment call that failed with a `503` or a `429` and `retry_after` used to answer every repeat under its key with the same stored failure, for good. Now the key returns that failure only until `retry_after`; a repeat after it runs the call again, and the key then names the new run. Every other outcome (a completed run, a refusal of the input, a final failure) answers the key as before. **No `task-failed` for a Sales Navigator call that went through.** When LinkedIn refuses a Sales Navigator call with `SALES_SEAT_REQUIRED` while the seat is live, the platform reopens the session and runs the call again. The refused first attempt no longer fires `linkedin-account-activity-log.task-failed`, and a Sales Navigator message sent on the retry no longer fires `linkedin-messages.send-failed` first. **A GoLogin profile that comes back empty says so.** A browser whose GoLogin profile loaded with no data used to show "An unexpected error occurred. Press Run to fix it." It now says what happened and what to do: close the profile wherever else it is open and press Run, and restore or rebind it if it keeps failing. **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](/kb/inbox-and-message-sync) has the details. **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](/kb/billing-and-plans) has the details. **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](/concepts/sending-once) and [Receive webhooks](/guides/receive-webhooks) have the details. **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](/concepts/sending-once) and [Run a mass action](/guides/run-a-mass-action) have the details. **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](/kb/antidetect-browsers-and-proxies#browser-lifecycle) has the details. **`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. **`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. **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](/kb/browser-troubleshooting#restricted-linkedin-took-the-account-away). **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_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:` 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. **`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. **`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 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. **`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`. **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. **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](/concepts/reasons) page, generated from the same table the backends answer from. **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. **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. **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. **`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`. **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 `), 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. **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--` share link and a `/feed/update/urn:li:ugcPost:/` 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. **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.`). 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. **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. **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. **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. **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. **`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. **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/` 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 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. **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. **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 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. **`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. **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. **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`. **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`. **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. **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. **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. **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. **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](/mcp/overview). **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. # Envelopes and errors Source: https://docs.gtm-api.com/concepts/envelopes-and-errors One response wrapper for every success, one error format with a closed 16-code taxonomy. Every endpoint in every service answers with one of two envelopes. You write the parsing code once. ## The success envelope A success body always carries `success: true`, the `operation` that names the payload shape, the payload itself, and a `meta` block. | `operation` | Payload | Returned by | | - | - | - | | `search` | `items[]`, `counts`, `pagination`, `applied_filters`, `includes` | `POST /api/{entity}/search` | | `get` | `item`, `included` | `GET /api/{entity}/{sid}` | | `create` | `item`, plus flags such as `already_exists` where relevant | `POST /api/{entity}` | | `update` | `item`, `changed_fields` | `PATCH /api/{entity}/{sid}` | | `delete` | deletion report | `DELETE /api/{entity}/{sid}` | | `metrics` | period-bound aggregates | `POST /api/{entity}/metrics` | | `action` | `action` name plus a verb-specific result | verb endpoints like `POST /api/mass-actions/{sid}/pause` | A `search` response in full: ```json theme={null} { "success": true, "operation": "search", "items": [ { "item": { "sid": "wh_hk_Jk2mP9nR4qW1", "status": "active" }, "included": {} } ], "counts": { "total_count": 247, "groups": { "status": { "active": 240, "disabled": 7 } } }, "pagination": { "next_cursor": "eyJpZCI6MTAwfQ==", "has_more": true, "total_count": 247 }, "applied_filters": { "status": { "eq": "active" } }, "includes": [], "meta": { "trace_id": "0198f2ab-7c11-7e32-9a41-d2b64f2a91c3", "span_id": "9a41d2b64f2a91c3", "timestamp": "2026-07-28T12:00:00Z", "duration_ms": 42 } } ``` Points worth knowing: * **`items[]` splits `item` from `included`.** The `item` is always the same typed shape; relations you request via `include` arrive next to it under `included`, never mixed into it. * **`counts` covers the full filter scope, not the page.** With 50 rows shown out of 1,000 matching, `counts.total_count` is 1000. * **`applied_filters` echoes what the server actually applied**, so a client (or an AI agent) can confirm the query it asked for is the query that ran. * **`meta.trace_id` is the support handle.** It is a UUID v7, identical to the `X-Trace-Id` response header. Quote it when reporting an issue. ## The error envelope Every failure, from any service, is the same shape: ```json theme={null} { "success": false, "error": { "code": "validation_failed", "message": "URL does not match the Sales Navigator format", "recoverable": true, "suggestion": "Make sure the URL starts with linkedin.com/sales/ and contains search parameters", "field_errors": { "source_url": [ { "rule": "regex", "message": "Expected URL like linkedin.com/sales/search/*" } ] } } } ``` * `code` is always one of the 16 values below, never an arbitrary string. * `recoverable` says whether retrying with corrected input can succeed (`true`) or the state itself is the problem (`false`). * `field_errors` appears only on `validation_failed`, keyed by field. Each entry's `rule` is the machine reason (`ln_member_id_not_numeric`, `resend_not_available`) and its `message` the sentence for it; the same reason appears as `error.context.reason` on a `conflict`, `rate_limited` or `service_unavailable`. Every reason and its sentence is listed on the [Reasons](/concepts/reasons) page, generated from the backends' own catalogs. * `delete_blocked` errors additionally carry `error.blockers[]`: each blocker names its `type`, a `hard` or `soft` severity, and the `resolution` that clears it. ## Send endpoints name their outcome An error from an endpoint that sends something to LinkedIn (a message, an InMail, a voice note, a connection request, a comment, a post, a repost, a reaction) also carries `error.context.send_outcome`. It is `not_sent` when nothing went out, `in_flight` while the send is running, `unknown` when it may have gone out, or `sent`. Read it before you retry. Anything but `not_sent` is handled by sending the same request again under the same `client_reference` or by asking check-sent, never by sending it as a new one. A reaction is the exception: it takes no key, and the same reaction sent again changes nothing on LinkedIn. [Sending once](/concepts/sending-once) has the whole rule. ## The 16 error codes | Code | HTTP | Meaning | | - | - | - | | `validation_failed` | 422 | Invalid input; details in `field_errors` | | `nothing_to_update` | 422 | PATCH with no real changes | | `not_found` | 404 | The sid does not exist in this team's scope | | `relation_not_found` | 422 | A referenced entity (for example a `*_sid` field) does not exist | | `invalid_transition` | 409 | The state machine forbids this move | | `limit_exceeded` | 429 | A business limit was hit (daily budgets, plan caps) | | `payment_required` | 402 | The plan does not include this | | `duplicate_rejected` | 409 | An entity with the same natural key already exists | | `conflict` | 409 | The request conflicts with current state; editing fields will not fix it | | `delete_blocked` | 409 | Blockers exist; see `error.blockers[]` | | `unauthorized` | 401 | Missing or invalid token | | `forbidden` | 403 | No rights on this team or resource | | `rate_limited` | 429 | Throttled; slow down and retry | | `not_implemented` | 501 | Contract is final, capability not shipped; do not retry | | `internal_error` | 500 | A bug on our side; report the `trace_id` | | `service_unavailable` | 503 | A downstream dependency is down; retry with backoff | A practical mapping: retry `rate_limited` and `service_unavailable` with backoff, fix and resend `validation_failed` and `relation_not_found`, treat the 409 family as "re-read the current state first", and never auto-retry `not_implemented` or `internal_error`. On a send endpoint, read `error.context.send_outcome` before any of these. # Pagination and filtering Source: https://docs.gtm-api.com/concepts/pagination-and-filtering Cursor paging, typed per-field filters, includes, and how objects travel in query strings. Every list endpoint (`POST /api/{entity}/search`) takes the same four inputs: `filter`, `sort`, `page_size` and `cursor`, plus an optional `include` list. ## Paging * `page_size` defaults to 50. The ceiling is per endpoint, 100, 200, 500 or 1000 depending on how heavy the table is; each endpoint's reference states its own range, and asking for more than it allows is a `validation_failed` rather than a clamp. * Responses return `pagination.next_cursor`, an opaque string. Pass it back as `cursor` to get the next page; `null` means the last page. Cursors are forward-only, and there is no offset or page number. * `page_size: 0` is count-only mode: `items` comes back empty and you read `counts.total_count`. It is the cheapest way to answer "how many match". * `page_size: 1` is the get-first idiom: one row plus a cursor, for "does anything match, and what is the newest one". ```json theme={null} { "filter": { "status": { "eq": "active" } }, "page_size": 100, "cursor": "eyJpZCI6MTAwfQ==" } ``` ## Filtering `filter` is a typed object, validated per entity. Each filterable field takes an object of operators; which operators a field supports is spelled out in the reference for that endpoint. ```json theme={null} { "filter": { "status": { "in": ["active", "sync_failed"] }, "created_at": { "gte": "2026-07-01T00:00:00Z" }, "q": "cooper" } } ``` ### The operator vocabulary Nine operators exist. Which ones a given field accepts is stated in the reference for that endpoint, and asking for one a field does not accept is an error rather than a silent no-op. | Operator | Takes | Reads as | | - | - | - | | `eq` | a scalar | equals | | `ne` | a scalar | does not equal | | `in` | an array | matches any of | | `nin` | an array | matches none of | | `gt`, `gte` | a scalar | after / at or after (dates), greater than (numbers) | | `lt`, `lte` | a scalar | before / at or before, less than | | `is_null` | a boolean | the column is empty, or is not | Two rules that are easy to miss: * **Multiple operators on one field are ANDed.** `{"created_at": {"gte": "...", "lt": "..."}}` is a half-open range, which is how you page a window by time rather than by cursor. * **A bare scalar is shorthand for `eq`.** `{"status": "active"}` and `{"status": {"eq": "active"}}` mean the same thing. `q` is full-text search where the entity supports it. It is a reserved field inside `filter`, not a top-level parameter, and each endpoint's description names the columns it scans. An unknown filter field, an unsupported operator, or a value of the wrong type answers `validation_failed` with a `field_errors` entry naming the offending path. Nothing is silently dropped, so a filter that returns everything is a filter you did not send, not one that was ignored. The response echoes what actually ran in `applied_filters`, which is the fastest way to confirm a filter reached the backend in the shape you meant. ## Sorting `sort` is a single object with a `field` and an optional `direction`, for example `{ "field": "created_at", "direction": "desc" }`. Direction defaults to `desc`, and one sort field applies per request. The sortable fields are listed per endpoint. ## Includes `include` names relations to load with each row, for example `["metrics"]` on API keys. Loaded relations arrive under `items[].included`, keyed by relation name, and the response's top-level `includes` echoes what you asked for. If a requested relation is absent under `included` for a row, that row has none; if it was never in `includes`, it was not requested. The two cases stay distinguishable. ## Objects in query strings Search runs over `POST` with a JSON body, so this mostly concerns `GET` and `DELETE` endpoints that accept the same parameters: * An object parameter (`filter`, `sort`) travels as JSON text in the query string: `?filter={"status":{"eq":"active"}}` (URL-encoded). * An array parameter repeats as `name[]=value`: `?include[]=metrics&include[]=owner`. The playground on each reference page builds these for you, which is the quickest way to see the exact wire format. # Reasons Source: https://docs.gtm-api.com/concepts/reasons Every machine reason the platform answers with, and what it means. Generated from the backends' error catalogs; do not edit by hand. An error envelope carries one of the 16 [error codes](/concepts/envelopes-and-errors#the-16-error-codes) and, under it, a **reason**: the machine word for what exactly went wrong. On `validation_failed` it is `field_errors.[].rule`, on `conflict`, `rate_limited` and `service_unavailable` it is `error.context.reason`. The `message` next to it is the sentence below, so an agent branches on the reason and a person reads the message; the ids, dates and limits the sentence refers to are in `error.context` (a `retry_after` timestamp, a `resend_available_at`, a `max_bytes`). This page is generated from the same table the backends answer from (one catalog per service, checked in CI so no reason is thrown without a sentence). 230 reasons across 4 services. | Reason | What it means | Service | | - | - | - | | `access_type_identity_mismatch` | *the field* does not match access\_type: an internal session carries no access key, an external one carries no connected user. | linkedin | | `account_already_shared` | This account is already shared; recall the share before sharing it again. | id | | `account_bound_other_team` | **linkedin:** This LinkedIn member is already connected in another workspace; one member lives in one workspace at a time.
**email:** This mailbox is already connected in another workspace. | linkedin, email | | `account_currently_shared` | This account is shared out right now; recall the share before transferring it. | id | | `account_erasure_pending` | This account is deactivated and waiting for erasure; use the cancel link in the deactivation email to sign in again. | id | | `account_missing_ln_id` | The LinkedIn account behind this call has no profile URN of its own yet, so it cannot address the wire; wait for its initial sync to finish (linkedin\_accounts.ln\_id fills on the first profile read). | linkedin | | `account_not_active` | This email account is not active (see context.status), so it cannot send; reconnect it first. | email | | `account_not_in_team` | That LinkedIn account is not usable here: it is missing, deleted, or belongs to another workspace. | linkedin | | `account_not_lendable` | This account cannot be lent: it has not completed its login yet. | linkedin | | `account_not_shareable` | The channel refused to lend this account (see context.detail): it is not in a state that can be handed over. | id | | `account_not_syncable` | This account has no sync runs (see context.detail). | email | | `account_not_transferable` | The channel refused to transfer this account (see context.reason): it is not in a state that can be handed over. | id | | `account_sending_paused` | Sending from this account is paused (see context.reason); resume it, or send from another account. | email | | `account_shared_out` | This LinkedIn member is lent to another workspace right now, and only that workspace acts with it; recall the share before connecting it again or sending from it. | linkedin | | `acme_unreachable` | The certificate authority did not answer; retry in a minute. | id | | `addon_not_on_subscription` | This add-on is not on the subscription, so there is nothing to remove or change. | id | | `already_erased` | This account was erased; nothing of it remains to act on. | id | | `attachment_fetch_failed` | The attachment could not be downloaded from the provider (see context.detail). | email | | `attachment_too_large` | The attachments together exceed the size a send may carry (context.max\_bytes). | email | | `attachments_too_large` | The attachments together exceed the 35 MB a send may carry; send fewer or smaller files. | linkedin | | `attributes_not_supported_in_thread` | Rich-text attributes apply to the first message that opens a Recruiter thread only; a reply into an existing thread takes plain text. | linkedin | | `audio_transcoder_unavailable` | The audio converter is not available on this host right now; retry in a minute. | linkedin | | `audio_unreadable` | The audio could not be read as a sound file; send an mp3, m4a, ogg, wav or webm file, as a URL or as base64. | linkedin | | `auto_scrape_not_active` | This auto-scrape is not active; resume it before running. | linkedin | | `automation_server_in_maintenance` | This automation server is in maintenance and takes no browsers. | linkedin | | `automation_server_mismatch` | The browser is registered on a different automation server than the caller; the fleet reconciles this on its own. | linkedin | | `billing_change_unconfirmed` | The billing provider did not confirm this change, and it may have gone through; check the subscription before trying again. | id | | `body_fetch_failed` | The message body could not be fetched from the provider (see context.detail); a sync refreshes its locator. | email | | `bounded_scan_required` | *the field* must narrow the search with eq or in; a search of this table across the whole workspace is refused. | linkedin | | `browser_already_bound` | This browser is already bound to a different LinkedIn member; unbind it, or use another browser. | linkedin | | `browser_already_connected` | A cloud browser session on this browser is already open; join it or wait for it to end. | linkedin | | `browser_maintenance` | The browser's host is in maintenance; retry after the window. | linkedin | | `browser_profile_missing` | The bound browser has no profile at the vendor, so there is nothing to launch; create or import a profile first. | linkedin | | `browser_starting` | The browser is starting; retry in a few seconds. | linkedin | | `browser_unavailable` | The browser could not serve this call (the tab died or the browser is gone); retry once it is running. | linkedin | | `browser_unreachable` | The browser is not reachable right now (stopped, restarting, or its host is down); start it and retry. | linkedin | | `bucket_saturated` | This account has used its budget for this action type; retry after retry\_after. | linkedin | | `callback_host_registry_unavailable` | The identity service did not answer the callback-host lookup; retry in a minute. | email | | `cancelled_cannot_retry` | This item's run was deleted (see context.status): a deleted run stays stopped and its items are not retried. Create a new run to reach this target again. | orchestration | | `cannot_issue_deleted_certificate` | This certificate was deleted; add the domain again to issue a new one. | id | | `cannot_rotate_inactive_key` | Only an active API key can be rotated; this one is revoked or expired. | id | | `channel_busy` | The channel is handling its maximum number of handovers; retry this call unchanged in a minute. | id | | `channel_no_cross_cluster` | The two workspaces live on different clusters and this channel cannot hand an account across clusters. | id | | `channel_unavailable` | The channel service did not answer; retry in a minute. | id | | `channel_unreachable` | The channel service could not be reached; retry in a minute. | id | | `client_reference_frozen` | This message was already sent (or tried) under its client\_reference, which its repeat and check-sent find it by; the client\_reference cannot be set, changed or cleared now. | email | | `comment_not_deleted` | LinkedIn did not delete this comment: it is not this account's, or it is already gone. | linkedin | | `commit_in_progress` | This commit\_token is being committed by another request right now; wait a moment and send the same commit again, it answers the run that request created. | orchestration | | `commit_token_expired` | commit\_token has expired; run the preview again and commit with the new token. | orchestration | | `company_anchor_not_numeric` | source\_input.anchor\_company\_ln\_id must be the numeric LinkedIn company id, not a slug or a URL. | linkedin | | `concurrent_send_in_flight` | An earlier attempt of this send, or another send to the same place, is in the browser, in doubt or already went out (context.send\_outcome says which, context.activity\_log\_sid or blocking\_activity\_log\_sid names it), so nothing was sent now. Repeat the same request after retry\_after, or ask check-sent about the attempt the context names. | linkedin | | `confirmation_count_mismatch` | confirmation\_count does not match the number of accounts the filter matched; re-read the count and confirm again. | id | | `confirmed_sent_contradicts` | The attempt confirmed\_sent names was proved not sent, so the word is not taken and nothing was sent now. Look on LinkedIn again: if it is there, do not send it again; if it is not, repeat the request without confirmed\_sent. | linkedin | | `conversation_deleted` | This thread was deleted and cannot be acted on; sync the inbox again if it should still exist. | linkedin | | `conversation_not_deletable` | This thread has no LinkedIn-side object to delete yet (it was opened by a send that never landed); nothing to remove. | linkedin | | `counterpart_copy_exists` | The receiving workspace already holds a copy of this account. | linkedin | | `daily_limit_exceeded` | This account has used its daily sending budget; pass scheduled\_at and the queue sends when budget allows. | email | | `degree_indeterminate` | LinkedIn did not expose a network distance for this profile (the account itself, a hidden profile, or a page that carries none), so no degree can be recorded. | linkedin | | `disconnect_in_progress` | This cloud browser is disconnecting; wait for it to finish. | linkedin | | `dispatch_in_flight` | A send of this message is on its way (context.send\_outcome in\_flight), or it went out and its row is not stored (sent); ask again after context.retry\_after, or ask check-sent. A sent answer without context.retry\_after is final: the mail went out and no row will come. | email | | `dns_resolvers_unavailable` | The DNS resolvers did not answer, so the domain could not be verified; retry in a minute. | email | | `domain_active_in_another_team` | This domain has an active certificate in another workspace. | id | | `domain_already_active` | This domain already has an active certificate. | id | | `domain_bound_other_team` | This domain is already registered in another workspace. | email | | `domain_required` | domain is required. | id | | `duplicate_host_port` | An automation server is already registered at this ip and port. | linkedin | | `email_box_dump_not_ready` | The mailbox dump is not finished (see context.status); retry once it is done. | email | | `email_box_has_no_bundle` | A secrets merge needs a stored bundle to merge into, and this box has none; send the full bundle. | id | | `email_box_not_replaceable` | **id:** The box named in replaces\_email\_box\_sid is not the workspace's single live box (or is not active or suspended), so it cannot be replaced.
**orchestration:** The current email box is not in a state a new one can replace (see context.status or context.detail); only an active or suspended box is replaced. | id, orchestration | | `email_box_ops_disabled` | This deployment is not a workspace mailbox host; box operations are off here. | email | | `email_box_unaddressable` | This box is not live or has no hostname and access key yet, so it cannot be addressed. | id | | `email_taken` | An account with this email already exists; sign in and connect the agent from the app instead. | id | | `event_invite_rejected` | LinkedIn refused at least one invitation of this batch and the wire does not say which; check that this account may invite to the event (LinkedIn offers it the Invite button there) and that every target is a 1st-degree connection, then retry with the targets that are. | linkedin | | `facet_id_required` | *the field* needs the id: LinkedIn Sales Navigator matches this filter by id only and ignores a value without one. Resolve the text with sales-nav-param-id-lookup, or build the search with build-search-url. | linkedin | | `facet_value_not_found` | LinkedIn's typeahead offers nothing for this text, and this filter takes ids only; check the spelling, or pass an id from the lookup tool. | linkedin | | `filters_must_be_a_non_empty_object` | source\_input.filters must be an object with at least one search filter. | linkedin | | `handover_in_flight` | A handover of this account is already in progress; wait for it to finish. | id | | `handover_in_progress` | Another handover of this account is still in progress; finish or cancel it first. | linkedin | | `handover_payload_mismatch` | The handover rows do not carry the role this step expects; the handover is out of order. | linkedin | | `header_not_allowed` | A send may set X-\* headers, List-Unsubscribe and Reply-To, each named in printable ASCII with no space or colon; drop or rename the header *the field* names. | email | | `header_reserved` | X-Gtm-Message-Sid is the mark the platform stamps on every send, by which check-sent and the Sent-folder sync find it, so a send cannot set it; drop it. | email | | `header_set_by_platform` | This send carries the platform's own one-click List-Unsubscribe, which records an unsubscribe on the stop-list, so it cannot carry another; send without the header, or with track\_opens and track\_clicks off and no unsubscribe link in the body. | email | | `header_value_invalid` | A header value cannot hold a line break, and Reply-To takes one or more email addresses separated by commas; fix the value *the field* names. | email | | `held_by_another_send` | Another send of the same mail to the same recipients is on its way or may already have gone out (context.blocking\_activity\_log\_sid names its attempt, context.blocking\_email\_message\_sid its row, when known), so this one was not sent; ask again after context.retry\_after. | email | | `holder_team_unavailable` | The receiving workspace does not exist or is not active. | id | | `idempotency_key_busy` | A send with this idempotency\_key is in progress; ask again after context.retry\_after. | email | | `idempotency_key_reused` | This idempotency\_key was used for a different send; use a new key for a new message. | email | | `infrastructure_maintenance` | The automation fleet is in maintenance; browsers cannot be started until it ends. | linkedin | | `infrastructure_unavailable` | The automation infrastructure did not answer; retry in a minute. | linkedin | | `initial_sync_pending` | The account's initial sync has not finished; the latest-\* reads work once it has. | linkedin | | `inmail_requires_premium` | InMail needs a Premium LinkedIn account and this one is not Premium; send a regular message, or send from a Premium account. | linkedin | | `invalid_base64` | *the field* is not valid base64; encode the file bytes as standard or url-safe base64 without a data: prefix. | linkedin | | `invalid_commit_token` | commit\_token does not match this plan: preview the same title, scope, plan and schedule again and commit with the token the preview returns. | orchestration | | `invalid_group_by` | group\_by names a field this metrics call cannot split on; use one of the fields the tool lists. | linkedin | | `invalid_source_method` | source\_method is not one of the auto-scrape sources; use one of the values the tool lists. | linkedin | | `invalid_status_transition` | **linkedin:** This status change is not allowed from the current status; check the status first.
**orchestration:** This status change is not allowed from the run's current status (see context.detail). | linkedin, orchestration | | `invalid_transition` | **id:** This status change is not allowed from the member's current status.
**email:** This status change is not allowed while the account is connecting. | id, email | | `invalid_url` | The URL is not a LinkedIn search, post, or people-you-may-know page this auto-scrape can read; paste a linkedin.com search URL or a post link. | linkedin | | `invitation_already_redeemed` | This invitation was already used. | id | | `invitation_expired` | This invitation has expired; ask for a new one. | id | | `invoice_fetch_failed` | The billing provider did not return the invoice; retry in a minute. | id | | `invoice_not_available` | No invoice exists for this transaction (it never billed, or it was refunded before billing). | id | | `invoice_not_ready` | This transaction is not billed yet, so it has no invoice; retry after it bills. | id | | `issuance_already_in_progress` | A certificate for this domain is being issued right now; wait for it. | id | | `latest_refresh_failed` | The live read did not complete; retry in a minute. | linkedin | | `linkedin_account_exists` | A LinkedIn account is already bound to this browser; delete it first, or use it. | linkedin | | `linkedin_page_unreadable` | LinkedIn answered this page in a shape the reader could not read (an interstitial, a session surface or a changed page), so nothing is reported rather than a wrong empty page; retry in a minute, and if it repeats the page reader needs an update. | linkedin | | `login_already_confirmed` | This session already carries a different login result. | linkedin | | `login_error_reason_required` | A login result other than success needs login\_error\_reason. | linkedin | | `login_not_confirmed` | The login of this session was not confirmed, so the browser cannot be run from it. | linkedin | | `mass_action_incompatible` | The mass action targets an entity this auto-scrape's results cannot feed; pick a run whose target\_entity matches the result kind. | linkedin | | `mass_action_registry_unavailable` | The mass-action service did not answer; retry in a minute. | linkedin | | `message_already_recalled` | This message is already recalled; a second recall would spend a call to be told nothing. | linkedin | | `message_not_outbound` | Only a message this mailbox sent can be checked for having gone out; this one was received. | email | | `message_not_reactable` | This message no longer exists on our side (a reset removed it), so it cannot be reacted to. | linkedin | | `message_not_recallable` | This message no longer exists on our side (a reset removed it), so it cannot be recalled. | linkedin | | `message_too_old_to_recall` | LinkedIn allows a recall within 60 minutes of sending, and this message is older; it stays. | linkedin | | `messenger_type_mismatch` | This thread lives on another messenger surface than the verb you called; use the send, delete or mute verb of the thread's surface (LinkedIn, Sales Navigator or Recruiter). | linkedin | | `method_not_available` | This source method has no live fetcher yet; pick another source, or feed the run through append-items. | linkedin | | `missing_invitation_id` | This invitation carries no LinkedIn invitation id or secret, so the wire cannot act on it; sync the invitations and retry. | linkedin | | `missing_profile_id` | This connection row stores no profile URN, so the wire cannot be asked about it; sync the connections and retry. | linkedin | | `no_automation_server_available` | No automation server has capacity for a new browser right now; retry later. | linkedin | | `no_canary_available` | No canary account is available to run the check right now; retry later. | linkedin | | `no_default_team` | This user belongs to no workspace yet; create one or accept an invitation first. | id | | `no_endorsable_skills` | The target lists no skills this account can endorse (none, or all already endorsed). | linkedin | | `no_erasure_pending` | No erasure is pending for this account. | id | | `no_live_browsers` | No browser of this workspace is live to serve the call; start one. | linkedin | | `no_mailbox_license` | This Microsoft account has no mailbox (no Exchange license), so there is nothing to connect. | email | | `no_mass_action_set` | This auto-scrape feeds no mass action; set one with set-mass-action before running. | linkedin | | `no_recognised_filter_members` | None of the keys in source\_input.filters is a filter this search understands; use the keys the tool lists. | linkedin | | `not_a_login_session` | This session is not a login session, so it has no login result to confirm. | linkedin | | `not_connected` | The target is not a 1st-degree connection of this account, and this action needs one; send a connection request first, or act from an account that is connected. | linkedin | | `not_this_message` | **linkedin:** *the field* names no attempt of this same send that is in doubt; name the activity\_log\_sid a 409 or check-sent gave for this client\_reference and place, or for these words and this place without a key.
**email:** *the field* names the activity\_log\_sid of an earlier attempt of this same message (the same client\_reference and recipients, or without a key the same content and recipients) that is in doubt; this one is not. | linkedin, email | | `oauth_app_failed` | This OAuth app is not verified (see context); fix its credentials and probe it again. | email | | `oauth_app_probe_rate_limited` | The OAuth app was probed too often; retry after retry\_after. | email | | `oauth_grant_invalid` | The provider refused this refresh token (revoked, expired, or from a mailbox that changed); connect the account again with a fresh consent. | email | | `oauth_mint_lock_timeout` | Another token mint for this client is in progress; retry in a few seconds. | id | | `oauth_scope_insufficient` | The consent did not grant the mailbox scope this connection needs; connect again and accept the mail permission. | email | | `operation_blocked` | LinkedIn refused this operation for this account; see context.reason for the cause. | linkedin | | `outside_sending_window` | The account's sending window is closed right now; pass scheduled\_at, or widen the window on the account. | email | | `owner_cannot_be_removed` | The workspace owner cannot be removed; transfer ownership first. | id | | `owner_cannot_be_rescoped` | The workspace owner sees every account; a sender slice applies to other members only. | id | | `paddle_unavailable` | The billing provider did not answer; retry in a minute. | id | | `page_size_above_linkedin_max` | LinkedIn's content search serves at most 50 posts per page and answers a larger request with an empty page, so page\_size stops at 50 here; ask for 50 or fewer and follow paging.next\_cursor for the rest. | linkedin | | `paid_subscription_applied` | A paid subscription is applied to this workspace; unapply and cancel it before deleting the workspace. | id | | `partner_sentinel_missing` | The partner-managed workspace has no internal subscription row; provisioning did not finish. | id | | `password_already_set` | This account already has a password; use the reset flow to change it. | id | | `payload_too_large` | The webhook envelope exceeds 65536 bytes once encoded and cannot be delivered; the emitting service keeps an event under the platform cap (gtm.lib.common WebhookPayloadCap). | orchestration | | `period_inverted` | period.from is after period.to; swap them. | linkedin, orchestration | | `period_start_must_be_monday` | period\_start must be the Monday that opens the ISO week. | linkedin | | `period_too_wide` | The period spans more than 90 days; narrow period.from / period.to. | linkedin, id, orchestration, email | | `place_required` | **linkedin:** This client\_reference names sends to more than one place, so check-sent cannot answer for one; ask again with one of the places in context.places.
**email:** This client\_reference names sends to more than one set of recipients; ask with the recipients of the send you mean (to, cc, bcc). context.places lists them. | linkedin, email | | `place_unmatched` | The place asked about is none of the places this client\_reference was sent to; ask with one of the places in context.places, or by the activity\_log\_sid of the send. | linkedin | | `plugin_unavailable` | The browser plugin did not answer; retry once the browser is running. | linkedin | | `post_not_created` | LinkedIn answered the publish without naming a post, so whether this post went out is not known yet and LinkedIn is read to find out; do not publish it again, repeat this same request after retry\_after. A company-page post (author\_organization\_id) also needs an account that administers that page. | linkedin | | `post_not_deleted` | LinkedIn did not delete this post: it is not this account's, or it is already gone. | linkedin | | `post_not_resolvable` | The post link could not be turned into a post id (a shortlink that does not expand, a URL without an activity id); paste the full linkedin.com post URL or the urn:li:activity id. | linkedin | | `post_target_missing` | source\_input names no post: pass activity\_urn, or a post URL. | linkedin | | `price_is_not_an_addon` | This price is not an add-on and cannot be added to a subscription. | id | | `price_is_the_subscription_base_price` | This price is the subscription's base plan, not an add-on; change the plan instead. | id | | `profile_id_or_conversation_required` | Address the send: pass linkedin\_conversation\_sid for an existing thread, or profile\_id (a profile URN or a Sales Navigator id) to open one. | linkedin | | `provider_app_not_configured` | No OAuth client of this provider is configured on this deployment; the connection cannot be probed. | email | | `provider_not_reconnectable` | A hosted mailbox has no external provider to reconnect. | email | | `provider_probe_failed` | The provider's token endpoint could not be reached or refused our client; retry in a minute. | email | | `provider_probe_not_available` | This provider cannot be probed on this deployment yet (see context.detail). | email | | `provision_lock_timeout` | Another provisioning of this partner workspace is in progress; retry in a few seconds. | id | | `proxy_managed_by_owner` | The proxy of a borrowed browser is managed by its owner; ask them to change it. | linkedin | | `quantity_must_be_positive` | quantity must be 1 or more. | id | | `reaction_not_removed` | LinkedIn did not remove a reaction here: this account had none on the post. | linkedin | | `recipient_or_conversation_required` | Address the Recruiter send: pass linkedin\_conversation\_sid for an existing thread, or a recipient (recruiter\_id, ln\_id or sn\_id) to open one. | linkedin | | `recipient_suppressed` | Every recipient is on the suppression list (bounced or unsubscribed); nothing was sent. | email | | `recipient_unresolvable` | This thread stores no id of its counterpart, so a new Recruiter message cannot be addressed; sync the recruiter inbox and retry, or send by recipient id. | linkedin | | `recruiter_reauth_required` | The Recruiter session of this account expired; sign in to Recruiter again from the account page. | linkedin | | `redirect_host_not_allowed` | success\_redirect\_url must point at an allowed host. | email | | `registry_unavailable` | The identity service did not answer the email-box registry call; retry in a minute. | orchestration | | `relation_not_found` | reply\_to\_email\_message\_sid names no message this account holds. | email | | `release_not_supported` | A share is recalled, not released; use recall. | linkedin | | `replied` | The person wrote in this thread after stop\_if\_replied\_after, so nothing was sent; read their message (context.linkedin\_message\_sid) before you send again. | linkedin | | `reply_check_unavailable` | The thread could not be read from LinkedIn right now, and stop\_if\_replied\_after asks for that read before the send; nothing was sent, retry after retry\_after. | linkedin | | `report_end_not_supported` | Only a share reports its end; a transfer completes on its own. | id | | `requester_has_no_email` | The requester's account has no email to reply to; add one before opening a support request. | id | | `resend_not_available` | An earlier connection request from this account to this member stands in the way: it is pending, accepted, cooling down or left on record by a sync reset (context.cause says which). Withdraw a pending one, message an accepted member, or wait for context.resend\_available\_at. | linkedin | | `resend_unverified_needs_sid` | resend\_unverified is your word that one send nobody could confirm did not arrive: give it with the sid of the item you checked, never with a filter. | orchestration | | `resolver_unavailable` | The subject profile could not be resolved right now; retry later. | linkedin | | `retry_limit_exceeded` | This item has been retried 10 times, the most one item takes; check why its step keeps failing (error\_message), then create a new run for this target if it should still go. | orchestration | | `run_in_progress` | A run of this auto-scrape is still in flight; wait for it to finish or stop it. | linkedin | | `run_not_active` | This run does not take new items (see context.detail: it is paused or deleted); resume it, or create a new run. | orchestration | | `run_not_failed` | Only a failed provisioning run can be retried; this one has not failed. | orchestration | | `run_window_expired` | More than 15 minutes passed since the login was confirmed; open a new session. | linkedin | | `sales_nav_required` | This source reads Sales Navigator and the account has no Sales Navigator seat; pick an account with one, or a LinkedIn-search source. | linkedin | | `sales_session_stale` | LinkedIn refused this Sales Navigator call for a stale seat session; the platform re-arms the session or moves the call off the seat on the next attempt, retry after retry\_after. | linkedin | | `scheduled_at_in_past` | scheduled\_at is in the past; omit it to send now, or pass a future time. | email | | `scheduled_change_is_not_cancel` | The scheduled change on this subscription is not a cancellation, so there is no cancellation to undo. | id | | `scheduled_post_not_deleted` | LinkedIn did not delete this scheduled post: it is not this account's, or it was already published or removed. | linkedin | | `search_filters_empty` | *the field* carries no filter the search can use; give at least one member: keywords, a facet value or a toggle. | linkedin | | `send_as_denied` | from\_email is not an address this account owns or is allowed to send as. | email | | `send_outcome_settled` | confirmed\_sent names an attempt that was already proven not sent (context.activity\_log\_sid, why in context.check\_reason), so the word cannot be taken; nothing was sent. | email | | `send_outcome_unknown` | **linkedin:** The answer of an attempt of this send never came back, so it may or may not be on LinkedIn, and nothing more is sent until that is known. Repeat the same request after retry\_after, or settle it with confirmed\_not\_sent or confirmed\_sent naming context.activity\_log\_sid.
**email:** This message may already have gone out (the provider gave no verdict), so it is not sent again until the mailbox shows whether it left; ask again after context.retry\_after, or ask check-sent (context.activity\_log\_sid names the attempt). | linkedin, email | | `session_already_disconnected` | This session is already disconnected. | linkedin | | `shadowban_check_not_pending` | This check is not pending, so its send cannot be confirmed. | linkedin | | `share_not_active` | This share is not active, so it cannot be recalled or returned. | id | | `sid_xor_filter_required` | Provide sid for one item or filter for a set, not neither. | orchestration | | `skill_discovery_failed` | Reading the target's skills from LinkedIn failed; retry, or endorse a skill by id. | linkedin | | `slug_collision` | Another product already carries this slug; the catalog cannot map two products to one slug. | id | | `source_not_parked` | The source rows are not parked under a handover; park them before exporting. | linkedin | | `subject_already_connected` | The subject is already connected to every canary; the check needs a canary that is not connected to them. | linkedin | | `subscription_canceled` | This subscription is canceled and cannot be changed. | id | | `subscription_locked_pending_changes` | This subscription has a scheduled change pending; wait for it to apply or cancel it first. | id | | `subscription_not_floating` | This subscription is already applied to another workspace. | id | | `subscription_required` | This LinkedIn account does not fit the workspace's plan right now, so it takes no action; add a seat to the plan or free one, then retry. | linkedin | | `sync_in_progress` | **linkedin:** A sync of the same kind is running on this account; retry after retry\_after.
**email:** A sync of this account is already running (context.open\_sync\_run\_sid); wait for it to finish. | linkedin, email | | `target_unresolvable` | LinkedIn answered no profile for the target nickname; check the vanity slug, or address the person by their profile URN. | linkedin | | `team_already_has_subscription` | This workspace already has a subscription applied; cancel or replace it first. | id | | `team_partner_managed` | This workspace is managed by a partner; billing changes come through the partner. | id | | `team_unavailable` | That workspace does not exist or was deleted. | id | | `too_many_lookups` | These filters need more typeahead lookups than one call resolves; pass ids for some values (the lookup tools return them), or split the search. | linkedin | | `too_many_recipients` | A send reaches at most 50 recipients across to, cc and bcc together; split it into several sends. | email | | `too_many_splits` | group\_by would produce more than 500 groups; narrow the filter or split on a field with fewer values. | linkedin, id, orchestration | | `tool_busy` | The shadowban checker is at its daily budget; retry after retry\_after. | linkedin | | `tool_disabled` | The shadowban checker is switched off right now. | linkedin | | `tool_misconfigured` | The shadowban checker is not available right now. | linkedin | | `tracking_domain_not_verified` | The account's tracking domain is not verified, so tracking cannot be injected; verify the domain or send without tracking. | email | | `unpark_giver_not_supported` | A share has no giver-side unpark; recall it instead. | linkedin | | `unpark_not_supported` | A transfer is not unparked; it completes or it is cancelled. | linkedin | | `vendor_profile_busy` | The vendor profile is in use by another browser right now; wait for it to be released. | linkedin | | `vendor_proxy_update_failed` | The vendor did not accept the proxy change; the browser was restarted with its previous proxy, retry. | linkedin | | `voice_exceeds_60s` | A voice message is at most 60 seconds long and this audio is longer; trim it. | linkedin | | `voice_not_supported_on_recruiter` | Voice messages exist on the LinkedIn messenger only; this thread is a Recruiter thread, use send-recruiter with text. | linkedin | | `voice_not_supported_on_sales_navigator` | Voice messages exist on the LinkedIn messenger only; this thread is a Sales Navigator thread, use send-sales-nav with text. | linkedin | | `wildcard_must_be_alone` | The events wildcard (\*) stands alone; do not list other event types next to it. | orchestration | # Sending once Source: https://docs.gtm-api.com/concepts/sending-once One key per send. A repeated send is answered with what the first one came to, and nothing goes out twice. A send can lose its answer. The request times out, the connection drops, a proxy returns an error page, and by then the message, the invitation or the comment may or may not be on LinkedIn. Only the platform can look and tell, so the send endpoints are built to make guessing unnecessary: you give every send a key, every answer says what came of it, and a repeat under the same key is answered instead of sent. This page covers every LinkedIn endpoint that puts something in front of another person: * the message sends under `/api/linkedin-messages/`: `send`, `send-voice`, `send-inmail`, `send-sales-nav`, `send-recruiter` and `start-group` * the connection request, `POST /api/linkedin-connection-requests/send` * the posting verbs under `/api/linkedin-posting/`: `comment` (a comment or a reply), `create-post` and `repost` A reaction is the one write left outside the rule, for the reason given [below](#reactions). ## The key `client_reference` is your own id for one send. It takes up to 255 characters and is compared byte for byte. Mint one per send (your task id, your outbox row id) and use the same key every time you send that one again. A key and its place make one send. The place depends on what is sent: | Send | Its place | | - | - | | A message | The conversation the send names or the one the person resolves to, else the person. For `start-group`, the set of attendees | | A connection request | The person: the member their profile URN decodes to | | A comment | The post, as LinkedIn files it, so any urn of the same post names the same place | | A reply | The comment it answers, in either of its forms | | A post | Where it lands: your own feed, the company page you post as, or the group you post into | | A repost | The post it reposts, as LinkedIn files it | * A repeat to the same place is the same send, whatever its text says. If the first send went out, the repeat answers `200` with what it made (the message or the connection request row, the comment, the post or the repost) and `result.idempotent_replay: true`, plus `result.content_differs: true` when the repeat said something else (a re-rendered template, an edited draft). Nothing is sent. * The same key at another place is another send. That is how a bulk run can tag all of its sends with one value. * The time of a scheduled post is not part of its place under a key: a retry that computes a new `scheduled_at` is the same post. * A send without a key gets a weaker net. One that matches an earlier send's words and place (for a post, its pictures or video too) is held while that send is on its way or in doubt, and answered with it for an hour after it went out. To send the same words twice on purpose, use two keys. ## Four outcomes What a send came to is one of four values. Every error of a send endpoint carries it in `error.context.send_outcome`, the request's own `422` included. | `send_outcome` | What it means | What to do | | - | - | - | | `sent` | It is on LinkedIn. | Record it. Do not send it again. | | `not_sent` | Nothing went out, and there is proof: a refusal before the send, LinkedIn's own refusal, or two clean reads of LinkedIn after the send could no longer land. | Follow the error. Fix and resend, wait for `retry_after`, or give up. | | `in_flight` | The send is running now. | Ask again after `retry_after`. | | `unknown` | The answer was lost and LinkedIn has not shown the send yet. The platform keeps reading on its own. | Keep the send pending and ask again after `retry_after`. | An answer with no `send_outcome` at all, such as a gateway page or a dropped connection, is `unknown` to you. ## What a repeat is answered | The earlier send under the key | The repeat answers | | - | - | | is running | `409 concurrent_send_in_flight` with `send_outcome: in_flight`, `activity_log_sid` and `retry_after` | | is in doubt | `409 send_outcome_unknown` with `send_outcome: unknown`, `activity_log_sid`, `waiting_for`, `retry_after` and `send_decisive_at`, the moment it can no longer land | | went out, and what it made is stored | `200` with it and `result.idempotent_replay: true` | | went out, and what it made is not stored yet | `409 concurrent_send_in_flight` with `send_outcome: sent` and `retry_after` | | is proven not sent, or there is none | the send goes out as a new attempt under the same key | One more `409` comes from the place. When another send to the same place is running or in doubt (another message to the conversation, another invitation to the person, another comment on the post), yours waits: `409 concurrent_send_in_flight` with `send_outcome: not_sent`, the other attempt in `blocking_activity_log_sid`, and `retry_after`. Nothing of yours went out, so send the same request again at `retry_after`. `activity_log_sid` always names an attempt of your own send. Another send's attempt is only ever `blocking_activity_log_sid`. Two answers belong to one kind of send each: * A connection request meets LinkedIn's own rule of one pending invitation per person. When LinkedIn refuses an invitation because one of the account's is out to the person, the platform answers with that one: as the repeat of the same invitation, with a `409` naming it while it is in doubt, or with `422 resend_not_available` and `context.cause: "pending"` when another invitation of the account is pending. The person never gets two. * A post LinkedIn answered without naming one is `409 post_not_created` with `send_outcome: unknown`. The post may be out, so the platform reads LinkedIn for it. Repeat the same request after `retry_after`, never as a new post. ## Asking without sending Three endpoints answer the same question and send nothing. Ask by the key, by the key and a place, or by the `activity_log_sid` an answer named. | Send | Endpoint | MCP tool | Place, next to the key | | - | - | - | - | | A message | `POST /api/linkedin-messages/check-sent` | `check_linkedin_message_sent` | `linkedin_conversation_sid`, `ln_member_id` or `profile_urn` | | A connection request | `POST /api/linkedin-connection-requests/check-sent` | `check_linkedin_connection_request_sent` | `profile_id` or `ln_member_id` | | A comment, a post, a repost | `POST /api/linkedin-posting/check-sent` | `check_linkedin_posting_sent` | `entity_urn` (the post of a comment or a repost, in any of its forms), `parent_comment_urn` (a reply) or `post_place` (a post). The key also needs its `verb`: `comment`, `create-post` or `repost` | ```json POST /linkedin/v4/api/linkedin-messages/check-sent theme={null} { "linkedin_account_sid": "ln_ac_YOUR_ACCOUNT", "client_reference": "task-48213" } ``` ```json theme={null} { "success": true, "operation": "action", "action": "check-sent", "item": null, "result": { "outcome": "unknown", "reason": "may_still_land", "retry_after": "2026-10-05T09:14:30+00:00", "activity_log_sid": "ln_al_Q8rT2vW4xY6z", "send_decisive_at": "2026-10-05T09:16:00+00:00" }, "meta": { "trace_id": "0198f2ab-7c11-7e32-9a41-d2b64f2a91c3" } } ``` A comment is asked about with its verb and, when the key went to more than one post, the post: ```json POST /linkedin/v4/api/linkedin-posting/check-sent theme={null} { "linkedin_account_sid": "ln_ac_YOUR_ACCOUNT", "client_reference": "task-48214", "verb": "comment", "entity_urn": "urn:li:activity:7381234567890123456" } ``` `item` is what the send made once it is known: the message row, the connection request row, a comment's `comment_urn`, or a post's or a repost's urns and url. `result.reason` says why. For `not_sent` it is `no_send_under_key`, `refused`, or the read that proved it (`not_in_thread`, `not_in_invitations`, `not_on_linkedin`). While the send is in doubt it is `answer_lost`, `may_still_land`, `unprovable`, or a read that could not be done (`thread_unreadable` for a message, `unreadable` for the others). The tools list a few more. When the last read is old enough, check-sent reads LinkedIn during the call, so it can take as long as an inbox read. `post_place` is where a post landed: `feed:member` for your own feed, `feed:org:` for a company page, `group:` for a group (`group::org:` when a page posted into it). A key that went to more than one place, such as a bulk run's tag, answers `422 place_required` when you ask by the key alone. `error.context.places` lists the places. Ask again with one of them. A place the platform cannot tell from the key's own (named in another form than the sends recorded, or a post LinkedIn could not be read for) answers `422 place_unmatched`, with the places too. ### What LinkedIn shows, and what it cannot The platform settles a send in doubt by reading what LinkedIn shows, by the account and the words: | Send | What is read | | - | - | | A message | Its conversation | | A connection request | The account's sent invitations, then its connections, for an invitation accepted before any read saw it pending | | A comment | The post's comments | | A reply | The account's own comments | | A post on your feed, a repost | The account's own feed | | A scheduled post | The queue of scheduled posts, and once its time has come the feed it publishes to | | A company page's post | The page's feed. LinkedIn ranks it by relevance, so a read can find the post but never prove it absent | | A post into a group | Nothing: no read serves a group's feed | A post into a group whose answer was lost stays `unknown` until a person looks and gives their word, and so does a page post the page's feed has not shown. ## A person's word The platform reads LinkedIn by itself for up to a day, and check-sent reads it on demand after that. If a send stays `unknown` longer than your process can wait, a person opens LinkedIn and looks. Their answer rides on the same send request and names the attempt they checked by its `activity_log_sid`. * `confirmed_not_sent`: it is not there. It is accepted once the attempt can no longer land. Before that the request answers `409 send_outcome_unknown` with `waiting_for: may_still_land` and that moment in `retry_after` and `send_decisive_at`. Once accepted, the attempt is settled `not_sent` and your request goes out. * `confirmed_sent`: it is there. The attempt is settled `sent` at once and nothing is sent. The answer is a `409` with `send_outcome: sent`, or a `200` once what it made is stored. A request takes one of the two, and only for an attempt of the same send. Another send's sid answers `422 not_this_message`, and `confirmed_sent` naming an attempt already proven not sent answers `409 confirmed_sent_contradicts`. ## The rule for a caller 1. Mint one key per send and keep it with the send. 2. On `sent` or a `200`, record it. 3. On `not_sent`, follow the error. Send the same request again later, or give up. 4. On anything else, no answer included, keep the send pending. Ask check-sent or send the same request after `retry_after`. It goes out again only once the answer is `not_sent`, and always under the same key. 5. Past your own deadline, a person looks and gives their word. ## Example A reply to a lead goes out under `task-48213`: ```json POST /linkedin/v4/api/linkedin-messages/send theme={null} { "linkedin_account_sid": "ln_ac_YOUR_ACCOUNT", "linkedin_conversation_sid": "ln_cv_YOUR_THREAD", "text": "Thanks, Thursday at 3 works for me. I'll send the invite.", "client_reference": "task-48213" } ``` Your client gives up after 90 seconds with no answer. A minute later you send the same request again, same key and all, and the platform answers for the first send: ```json theme={null} { "success": false, "error": { "code": "conflict", "message": "An earlier send of this message is in doubt: its answer never came back and LinkedIn's thread has not shown yet whether it landed (activity log ln_al_Q8rT2vW4xY6z). Nothing was sent now.", "recoverable": true, "context": { "reason": "send_outcome_unknown", "send_outcome": "unknown", "activity_log_sid": "ln_al_Q8rT2vW4xY6z", "linkedin_account_sid": "ln_ac_Hx7kQ3mN2pL4", "waiting_for": "answer_lost", "retry_after": "2026-10-05T09:14:30+00:00", "send_decisive_at": "2026-10-05T09:16:00+00:00" } } } ``` A few minutes later the platform has read the conversation and found the message. It stores the row under `task-48213` and sends the `linkedin-messages.sent` webhook, and the same request now answers `200` with that row and `result.idempotent_replay: true`. The lead got one message. Had the conversation shown nothing after the send could no longer land, in two reads five minutes apart, the answer would have been `not_sent` and the same request would have sent the message once. ## In a mass action Each send a bulk run makes goes out under a key of its own: a message, a connection request, a comment. A target's own `client_reference` is used as given when the plan sends with that tool once. A `client_reference` T in a send step's `args` is a tag: each send goes out under `T:{item sid}:{step id}`, so an exact search for T finds nothing. Find the run's sends by each item's `created_object_sid` or by their exact keys, and match the `T:` prefix in your own code when you read webhooks. Do not create the run again because T matched nothing. That sends everything twice. A send step whose answer was lost waits with `wait_reason: send_outcome_unknown` and asks check-sent under its key before anything goes again. The retry is in [Run a mass action](/guides/run-a-mass-action). ## Reactions A reaction takes no key and has no check-sent. LinkedIn keeps one reaction per account and post, so sending the same reaction again changes nothing on LinkedIn. Every error of `react` still names `send_outcome`, and after `unknown` you can send the same reaction again. # Receive webhooks Source: https://docs.gtm-api.com/guides/receive-webhooks Register an endpoint, verify signed deliveries, filter what you receive, and read the delivery log. Includes the retry policy and the auto-disable rule. Webhooks live on the Orchestration service and cover events from the whole platform, so one subscription endpoint serves every service. **Give this to your AI agent** with your endpoint URL: "On gtm-api (MCP connector at `https://mcp.gtm-api.com/mcp`, or REST at app.gtm-api.com with my key): register a webhook for my endpoint URL with the events I name. The event vocabulary is the enum on the create-webhook schema, so read it there rather than looking for a catalog call, and use `events: [\"*\"]` if I say I want everything. Then watch the delivery log and show me the first delivery's status and response code." ## 1. Register an endpoint `POST /api/webhooks` takes a `name`, your `target_url` and the `events[]` you want: ```json POST /orchestration/v4/api/webhooks theme={null} { "name": "Mass action outcomes", "target_url": "https://example.com/hooks/gtm", "events": ["mass-actions.settled", "mass-actions.paused"] } ``` * `name` is required (1 to 255 characters). Omitting it fails validation. * `target_url` must be `https://` and must not resolve to a loopback, private or link-local address. DNS names are resolved and checked too, not just IP literals. * Each `events[]` value is validated against the platform event catalog, which currently carries 105 types across the services. * The same `target_url` cannot be registered twice in one team. * `events: ["*"]` subscribes to the entire catalog, including event types added later. The response returns the webhook's `secret`, a 32-character hex string, exactly once. Store it: every later read masks it, and it is what you verify signatures with. Webhooks are capped per plan. Creating one past the cap answers `402` with `webhook_limit_reached` and a body carrying `used`, `limit` and a suggested action. The free Sandbox plan includes one webhook, so subscribe that endpoint to `events: ["*"]` and route on your side. Paid plans carry 100. A trigger in n8n, Make or Zapier registers a webhook of its own, so on Sandbox one trigger runs at a time. Sandbox also has no realtime push: a received message reaches the webhook when a sync reads it, not within seconds ([inbox and message sync](/kb/inbox-and-message-sync)). See [billing and plans](/kb/billing-and-plans). ### Filter what you receive Two optional filters narrow deliveries at the source, so your endpoint is not woken by events it would discard anyway: * `filters.account_sid` restricts deliveries to one account (`ln_ac_...` for LinkedIn). The sid is matched verbatim, so channels added later work here without a change. * `filters.where` takes a small filter expression over the event payload. It is validated strictly at write time: nesting depth is capped at 5 and the whole expression at 20 leaf conditions, answering `filter_grammar_invalid` or `filter_leaves_limit_exceeded` when it exceeds either. ## 2. Verify deliveries Every real delivery is an HTTPS `POST` with a JSON body and these headers: | Header | Content | | - | - | | `X-Webhook-Event` | The event type, for example `mass-actions.settled` | | `X-Webhook-Signature` | `t={unix_seconds},v1={hex_hmac}` | | `X-Webhook-Timestamp` | The same unix timestamp as `t` | | `X-Webhook-Id` | The subscription's sid | | `X-Webhook-Log-Id` | This delivery's sid in the delivery log | | `X-Trace-Id` | Trace id, also present in the body | The body is a fixed envelope, with the event's own data nested under `payload`: ```json theme={null} { "webhook_log_sid": "wh_lg_...", "type": "mass-actions.settled", "emitted_at": "2026-08-13T09:12:44Z", "occurred_at": "2026-08-13T09:12:41Z", "team_sid": "ts_tm_...", "trace_id": "019ffa3d-...", "payload": {} } ``` The signature is `HMAC-SHA256(secret, "{t}.{raw_body}")` over the raw request body, with the timestamp mixed in to block replays. Verify before parsing, and reject signatures older than about 5 minutes: ```typescript Node theme={null} import crypto from "node:crypto"; function verify(rawBody: string, header: string, secret: string): boolean { const parts = Object.fromEntries( header.split(",").map((p) => p.split("=") as [string, string]), ); if (!parts.t || !parts.v1) return false; const age = Math.abs(Date.now() / 1000 - Number(parts.t)); if (age > 300) return false; const expected = crypto .createHmac("sha256", secret) .update(`${parts.t}.${rawBody}`) .digest("hex"); const received = Buffer.from(parts.v1); const digest = Buffer.from(expected); // timingSafeEqual throws on a length mismatch, so check that first. return ( received.length === digest.length && crypto.timingSafeEqual(digest, received) ); } ``` ```python Python theme={null} import hashlib, hmac, time def verify(raw_body: bytes, header: str, secret: str) -> bool: # Parse defensively: a malformed header must return False, not raise. parts = dict(kv.split("=", 1) for kv in header.split(",") if "=" in kv) if "t" not in parts or "v1" not in parts: return False try: ts = int(parts["t"]) except ValueError: return False if abs(time.time() - ts) > 300: return False expected = hmac.new( secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, parts["v1"]) ``` Compute the HMAC over the raw bytes you received, before any JSON re-serialization: parsing and re-encoding the body can reorder keys and break the signature. Redirects are not followed, deliberately. A `302` from your endpoint counts as a failed delivery, not a hop. Deliveries also time out at 30 seconds total (5 seconds to connect), so acknowledge fast and do the work asynchronously. ## 3. Test before relying on it `POST /api/webhooks/{sid}/test` fires a synthetic delivery at your endpoint, so you can confirm reachability, signature handling and parsing without waiting for a real event. Three things to know about it: it is rate-limited to 10 calls per minute per caller (the budget is shared across all your webhooks), it is rejected with `invalid_transition` while the webhook's status is `off`, and it writes no row to the delivery log. It also sets a subset of the headers above, so build your verifier on the signature, timestamp, event and id headers rather than requiring all six. ## 4. Retries and auto-disable A delivery is attempted up to **5 times**, with waits of 1 minute, 5 minutes, 30 minutes and 2 hours between attempts. The fifth failure is terminal, so a dead endpoint settles about 2.5 hours after the first attempt. | Response | What happens | | - | - | | `2xx` | Delivered, done | | `5xx`, `408`, `429`, network or TLS timeout | Retried on the schedule above | | Any other `4xx` | Terminal, no retry: the request was understood and refused | | `410 Gone` | Terminal, and it disables the subscription immediately | After **20 consecutive failed attempts**, or a single `410 Gone`, the subscription flips to `failed` and stops delivering. Its deliveries still waiting for a retry are cancelled with `cancel_reason: "webhook_failed"`, and the failure counter stops moving from that point. That transition itself emits `webhooks.failed`, so a second webhook can page you when the first one dies; the failing endpoint also gets that one event, a single attempt with no retries. Bringing it back is three calls: fix the endpoint, `PATCH /api/webhooks/{sid}` with `{"status": "on"}` (this resets the counter), then replay what was lost: ```json theme={null} POST /api/webhooks/{sid}/replay { "from": "2026-09-04T16:00:00Z" } ``` Replay puts every delivery of that webhook created since `from` whose status is `failed` or `cancelled` (by the platform, never by your own cancel) back into the queue, oldest first, one per second, so your endpoint is not hit by the whole backlog at once; the response says how many rows it re-armed and when the last one is due. `to` defaults to now, `statuses` to `["failed", "cancelled"]` (pass `["success"]` to re-fire delivered events into a rebuilt downstream), `event_types` to every type. The window spans at most 30 days and one call re-arms at most 5000 rows; beyond either cap the call is refused with the cap named, so narrow the window and call again. Replayed rows keep their `webhook_log_sid`, so a receiver that deduplicates on it sees the same delivery, not a new one. ## 5. Use the delivery log The log holds **one row per event per subscription**, not one per attempt: a retry updates that row in place, bumping `retry_count` and overwriting the response code. The row is the delivery, and its history is the counter. * `POST /api/webhook-logs/search` lists deliveries with status, response code and timing; filter by webhook or event type. * `POST /api/webhook-logs/{sid}/retry` re-sends. Allowed from every state but `in_progress`: `pending`, `retrying`, `failed`, `success` (a manual re-send of a delivered event is legitimate) and `cancelled`, whatever cancelled it. It re-arms the row without counting as an attempt, so `retry_count` stays the number of attempts that actually went out (at most 5). Only while the parent subscription is still live: a deleted or `off` webhook rejects the retry. * `POST /api/webhook-logs/{sid}/cancel` stops a delivery that has not settled. Allowed from `pending`, `retrying` and `in_progress`. * `POST /api/webhook-logs/metrics` aggregates outcomes. `period` with `from` and `to` is required and may span at most 90 days. Both `retry` and `cancel` answer `409` with `invalid_transition` when the row is not in a state that allows the verb, and the error names the state it found. ## 6. Read the payload `payload` is the event's own row, under the field names the REST API uses for that entity, plus a few fields that exist only on the wire: * `client_reference` on `linkedin-connection-requests.sent`, `.accepted`, `.withdrawn`, `.expired-detected` and on `linkedin-messages.sent` echoes the `client_reference` you passed to the send. Match on it instead of on a LinkedIn identifier. It is `null` on rows the sync picked up from LinkedIn's own UI. Every send verb accepts it: `send_linkedin_connection_request` and all six message sends (the basic message, voice, InMail, Sales Navigator, Recruiter and the group opener), and the comment, post and repost verbs, which emit no event of their own. A mass-action plan step's `args.client_reference` T reaches every row the run creates as `T:{item sid}:{step id}`, so match the `T:` prefix. * `counterpart` on `linkedin-conversations.created`, `linkedin-messages.received` and `linkedin-messages.sent` is the other side of the thread: `{ "full_name", "headline", "picture_url" }`, or `null` when we hold no participant data for the thread yet. `picture_url` is LinkedIn's signed CDN link and expires within hours, so render it on arrival rather than storing it. * `created_by`, where the row has one, is a reference to the actor: `{ "actor_type", "actor_sid", "actor_name", "oauth_client_sid", "reason" }`. `actor_name` and `oauth_client_sid` name the OAuth client that acted (an MCP client such as Claude, or n8n) and are `null` for a user, an API key or a system job. `reason` is set on system jobs, for example `snapshot_capture_job`. * `nickname`, the profile's vanity URL slug, is present only where LinkedIn exposes it: the connections list and received invitations. Followers, conversations, messages and sent invitations name people by hash id, so those rows carry `nickname` only once the same person is also a 1st-degree connection of the account. Match on `ln_member_id` (stable) or `ln_id` (the `ACoAA...` profile id, present on every event) rather than on the slug. * `linkedin-account-activity-log.task-failed` fires once for each call an account dispatched that ended `failed`. Its payload is the activity-log row (`sid`, `linkedin_account_sid`, `action_type`, `error_message`, `duration_ms`, `created_at`, `updated_at`, `trace_id`) with `pending_age_seconds`, how long the call was pending, and `failure_source`, what failed it: `plugin` (the call's own answer), `reconcile` (nobody finished the call in time) or `browser_deleted` (its browser was deleted while it ran). On a send's row, `send_outcome` and `send_outcome_reason` say what came of the send, and they are `null` on every other action. `not_sent` (`refused`) means nothing reached LinkedIn. `unknown` (`answer_lost`) means the send may be out: a failed call is not a send that did not go out, so ask check-sent before you send it again ([Sending once](/concepts/sending-once)). To receive only the sends in doubt, subscribe with `"filters": {"where": {"field": "payload.send_outcome", "op": {"eq": "unknown"}}}`. One kind of failed call does not fire it: a Sales Navigator call that LinkedIn refused with `SALES_SEAT_REQUIRED` while the seat is live. The platform reopens the Sales Navigator session and runs the call once more, and that second call's own outcome is what you get (a sync run retries on its next pass instead). The refused first attempt stays in the activity log. When the seat is really gone, the failure fires as usual. * `deleted_at` on a terminal `linkedin-connection-requests.*` event (`accepted`, `withdrawn`, `expired-detected`) is the moment we detected the transition, not LinkedIn's own timestamp of it. Acceptance is detected by the next sync of the account's pending invitations, so it can trail the real acceptance by up to the sync interval. `removal_kind` says which transition it was. ## Rotating the secret There is no self-service rotation endpoint. If a secret is exposed, delete the webhook and register a new one, then point your verifier at the new secret. Deleting and re-registering also gives you a clean delivery log boundary, which is easier to reason about than a rotation with no overlap window. # Run a mass action Source: https://docs.gtm-api.com/guides/run-a-mass-action Preview a bulk plan, commit it with the consent token, then monitor the run to settlement. A mass action is a bulk dispatch on the Orchestration service: up to 100 items, each run through a plan of 1 to 3 steps, paced by a randomized schedule. The API forces a two-phase flow so nothing bulk ever fires from a single call: first a preview that validates everything and mints a consent token, then a commit that consumes it. **Give this to your AI agent** and keep the go-ahead for yourself: "On gtm-api (MCP connector at `https://mcp.gtm-api.com/mcp`, or REST at app.gtm-api.com with my key): build a mass-action plan for the targets I give you, run the preview, and show me the findings and the consent summary. Wait for my explicit approval before committing. After the commit, monitor the run and report per-item outcomes when it settles." ## 1. Preview the plan `POST /api/mass-actions/preview` validates the whole plan without persisting, charging or sending anything, and reports every finding at once. ```json POST /orchestration/v4/api/mass-actions/preview theme={null} { "title": "July outreach, batch 3", "target_entity": "linkedin-connection-requests", "scope": { "kind": "targets", "targets": [ { "ln_id": "ACoAAAdbzRUBAAAAAAAAAAAAAAAAAAAAAAAAAAA" } ] }, "plan": { "steps": [ { "tool": "linkedin-connection-requests.send-linkedin-connection-request", "args": { "linkedin_account_sid": "ln_ac_Hx7kQ3mN2pL4", "note": "Hi, enjoyed your post on pipeline reviews." } } ] }, "schedule": { "interval_seconds_min": 120, "interval_seconds_max": 600 }, "canary_mode": "first_item" } ``` What the validator enforces: * **Step vocabulary.** Only step-eligible verbs can appear in `plan.steps[].tool`. Anything else fails with `validation_failed` and a `field_errors` entry naming the authorable set, so the plan is repairable in one pass. * **One sender per run.** A send-class step names the account it sends from in `args.linkedin_account_sid`. A target's payload carries the target (`ln_id`, the profile URN) and its per-send extras (`note`, `client_reference`), never the sender: a plan that leaves the sender out of `args` fails preview with `sender_required`. * **Scope shape.** `objects` (existing rows by sid), `targets` (per-item payload identities), `generate` (N slots, step 1 must create the object), or `none` (a standing run an auto-scrape feeds later). All bounded at 100 items. * **A target is one person.** Name them by `ln_id` (the profile URN, `ACoAA...`) or `sn_id` (the Sales Navigator id, `ACwAA...`); `profile_id` takes either. `ln_member_id` is the decoded member number (digits only) and cannot be dispatched by a send verb on its own; a vanity slug goes in `nickname`. A slug in `ln_member_id`, an id that does not decode, or a Recruiter id where a member verb wants a profile URN fails the preview on that target (`scope.targets.3.ln_member_id`, `ln_member_id_not_numeric`), and the same person named twice (their URN and their Sales Navigator id) counts once. * **Schedule mandate.** A plan with a send-class step must carry a `schedule`; the per-gap interval is randomized between your min and max, because a fixed cadence is itself a detectable pattern. Omit `schedule` only for plans that can drain immediately. On success the `action` envelope returns a `preview` block (`items_count`, `dangerous_steps`, `eta`, warnings) plus `commit_token` and `expires_at`. ## 2. Commit it `POST /api/mass-actions` takes the exact same inputs plus the `commit_token`. The token is an HMAC over those inputs and the caller: edit anything, and the commit fails with `validation_failed`; wait past 15 minutes, and it expires. Re-preview in either case. Two behaviors to design around: * **The run is always asynchronous**, even for one item. The returned `sid` is your monitoring handle. * **A still-valid token can be replayed**, and a replay creates a second identical run. Discard the token the moment the commit succeeds. With `canary_mode: "first_item"`, only item 1 dispatches until it succeeds. A canary failure pauses the whole run with `paused_reason: canary_failed`, so a broken template costs you one send, not a hundred. ## 3. Monitor to settlement Runs are visible in the app whoever authored them: yourself, an API key, an agent over MCP, or the platform. **Mass Actions** lists them with their pacing, live counts and state. Mass Actions list showing runs with item counts, pacing intervals, states and the actor that created each Opening a run shows the plan it plays and every item's outcome, which is the fastest way to tell a broken template from a transient failure. Mass action run detail showing a two-step plan and a per-item table with succeeded and failed rows, error text and try counts Over the API: * `GET /api/mass-actions/{sid}?include[]=metrics` returns the run with per-status item counts. * `POST /api/mass-action-items/search` with `filter: { "mass_action_sid": { "eq": "..." } }` lists the individual items and their step logs. * Or skip polling: subscribe a [webhook](/guides/receive-webhooks) to the `mass-actions.settled` and `mass-actions.paused` events. Control verbs while it runs: `POST /api/mass-actions/{sid}/pause` and `/resume`, and `/release` for a standing run. ## 4. Retry failures `POST /api/mass-action-items/retry` re-enters failed items at their current step. Completed steps are never re-executed, so there are no duplicate creates and no double sends. Target exactly one of a single item `sid` or a `filter` (pass `mass_action_sid` in the filter unless you mean every run). Only `status: failed` rows match, and a retry matching nothing returns `retried_count: 0`, not an error. Two guards hold whatever you pass. An item is retried at most 10 times, and an item of a deleted run, or a cancelled one, is never retried. By `sid` these answer `422 retry_limit_exceeded` and `409 cancelled_cannot_retry`. A filter leaves such items alone and reports the first kind in `left_at_retry_limit_count`. A send is not sent again on a guess. Each send of a run (a message, a connection request, a comment) goes out under a key of its own ([Sending once](/concepts/sending-once) explains the keys), and a send whose answer was lost waits with `wait_reason: send_outcome_unknown` while the run asks the owning service what came of it. It goes again only when the answer is `not_sent`. If nobody can tell within 24 hours, the item fails with `send_unconfirmed:` in its `error_message`, and a plain retry asks again for another day. An `item_timeout:` failure on a send step is asked about the same way. To send it anyway, look on LinkedIn first: the conversation, the person's invitations, the post's comments. If it is not there, retry that one item with your word: ```json POST /orchestration/v4/api/mass-action-items/retry theme={null} { "sid": "ma_im_YOUR_ITEM", "resend_unverified": true } ``` The step goes again naming the attempt in doubt, and the owning service still holds it while that attempt could land. `resend_unverified` vouches for one send, so it takes a `sid` and never a `filter` (`422 resend_unverified_needs_sid`). A comment carries a key since 2026-10-05 like every other send. A send step that began before its key existed, such as a comment of a run that was already going, cannot be asked about: a plain retry leaves it failed with a note and counts it in `left_unconfirmed_count`, and only `resend_unverified` sends it again. On a step that is not a send, an `item_timeout:` failure still means the outbound call may have landed: check the target before retrying a step that is not safe to repeat. # gtm-api documentation Source: https://docs.gtm-api.com/index A typed REST API and MCP server for LinkedIn outreach infrastructure: connected accounts, messaging, enrichment, mass actions and webhooks. gtm-api runs LinkedIn accounts as managed infrastructure. Each account you connect gets its own anti-detect cloud browser and dedicated proxy, and everything an account can do (search, connect, message, enrich, post) is exposed twice from one schema: as typed REST endpoints, and as MCP tools an AI agent can call. Server-side limits, warm-up and pacing run under every call, whichever surface it arrives on. ## The API surface Three services, one contract style. Every endpoint authenticates the same way, returns the same response envelope, and fails with the same error taxonomy. | Service | Base URL | What it covers | | - | - | - | | LinkedIn | `https://app.gtm-api.com/linkedin/v4` | Connected accounts, health and smart limits, conversations and messages, the connection graph, posting, scraping, profile and company enrichment, anti-detect browsers | | ID and Teams | `https://app.gtm-api.com/id/v4` | Users, teams, API keys, OAuth, billing, subscriptions, notifications | | Orchestration | `https://app.gtm-api.com/orchestration/v4` | Mass actions (preview, commit, pace, canary) and the platform-wide webhook registry | Paths in the reference are relative to these base URLs. `GET /api/linkedin-accounts/{sid}` means `https://app.gtm-api.com/linkedin/v4/api/linkedin-accounts/{sid}`. ## Start here Mint an API key and make your first call in a few minutes. API keys, JWTs and the Team-SID header. How the platform works, for the person running outreach: accounts and browsers, safety and pacing, finding and enriching datafinding and enriching data. With screenshots. Every endpoint of the three services, with an interactive playground. Give Claude, Cursor or ChatGPT the same tools over the Model Context Protocol. What shipped, in the order it shipped. ## How the contract behaves * Every success body is an envelope: `success: true`, one typed `operation` shape (`search`, `get`, `create`, `update`, `delete`, `metrics`, `action`), and a `meta` block with a `trace_id` you can quote to support. * Every failure is one `McpError` envelope with a code from a fixed 16-code taxonomy, so you map errors once and reuse the mapping everywhere. See [Envelopes and errors](/concepts/envelopes-and-errors). * Lists page with `page_size` plus an opaque forward `cursor`, and filters are typed per field. See [Pagination and filtering](/concepts/pagination-and-filtering). * Endpoints marked deprecated in the reference are `501` stubs: the request contract is final and validated, but the capability has not shipped yet. Build against them, do not ship against them. The reason is in each operation's description. ## Where the reference comes from The reference is generated from the same registry that defines the MCP tools, one endpoint per tool. It is not written by hand, so what the playground shows is what the server actually validates. # Account health Source: https://docs.gtm-api.com/kb/account-health A sender health summary has four parts: the browser login state (signed out of LinkedIn or not), a warmup score built from six compounding signals, the smart-limit budgets, and two incident logs. What each one records and what to do about it. Every connected LinkedIn account carries a health record: the login state of its browser, a warmup score with its six inputs, a snapshot every 22 hours, and two incident logs. What each signal measures, and what to do about it. ## Where health lives Open **Senders**, click the account row, open the **Smart Limits** tab. The warmup card is at the top: the score, a band label, and a table of **Metric**, **Health** and **Comment**, one row per signal. A healthy signal reads `ok`; an unhealthy one shows the percentage it costs and the reason. Below it, **Incident logs** holds the **Limit-hit log** and the **Block log**, then the per-action caps. The **Senders** list carries a **Safety** column for a quick per-account read. ## Signed in at all: check the browser first A warmup score is meaningless for a sender that is signed out of LinkedIn, so every health read starts with the login state, and the login state lives on the account's **browser**, not on the account itself. The account's `status` field is platform lifecycle (`new`, `initial_syncing`, `active`, `sync_failed`, `shared_out`, `subscription_required`). It is not health and not login state: a sender whose LinkedIn session died still reads `status: "active"`. The signed-out fact is the bound browser's `status: "login_issue"`, or `"restricted"` when the platform checked the account right after the logout from another LinkedIn account and found its public profile gone (a restriction, or a closed account). There is no separate `logged_out` value. In the app the signal is the red **Signed out of LinkedIn** state on the sender row and in the drawer. Over the API it is two things on `/api/linkedin-accounts/search`: the always-present `counts.signed_out_count` (how many matching senders have a signed-out browser), and per row the `include[]=antidetect_browser` relation, whose `status`, `error_reason`, `logout_count` and `last_logout_at` say who is out, since when, and what LinkedIn answered. A signed-out browser is powered off, sticky, and never woken automatically; re-login is the only fix ([browser troubleshooting](/kb/browser-troubleshooting)). ## The warmup score One integer from 0 to 100, recomputed on every snapshot as `round(100 * (1 - p1) * ... * (1 - p6))`: six signals each remove a fraction of what is left, so they compound rather than add up. A signal with no penalty is left out of the breakdown, which is why `warmup_breakdown` comes back empty on a healthy account. The table itself always lists all six signals, the healthy ones marked `ok`. | Signal | UI label | What it reads | Penalty | | - | - | - | - | | `account_age` | **Account age** | Months since the account's earliest known connection | `0` at 4 months or older, otherwise `0.80 * (4 - age) / 4` | | `limit_hits` | **Limit hits** | Rows in the limit-hit log over the last 30 days | `0` up to 3 hits, then `7.14%` per extra hit, capped at `70%` | | `blocks` | **Blocks** | Rows in the block log over the last 30 days | `1 - 0.60^blocks`, capped at `85%` | | `activity` | **Activity** | Connection requests and messages this account sent through the platform in the last 30 days | `0.90 * (1 - activity / 80)`, so `0` at 80 sends or more | | `sessions_ip` | **Sessions & IP** | Concurrent LinkedIn sessions read from the account itself | `0` up to 2, `10%` for 3 or 4, `20%` above 4 | | `completeness` | **Completeness** | Profile completeness percentage | `0` at 80% or above, `10%` for 50% to 79%, `20%` below 50% | A dormant account carries the full 90% activity penalty, which alone caps its score at 10. The band label under the number is presentation only, nothing in the engine keys off it: `New` below 50, `Warming up` from 50 to 89, `Established` at 90 or above. ### What counts as activity, and what does not The floor is **80 platform-executed sends per 30 days**, read from the platform's own activity log. It deliberately ignores the connection and message tables, because a freshly connected account imports a backlog of previously sent invitations whose timestamps would make a dormant account look busy. So sends you make by hand in LinkedIn earn **no activity credit**, and they **do** show up in **Sessions & IP** as a second device or IP. A refusal LinkedIn hands you in its own UI is never logged here: a block you provoke by hand surfaces only later, if one of the platform's own actions against that same target is refused and the two-browser check runs. **Profile completeness** is scored backend-side over twelve weighted fields: `experience` (8), `about` (5), `educations` (4), `location` and `skills` (3 each), `full_name`, `position`, `headline`, `company_name`, `email` and `top_voices` (2 each), `supported_languages` (1). List fields earn full weight for one entry and 1.5x for two or more. The drawer's short version: headline, photo, experience. ## Snapshots A snapshot is one immutable row holding everything health knew about the account at one moment: the score and its `warmup_breakdown` (`[{driver, penalty_pct}]`, unhealthy signals only), the profile summary, the session list, 30-day counters, `executed_actions` per bucket, and the limit surface frozen as it stood. Capture runs on a **1320 minute (22 hour)** cadence; a freshly connected account gets its first one during onboarding, not 22 hours later. * **Append only, and the score moves only when a snapshot lands.** Roughly 33 rows a month per account, kept indefinitely, with no create, update or delete through the API. Fix a signal now and the number changes at the next capture, not immediately. * **Capture needs the account's browser**, since it reads the account's own profile and session list. A browser stuck in an issue state stops producing new snapshots ([browser troubleshooting](/kb/browser-troubleshooting)). * **Its reads are charged to `self_account_sync`**, the one budget the warmup ramp never scales down, so health keeps capturing even on an account whose outreach budgets are tiny. * **Two derived fields are worth knowing:** `sessions_diff`, the 0 to 100 change in the source IP set versus the previous snapshot, and `account_diff`, 0 to 100 weighted profile drift versus it, where 0 means unchanged. `last_month_messages_blocked` is the 30-day block count, the same number that feeds the **Blocks** signal. The name is historical: a LinkedIn block is total, not message-only. ## Limit-hit log: LinkedIn refused us One row records a LinkedIn rate-limit the background sync engine ran into. Only the connection-request and message surfaces write a typed row; a rate-limit on any other surface flips the browser to an issue status instead. Rows are immutable and never deleted. Each reason carries its own temporary lock on the matching budget. | `reason` | Lock | Budget it blocks | | - | - | - | | `connection_request` | **1 hour** | Connection requests | | `inmail` | **3 hours** | InMails | A repeat refusal within **12 hours** for the same account and reason re-arms the lock but writes **no second row**, so the 30-day count behind **Limit hits** is not inflated by one bad hour. Every row freezes the volume at the moment LinkedIn struck: `daily_count` (since local start of day), `seven_days_count`, `thirty_days_count`, and `linkedin_quota_hit_till`, when the lock expires. The `daily_count` spread across rows answers "at what volume does this account get refused". Two events restrict an account without writing a limit-hit row: a **captcha**, which blocks **every budget for 6 hours** on purpose so a person intervenes rather than automation grinding on, and a browser or proxy fault, which flips the browser to an issue status. Both show up as a red account with an empty log. ## Block log: a prospect blocked us One row records the first time a target profile was found to be blocking one of your accounts. Rows are immutable and never deleted. LinkedIn publishes no "you were blocked" signal, so the platform derives it with a two-browser check: an outbound action fails with a LinkedIn 4xx refusal (429 rate limits excluded, they mean something else), the target is read through the affected account's own browser, and if that fails it is retried through another live browser on your team. The triggering actions are connection requests, messages, InMails, sales messages, voice messages, skill endorsements, post likes, post comments and profile visits. Follow and unfollow are deliberately excluded, a follow refusal having too many innocent causes. | Outcome | Meaning | | - | - | | `not_blocked` | The affected account can read the target. Nothing recorded | | `target_blocked` | The account's own read failed, the second browser succeeded. Row written | | `target_unavailable` | Both reads failed. The profile is deleted, restricted or hibernated, not blocking you. No row | | `inconclusive` | Transport noise, or no second browser was available | | `skipped_recent` | The same pair was checked within the last **24 hours** | Deduplication is **permanent**, on the pair of account and target member id: one row per genuinely new hostile target, forever, because a LinkedIn block does not expire. We cannot tell "blocked" from "reported", since both surface to us as the same opaque refusal, so the row carries no discriminator. If the target was a connection, the same transaction soft-deletes that connection with `removal_kind` set to `blocked` and links it to the block row; soft-deleted connections stay queryable, and **no conversation and no message history is touched**. Counts ride search results and the account include (`total_count`, `last_24h_count`, `last_7d_count`, `last_30d_count`, `with_connection_count`, `without_connection_count`), where the with-connection share says whether blocks burn existing relationships or cold prospects. Our own operational threshold is **more than 3 new blocks in 7 days on one account**: past that we treat the targeting as the problem, not the account. It is our number, not a rule LinkedIn publishes. ### Checking one target on demand `check-target-block` runs the same two-browser check synchronously for a target you name. The first read spends **1 unit** of the account's `self_account_sync` budget. A target already in the block log and a `skipped_recent` result inside the 24 hour cache spend nothing. `"force": true` bypasses the cache. ```bash curl theme={null} curl -X POST "https://app.gtm-api.com/linkedin/v4/api/linkedin-accounts/ln_ac_YOUR_ACCOUNT/check-target-block" \ -H "Authorization: Bearer gtm_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"target": {"nickname": "some-public-profile-slug"}, "force": false}' ``` The only failure answer is `422 validation_failed`, when `target` carries none of `ln_id`, `sn_id` or `nickname`. A team with no second browser free is not an error: it answers 200 with `outcome: "inconclusive"`, nothing having run on a second browser. ## How health drives smart limits On each snapshot the score becomes a ceiling per action type (`platform_max[bucket] * warmup_score / 100`, rounded) and the budget walks toward it. Up is gradual, down is instant: each recompute moves a budget up by about **20%** of the gap to its ceiling (at least 1), so recovery takes several snapshots, while a ceiling below the current budget drops it at once, and the upward step is held while the bucket's LinkedIn lock (`linkedin_quota_hit_till`) is still in the future, that is after a recent limit hit or a captcha. A prospect block holds nothing directly; it lowers the next snapshot's score, and the score lowers the ceiling. A fresh refusal does not recompute budgets by itself: it writes the temporary lock, and its presence in the 30-day count changes the score at the next capture. Your own configured ceiling is never touched. Budgets, holds, the `active` / `held` / `linkedin_blocked` statuses and how to release a hold are in [smart limits and warmup](/kb/smart-limits-and-warmup). ## Reading a state and acting on it | What you see | What it means, what to do | | - | - | | Everything reads fine but the sender does nothing | Check the browser first: a sender signed out of LinkedIn keeps `status: "active"` on the account row, and its score and logs freeze at their last values. The **Signed out of LinkedIn** state on the row (browser `status: "login_issue"`) is the tell; re-login is the only fix ([browser troubleshooting](/kb/browser-troubleshooting)) | | The score dropped overnight | Read the **Comment** column of the rows that are not `ok`; a drop is always one of the six signals. The score moves only on a capture, so a fix shows up at the next snapshot, up to 22 hours later | | **Activity** penalises an account that is sending | The floor counts only actions the platform executed. Sends you made by hand in LinkedIn are not in it. Route the volume through the platform, or accept the penalty as accurate | | **Blocks** shows 1 or more | One block removes 40% of the remaining score, two remove 64%, capped at 85%. They age out of the 30-day window on their own: slow down or change the segment you contact, and never retry a target that is already blocking you | | **Limit hits** shows 4 or more | Three hits in 30 days are free, then about 7.14% each. Hits age out of the window. Lower the daily budget for the action being refused instead of pushing into the same wall | | **Sessions & IP** is flagged | These are real LinkedIn sessions read from the account, phone included, so a laptop plus a phone plus our browser is already 3. Sign out where you do not need to be signed in | | **Account age** penalises a genuinely old account | Age is measured from the earliest connection the platform has synced, as a proxy for real age, so an account with few synced connections reads younger than it is. Nothing to do but wait: at 4 months the penalty is exactly 0 | | A budget is blocked and the limit-hit log explains it | Read `reason` and the unlock time. The platform restores the budget within a minute of expiry and parked work resumes on its own; nothing needs re-queuing | | The account is red but both incident logs are empty | Either a captcha (every budget blocked for 6 hours, no row written, because it wants a person) or a browser or proxy fault. Go to the **Browser** tab, then to [browser troubleshooting](/kb/browser-troubleshooting) | ## A sender health summary over the API One `/api/linkedin-accounts/search` call carries all four dimensions of health when you ask for the right includes. `antidetect_browser` is the one that answers "is this sender signed in at all"; the account item alone cannot. ```bash curl theme={null} curl -X POST "https://app.gtm-api.com/linkedin/v4/api/linkedin-accounts/search" \ -H "Authorization: Bearer gtm_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "filter": {"deleted_at": {"is_null": true}}, "include": ["antidetect_browser", "linkedin_account_smart_limits", "linkedin_account_snapshot", "linkedin_account_block_log_counts"], "page_size": 10 }' ``` Read it per sender, in this order: | Dimension | Where in the response | Healthy looks like | | - | - | - | | Login state | included `antidetect_browser.status` | Anything but `login_issue` or `restricted`. `login_issue` = signed out of LinkedIn, a re-login fixes it; `restricted` = signed out and the public profile is gone from LinkedIn, so the holder has to sort it out with LinkedIn first. `error_reason` and `last_logout_at` say what and when. NOT the account's own `status`, which is lifecycle and stays `active` through a logout | | Warmup | included `linkedin_account_snapshot.warmup_score` | 90 or above; unhealthy signals are listed in `warmup_breakdown` | | Budgets | included `linkedin_account_smart_limits` | `active` rows; `held` / `linkedin_blocked` mean a budget is parked | | Blocks | included `linkedin_account_block_log_counts` | `last_7d_count` of 3 or fewer; more says the targeting is the problem | The `counts` block rides every search response, includes or not: `counts.signed_out_count` is the number of matching senders whose browser sits in `login_issue` or `restricted`, so a summary that skips the browser include still cannot miss them, and `counts.restricted_count` is the subset a re-login alone will not bring back. `active_today_count` and `stale_count` are the heartbeat tallies. Items with these includes are large. Keep `page_size` at 10 or below and follow `pagination.next_cursor` instead of requesting the whole fleet in one page. The three record surfaces are also searchable directly at `/api/linkedin-account-snapshots/search`, `/api/linkedin-account-quota-hits/search` (filter `linkedin_quota_hit_till` with `gt` now for live locks only) and `/api/linkedin-account-block-log/search`; shapes are in the [API reference](/api-reference/overview). Includes on the account are cheaper than three searches: `linkedin_account_snapshot` returns the latest snapshot, `linkedin_account_quota_hits` and `linkedin_account_block_log` the last 50 rows each, newest first. Paste this to your coding agent to read the whole fleet at once: > Using the GTM API at `https://app.gtm-api.com/linkedin/v4` with the bearer token in `GTM_API_KEY`, post to > `/api/linkedin-accounts/search` with `include[]` of `antidetect_browser`, `linkedin_account_snapshot`, > `linkedin_account_quota_hits` and `linkedin_account_block_log_counts`, `page_size` 10, following > `pagination.next_cursor` to the end. For every account print the nickname, the browser `status` > (flag `login_issue` as SIGNED OUT with its `last_logout_at`), `warmup_score`, each > `warmup_breakdown` entry as `driver: penalty_pct`, and `last_7d_count` / `last_30d_count`, sorted by > `warmup_score` ascending, flagging any account with more than 3 blocks in the last 7 days. Do not > report the account `status` field as health; it is platform lifecycle. ## Health events | Event | Fires when | | - | - | | `linkedin-account-snapshot.captured` | A snapshot is written, so the score has been recomputed | | `linkedin-account-quota-hits.recorded` | LinkedIn refused an action and a row was written | | `linkedin-account-block-log.recorded` | A target was confirmed to be blocking an account | | `linkedin-accounts.logged-out` | LinkedIn signed a sender out; `antidetect-browsers.logged-out` is the browser-level twin | | `linkedin-accounts.restricted` | Right after the logout, the sender's public profile no longer answered from another LinkedIn account: a restricted (or closed) account. Follows `.logged-out`, never replaces it; `antidetect-browsers.restricted` is the browser-level twin | | `linkedin-account-activity-log.task-failed` | A call the account dispatched ended `failed`, once per call: `failure_source` says what failed it, and on a send `send_outcome` says whether it may still be out ([the payload](/guides/receive-webhooks#6-read-the-payload)) | ## Related * [Smart limits and warmup](/kb/smart-limits-and-warmup) * [Sync windows and auto-suspend](/kb/sync-windows-and-auto-suspend) * [Browser troubleshooting](/kb/browser-troubleshooting) # Share or transfer a connected account Source: https://docs.gtm-api.com/kb/account-sharing-and-handover Lend a connected LinkedIn account to another workspace and get it back, or hand ownership over permanently. Covers what the receiving side gets, what happens to the browser and the sync, and every status both sides see. Sharing lends a connected LinkedIn account to another workspace and is revocable at any moment; a transfer moves ownership for good. Both keep the same LinkedIn session and the same antidetect browser profile, so nobody re-logs in. Prefer sharing even when the move sounds permanent: only a share can be rolled back. **Give this to your AI agent** to lend an account without touching the app: "Lend a LinkedIn account to another gtm-api workspace. POST to `https://app.gtm-api.com/id/v4/api/account-shares` with `{"channel":"linkedin","account_sid":"ln_ac_...","to_email":"..."}` using my API key. It answers 201 with status pending, which means nothing has started yet. Poll POST /api/account-shares/search with filter `{"sid":{"eq":"ac_sh_..."}}` every 15 seconds until status is active or failed, then report the status, holder\_over\_slot and failure\_reason. Do not call recall or return unless I ask. Never touch account-transfers, it is permanent." ## Share or transfer | | Share | Transfer | | - | - | - | | Ownership | stays with you | moves to the receiver | | Reversible | yes, `recall` at any time | no | | Your copy | parked, comes back on recall | gone | | API | `POST /api/account-shares` | `POST /api/account-transfers` | | Available to an AI agent | yes, MCP tools exist | no, deliberately | | Permission | `can_manage_account_shares` (create, recall, return) | `can_manage_account_transfers` (reads included) | ## Lend an account The account must be one your workspace owns, past its LinkedIn login, and not already a side of another share or transfer. A billing hold does not block a loan. LinkedIn only, so passing `"channel": "email"` is not supported. The receiving workspace must exist and be neither suspended nor deleted; target it by `to_email` (resolved to that user's working team) or by `to_team_sid`, exactly one of the two. One call lends one account, so lending five accounts is five calls. ```bash theme={null} curl -X POST "https://app.gtm-api.com/id/v4/api/account-shares" \ -H "Authorization: Bearer gtm_live_YOUR_KEY" -H "Content-Type: application/json" \ -d '{"channel": "linkedin", "account_sid": "ln_ac_YOUR_ACCOUNT", "to_email": "ops@partner.example", "planned_return_at": "2026-09-30T12:00:00Z"}' ``` The `201` answers with `item.sid` (`ac_sh_...`) and `item.status: "pending"`: the row was written, the loan has not started. Poll `POST /api/account-shares/search` (the loans you issued) until the status is `active`. The receiving side polls `POST /api/account-shares/list-received`, scoped to loans made to them. There is no error response for a loan that fails after the `201`: the outcome lands on the row as `status` plus `failure_reason`, so poll it. A background reconciler re-drives any share that stalls, **every 5 minutes**, until it advances or a human intervenes. ## Statuses | Share | Transfer | What it means | | - | - | - | | `pending` | `pending` | The row is committed, nothing has happened on LinkedIn's side yet | | `owner_parked` | `giver_parked` | Your account and browser are parked, the copy is being made | | `active` | `receiver_copied` | The other side has the account. Only this status means the loan started, and from here there is no failure path | | `closing` | | An ending was requested (recall, return, deletion or purge) | | `holder_released` | | The borrowed copy is gone, your original is still parked | | `rolling_back` | `rolling_back` | An early phase gave up and the un-park is owed. Not terminal, so a fresh share on the same account is still refused | | `returned` / `recalled` | `completed` | Terminal: ended by the borrowing side, ended by you, or ownership changed at `completed_at` | | `failed` | `failed` | Terminal, never started, and your account is provably back in service | Nothing leaves a terminal status, and there is no `expired` status. `failure_reason` is one of `park_failed`, `export_failed`, `copy_refused`, `probe_stale` or `team_purged` on a share, and `park_failed`, `copy_refused` or `probe_stale` on a transfer. A row on `owner_parked` whose `phase_attempts` passes 10 with no `failure_reason` is stuck in the copy retry, which never terminates on its own. An export failure is different: it gives up after 5 attempts and lands `failed` with `export_failed`. Send support the share sid plus the exact `status` and `phase_attempts`. ## What happens on your side Account status and browser status both become `shared_out` (the Senders list shows the browser as `shared out`, hold reason "Lent to another workspace"), the browser session is stopped and its automation server is released. The account leaves every scheduler, dispatcher and stale-heartbeat sweep, so silence from a parked account is expected, not a fault. Parking stops in-flight work: running sync runs are **cancelled** with the reason `account_shared_out`, auto-scrapes and mass-action runs are **paused** with the reason `account_unavailable`. On recall only what the park paused is resumed, so a manual pause or a limit hold you set survives the round trip. Cancelled sync runs are never recreated: the scheduler mints fresh ones once the account is back. You keep two things while the account is away: a cloud-browser session on the parked original, and its proxy. That session is how you fix a LinkedIn logout mid-loan, and nobody is notified when one happens, so the borrower has to tell you. You cannot delete the parked account or re-connect the same LinkedIn member to work around the share: both answer `409 conflict`, `account_shared_out`. ## What the receiving side gets | Crosses with the copy | Does not cross | | - | - | | The same LinkedIn identity and browser profile, so no re-login | Every synced child record (connections, conversations, messages, invitations), all ten timestamp clocks, including `initial_sync_completed_at`, so the copy runs its own initial sync before it may do outbound work | | LinkedIn member ids, name, avatar, premium and Sales Navigator flags, InMail credits | Warmup and smart limits, re-derived from scratch on the receiving side | | The sync schedule and the webhook config | The proxy (`custom_proxy_config`, the proxy sid, the proxy country) and cloud-browser access keys, which the receiving side mints itself | One exception: if the receiving workspace held this account before and its old copy is still soft-deleted, that row is restored rather than created fresh: same sid, old conversation and connection history back, sync clocks preserved, no full backfill. Use `reset-sync` to force one. The receiving side also cannot: * **See the browser profile id.** `vendor_profile_id` reads back as `***` on every borrowed browser, during the loan and after it ends, so an ex-holder cannot re-bind the profile later. * **Re-share it** (an account can be in at most one live share), **manage the proxy** (no proxy field crosses), or **inherit your roles**: permissions are never granted across workspaces, and the copy is governed entirely by their own roles. Lending an account out does **not** free your slot. Your claim is held for the whole loan, and the receiving workspace claims a slot of its own. If they have no free slot, the share still reaches `active` with `holder_over_slot: true` and their copy lands in the `subscription_required` hold, so they cannot run it until they upgrade. A share never fails because of slots. Plan slot caps are in [Billing and plans](/kb/billing-and-plans). A lent-out account is skipped by the downgrade sweep, so it cannot slip into `subscription_required` while away; the check re-runs when it comes back. Lending out an already-downgraded account is allowed. ## End a share | How | Who | `end_reason` | Final status | | - | - | - | - | | `POST /api/account-shares/recall` | Owner, no consent needed from the other side | `recalled_by_owner` | `recalled` | | `POST /api/account-shares/return` | Borrower | `returned_by_holder` | `returned` | | Borrower deletes the borrowed copy | Borrower | `holder_deleted_copy` | `returned` | | Either workspace is purged | System | `holder_team_purged` / `owner_team_purged` | `returned` / `recalled` | Teardown soft-deletes the borrowed copy, stops its browser, revokes every cloud-browser key it minted, and fails its pending activity rows with `browser_deleted`. The borrower keeps the conversations and connections that copy synced. Your original is un-parked: browser back to `stopped`, account status recomputed from its own signals. Instead of `sid` you may pass a `filter` plus a `confirmation_count`. Filter mode pins `status` to `active` server-side (your filter can only narrow the set), drains **at most 100 shares per call**, and requires `confirmation_count` to equal the count that same filter returns with `status: {"eq": "active"}` pinned and `page_size: 0`. Anything else answers `409 confirmation_count_mismatch`. It is not atomic: on a mid-batch error the response carries no counters and the shares already stamped keep closing, so re-query rather than retrying the same body, and take a fresh count before each call. `closing_count` counts shares that **started** closing, never ones already recalled. `planned_return_at` does not auto-return anything. When it passes, an hourly job emails the owner **once** that the date passed, and stamps `expiry_notified_at`. The loan runs until somebody ends it. A soft-deleted workspace changes nothing: the 30-day cleanup period has to elapse and the purge has to run before the loan is driven to a terminal status. ## Refusals | Situation | HTTP | `error.code` | Discriminator | | - | - | - | - | | Bad target: your own workspace; neither or both of `to_email` and `to_team_sid`; no workspace for that email | 422 | `validation_failed` | `field_errors.to_team_sid[0].rule = "cannot_share_to_self"`; `field_errors.to_email[0].rule = "exactly_one_of_to_email_or_to_team_sid"`; or `field_errors.to_email` with the unresolved address | | Target workspace suspended or deleted | 409 | `conflict` | `context.reason = holder_team_unavailable` | | Account is not lendable, already lent out, or has a transfer in flight | 409 | `conflict` | `context.reason = account_not_shareable`, `account_already_shared`, `handover_in_flight` | | The share is not `active` | 409 | `conflict` | `context.reason = share_not_active`, plus `context.current_status` | | Account is not yours, recall by a non-owner, return by a non-borrower | 403 | `forbidden` | `context.reason = not_account_owner`, `not_share_owner`, `not_share_holder` | An active share blocks a transfer of the same account (`409 account_currently_shared`, recall first), and an in-flight transfer blocks a share. ## Transfer permanently Transfers use the same machinery and the same body as a share, minus `planned_return_at`. Create with `POST /api/account-transfers`, then poll `GET /api/account-transfers/{sid}` or `POST /api/account-transfers/search`. Transferring out of a billing hold is allowed, and is the intended exit for an account you no longer want to pay for. There is no MCP tool for any transfer route, including the read ones, and that is on purpose: an AI agent must not be able to give an account away. There is also no view-only permission tier, so `can_manage_account_transfers` gates reads as well as writes. Point agents at sharing instead. ## Who gets told Emails go to the counterparty, not to whoever made the call. The receiving workspace owner gets `account_share_received`, `account_share_recalled` and `account_transfer_received`; you get `account_share_returned`, `account_share_expired` and `account_share_failed`. Webhooks are owner-side only and fan out to the workspace that owns the row: `account-shares.created`, `account-shares.returned`, `.recalled` and `.failed`, plus `account-transfers.completed` and `.failed`. The borrowing side has no webhook, so its arrival signal is the email plus polling `list-received`. ## Related * Full request and response shapes: [API reference](/api-reference/overview) * [Connect a LinkedIn account](/kb/connect-a-linkedin-account) * [Antidetect browsers and proxies](/kb/antidetect-browsers-and-proxies) * [Billing and plans](/kb/billing-and-plans) * [Sync windows and auto-suspend](/kb/sync-windows-and-auto-suspend) # Antidetect browsers and proxies Source: https://docs.gtm-api.com/kb/antidetect-browsers-and-proxies Every LinkedIn account runs in its own isolated browser profile with a stable fingerprint and its own proxy. What the browser does, how proxies attach, and how to diagnose both. Every connected LinkedIn account lives in exactly one antidetect browser: an isolated browser profile with a stable device fingerprint and its own proxy. LinkedIn always sees the same device in the same location, which is what keeps automated activity looking like the account's normal usage. You never hand over the LinkedIn password; the login happens inside that browser. ## Browser lifecycle A browser is **created** (cloud-minted or bring-your-own), **run** when work needs it, and **stopped** when idle. An idle running browser is suspended automatically after about 15 minutes without activity, and due syncs or actions wake it on their own. See [Sync windows and auto-suspend](/kb/sync-windows-and-auto-suspend). If a running browser becomes unhealthy (its LinkedIn tab dies, the profile crashes), the platform marks it with an issue status and powers it off instead of leaving it running blind. Fix the underlying problem, most often the proxy, then run it again. Deleting a browser removes the profile; the bound LinkedIn account can be reconnected later through a new browser. **A browser nobody signs in on is deleted after 30 days.** A browser takes a seat of your plan from the moment it is created, before anyone signs in on it. If no LinkedIn account signs in, the platform deletes it 30 days after the last chance anyone had to sign in: 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. Its seat and its proxy are freed. A profile you brought from your own GoLogin account stays in your GoLogin account; only the browser on our side goes. To connect later, create a new browser. Browsers with a connected LinkedIn account are never deleted this way, even while the account is signed out. ## Where to see all of this Open the account's drawer on the **Senders** page and switch to the **Browser** tab: the browser block (vendor, owner, status, fails and logouts), the proxy block (latency, uptime), and the event log underneath. Account drawer Browser tab showing the antidetect browser block, proxy block and browser event log ## Connect a profile you already own Instead of letting the platform mint a browser, you can bind a GoLogin profile that already exists in your own account. Open **Senders**, click **Connect account**, switch to **Existing profile ID**. The profile keeps its own proxy and its own fingerprint, and it stays yours. The one thing that trips people up: we read that profile through **our** GoLogin account, not through yours. GoLogin's API only answers for profiles the calling account can see, so a profile sitting in your account is invisible to us until you share it. Open the profile in GoLogin, click **Share**, and add **[pro@getsales.io](mailto:pro@getsales.io)**. The share has to let us RUN the profile, not only see it. Before anything is created we read GoLogin's own run signals for our account: the profile's `canBeRunning` flag, the share's `runProfile` permission, and whether GoLogin has switched running off for it. A view-only share is refused right there. GoLogin's Session Lock (the padlock on the profile, `lockEnabled`) is not a permission and we do not read it. It only stops a second launch while the profile is already running somewhere, so keep one thing in mind: while your sender is running here, the profile will not open on your desktop, and if it is open on your desktop, a start from here fails until you close it. Copy the profile ID from GoLogin (the long hex string, e.g. `88857ca36ce45f284aab6213`) and paste it into **GoLogin profile ID**, then click **Verify & connect**. Both checks happen before the browser exists, so a refusal costs you nothing: | What you see | What it means | | - | - | | Profile not found, or it is not shared with gtm-api | The ID is wrong, or the share never happened. GoLogin answers the same way for "no such profile" and "not yours to see", so check both. | | This profile is shared with gtm-api, but GoLogin does not let our account run it | We can see it, but our account may not launch it: the share is view-only, or the owner's GoLogin plan does not allow running a shared profile. Session Lock is not the reason. | | This vendor profile ID is already in use | The profile is already bound to a browser, here or in another workspace. One profile drives one sender. | Sharing a profile does not hand over your GoLogin account, and we never change the profile's proxy or fingerprint. Un-share it in GoLogin whenever you want; the browser stops working on our side and nothing else about your account changes. ## Proxies: cloud and BYO * **Cloud-minted browsers require a proxy.** You pick the country when creating the browser; the platform attaches the least-loaded active proxy from the pool in that country automatically. * **Bring-your-own browsers keep their own.** An existing vendor profile (for example GoLogin) arrives with its proxy; the platform reads the session and never changes the proxy. Choose the proxy country to match where the account genuinely operates. A sudden country change is a classic trigger for LinkedIn verification challenges, and changing the proxy of a live browser restarts it: a running session keeps the old proxy until the browser respawns. ## Diagnose a proxy Two checks are available per browser in the drawer: | Check | What it verifies | | - | - | | Connectivity check | The proxy actually answers | | Location check | The proxy's real-world geo matches what is configured | If LinkedIn does not load inside the browser, run the connectivity check first. A browser whose proxy is dead cannot sync or act; parked work resumes automatically once the browser is healthy again. ## The same over the API ```bash curl theme={null} # List the team's browsers with their status curl -X POST "https://app.gtm-api.com/linkedin/v4/api/antidetect-browsers/search" \ -H "Authorization: Bearer gtm_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"filter": {"status": {"eq": "running"}}}' ``` ```typescript TypeScript theme={null} const res = await fetch( "https://app.gtm-api.com/linkedin/v4/api/antidetect-browsers/search", { method: "POST", headers: { Authorization: "Bearer gtm_live_YOUR_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ filter: { status: { eq: "running" } } }), }, ); const { items } = await res.json(); ``` `POST /api/antidetect-browsers/run` and `/stop` control the session, `/update-proxy` moves a browser to another proxy (country included), and `/replace-proxy` rotates the IP in place. The full contract lives in the [API reference](/api-reference/overview). ## Related * [Connect a LinkedIn account](/kb/connect-a-linkedin-account) * [Sync windows and auto-suspend](/kb/sync-windows-and-auto-suspend) * [Smart limits and warmup](/kb/smart-limits-and-warmup) # Collect leads with an auto-scrape Source: https://docs.gtm-api.com/kb/auto-scrapes Save a LinkedIn or Sales Navigator search as a recurring collector, deduplicate what it finds across runs, and feed the new leads into a standing mass action. An auto-scrape is a saved job that paginates one LinkedIn or Sales Navigator list surface through one of your own accounts' browsers, files every unique person or company into its own registry, and appends every never-seen-before lead to a standing mass action as one item. A [search](/kb/people-and-company-search) returns a page; an auto-scrape keeps returning on a cadence. **Give this to your AI agent** and keep the go-ahead for yourself: "On gtm-api (MCP connector at `https://mcp.gtm-api.com/mcp`, or REST at app.gtm-api.com with my key): create an auto-scrape from the search URL I give you, using account `ln_ac_...` as the executor and a weekly cadence. Show me the create payload and wait for my approval before calling it, because run 1 starts real LinkedIn automation immediately. After it runs, report the run counters and pull the leads with search\_linkedin\_auto\_scrape\_results filtered on the parent sid. Do not link a mass action unless I name one." ## Before you start * A connected LinkedIn account with a working browser. It is the **executor**: its browser pages the source and spends its own [scraping budget](/kb/smart-limits-and-warmup). * A Sales Navigator seat on that account for Sales Navigator sources and company decision-makers. * A standing mass action, if collected leads should be acted on. Author it first with scope `kind: "none"` (see [Run a mass action](/guides/run-a-mass-action)) and reference its `ma_ac_` sid. ## Create one in the app **Auto Scrapes** in the sidebar (`/auto-scrapes`), then **New auto scrape**. Paste a link into **Source URL** and the form shows `Detected:` with the method and result kind, or refuses anything outside the four families below with "This URL isn't supported. Use a LinkedIn or Sales Navigator people or company search URL." Accounts without a seat show as `no Sales Nav seat`; **Mass action** is optional and disabled for company sources. Run #1 dispatches at once. Source and executor are then frozen (the update form writes **Title**, **Replay** and **Mass action**), so changing the source or the executor means a new job with a fresh history. Auto Scrapes list showing two collectors with their source methods, executor accounts, paused status and daily replay ## Which sources it can run on The modal classifies four URL families (`/search/results/people`, `/search/results/companies`, `/sales/search/people`, `/sales/search/company`) into the matching `-by-url` method. Over the API the eligible set is 17 paginated verbs, addressed as `source_method` plus `source_input` instead of `url`: | `source_input` key | `source_method` values (SN = needs a Sales Navigator seat) | | - | - | | `url` | `search-people-by-url`, `search-companies-by-url`, `search-service-providers-by-url`, `search-sales-nav-people-by-url` (SN), `search-sales-nav-companies-by-url` (SN) | | `filters` | `search-people-by-params`, `search-companies-by-params`, `search-service-providers`, `search-sales-nav-people-by-params` (SN), `search-sales-nav-companies-by-params` (SN) | | `anchor_company_ln_id` | `company-employees`, `company-decision-makers` (SN), `similar-companies` | | `anchor_nickname` | `similar-profiles` | | `activity_urn` | `get-post-comments`, `get-post-reactors`, `get-post-resharers` | `result_kind` is `company` for the four company searches and `similar-companies`, `person` for the rest, frozen at create. One detail costs a create-time 422: `company-decision-makers` needs a Sales Navigator seat (`sales_nav_required`) although its platform reads as plain LinkedIn. Two more are never checked at create: `anchor_company_ln_id` must be numeric, and `similar-profiles` takes the vanity slug, never a URN (`anchor_nickname: "jcooper"`). A bad value there still creates the job, and run #1 fails instead: a non-numeric company anchor lands as `company_anchor_not_numeric` in `error_message`. ## How often it runs `replay_frequency` plans the next run as finish time plus the interval, not on a fixed wall clock: a daily job whose run takes three hours next runs 24 hours after that run finished. | `replay_frequency` | Interval after the previous run finalizes | | - | - | | `hourly` | 60 minutes. API only, not offered in the modal | | `daily` | 1440 minutes (24 hours) | | `weekly` | 10080 minutes (7 days) | | `monthly` | 43200 minutes (30 days) | | `null` | Never, the job is one-shot | Two runs of one job never overlap: minting a run parks the clock (`next_run_at` becomes `null`) and only finalizing re-plans it, so a run in flight picks a cadence change up when it finalizes. `next_run_at: null` means one of three things, and the drawer says which: a run is in flight, the job is paused ("clock frozen"), or a one-shot already ran. Auto-scrapes are not gated by the [sync window](/kb/sync-windows-and-auto-suspend): a due job runs when it comes due, and a suspended browser wakes. One drive attempt walks at most **100 pages**, then the run completes normally, the rest of the source is not collected, and the next run starts again at page 1. A run resumed after a process death gets a fresh 100-page budget, so the cap tell is `pages_applied` of 100 or more, not exactly 100. Split a source deeper than 100 pages into narrower searches. ## How runs report themselves Use the **Runs** tab, or the runs endpoint. Each run carries `results_seen` (rows the pages returned), `new_count` (identities first seen on this run), `seen_count` (re-sightings) and `enrolled_count` (leads appended to the mass action, max `new_count`). | Status | Meaning | | - | - | | `pending` | Minted and waiting for a worker | | `in_progress` | The page loop is running, the clock stays claimed until it finishes | | `completed` | Terminal, counters are final | | `failed` | Terminal, counts toward the auto-pause streak. There is no retry verb: the next attempt is a new run, from the schedule or from **Restart now** | On failure, `error_message` names the class: | Prefix | What happened | | - | - | | `limit_reached: {bucket}` | The executor's smart-limit bucket ran out mid-run. The cause is appended: `linkedin_quota_hit`, `held`, `daily_saturation` or `delay_not_elapsed` | | `run_timeout: no progress` | The driver died and the run stopped moving | | `run_timeout: never started` | The dispatch was lost between the mint and the queue | | `abandoned: parent deleted` | The job was deleted mid-run. This one never counts toward the auto-pause streak | | `{ErrorClass}: {message}` | Anything else the driver caught | A reaper scans every **15 minutes** and finalizes stuck runs: an `in_progress` run with no movement for **2 hours**, and a `pending` run older than **30 minutes** that never started. Both are stamped `failed` with `run_timeout:`, releasing the clock. ## How leads are deduplicated across runs Each job keeps its own registry: one row per unique person or company, ever, and a re-sighting updates that row, never adds a second. Scope is the job, not the team, so "new" means new for this collector, and one person can hold a row under several auto-scrapes by design. The match walks an identity ladder, first hit wins: person `ln_member_id` → `ln_id` → `sn_id` → `nickname`, company `company_ln_id` → `nickname`. * **Any overlapping identifier matches.** Every identifier on the incoming row is probed against its own column, so a LinkedIn sighting and a Sales Navigator sighting of one person collapse onto one row. * **Blanks are filled in, never overwritten.** A missing identifier is backfilled onto the existing row, and the `dedup_key` stamped at insert is a historical label that never changes. * **Rows with no decodable identity are dropped.** They count in `results_seen`, then are never stored and never enrolled. That, plus `seen_count`, is why the registry holds fewer rows than the pages showed. The registry is the ground truth and the counters are advisory: a page re-applied after a crash reclassifies its own inserts as re-sightings, inflating `seen_count` and undercounting `new_count` while the row set stays exact. Per-run delta is a filter, not a flag: `first_seen_run_sid` equal to a run's sid gives the leads that run discovered, `seen_runs_count` the recurrence signal. ## Feed a standing mass action The mass action is authored separately with scope `kind: "none"`, so it starts as a standing run with zero items, and the auto-scrape appends to it: every new lead becomes one item. * Link at create with `mass_action_sid`, or later with `set-mass-action`. A new link replaces the previous one, returned as `previous_mass_action_sid`, which keeps draining what it holds. * **The link applies from the next run.** A run gates on the link it was minted with, so nothing enrolls retroactively and a run in flight keeps what it started with. * **Only new leads enroll**, and **company jobs cannot enroll yet**, because no company-capable target family is step-eligible. The 1 to 100 item cap applies at create time to the scope you author; a standing run has no ceiling, so an auto-scrape can grow it past 100. Appends are deduplicated inside the mass action too, a paused or deleted one rejects them, and several auto-scrapes can feed one run. `enrolled_count` below `new_count` has four causes the aggregate cannot separate: the mass action already held that lead (healthy), the enrollment hop failed and was logged as a shortfall, the run crashed between saving rows and enrolling them, or it was collect-only. A failed enrollment is a shortfall, not a run failure, and it is not retried. Those leads are now `seen` for every later run, and enrollment only ever targets new leads. To recover them, pull the rows from the registry and author a `targets`-scope mass action over them. ## Pause, restart, or stop one | Action | Where | Effect | | - | - | - | | **Pause** | Row action, or `POST /{sid}/pause` | `status` becomes `paused`, `paused_reason` becomes `manual`, the clock freezes. A run already in flight finishes | | **Resume** | Row action, or `POST /{sid}/resume` | Back to `active`, the failure streak resets to 0, the next run is due immediately | | **Restart now** | Kebab menu, or `POST /{sid}/run-now` | Mints a `manual` run at once. Blocked while a run is in progress ("a run is in progress") and while paused ("paused, resume first") | | **Delete** | Kebab menu, or `DELETE /{sid}` | Soft delete, and permanent: no further run is minted, an in-flight run abandons at its next page boundary, runs and results stay for audit. A recreated job re-collects everything as new | An exhausted one-shot stays `active` with a frozen clock, so **Restart now** is how you run it again. Sharing the executor account out, or transferring it, pauses that account's auto-scrapes with `paused_reason: account_unavailable`, and when a share ends the platform resumes exactly those rows. A transfer does not come back: the executor cannot be repointed. A job auto-pauses after **10 consecutive failed runs**, with `paused_reason: too_many_failures` and a `linkedin-auto-scrapes.paused` webhook. The streak resets to 0 on the first successful run. `limit_reached:` failures count deliberately, so a cadence that chronically outruns the executor's scraping budget walks itself to a pause instead of failing quietly for months. ## Over the API Create it with either `url` or the `source_method` plus `source_input` pair, never both. ```bash curl theme={null} curl -X POST "https://app.gtm-api.com/linkedin/v4/api/linkedin-auto-scrapes" \ -H "Authorization: Bearer gtm_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "linkedin_account_sid": "ln_ac_YOUR_ACCOUNT", "url": "https://www.linkedin.com/sales/search/people?query=growth%20marketer", "replay_frequency": "weekly", "mass_action_sid": "ma_ac_YOUR_MASS_ACTION" }' ``` Read the leads back from `/linkedin-auto-scrape-results/search`, filtered on `linkedin_auto_scrape_sid` for the whole registry or `first_seen_run_sid` for one run's delta; `page_size: 0` answers "how many unique leads has this job collected" in one call. Request shapes, filters and sorts are in the [API reference](/api-reference/overview). `page_size` tops out at **200** and asking for more is a `validation_failed` rather than a clamp; `dedup_key` needs `linkedin_auto_scrape_sid` alongside it or it answers 422 `bounded_scan_required`, and the same rule covers `run_number` on the separate runs search; `nickname` matches exactly, and no endpoint here supports `q`. Create and link errors are **422** unless noted. Neither or both of `url` and the `source_method` plus `source_input` pair answers `validation_failed` on field `source`, and a mass action that is missing, deleted or on another team answers it on field `mass_action_sid`. The rest: `invalid_url`, `invalid_source_method` (not auto-scrape eligible), `method_not_available` (no live wire verb yet), `source_input` missing the family's required key, `sales_nav_required`, `account_not_in_team`, `mass_action_incompatible` (the target family rejects this `result_kind`), `nothing_to_update` (`PATCH` with neither `title` nor `replay_frequency`), and **503** `service_unavailable` (reason `mass_action_registry_unavailable`), retryable, when the cross-check cannot reach Orchestration. The verbs answer **409**: `no_mass_action_set`, `invalid_status_transition` (`resume` on an active job), `auto_scrape_not_active` (`run-now` while paused) and `run_in_progress` (`run-now` mid-run). ## Watch it with webhooks | Event | Fires when | | - | - | | `linkedin-auto-scrape-runs.completed` | A run finished. Payload: `run_number`, `trigger`, `new_count`, `seen_count`, `enrolled_count`, `pages_applied`, `duration_seconds` | | `linkedin-auto-scrape-runs.failed` | A run went terminal. Payload: `page_cursor`, `new_count`, `enrolled_count`, `error_message` | | `linkedin-auto-scrapes.paused` | A job paused. Payload: `status`, `paused_reason`, `consecutive_failures` | There is deliberately no per-page and no per-lead event. Subscribe to both run events, because a failed run's already-applied pages wrote durable rows, then call the results search on the payload's `sid` unconditionally, whatever the counters said. Endpoint setup is in [Receive webhooks](/guides/receive-webhooks). ## What it costs Every page runs on the pinned executor account. A page spends that account's own `scraping` smart-limit bucket, one spend per page, and writes a row in Data Requests. The ceiling is per account, not per job, and the daily `scraping` budget is warmup-driven, so a young account collects less per day. Pages are never served from cache. If a collector keeps failing with `limit_reached: scraping`, lower the cadence or move it to an account with headroom rather than raising the budget past what [warmup](/kb/smart-limits-and-warmup) supports. The drawer is a sample plus an export, not a browser. **Results** loads the newest 100 rows, **Runs** the newest 50, and the list page loads up to 200 jobs and filters them in memory. **Download CSV** exports the rows currently loaded, not the whole registry: for the full set, page the results endpoint. ## Related * [Run a mass action](/guides/run-a-mass-action) * [People and company search](/kb/people-and-company-search) * [Smart limits and warmup](/kb/smart-limits-and-warmup) # Billing and plans Source: https://docs.gtm-api.com/kb/billing-and-plans Sandbox, the forever free plan you start on, how paid accounts are priced, and what happens when a payment fails or a subscription ends. You start on Sandbox, the forever free plan: one connected LinkedIn account, the full product, no card and no time limit. This page covers what each plan grants, what changes at each boundary, and where to see it. ## Where to see your plan The sidebar card is the workspace's plan and usage at a glance: the plan name and how many account slots are used against the cap. Every browser holds a slot from the moment it is created, including one nobody has signed in on yet. A browser that no LinkedIn account signs in on is deleted after 30 days, which frees its slot ([details](/kb/antidetect-browsers-and-proxies#browser-lifecycle)). Sidebar plan card showing the plan name and 7 of 5 account slots used Slots can read over the cap (`7 / 5` above) whenever the cap drops below what is already connected, for example after a paid downgrade. The newest accounts are parked and their browsers powered off until you free a slot or raise the cap. A parked account keeps its browser profile for as long as it stays signed in to LinkedIn. The profile is deleted only once LinkedIn has logged the account out or restricted it for more than 30 days, and a warning email comes a week before. ## Sandbox, the forever free plan Signing up lands you here, with no card and no time limit. Sandbox is one workspace per account: a further workspace you create comes up without capacity until you buy a plan for it. It is the full product on one connected account: the UI, the REST API, the MCP server and every LinkedIn action verb stay available, and the plan caps volume and frequency instead of features. | Grant | Value | | - | - | | Connected accounts | 1 | | Card required | None | | Parallel cloud-browser sessions | 1 | | Webhooks | 1 | | Expiry | None; the plan does not end | Two things are genuinely off rather than slowed: **team members** (the workspace is single-user) and the **AI copilot**. **One webhook** is included: one registered endpoint, and it can subscribe to every event type with `events: ["*"]`. Registering a second answers `402` with `webhook_limit_reached`. A trigger in n8n, Make or Zapier registers a webhook of its own, so on Sandbox one trigger runs at a time. Realtime push is off on Sandbox (below), so an event that starts on LinkedIn, such as a received message, reaches the webhook when a sync reads it rather than the moment it happens. Daily action caps on Sandbox, per connected account: | Action | Per day | | - | - | | Connection requests | 5 | | Messages | 10 | | InMails | 5 | | Profile visits | 8 | | Scraping actions | 15 | | Data requests | 20 | | Enrichments | 12 | | Comments, reactions, endorsements | 5 each | | Posts | 1 | | Profile edits | 2 | | Custom requests | 9 | | Networking, general | 6 | | Messaging, general | 15 | Two more Sandbox behaviors worth planning around: * **Sync cadence is relaxed.** Each sync surface refreshes at most once every 24 hours, and realtime message push is off. Initial backfills after connecting are exempt. * **An account signed out of LinkedIn for more than 30 days is parked.** That covers an account LinkedIn logged out and one it restricted. The browser profile is deleted, so coming back means connecting the account and logging into LinkedIn again. An account that stays signed in is never parked, however little you use it. Account health keeps syncing on Sandbox regardless, so smart limits and warmup stay accurate. ## Paid plans Pricing is per connected account per month, in volume brackets. Every account bills at the bracket its total lands in, so the price per account drops for the whole workspace as you add accounts. | Accounts | Per account / month | Parallel cloud-browser sessions (workspace total) | | - | - | - | | 1 to 4 | \$39 | 1 | | 5 to 9 | \$30 | 2 | | 10 to 19 | \$25 | 2 | | 20 to 49 | \$20 | 3 | | 50 to 99 | \$16 | 3 | | 100 to 299 | \$13 | 4 | | 300 to 999 | \$10 | 5 | | 1,000+ | Custom | Custom | Two allowances do not scale with the account count: **100 webhooks** and the session column above, which is a total for the workspace rather than a per-account number. * **Annual billing** is pay for 12 months, get 15 months of service. * **5G Proxy** is an optional add-on at **+\$15 per selected account per month**, for accounts that need a dedicated premium 5G mobile route: commands execute faster, with fewer retries and fewer drops. The current published pricing is on the [pricing page](https://gtm-api.com/pricing/). ## Subscription statuses | Status | What it means | | - | - | | `active` | Paid and current, or the free Sandbox plan | | `past_due` | A payment failed; a grace window is running | | `paused` | Billing stopped by request; the workspace is gated, data is kept | | `canceled` | Ended. Terminal: a new subscription is a new purchase | The workspace itself is either active or suspended, derived from the above: `active` is active, `paused` is suspended, and `past_due` stays active until the grace window closes. ### When a payment fails The workspace keeps working for **7 days** past the end of the paid period. Update the card within that window and the subscription returns to `active` on its own. After it, the workspace is suspended until payment succeeds. Card updates happen in the payment provider's hosted portal, reached through a short-lived link the API mints (`POST /api/billing-subscriptions/{sid}/get-update-payment-method-link`). Workspaces billed by sales invoice have no card portal; talk to us instead. ### When a subscription ends Cancelling takes effect at the end of the paid period, with no immediate charge and no refund for the remainder. The workspace then falls back to Sandbox and keeps working at free-plan volume: the account cap reconciles to 1, and the accounts beyond the cap are parked newest-first with their browsers powered off. Reconnect or upgrade and they come back. A canceled workspace that cannot fall back to Sandbox (the owner already has a free workspace, for example) has no coverage at all, and it is scheduled for deletion **30 days** after the subscription ends. A paused or past-due subscription never schedules it. The workspace owner gets an email with the date within a day, and a final one 3 days before it. No workspace is deleted before that final email has gone out. Resubscribe before the date and the deletion is canceled: the workspace comes back as it was. On the date the workspace is deleted, and that cannot be undone. API keys are revoked, members lose access at once, and the data is erased 30 days later. Pausing is the alternative: billing stops at the next period, the workspace is gated like a suspended one, and every account, browser and row is retained until you resume. ## Changing plan over the API Seat count is the subscription's `quantity`, and it is what the account cap follows. Changing it re-brackets the price automatically. The change is a two-step flow, and the first step charges nothing: ```bash preview theme={null} # What would this cost? Read-only: no charge, no change. curl -X POST "https://app.gtm-api.com/id/v4/api/billing-subscriptions/bl_sb_YOUR_SUB/preview-change" \ -H "Authorization: Bearer gtm_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"quantity": 12}' ``` ```bash apply theme={null} # Commit the same change curl -X POST "https://app.gtm-api.com/id/v4/api/billing-subscriptions/bl_sb_YOUR_SUB/change" \ -H "Authorization: Bearer gtm_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"quantity": 12}' ``` ```bash read theme={null} # The workspace's current subscription and its status curl -X POST "https://app.gtm-api.com/id/v4/api/billing-subscriptions/search" \ -H "Authorization: Bearer gtm_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"page_size": 5}' ``` Buying the first paid subscription is `POST /api/billing-subscriptions/create-checkout`, which returns a `checkout_url` for a human to complete. The subscription exists once the payment settles, not when the link opens; an abandoned checkout leaves nothing behind. The subscription verbs take no idempotency key. `change` charges the prorated difference immediately, so if a call returns unclearly, read the subscription back before retrying rather than sending it twice. Pause, resume, cancel, and add-ons (`add-addon`, `remove-addon`) are the remaining verbs on the same subscription. Add-ons charge prorated immediately; removing one takes effect at the next period. None of these apply to Sandbox workspaces, which have no payment provider subscription behind them. ## Related * [Smart limits and warmup](/kb/smart-limits-and-warmup) * [Connect a LinkedIn account](/kb/connect-a-linkedin-account) # Browser will not start Source: https://docs.gtm-api.com/kb/browser-troubleshooting The antidetect browser sits in an issue status, the session dies right after starting, or LinkedIn keeps asking to log in. How to read the status, what the platform already did on its own, and when re-login is the only fix. An account has stopped working and its antidetect browser shows an issue status. The status pill is the diagnosis, and each fault has exactly one fix. ## Read the status first Open **Senders**, click the account row, open the **Browser** tab of the drawer. The first four statuses below are exactly what the **Browser issue** filter chip on the list selects. | Status | Pill | What happened | What clears it | | - | - | - | - | | `start_issue` | amber | The launch was reported as failed. The browser is not running. | Run it again | | `running_issue` | amber | The session started, then hit a runtime or proxy error. Powered off. | Run it again, then check the proxy | | `client_error_investigation` | amber | Repeated failures crossed the escalation threshold on a cause only you can fix: the vendor profile was deleted, the LinkedIn session or account is the problem, or the proxy you supplied is down. Powered off. | Fix what **Last error** names, then Restart. A proxy fault is also retried on its own (three hours, then six, then twelve, then daily for two weeks), so a proxy you fix in GoLogin comes back without a Restart | | `support_error_investigation` | amber | Repeated failures crossed the threshold on our side (a pool proxy, the node, the host) or on a cause we could not classify. Powered off. | Nothing on your side: our team is paged, and the platform tries the browser again every three hours until it runs | | `login_issue` | red | LinkedIn invalidated the session. Powered off, and sticky. Right after the logout the platform checked the account from another LinkedIn account and the profile was still up, so this is a lost session. | Re-login inside the browser, nothing else | | `restricted` | red | LinkedIn invalidated the session and, checked right after from another LinkedIn account, shows no public profile for the member: a restricted (or closed) account. Powered off, and sticky. | Sort it out with LinkedIn on your own device (its verification or appeal), then re-login inside the browser | | `shared_out` | grey | Not a fault. The browser is lent to another workspace. | The share ending | | `subscription_required` | amber | Not a fault. The browser does not fit the team's subscription. | Upgrading, or freeing a slot | `shared_out` and `subscription_required` are HOLD states, not failures. The row action reads **Parked, no browser action available**, and a `run` call against either is refused with HTTP 409 and `error.context.reason = "invalid_status_transition"`. ## What the platform already did | Behaviour | Exact rule | | - | - | | Auto-suspend | A `running` browser with no activity for 15 minutes is powered off and committed as `idle`. The reaper runs every 1 minute. | | Wake on demand | Only an `idle` browser wakes by itself. An action waits for it to come up; a due sync defers and resumes. See [sync windows and auto-suspend](/kb/sync-windows-and-auto-suspend). | | Power-off on fault | Landing in `running_issue` or either `error_investigation` status triggers a best-effort stop of the session. The stored status stays at the fault value so you can still see it. | | Power-off on logout | The `login_issue` branch tears the session down immediately, and the row stays `login_issue`. | | Proxy fault | A proxy error marks the proxy as `no_connect` and migrates the browser onto a fresh proxy in the same country. The browser's fail count is not incremented, because the proxy was at fault. | | Sync backoff | A sync run that cannot reach the browser stays `in_progress` and re-checks. No data is lost and the cursor does not move. | | Profile survives | Powering off a faulted browser does not log the account out. The profile persists on disk, so the LinkedIn session survives a power-off, a suspend and a cold start. | `start_issue`, `login_issue` and `restricted` never recover on their own. A sync run that hits one of them re-defers every 30 minutes, indefinitely, with `wait_reason = "browser:start_issue"`, `browser:login_issue` or `browser:restricted`, until a person or an API call intervenes. A browser that is merely waking re-checks every 60 seconds instead. ## Ordered checks Stop as soon as one of these explains what you see. 1. **Confirm it is a fault, not a hold.** `shared_out` and `subscription_required` are parked on purpose, and no lifecycle action is available. 2. **If the pill is red, read which red it is.** `login_issue` goes straight to [re-login](#re-login-is-the-only-fix); restarting it will not help. `restricted` goes to [LinkedIn first](#restricted-linkedin-took-the-account-away): a re-login cannot clear it. 3. **Read the reason.** On the **Browser** tab, scroll to **Browser event log**. The newest `error` row names the cause in its `event_type`, `code` and message. 4. **Restart once.** For `start_issue` or `running_issue`, use **Restart now** on the drawer banner, or **Restart** in the row action menu. Most clear on a fresh start, so give it one attempt and watch the status. 5. **Only then look at the proxy.** If the restart lands back in `running_issue`, run the connectivity and location checks over the API (`antidetect-browser-proxies/check-proxy-connectivity` and `check-proxy-location`). See [proxy troubleshooting](/kb/proxy-troubleshooting). 6. **Escalate.** `client_error_investigation` is the platform saying it has stopped guessing and the cause is on your side: fix what **Last error** names, then Restart. `support_error_investigation` means the cause is ours (or unclassified): our team is already paged, there is nothing to do on your side. ## `start_issue`: nothing came up Two physically different things produce it, and the event log says which: **nothing ever launched**, a host-side or network failure that killed the launch before a browser existed (usually a raw error string, for example a DNS failure), or **the process came up and the handshake did not**, where the post-start check that confirms the LinkedIn session never completed. Both leave the browser powered off, because the node tears down its own browser after reporting a failed start, and both take the same fix: **Restart now** on the **Browser did not start** banner. ## `running_issue`: the session dies right after starting Restart once. A proxy error also marks the proxy dead and swaps in a replacement by itself. If the browser keeps flapping, it escalates. Escalation is exact: more than 10 recorded failures **and** more than 10 error log rows in the last 48 hours moves the browser to `client_error_investigation` (the cause is yours to fix) or `support_error_investigation` (ours, or unclassified). The bound LinkedIn account is cascaded to `sync_failed`. A client parking emails the workspace owner (at most one per owner, per browser, per hour); a support parking pages our team instead. Recovery is manual: open the browser and look at it. Identical log rows are collapsed: two events with the same browser and the same `event_type` inside 60 seconds produce one row, not two, so the log undercounts a rapidly flapping browser. The fail count on the browser block is the honest counter. ## `login_issue`: LinkedIn asks to log in again The drawer shows **Signed out of LinkedIn**. During an initial sync the sync box shows **Sync paused, action needed**, with `browser:logged_out` next to the **Reason:** label and the reason line **LinkedIn session lost**. That code is a fixed display string in the app, not the sync run's `wait_reason`. There is no separate "logged out" status, and three properties of this one explain almost every ticket in this category: * **It is sticky.** Later runtime errors, proxy errors and repeat logout reports are recorded in the log but cannot reclassify the status or bump the counters. Only a successful re-login moves the row off it. * **The browser is already powered off**, which is why the row action reads **Start** rather than **Restart**. * **Nothing will wake it.** Automatic wake covers `idle` browsers only. A logout is almost always noticed at the next start, not mid-run: the post-start check is what probes the LinkedIn session, so a session LinkedIn invalidated while the browser sat idle only surfaces when the browser comes back up. The ACCOUNT row does not change when this happens: its `status` is platform lifecycle and keeps reading `active` while the browser sits in `login_issue`. Any health read, human or API, must look at the browser: over the API that is `include[]=antidetect_browser` on the account search, plus the always-present `counts.signed_out_count` ([account health](/kb/account-health)). Two emails can follow a logout, and the platform decides which one before it sends anything. Right after the crossing it reads the member's public profile from another LinkedIn account of the fleet, the way any visitor would. A profile that answers means a lost session: the person who connected the browser gets **Your LinkedIn account was signed out**, with a **Profile check** line saying the profile is still up. No profile for the member's stable id means LinkedIn restricted the account (or the holder closed it): the browser moves to `restricted` and the mail is **LinkedIn restricted your account**, with LinkedIn's verification and appeal steps instead of a re-login (see [below](#restricted-linkedin-took-the-account-away)). When the check itself cannot run, the plain signed-out mail goes out without the check line. The events go to analytics and to webhooks as `antidetect-browsers.logged-out` and, for the verdict, `antidetect-browsers.restricted`; on screen they are the two red pills. If you run many accounts, [subscribe to those webhooks](/guides/receive-webhooks) rather than waiting to be told. ### Re-login is the only fix Pressing **Start** re-verifies the session end to end rather than repairing it: either the session turns out intact and the browser recovers to `running`, or the login wall is hit again and the row goes straight back to `login_issue` with another power-off. 1. In the account drawer, the red **Signed out of LinkedIn** banner sits above the tabs; click **Log in via cloud browser** there (the same link is on the account row). Connecting takes up to 30 seconds. 2. Click **Log into LinkedIn** and complete the login in that window, including any verification LinkedIn asks for. You are typing into the account's own browser, on its own proxy, so the device and location LinkedIn sees do not change. 3. Click **I have logged in** and keep the window open while the profile uploads to secure storage. The upload is confirmed from the launcher log within about 90 seconds; the app then closes the window and watches the browser every 3 seconds for up to 4 minutes. Reaching `running` reports **LinkedIn session restored, the browser is back up**; hitting the 4-minute deadline reports **Still verifying the session, we'll keep working in the background**. Syncing and automations resume on their own once the browser is running again, from where they stopped. If the person who can pass LinkedIn's verification is not a user of your workspace, issue a temporary link to the same browser session with `antidetect-browsers/generate-cloud-browser-access-key`. Call `revoke-cloud-browser-access-key` on the same `sid` first, so any older link dies. A cloud-browser access key is a bearer secret: anyone holding the link can drive that browser. The default lifetime is 8 hours, `ttl_hours` accepts 1 to 720, and you can cap `max_connects` (1 to 1000) and restrict `allowed_ips` or `allowed_countries`. Without a cap the link is redeemable until it expires, so pass `max_connects: 1` to make it single-use. Send it through a private channel and revoke it once the login is done. ## `restricted`: LinkedIn took the account away The drawer shows **Restricted by LinkedIn** in place of the signed-out banner, and the status popover names the verdict: LinkedIn has no public profile for this member, checked from another LinkedIn account right after the sign-out. That is what a restricted account looks like from the outside (a closed account looks the same, so if you closed it yourself this is a false alarm and a re-login clears it). The **Browser event log** carries an `account_restricted` row with the identities that were asked and LinkedIn's answer. Three things follow from the verdict: * **A re-login alone will not fix it.** Pressing **Start** re-verifies the session, hits the login wall again, and the probe runs again: the row lands back on `restricted`. * **LinkedIn is the only place to lift it.** Sign in to LinkedIn on your usual device or browser, not through the cloud browser: LinkedIn shows the restriction notice and the steps it wants, usually a photo ID check, sometimes an appeal form through LinkedIn Help. Most temporary restrictions clear within a few days once the steps are done. Until then, do not run the account from another tool: every new sign-in attempt counts against it. * **Then re-login here.** Once LinkedIn lets you in, follow the [re-login steps](#re-login-is-the-only-fix) exactly as for `login_issue`; the account picks up where it stopped, and nothing is lost in the meantime. Over the API, a dispatch on a restricted sender is refused with HTTP 409 and `error.context.reason = "browser_account_restricted"` (the logged-out refusal reads `browser_logged_out`), and the account search's `counts.restricted_count` says how many senders sit there; `counts.signed_out_count` counts both red statuses. ## Read the event log The **Browser event log** lists When, Level, event type, Code and Message. The rows that matter: | `event_type` | `code` | Means | | - | - | - | | `start_failure` | `null`, or the HTTP status of the failed start dispatch | The launch was reported as failed | | `login_issue` | `401`, or `null` when the logout was seen as an authwall redirect | LinkedIn logged the session out | | `proxy_error` | `null` | The proxy did not answer; a replacement is being attached | | `runtime_error` | `null` | The session broke after starting | | `server_unreachable` | `777` | The automation server hosting the browser went unreachable | | `error_escalation` | none | The threshold tripped, status moved to `client_` or `support_error_investigation` | Most rows carry no code at all, so filter on `event_type` and read the cause out of the message rather than the `code` column. The same log is searchable over the API. `counts.groups.event_type` and `counts.groups.code` answer "logged out, or proxy, or runtime" on their own, so ask for `page_size: 0` when you do not need the rows themselves. ```bash curl theme={null} curl -X POST "https://app.gtm-api.com/linkedin/v4/api/antidetect-browser-logs/search" \ -H "Authorization: Bearer gtm_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "filter": { "antidetect_browser_sid": {"eq": "ab_br_Hx7kQ3mN2pL4"}, "level": {"eq": "error"}, "created_at": {"gte": "2026-08-12T00:00:00Z"} }, "sort": {"field": "created_at", "direction": "desc"}, "page_size": 50 }' ``` ## Restart over the API `POST /linkedin/v4/api/antidetect-browsers/run` with `{"sid": "ab_br_..."}` is the same action as **Restart now**. It is accepted from `stopped`, `idle`, `start_issue`, `running_issue`, `login_issue`, `restricted`, `client_error_investigation`, `support_error_investigation` and `maintenance`. Every refusal returns HTTP 409 with `error.code = "conflict"`, so switch on `error.context.reason`, never on the code alone. | `error.context.reason` | Meaning | | - | - | | `invalid_status_transition` | A status `run` does not accept, for example a HOLD state. `context.from` carries it | | `vendor_profile_busy` | Another row is already running this browser profile | | `no_automation_server_available` | No healthy host to launch on. Retry later | Two more come from action endpoints rather than from `run`: HTTP 503 with `recoverable: true` means the session is not alive and the action should be retried after a restart, and HTTP 409 with reason `captcha_required` or `account_soft_locked` means LinkedIn wants a human in the browser first. Full request shapes are in the [API reference](/api-reference/overview). Copyable prompt for your own agent: "Triage a stuck LinkedIn account for me using the GTM API. Find the account's antidetect browser and read its status. If the status is `login_issue`, stop and tell me to re-login through the cloud browser; do not call run. If it is `start_issue` or `running_issue`, search antidetect-browser-logs for that browser sid with level error over the last 24 hours, sorted by created\_at descending, and report the newest event\_type, code and message. Then call run once and report the resulting status. If run returns 409, print `error.context.reason` verbatim and stop." ## Still stuck Send us the account name and the browser sid (`ab_br_...`, on the **Browser** tab), a screenshot of the drawer showing the status pill and the banner text, and the newest error row from the event log verbatim: event type, code and the full message string. ## Related * [Antidetect browsers and proxies](/kb/antidetect-browsers-and-proxies) * [Connect a LinkedIn account](/kb/connect-a-linkedin-account) * [Sync windows and auto-suspend](/kb/sync-windows-and-auto-suspend) # Connect a LinkedIn account Source: https://docs.gtm-api.com/kb/connect-a-linkedin-account Connect an account in the app, wait out the initial sync, then work with it over the API. The login happens inside a dedicated antidetect browser; no password ever reaches the platform. Connecting is the one step that happens in the app rather than the API: the account owner signs in to LinkedIn inside a dedicated antidetect browser, because a live human login is what keeps the session real. Everything after that is API territory. **Give this to your AI agent** and it will handle the API side once you finish the in-app login: "Connect to gtm-api: add the MCP connector at `https://mcp.gtm-api.com/mcp` (OAuth) or call the REST API at app.gtm-api.com with my key. Then: search linkedin-accounts for my newly connected account, watch its status until it is active, read its smart limits, and tell me what outbound budgets I have today. Do not send anything yet." ## Prerequisites * A gtm-api workspace on any plan (Sandbox, the forever free plan, includes one account slot) * The LinkedIn credentials stay with you; you will type them into LinkedIn itself ## 1. Connect in the app On [app.gtm-api.com](https://app.gtm-api.com/login), open **Senders** and click **Connect account** in the top right. Senders page listing LinkedIn accounts with statuses, browsers and health scores **Cloud browser** (recommended): the platform mints a fresh anti-detect profile with a managed residential proxy. Pick the **Proxy location** country to match where the account usually logs in from. Connect LinkedIn account modal on the Cloud browser tab with vendor and proxy location fields **Existing profile ID** (bring your own): you already have a GoLogin profile. It keeps its own proxy; the platform verifies the profile exists and is runnable, and never changes it. Connect LinkedIn account modal on the Existing profile ID tab with the GoLogin profile ID field Run the browser and sign in to LinkedIn in the window that opens. The platform detects the logged-in account and binds it to the browser. Accounts are deduplicated by their LinkedIn member identity: reconnecting an account your team connected and deleted before restores it instead of duplicating it. If the account is already connected on a different team, binding is refused. A sudden proxy-country change is a classic trigger for LinkedIn verification challenges. Pick the country the account genuinely operates from, and keep it. ## 2. Watch the initial sync The account enters onboarding: an initial import of the profile, conversations, connections and invitations, in a strict order. Outbound actions are held until it completes. ```bash theme={null} curl -X POST "https://app.gtm-api.com/linkedin/v4/api/linkedin-accounts/search" \ -H "Authorization: Bearer gtm_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"filter": {"q": "cooper"}}' ``` The `status` field is the account lifecycle: | Status | Meaning | | - | - | | `new` | Connected, sync not started | | `initial_syncing` | First sync in progress | | `active` | Usable; outbound actions allowed | | `sync_failed` | The sync hit a problem; check the account in the app | | `shared_out` | Lent to another team via account sharing | | `subscription_required` | The plan lapsed; renew to reactivate | Until the account reaches `active`, outbound endpoints (connection requests, messages) answer with a `conflict` error. Read endpoints work as soon as data lands. When the first sync finishes, the person who connected the account gets an email saying it is ready, with what the sync brought in; a large account can run out of its first-day read budget part-way, in which case the account carries `initial_sync_held_at`, the sync resumes on its own after the daily reset, and the email arrives a day later and says so. ## 3. Check capabilities and limits * `POST /api/linkedin-accounts/{sid}/check-premium-subscription` reports what the account's LinkedIn subscription actually supports (`has_premium`, `has_sn`, `recruiter_status`, and for a Recruiter seat its number and the session's expiry); `checks` narrows it to one step. Sales Navigator endpoints on an account without Sales Navigator fail upfront with `422 sales_nav_required`, Recruiter ones with `422 recruiter_required`, rather than mid-run. * `POST /api/linkedin-account-smart-limits/search` returns the account's current per-action daily budgets. A newly connected account starts well below platform maximums and warms up as it ages; read these limits instead of assuming a number. See [Smart limits and warmup](/kb/smart-limits-and-warmup). * `POST /api/linkedin-account-snapshots/search` gives the health timeline the platform records for the account. ## 4. First action Once `active`, the account's sid (`ln_ac_...`) is the handle every LinkedIn endpoint takes. A sensible first write is something low-risk, for example reacting to a post, before you schedule real outreach through a [mass action](/guides/run-a-mass-action) with pacing. Every outbound call re-checks the smart limits server-side before dispatch. Hitting a budget answers `limit_exceeded`, and the action can resume the next day or after the limit is raised; the account is not penalized for your retry logic. ## If the connection fails * The browser must be **running** and the LinkedIn tab reachable: a stopped or faulted browser cannot bind an account. * Check the proxy: a dead or blocked proxy prevents LinkedIn from loading. Run the connectivity check in the browser drawer ([how](/kb/antidetect-browsers-and-proxies#diagnose-a-proxy)). * If the account was connected on another team, disconnect it there first. * A browser nobody signs in on is deleted after 30 days ([details](/kb/antidetect-browsers-and-proxies#browser-lifecycle)). If yours is gone, click **Connect account** again: a new browser is created. # Email is not supported yet Source: https://docs.gtm-api.com/kb/email-is-not-supported-yet Email is not a working channel on this platform yet. What does not work today, why you may still see email mentioned in the API reference, and what to use instead. Email is not supported yet. There is no way to send email, run email sequences, or connect an email account on this platform today. Everything that ships right now runs on LinkedIn. If you came here from a mention of email elsewhere in these docs, see [Why you may still see email mentioned](#why-you-may-still-see-email-mentioned) below: those references are forward-looking, not descriptions of something you can use. ## What does not work today, because email is not supported None of the following exist yet. They are listed so you can stop looking for them: * **Sending email.** No one-off sends, no replies, no templates. * **Email sequences and campaigns.** Multi-step email outreach is not available, in mass actions or anywhere else. * **Connecting an email account.** There is no mailbox connection flow, no SMTP or OAuth mailbox setup, no email sender profiles. * **Email in mass actions.** Bulk runs cover LinkedIn actions only. There is no bulk-email step type. * **Email inbox sync.** Nothing pulls an email thread into the platform, so email conversations do not appear alongside LinkedIn ones. * **Email delivery states.** Bounces, opens, clicks, spam complaints and unsubscribes are not tracked, because nothing is sent. ## Why you may still see email mentioned, even though it is not supported Email is not supported yet, but a few places in the API reference already name it. These are contracts written ahead of the feature, not descriptions of working behaviour: * **The `em_ac_` account prefix.** Account identifiers are described as prefix agnostic (`ln_ac_` for LinkedIn, `em_ac_` for email) so that the contract does not change when a channel is added later. No `em_ac_` account can exist today, because email accounts cannot be created. * **Webhook account filters.** `filters.account_sid` is documented as accepting either prefix, for the same reason. Filtering by an `em_ac_` sid matches nothing. * **Webhook delivery logs.** The log is described as receiving deliveries from every producer service, listing email among them. No email events are produced, so no email rows appear. Treat every one of these as a placeholder. If a tool, an agent or an AI assistant tells you email works because it read one of them, it is reporting a forward-looking contract as if it were a feature. ## What to use instead while email is not supported Since email is not supported, outbound has to run on LinkedIn or on your own tooling: * **LinkedIn messaging** covers direct messages, InMail, voice notes and Sales Navigator messages, individually or through mass actions. Recruiter InMail is also available on accounts with a Recruiter seat. * **Your own email stack, fed by webhooks.** Subscribe to lead and outreach events, deliver them to your endpoint, and trigger email from whatever you already use. The platform will not send it for you, but it can tell you when to. ## Planned, but not shipped and not dated Email is planned. It is not shipped, and there is no date. The references above exist because the contracts were designed to accommodate it, which is a design decision rather than a delivery commitment. If email support matters for your workflow, tell the team: that signal is what shapes what gets built next. # Find an email or phone number for a profile Source: https://docs.gtm-api.com/kb/enrichment Run contact enrichment on one LinkedIn profile: what comes back, which of your connected accounts it runs on, and how to read a partial result. Enrichment pulls one known person's own data from LinkedIn through a live browser session. This page covers the two methods that can return an email or a phone number, which of your accounts each one runs on, and how to read the receipt that comes back. A list of other people (search results, employees, post reactions) is scraping instead: a different surface. ## Before you start * A connected LinkedIn account: every enrichment runs on one of your own accounts. * The `can_act_linkedin_enrichment` permission on your API key, agent or user. * One identifier: `profile_id` (`ACoAA...` or `ACwAA...`) **or** `public_identifier` (the vanity slug from `/in/...`). Exactly one, never both, or the call is `422 validation_failed`. ## The two methods that return contact details | | `person-contact-info` | `person-full-profile` | | - | - | - | | Contact fields | `email`, `phones[]` (typed), `websites[]`, `twitter_handles[]` | flat `email`, `phone`, `twitter`, `facebook` | | Everything else | nothing else | headline, position, company, about, experience, and the rest | | Cache TTL | 24 hours | 24 hours | | Where it can run | only an account of yours that is a 1st-degree connection | your pinned account, or one the service picks for you | Both ride the same read but cache under their own key: a cached full profile does not satisfy a contact-info call, and the reverse is also true. The flat `phone` on `person-full-profile` keeps only the first mobile number. Home and work numbers are dropped. `person-contact-info` returns `phones[]` with a `type` on each entry (`MOBILE`, `HOME`, `WORK`, or `null`), so use it when the phone actually matters. ## Run it ```bash curl theme={null} curl -X POST "https://app.gtm-api.com/linkedin/v4/api/linkedin-enrichment/person-contact-info" \ -H "Authorization: Bearer gtm_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"public_identifier": "williamhgates", "idempotency_key": "contact-whg-2026-08-13"}' ``` No executor fields there: contact info is routed to whichever of your accounts is connected to the person. The call is synchronous, and the data comes back under `result.contact_info`, next to the ledger row at `result.data_request` (`sid` starting `er_rq_`, `status`, `executed_on`, `served_from_cache`, `cached_from_sid`). Per-method shapes are in the [API reference](/api-reference/overview), and batching is your own loop (no mass action, no sequence step), one target per call. `contact_info` is always present, and every field inside it can be empty. An empty block means the connected account still cannot see those details, never that the call was blocked. `address`, `birthday` and connection date are not parsed. A phone with `type: null` is normal on older cached payloads, not a defect. ## Which account runs the call Every enrichment runs on one of your own connected accounts. | What you send | What happens | | - | - | | `linkedin_account_sid` (`ln_ac_` + 12 chars) | Runs on that account only, spending that account's `enrichment` daily budget. `executed_on: "own_account"` | | Nothing | The service picks one of your connected accounts with remaining capacity and runs there. Accounts out of budget, on hold or LinkedIn rate-limited are skipped before the pick | | `idempotency_key` (max 128 chars) | Replay guard scoped to (team, key). A repeat returns the stored row, with no re-execution. One exception: a failure that answered with `retry_after` answers the key only until that time, and a repeat after it runs again | | Auto-pick situation | Result | | - | - | | At least one account has budget left | Runs there | | Accounts exist, all out of budget | `429 rate_limited`, reason `bucket_saturated`, with `retry_after` | | No connected account is ready | `422 validation_failed`, message starting `no_connected_accounts:`. Connect an account first | There is no silent fallback for a pinned account. If the account you named is out of daily budget the call returns `429 rate_limited`, reason `bucket_saturated`, with a `retry_after` timestamp. It will not quietly move to an account you did not name. ### Contact info is the exception `person-contact-info` requires a connected executor: LinkedIn reveals contact details to 1st-degree connections only, so the pick is narrowed to accounts that actually hold the edge. | Situation | Result | | - | - | | One of your accounts holds the 1st-degree edge and has budget left | Runs there | | Your accounts hold the edge but all of them are out of budget | `429 rate_limited`, reason `bucket_saturated`, with `retry_after` and a `connected_accounts` count | | Nobody on the team is connected to this person | `422 no_connected_account`: "none of your LinkedIn accounts is connected to this person, so this request cannot be run." Send a connection request, retry once it is accepted | | The target has neither a member id nor a slug to match on | `422 not_dispatchable` | Here `linkedin_account_sid` is a preference, not a command: another connected account is picked if the one you named does not qualify, and accounts out of budget, on hold or LinkedIn rate-limited are dropped before the pick. ## How long a result is cached | Method | Cache TTL | | - | - | | `person-contact-info`, `person-full-profile` | 24 hours | | `person-lite-profile`, `person-basic-profile` | 24 hours | | The rest (experience, skills, education, posts, certifications, company reads) | 7 days | | `company-public-identifier` | 30 days | A repeat for the same person and method inside the TTL is served from the stored result (`served_from_cache: true`, nothing dispatched); past it, the call executes again. Cache is per team, per method. ## How to read a partial result An empty answer is a real answer. A person who never published a phone number produces a `completed` row with an empty field, not an error. | What the receipt shows | What happened | | - | - | | `served_from_cache: true`, `cached_from_sid: "er_rq_..."` | Served from your team's cached copy. Open `GET /api/data-requests/{cached_from_sid}` and read its `completed_at` for the true age of the data | | `served_from_cache: false` | Ran live on the account named in `linkedin_account_sid`, spending its daily budget | | Status | Meaning | | - | - | | `running` | Dispatched to a browser, visible mid-flight | | `completed` | Result stored, whether executed or served from cache | | `rejected` | Refused before dispatch | | `failed` | Dispatched and terminally failed | A row is inserted either `running` or already terminal; `running` then moves to `completed`, `failed` or `rejected` (a reservation failure rejects an already running row). A call has a hard deadline of 90 seconds; typical latency is 30 seconds at p95 executed, 500 ms at p95 cache-served. Retrying is safe when you reuse the `idempotency_key`; inside that window the replay returns the `running` row, so poll `GET /api/data-requests/{sid}` until the status is terminal. To follow results without polling, subscribe to the `data-requests.completed` and `data-requests.failed` webhooks; the completed payload adds `result_ref` and `completed_at` to the receipt fields. Refusals have no event: a rejection appears only as the synchronous HTTP error. ## Daily budget Enrichment spends one shared bucket per account called `enrichment`. Read it before a batch, on the Smart Limits tab of the account drawer or through [smart limits and warmup](/kb/smart-limits-and-warmup) filtered on `limit_type`. Default budget: 80 enrichment actions per day per account, with a 240 second gap enforced between counted actions. When the budget is spent or the gap has not elapsed the call returns `429 rate_limited`, reason `bucket_saturated`, with a `retry_after` value; retry after that timestamp, not on a fixed schedule. The free plan clamps this to 12 enrichments per day per connected account. ## Errors you will actually hit | Scenario | HTTP | Code | What to do | | - | - | - | - | | Slug-only call on a method that needs a stored profile URN | 422 | `not_dispatchable` | Run `person-lite-profile` for that slug first: it stores the URN, and the sub-record methods work from then on | | The account's browser could not serve the call this time | 503 | `browser_starting`, `browser_unavailable`, `plugin_unavailable`, `browser_maintenance`, `infrastructure_maintenance` | Nothing is wrong with the input. Wait for `retry_after` and send the call again: 30 seconds while a browser is still starting, one to two minutes after a lost session or a page that did not load, five to ten minutes while the browser or the fleet is being updated. Retry under the same `idempotency_key`: before `retry_after` the repeat returns the stored failed row, after it the call runs again | | Terminal execution failure | 409 | `operation_blocked` | Do not retry as sent, change the input | | Method not implemented yet (`person-languages`, `post-details`) | 501 | `not_implemented` | Nothing is ledgered: no row, no budget spent | `bucket_saturated` on a 429 is your own account's daily enrichment budget, nothing wider. Wait for `retry_after`, or pin a different connected account that still has budget. Copy this to your AI assistant to enrich a list of profiles: "Enrich LinkedIn profile slugs with contact details using the GTM API at `https://app.gtm-api.com/linkedin/v4`. For each slug POST to /api/linkedin-enrichment/person-contact-info with body `{"public_identifier": SLUG, "idempotency_key": "contact-SLUG"}` and header Authorization: Bearer gtm\_live\_YOUR\_KEY. Do not send linkedin\_account\_sid: this method picks my connected account on its own. One profile per call, sequentially. Treat 422 no\_connected\_account as skip-and-log, and on 429 wait until the retry\_after timestamp. Write CSV: slug, email, first phone, phone type, websites, data\_request.status." ## Related * [Smart limits and warmup](/kb/smart-limits-and-warmup) * [Billing and plans](/kb/billing-and-plans) # Inbox and message sync Source: https://docs.gtm-api.com/kb/inbox-and-message-sync How conversations and messages reach the platform: what the initial backfill imports, how often each surface re-syncs, what realtime push covers, and why a message may not be there yet. Your LinkedIn inbox is not read live. The platform keeps its own copy of every thread and message, built by a sync through the account's own browser and topped up by a realtime signal. ## Three inboxes, synced separately The regular messenger, the Sales Navigator inbox and the LinkedIn Recruiter inbox are separate surfaces with their own threads and their own clocks. Every thread carries a `messenger_type`. | | Regular inbox | Sales Navigator | Recruiter | | - | - | - | - | | `messenger_type` | `linkedin` | `sales_navigator` | `recruiter` | | Row on the **Account Sync** tab | **Conversations** | **Sales Nav Conversations** | **Recruiter Conversations** | | Available when | always | only if the account has Sales Navigator | only if the account holds a Recruiter seat | | Muting and marking read | supported | not supported, `is_muted` is always `null` | not supported | | Verbs | `get-my-latest`, `sync-my-linkedin-conversations` | `get-my-latest-sales-nav`, `sync-my-sales-navigator-conversations` | `get-my-latest-recruiter`, `sync-my-recruiter-conversations` | SN and Recruiter threads sync only once the platform detects the subscription (`premium_check`, its own surface, every 360 minutes), which has to run before the **Sales Nav Conversations** or **Recruiter Conversations** row appears. A basic-messenger verb on an SN or Recruiter thread answers `422 messenger_type_mismatch`. ### The Recruiter inbox The Recruiter inbox belongs to a seat, not to the profile. The premium check stamps the seat number and its contract on the account (`recruiter_seat_id`, `recruiter_contract_id`), and every Recruiter call needs them: without a seat the verbs answer `422 recruiter_required`, without the number `422 recruiter_seat_unresolvable`. A candidate is identified by `talent_id` (the `AEMAA…` id the Recruiter UI shows), which the platform decodes to the same `ln_member_id` as the profile's `ln_id` and `sn_id`, so one candidate is one thread whichever id you send. One sync run walks the INBOX tab and then UNRESOLVED: a thread you opened with an InMail sits under UNRESOLVED until the candidate replies, and INBOX alone would never show it. ARCHIVED and SCHEDULED are not walked. Pages carry 15 threads, the Recruiter client's own size, and there is no realtime push for this inbox: a reply lands on the next sweep (120 minutes by default) or on a head refresh. LinkedIn keeps a separate Recruiter session of about 30 days, and the platform records its expiry as `recruiter_session_expires_at`. Past that clock every Recruiter call answers `409 recruiter_reauth_required` and the recurring run is cancelled with the same reason; the **Recruiter Conversations** row and the Premium block on the account say so. The fix is a sign-in to Recruiter in the account's own browser. The platform never stores the Recruiter password. Sending goes through `POST /api/linkedin-messages/send-recruiter`: a new thread by `talent_id`, `ln_id` or `sn_id`, or a reply into an existing thread by its sid. LinkedIn allows **one InMail per candidate per 24 hours** unless they reply sooner, so a second message inside that window answers `429 rate_limited` with `reason: "recruiter_inmail_cooldown"` and a `retry_after`; a reply the candidate has not answered yet is refused by LinkedIn under the same rule and surfaces the same way. `get-my-recruiter-seat` and `get-my-hiring-projects` read the seat's own entitlements and hiring projects. ## What the initial backfill imports Onboarding walks a fixed ordered set of surfaces (connections, connection requests, connection invitations, the regular inbox, the premium check, the Sales Navigator and Recruiter inboxes when the account holds those seats, then the profile snapshot) and the account unlocks when it finishes. Every thread is imported, since the run pages to an empty page. Not every message is. The message drain runs inline inside the conversations sync itself, while the account is still onboarding, and fetches exactly **one page of the 50 newest messages per thread**, then stops; it does not page backwards. A 400-message thread arrives with its 50 most recent ones, and older history stays outside the platform: `POST /api/linkedin-messages/get-my-latest` refreshes the newest page of up to **100 messages** and stops there. During onboarding the `self_account_sync` daily budget is raised from **1000 to 3000 calls per day**, and it drops back to 1000 the moment onboarding completes. A first-time backfill runs to completion whatever hours are configured; only recurring syncs wait for the window: an inbox sync spends that budget and nothing else. ## How often each surface re-syncs | Surface | Default interval | Editable per account | | - | - | - | | `conversations` | 180 minutes | yes | | `sales_navigator_conversations` | 120 minutes | yes | | `recruiter_conversations` | 120 minutes | yes | | `connections` | 180 minutes | yes | | `connection_requests` | 720 minutes | yes | | `connection_invitations` | 720 minutes | yes | | `premium_check` | 360 minutes | yes | | `snapshot` | 1320 minutes | no | | `followers` | 1440 minutes | no | Three things change the effective interval: your per-account setting on the **Account Sync** tab, the [sync window](/kb/sync-windows-and-auto-suspend) (a run due outside the configured hours parks with `schedule:closed` until it reopens), and the free plan floor. On the free plan every surface is floored to **once per 1440 minutes (daily)**: the effective interval is the larger of the two. Free plan accounts also get **no realtime push**, so a reply can sit for up to a day. Initial backfills are exempt from the floor. A recurring run is not a full re-read: it walks the directory newest first and stops once a page holds **10 threads it already knows**, and regular-inbox pages are capped at **25 threads** by the browser plugin (the Sales Navigator sweep pages at 100, the Recruiter sweep at 15). Only threads whose event count or last activity moved ahead of the stored copy are read. ## Realtime push, and what it does not do While the account's browser runs, the plugin listens to LinkedIn's own realtime stream and tells the platform when a message arrives: end to end, a received message reaches the `linkedin-messages.received` webhook in about **8 seconds**, against the 180 minute sweep. The push carries identifiers only and never writes a message row. For a thread the platform already knows it schedules a refresh of that one thread, read through the account's own browser, and the rows land tagged `source='push'`. For a thread it has never seen it falls back to an account-wide conversations refresh, whose rows are tagged `source='sync'` like the sweep's: the webhook still fires, but the live Inbox screen does not update until the sweep. Signals it drops: | Reason | Dropped when | | - | - | | `self_sender` | Your own send, echoed back by LinkedIn | | `duplicate_message` | The same message id arrived again within **600 seconds** | | `coalesced` | A refresh for this thread is in flight, within a **180 second** latch | | `rate_capped` | More than **10 refreshes in one minute** for this account, or the push path has already spent its half of the daily budget (**100 of 200** calls) | | `initial_sync_pending` | Onboarding is about to read the thread anyway | | `free_plan` | Free plan, where realtime push is off by design | The rest are environmental: `disabled` (off platform-wide), `unhandled_type` (typing indicator or read receipt), `no_linkedin_account`, `account_not_found` and `dispatch_failed`. A dropped signal is never a lost message: the worst case is that it appears on the next sweep instead of within seconds. Your own reply typed in the LinkedIn UI is always in that class, and lands on the sweep as `type='outbox'` with `automation='synced'`: discovered, not sent. ## Force a refresh now A **head refresh** reads the newest page of one thread or account synchronously and returns the rows themselves; use it in almost every case. A **full sync** reconciles the head of the directory in the background and returns a sync run to poll; like every recurring run it stops once a page holds 10 threads it already knows, so it will not reach an old thread that is missing locally. Only a sync reset re-walks the directory end to end. ```bash curl theme={null} curl -X POST "https://app.gtm-api.com/linkedin/v4/api/linkedin-messages/get-my-latest" \ -H "Authorization: Bearer gtm_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"linkedin_conversation_sid": "ln_cv_YOUR_THREAD", "page_size": 50}' ``` The response carries a `refresh` block with counters and a `stop_reason` of `overlap`, `covered`, `page_cap` or `exhausted`. A head refresh never stamps the account's sync clock and never emits `linkedin-conversations.sync-completed`, so it does not postpone the next drain. Every head refresh page and every background message pull spends one call from the account's `self_account_sync` budget: **1000 calls per day**, paced about **one call a second**. When it is spent, reads answer `429 rate_limited` with `limit_type: "self_account_sync"` and a `retry_after`, and parked runs resume at the next daily reset or when the limit is raised. See [smart limits and warmup](/kb/smart-limits-and-warmup). ## Why a message may not appear yet Three timestamps answer most of it: `last_activity_at` (newest activity LinkedIn reports), `last_stored_message_at` (newest message stored) and `last_message_sync_at` (last pull). Activity newer than stored means the thread is already flagged and the next run drains it. Otherwise: 1. **The account is still onboarding.** The drawer shows **Initial sync in progress** and head refreshes answer `429 rate_limited` with `context.reason: "initial_sync_pending"`. 2. **A sync is already running for that surface.** A second one does not queue: it answers `429 rate_limited` with `reason: "sync_in_progress"` and the working `sync_run_sid` to poll. Only a run that is reading counts: one waiting for its window, a browser or a limit does not, and a head refresh of one thread never waits for the inbox sync. 3. **The sync budget is spent or held.** The run shows a `wait_reason` starting with `limit:` (`limit:daily_saturation`, `limit:held`, `limit:linkedin_quota_hit`, `limit:delay_not_elapsed`). Raise the Self account sync limit on **Smart Limits** with reset hold to resume them all now. 4. **The browser is down or logged out.** Sync verbs still enqueue and hand back a `sync_run_sid`; the run then parks with a `browser:` `wait_reason` and retries roughly 30 minutes out, while a head refresh answers `503 service_unavailable`. Budget parks wake on a raised limit, [browser parks](/kb/browser-troubleshooting) wait for their retry. 5. **It is outside the sync window.** The run carries `wait_reason: "schedule:closed"` and a `next_attempt_at` set to the next opening. Widen the window or head-refresh now. 6. **The message is older than the newest 50 in its thread.** No sweep backfills older history: call `get-my-latest` on that thread to pull the newest page of up to 100 messages. 7. **It was a push drop and the sweep has not run.** Your own reply typed on LinkedIn, a duplicate signal, a burst above 10 refreshes a minute and free plan accounts all skip the fast path: the wait is then up to 180 minutes (regular), 120 (Sales Navigator and Recruiter), 1440 (free plan). 8. **Still nothing?** Send support the account sid (`ln_ac_…`), the **Account Sync** row with its status and next due time, and the sync run sid (`ln_sy_…`) with its `wait_reason`. A sync run never has a "failed" status: runs go `pending`, then `in_progress`, then `completed` or `cancelled`. One stuck at `in_progress` is almost always waiting, so read its `wait_reason` and `next_attempt_at`. Copyable prompt for your AI agent: Diagnose why a LinkedIn message is missing from my GTM API inbox copy, for the `ln_ac_` sid I give you. Search `linkedin-account-sync-runs` for it, `sync_type` `conversations`, newest first, and report the latest run's `status`, `wait_reason` and `next_attempt_at`. Then search `linkedin-conversations` for the thread and compare `last_activity_at` with `last_stored_message_at`; if activity is newer, call `get_my_latest_linkedin_messages` on that sid with `page_size` 50 and report the `refresh` block. Only full-sync if threads are missing. ## Reading the stored copy A message carries `type` (`inbox` or `outbox`), `automation` (`auto`, `manual` or `synced`) and `linkedin_type` (`message`, `inmail` or `connection_note`). A `connection_note` is the invitation note: LinkedIn puts it into the thread when the invitation is accepted, so it arrives through the sync like any other message (`automation: synced`) and carries that label. Messages are append-only on both surfaces: no edit, no delete, no unsend, no recall. There are no read receipts and no open tracking, and marking a thread read changes the state on LinkedIn only. There is no export button and no CSV. `linkedin-messages/search` and `linkedin-conversations/search` page by cursor and touch neither the browser nor the budget (filters and sorts are in the [API reference](/api-reference/overview)), and a conversation from another workspace answers `403 wrong_team`, not `404`. Webhooks deliver `linkedin-messages.received`, `.sent` and `.send-failed`, plus `linkedin-conversations.created` and `.sync-completed`. A send that fails never creates a message row: the failure lands on the activity log and is emitted as `linkedin-messages.send-failed` carrying the `linkedin_account_sid`, the `linkedin_conversation_sid` and an `error_code` (the activity-log sid is not in the payload; find the row by account and time). If a send times out and you are unsure whether it went through, search messages on that conversation with `type: "outbox"` and `sent_at` after the attempt. A blind retry can double-send. A sync reset with `conversations`, `sales_navigator_conversations` or `recruiter_conversations` selected **soft-deletes every stored conversation for that account and surface, and all of their messages**, in one transaction. The next sync rebuilds the directory, resurrecting the same rows (same sid) as it re-observes them, so resurrected threads fire no `linkedin-conversations.created` and only genuinely new ones do, and it re-drains the newest 50 messages per thread; anything you fetched deeper stays soft-deleted and out of results. Nothing else removes a thread: one that disappears on LinkedIn keeps its stored copy. ### What happens when a send fails A failed send is not retried automatically. Nothing picks it up on a timer, and the resend is something you or your team trigger. The failure never lands in the conversation: a message row is only written on terminal success, so a failed attempt leaves no half-sent message. If the failure was a timeout, the message may still have gone through on LinkedIn's side. Check the thread before resending, because a blind retry can double-send. ## Related * [Sync windows and auto-suspend](/kb/sync-windows-and-auto-suspend) * [Smart limits and warmup](/kb/smart-limits-and-warmup) * [Connect a LinkedIn account](/kb/connect-a-linkedin-account) * [Workspaces and team members](/kb/workspaces-and-team-members) # Knowledge base Source: https://docs.gtm-api.com/kb/index How the platform works: accounts and browsers, safety and pacing, finding and enriching people, conversations, billing, and what to do when something breaks. Everything the app does and why, written for the person running outreach. The [API reference](/api-reference/overview) documents the same platform for code; these pages document it for you. ## Accounts and browsers Connect in the app, wait out the initial sync, then work with it anywhere. One account, one isolated browser, one proxy. Lifecycle, checks, diagnostics. Lend an account to another workspace and take it back, or hand it over for good. ## Safety and pacing Per-action daily budgets, the warmup ramp, holds and how to release them. When recurring syncs run, what ignores the window, and how browsers sleep and wake. The signals behind the score, what each state means, and which ones self-heal. ## Finding and enriching Paste a search URL or build filters, on LinkedIn or Sales Navigator. Emails and phone numbers: what it returns, what it costs, and what an empty result means. Recurring collection that dedupes across runs and can feed a standing mass action. ## Conversations What the backfill imports, how often each surface refreshes, and why a reply may not be there yet. ## Workspace and billing What a workspace owns, how to invite people, and what each role can do. Sandbox, the forever free plan every workspace starts on, account pricing, and payment failures. ## When something breaks LinkedIn will not load, the account keeps asking for verification, the browser stops overnight. Start issues, sessions that die immediately, and login states that will not clear. # People and company search Source: https://docs.gtm-api.com/kb/people-and-company-search Run one live page of a LinkedIn, Sales Navigator or Recruiter people or company search: the pasted-URL and structured-filter input styles, how ids are resolved, how to get a search URL from filters, what a row contains, and what a page costs. A search runs now, returns one live page of results inside the HTTP response, and stores nothing. ## Before you start * A connected LinkedIn account to run on, or the managed path to run on our infrastructure. * The `can_act_linkedin_searches` permission on your API key, agent or user. * A Sales Navigator seat on the executing account for the Sales Navigator methods. * A Recruiter seat, with a live Recruiter session, on the executing account for the Recruiter methods. A search is a discovery list, not an enriched one. Rows carry an identity (member id, vanity slug, name, avatar) and nothing else: no headline, no title, no company, no location, no connection degree. Feed the ids into [enrichment](/kb/enrichment) for those, a separate surface. The one exception is the Recruiter people search, whose rows come back with the headline, current position, location and connection degree filled, because LinkedIn Recruiter resolves the profile inline. A search is never cached: every call re-runs. The only durable traces are one row in the **Data Requests** ledger (`result.data_request`) and one activity-log entry on the executing account. Nothing is stored, so nothing can be deduplicated away: rows arrive in LinkedIn's own order, with no sort axis and no cross-page deduplication. Past roughly five pages, or for anything on a schedule, use an [auto scrape](/kb/auto-scrapes). ## The engines Every method is a `POST` under `https://app.gtm-api.com/linkedin/v4/api/linkedin-scraping/`. | Method | Returns | Paging | Sales Navigator seat | | - | - | - | - | | `search-people` | people | `page` 1 to 100 | no | | `search-sales-nav-people` | people | `page` 1 to 100 | yes | | `search-companies` | companies | `page` 1 to 100 | no | | `search-sales-nav-companies` | companies | `page` 1 to 100 | yes | | `search-service-providers` | people | `page` 1 to 100 | no | | `similar-profiles` | people | none, `limit` 1 to 100, default 25 | no | | `similar-companies` | companies | none; `limit` 1 to 50 is accepted but has no effect, the whole list (\~20) comes back | no | | `company-employees` | people | `page` 1 to 100 | no | | `company-decision-makers` | people | none, `limit` 1 to 50, default 25 | yes | | `param-id-lookup` | typeahead options | none | no | | `sales-nav-param-id-lookup` | typeahead options | none, `limit` 1 to 25, default 10 | yes | | `search-recruiter-people` | people | `page` 1 to 40, 25 rows a page; `url` XOR `filters` | Recruiter seat | | `recruiter-param-id-lookup` | typeahead options | none, a fixed ten | Recruiter seat | | `build-search-url` | a search URL and its filters, nothing run | none | only to resolve Sales Navigator text | The same surface also searches posts, jobs, events, groups, courses, products and schools, and reads a post's commenters, reactors and resharers. Same rules, and their filter vocabularies are in the [API reference](/api-reference/overview). ## Two ways to address a search The five list searches take **either** a pasted `url` **or** a structured `filters` object. Same route, same parser, same price: the structured half composes the same LinkedIn URL and runs the same page. Send `filters` when a program or an agent composes the search, `url` when a human built it in LinkedIn or the vocabulary cannot express it. Exactly one of them is mandatory. Sending both returns `422 validation_failed` with a field error on each, and sending neither returns the same `422`. Sending `filters` where every member is empty (including an empty `` `{}` ``) returns `422` with the message `At least one filter member is required.` A `false` counts as empty, so a lone `"hiring_on_linkedin": false` is a 422 too. An explicit `"url": null` beside a filled `filters` is fine, and the reverse. Which half you sent only changes the label in the ledger: `input_kind` is `url` or `params`, and **Data Requests** shows "Search people by URL" or "Search people by params" accordingly. A pasted URL is prefix-checked first: a URL from another LinkedIn screen would scrape the wrong thing. On `https://www.linkedin.com`, the regular engines take `/search/results/people/`, `/search/results/companies/` and `/search/results/services/`; the Sales Navigator pair takes `/sales/search/people` and `/sales/search/company`, no trailing slash, checked literally. Your `page` overrides the page or offset baked into the URL; every other query parameter rides through untouched. ```bash curl theme={null} # The structured half, page 1 curl -X POST "https://app.gtm-api.com/linkedin/v4/api/linkedin-scraping/search-people" \ -H "Authorization: Bearer gtm_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"page": 1, "idempotency_key": "icp-growth-us-p1", "filters": {"keywords": "growth marketing", "network": ["2nd"], "locations": ["103644278"]}}' ``` With no `linkedin_account_sid` the service picks one of your connected accounts with remaining budget; swap `filters` for a pasted `"url"`, or pin `"linkedin_account_sid": "ln_ac_YOUR_ACCOUNT"` to run the same page on a specific account. ## Filters Every member, type and bound, engine by engine, is in the [API reference](/api-reference/overview). What cuts across all five engines: A misspelled filter name is not an error on the people, company and Sales Navigator engines. The unknown key is dropped before dispatch and the search runs unconstrained, so a broad result reads like a filtered one. Copy member names exactly. Retired names are the exception and fail loudly with a `422`: `title`, `geo_ids`, `industry_ids`, `current_company_ids` on Sales Navigator people, `service_ids` and `geo_ids` on service providers, `headcounts`, `annual_revenues`, `headcount_growth_min` on Sales Navigator companies. | Rule | Detail | | - | - | | Free text | a double quote inside a free-text member returns `422` on the people, company and service-provider searches, because LinkedIn wraps free text in quotes while the query is composed. The Sales Navigator engines accept quotes | | Id arrays | the numeric ones (locations, industries, current and past companies, service categories) are max 10 entries, digits only, up to 64 characters per entry; over the cap or a non-numeric entry is a `422`. `connections_of` and `followers_of` are the exception: profile ids up to 128 characters on the people search, any non-empty string up to 64 on Sales Navigator | | `network` | the people and service-provider engines take `1st`, `2nd`, `3rd_plus`, and the plain company search `1st` only; Sales Navigator people takes the raw codes `F` 1st, `S` 2nd, `A` group members, `O` 3rd and beyond. Each rejects the other's values with a `422` | | `has_jobs` | plain company search only: `true` keeps companies hiring on LinkedIn right now, `false` sends nothing | | Headcount bands | the plain company search takes named bands (`1_to_10` up to `10001_plus`) and refuses LinkedIn's letter codes; the Sales Navigator engines take the letters `A` to `I`, and the account search has no `A` | | Sales Navigator facets | the people search has nine such members and the account search three; each takes arrays of `` `{id, text, exclude}` ``, max 10 values each, `"exclude": true` for a negative filter. A value without an `id` works on the job titles and the companies only, where Sales Navigator matches the text itself, so `[{"text": "VP Marketing"}]` needs no lookup. Every other facet needs the `id`: LinkedIn ignores a value without one there, so it is refused with `422 facet_id_required` | ### Three filters that do not mean what they look like Types and bounds are in the reference; these are the semantics behind them, and getting one wrong costs a run rather than an error. | Filter | The trap | | - | - | | `annual_revenue` (Sales Navigator company search) | Expressed in **millions**. `10` means $10M, not $10. The bound reaches 100000000, so a wrong unit is a factor of a million, silently | | `company_headquarters` (Sales Navigator people search) | Filters where the person's **company** is headquartered, not where the person is. For the person, use the location members | | `account_activities` | `SLC` is a senior-leadership change in the **last 3 months**, `RFE` is a funding event in the **past 12 months**. The windows are fixed and not yours to set | ## The Recruiter search `search-recruiter-people` runs the LinkedIn Recruiter people search on an account that holds a Recruiter seat, addressed by `filters` or by a pasted Recruiter `url`, never both. The filter object mirrors the Recruiter search panel. Chip facets (`job_titles`, `occupations`, `skills`, `companies`, `current_companies`, `locations`, `postal_codes`, `industries`, `schools`, `fields_of_study`, `degrees`, `spoken_languages`) take arrays of `{id, text, exclude, required, scope}`: `id` comes from `recruiter-param-id-lookup`, `text` is the row's `display_name`, `exclude` negates the chip, `required` makes it a must-have, and `scope` is the facet's own axis (where in the career for titles and companies, lives-there or would-move-there for locations, minimum proficiency for spoken languages). Job titles, occupations, skills, companies and postal codes need the `text` even when they carry an `id`, because LinkedIn refuses an entity chip without its label; job titles, skills and companies also accept a chip with only `text`. Postal codes need the `zip` lookup id. School ids are the school's organization urn, exactly as the `school` lookup returns them. The closed sets (`network`, `seniority_levels`, `functions`, `company_sizes`, `company_types`, `profile_languages`, `recently_joined`, `workplace_preferences`, `employment_types`) take code arrays, listed in the [API reference](/api-reference/overview); note the Recruiter seniority codes run `1` to `10` and are not the Sales Navigator ones. The three year sliders take `{min, max}` in 1 to 30, where 30 means 30 and more. A page holds 25 rows and the ceiling is page 40. Unlike the other engines, `paging.total` is LinkedIn's real count. ### The Recruiter URL is a handle, not a query A Recruiter URL carries no filters. After a search, Recruiter shows `https://www.linkedin.com/talent/search?searchContextId=...&searchHistoryId=21502096450&searchKeyword=&start=0&uiOrigin=FACET_SEARCH`: the facets live on LinkedIn's side under `searchHistoryId`, the seat's "Recent searches" entry. Passing such a URL replays that stored search on the page you ask for (the URL's own `start` is ignored). A URL without a `searchHistoryId` is refused with `422`, because it names no search; LinkedIn is never asked for a keywords-only search with your filters silently dropped. Every answer returns `search_history_id` and `search_url`. Run page 1 with `filters`, then page on with `url: search_url` and `page: 2, 3, ...`: no filter object and no typeahead round trips on the later pages, and the same handle re-runs the search later. Both halves count the same against the daily `scraping` budget. LinkedIn Recruiter throttles searches per seat. When a seat searches too fast, the call answers `429` with reason `recruiter_search_usage_limit` and LinkedIn's own sentence: "You've reached a temporary usage limit. Wait a minute, then retry your search." Wait the minute, then re-run the same page. This is LinkedIn's throttle, not your daily budget. An executing account without a Recruiter seat answers `422 recruiter_required`. One whose Recruiter session has expired answers `409 recruiter_reauth_required` until the seat holder signs in to Recruiter again in the account's browser, the same rule as the [Recruiter inbox](/kb/inbox-and-message-sync). ## Resolving the ids that filters need Three typeahead endpoints turn a human term into an id, over separate id spaces: an id from one never works on the others. `recruiter-param-id-lookup` feeds the Recruiter search only: one `type` out of `occupation`, `skill`, `company`, `geo`, `zip`, `industry`, `school`, `fieldOfStudy`, `degree`, `language` and `group`, a required `query`, a fixed ten rows in the Sales Navigator row shape. Its ids are `urn:li:ts_*` urns, except `language`, whose id is the lowercase language name the spoken-language facet wants. | | `param-id-lookup` | `sales-nav-param-id-lookup` | | - | - | - | | Feeds | the people, company and service-provider engines | the Sales Navigator engines | | Input | one `type`, one `query`, 1 to 100 characters, required | one `type`, an optional `query` 1 to 100 characters (the closed sets ignore it), plus `limit` 1 to 25, default 10 | | Types | `location`, `industry`, `company`, `service_category`, `people`, `connections`, `school`, `product_category`, `job_location` | 19 passed through verbatim; ids are opaque, feed them back as they came. Skip the closed sets (`COMPANY_SIZE`, `FUNCTION`, `SENIORITY_V2`, `RELATIONSHIP`, `COMPANY_TYPE`, `TENURE`, `PROFILE_LANGUAGE`): calling one only re-fetches labels the reference lists | | Result size | a fixed short dropdown, around ten options, best match first | `limit` | | Row shape | `type`, `id`, `display_name`, `subtitle` (the option's second line, `null` on most types) | `type`, `id`, `display_name`, `headline`, `entity_urn`, `image_url` | | Seat needed | no | yes | Lookup type `school` returns real school ids, and the people search has no school-id member: its `school` is free text. Those ids only reach LinkedIn inside a pasted people-search URL, so do not put them in `filters.school`, where they are matched as text and return nothing useful. `job_location` is likewise its own id family, for the job search only. A lookup types into the account's already-open tab, so it loads no page, spends no LinkedIn search and leaves the commercial-use limit untouched. It still takes one slot of the daily `scraping` budget, and is never cached. Empty `rows` means the dropdown offered nothing for that term: an answer, not an error, ledger row `completed`. ## Getting the search URL `build-search-url` returns the LinkedIn URL of a people or company search instead of running it, plus the filters in the exact shape the search takes. Send exactly one of `people`, `sales_nav_people`, `companies` or `sales_nav_companies`, each in the vocabulary of `search-people`, `search-sales-nav-people`, `search-companies` or `search-sales-nav-companies`. Sending two of them, or none, is a `422`. Where a filter takes ids, give each value as `{"id": "..."}` or as plain text, `{"text": "Berlin"}`. A text goes through that search's own typeahead: the option labelled exactly like your text wins, otherwise LinkedIn's first one. `resolved` in the answer lists every text with the id it got and the other options LinkedIn offered, so a wrong pick is fixed by sending that value again with the id you want. ```bash curl theme={null} curl -X POST "https://app.gtm-api.com/linkedin/v4/api/linkedin-scraping/build-search-url" \ -H "Authorization: Bearer gtm_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"sales_nav_people": {"current_titles": [{"text": "CTO"}], "locations": [{"text": "Berlin"}], "seniority_levels": ["310"]}}' ``` | Case | What happens | | - | - | | Every value carries an id | Nothing runs on LinkedIn: no account needed, no budget spent, no ledger row | | A text value | One typeahead lookup on the executing account, one slot of the `scraping` budget each, about 5 seconds apart on one account. Equal texts are looked up once. Up to 10 lookups per call; more is a `422 too_many_lookups` before anything runs | | A text of several words the typeahead does not know | Asked again by its first word, and only an option labelled exactly like your text is taken. Sales Navigator's industry typeahead knows "Software" but not "Software Development" | | A text still unmatched on a Sales Navigator title or company | It stays in the search as free text, which Sales Navigator matches on those two; `resolved` shows it with `"id": null` | | A text still unmatched anywhere else | `422 facet_value_not_found` on that value, naming the closest options with their ids. Those filters work by id only, and LinkedIn would ignore the text | | `"free_text": true` on a Sales Navigator title or company | No lookup; the text goes in as free text. Refused on other facets | | `account_lists` given by name | Matched against your own Sales Navigator account lists, exactly or as the only list whose name contains it; no match is a `422` | The Sales Navigator URL comes in its `?query=` form, the one a browser or an import tool takes. To run the same search, pass the URL to the search's `url`, or the returned `filters` to its `filters`. Recruiter has no URL builder: a Recruiter search URL only exists once the search has run, and `search-recruiter-people` returns it as `search_url`. ## What comes back Rows sit at `result.rows`, paging at `result.paging`, and the ledger row at `result.data_request`. | Person field | People search | Sales Navigator people search | Recruiter people search | | - | - | - | - | | `ln_member_id`, `full_name`, `avatar_url` | filled | filled | filled | | `ln_id` | filled | `null` | `null` | | `sn_id` | `null` | filled | `null` | | `nickname` (vanity slug) | filled | `null` | filled | | `headline`, `position`, `company_name`, `location`, `connection_degree` | always `null` | always `null` | filled where LinkedIn shows them | The Recruiter rows add `talent_id` (what `send-recruiter` addresses as `recipient_profile_id`), `industry`, `can_send_inmail`, `open_to_work`, `connections_count` and `anonymized`, and the result block adds `search_history_id` and `search_url`, the handle to page by. Those last five are `null` by construction on the first two engines: the result page does not carry them. Service-provider rows use the same shape, with no ratings, review counts or service labels. When you need more, take `ln_id` or `nickname` from a people-search row, or `sn_id` from a Sales Navigator row, into enrichment (as `profile_id` / `public_identifier`) or into `check-degree`. `ln_member_id` is a join key only: it cannot be turned back into a signed URN, so no method accepts it. | Company field | Company search | Sales Navigator account search | `similar-companies` | | - | - | - | - | | `company_ln_id`, `name`, `logo_url` | filled | filled | filled | | `nickname` | filled | `null` | filled | | `industry` | `null` | filled | filled | | `employees_size`, `tagline` | `null` | filled | `null` | | `followers` | `null` | `null` | filled | | `location`, `website` | always `null` | always `null` | always `null` | To resolve a Sales Navigator company into a vanity slug, run `company-public-identifier` (`POST /api/linkedin-enrichment/company-public-identifier`). Paging: `page` is the page you asked for (default 1), `page_size` is how many rows LinkedIn returned rather than a knob you set, and `total` is always `null` on the people and company engines. Drive the loop from `has_more`, true while this page returned rows and `page` is below 100. The ceiling is page 100: a `page` above it is a `422`, `has_more` is `false` at 100 whatever LinkedIn still has, and an empty page is terminal. `search-people` carries an extra boolean, `commercial_use_limit_hit`: `true` means LinkedIn's monthly commercial-use paywall fired and rows may be truncated. The field is absent on the company engines, always `false` on service providers. ## Which account runs a search | Field | Rule | Effect | | - | - | - | | `linkedin_account_sid` | `ln_ac_` plus 12 characters | runs on that account only, `executed_on: "own_account"`, spends that account's daily `scraping` budget | | omitted | | the service picks one of your connected accounts with remaining budget (Sales Navigator methods pick only accounts with a seat, Recruiter methods only seat holders with a live Recruiter session) | | `idempotency_key` | string, max 128 characters | replay guard on (team, key) | There is no silent fallback for a pinned account: out of budget it returns `429` rather than quietly moving to another account. With no account connected at all, the call is a `422` telling you to connect one first. Idempotency replay matches on the team and the key alone, not the method and not the input, so reusing one key after switching a call from `filters` to `url`, or after changing the page, hands back the first stored result instead of running the new search. New search, new key. ## Daily budget Every method on this surface, searches and typeaheads alike, spends one shared per-account bucket called `scraping`. Default budget: 100 scraping calls per day per connected account, paced about one call every 5 seconds. A short wait for the slot is slept by the server, so a few typeaheads plus the search run one after another as slightly slower calls, and firing them in parallel gains nothing. When the budget is spent, the account is on hold, or the wait for the slot is longer than 20 seconds, the call returns `429 rate_limited` with `reason: "bucket_saturated"`, the `cause` (`daily_saturation`, `held` or `delay_not_elapsed`) and a `retry_after` timestamp. Retry at that timestamp, not on a fixed schedule and not earlier. The free plan clamps the bucket to 15 scraping calls per day per account, and a young account ramps lower still while it warms up. Read the bucket before a batch rather than discovering it through a 429: [smart limits and warmup](/kb/smart-limits-and-warmup). A search is synchronous and bounded at 120 seconds. Past that the run is abandoned and the row goes to `failed`. The typeaheads run on a 90 second ceiling. ## Zero rows is usually not a failure Every case below returns HTTP 200 with `"rows": []`. Stop as soon as one explains what you see. 1. **Check `commercial_use_limit_hit`.** `true` on `search-people` means LinkedIn truncated the page behind its monthly paywall. Re-run on `search-sales-nav-people`, which has no such limit, or wait for LinkedIn's monthly reset. 2. **Count what the page could show.** The regular people parser keeps only cards carrying a vanity slug, an id and a real name, and drops any card named "LinkedIn Member", so out-of-network people that LinkedIn renders anonymously never reach `rows`. The Sales Navigator parser has no such filter and often returns more for the same query. 3. **Check the executor's session.** An account whose LinkedIn session is degraded or sitting behind an auth wall returns a correctly shaped page with zero rows. Confirm the account and its browser are healthy. 4. **Re-read your filter names.** A misspelled member is dropped silently, so the search ran less constrained than you meant. 5. **Check the page number.** A page past the end of the results has nothing to return. A page above 100 is not this case: it is a `422` before anything runs. 6. **Read the ledger row.** `status: "completed"` with zero rows means LinkedIn genuinely answered with nothing; `status: "failed"` with an `error_code` means the response could not be read. Never the same thing. ## Errors The detail sits in one of two places, never both: a 422 carries it in `error.field_errors`, everything else in `error.context`. | Scenario | HTTP | `error.code` | Detail | | - | - | - | - | | Both or neither of `url` and `filters` | 422 | `validation_failed` | `field_errors` on `url` and `filters` | | `filters` present but every member empty | 422 | `validation_failed` | `field_errors`: `At least one filter member is required.` | | URL from the wrong LinkedIn screen, or over 2048 characters | 422 | `validation_failed` | `field_errors` on `url` | | Sales Navigator method on an account with no seat | 422 | `validation_failed` | `field_errors` on `linkedin_account_sid`, message starts `sales_nav_required:`; the ledger row is `rejected` | | Daily budget spent, account on hold, or cooldown not elapsed | 429 | `rate_limited` | `context`: `bucket_saturated`, `limit_type: "scraping"`, `cause`, `retry_after` | | LinkedIn's own quota lock on the account | 429 | `rate_limited` | `context`: `linkedin_strike`, `cause: "linkedin_quota_hit"`, `retry_after`; no bypass | | The account's browser could not serve the search this time | 503 | `service_unavailable` | `context.reason` says why, always with `retry_after`: `browser_starting` (a cold start still warming up, 30 seconds), `browser_unavailable` (the session was lost and is being restarted, two minutes), `plugin_unavailable` (the search page did not load or did not answer, one minute), `browser_maintenance` or `infrastructure_maintenance` (the browser or the fleet is being updated, five to ten minutes). The run is marked `failed`; a repeat under the same `idempotency_key` returns that row until `retry_after`, and runs the search again after it | The Sales Navigator refusal spells out its own fix: use an account that has a seat, or run `get-my-sales-navigator` to refresh that account's premium status first. That check also runs on its own every 360 minutes (6 hours), so a seat bought minutes ago usually needs the manual refresh. ## Where a search shows up in the app There is no run-now search screen; **Data Requests** is the receipt. Filter **Kind** to **Scrape**, then read **Request** for the method, **Executed on** for the account that ran it, and **View result payload** for the raw response. Copy this to your AI assistant to page through a search: "Page a LinkedIn people search through the GTM API at `https://app.gtm-api.com/linkedin/v4`. POST to /api/linkedin-scraping/search-people with header Authorization: Bearer gtm\_live\_YOUR\_KEY. Send either "url" (a pasted /search/results/people/ link) or "filters", never both, plus "page" and "linkedin\_account\_sid": "ln\_ac\_YOUR\_ACCOUNT". Start at page 1 and re-call with page + 1 while result.paging.has\_more is true, stopping at page 100. Use a distinct idempotency\_key per page. On 429, read error.context.retry\_after and wait until that timestamp. Log result.commercial\_use\_limit\_hit. Write rows to CSV: ln\_member\_id, nickname, full\_name." ## Related * [Auto scrapes](/kb/auto-scrapes) * [Smart limits and warmup](/kb/smart-limits-and-warmup) * [Find an email or phone number for a profile](/kb/enrichment) * [API reference](/api-reference/overview) # Proxy problems Source: https://docs.gtm-api.com/kb/proxy-troubleshooting LinkedIn will not load, the account keeps hitting verification, the browser stops overnight: how to tell a broken proxy from a lost session or normal auto-suspend, and what to do about each. Three symptoms bring people here, and only one is usually the proxy. Work the checks in order and stop at the first one that explains what you see. | Symptom | Most likely cause | Start at | | - | - | - | | LinkedIn will not load inside the browser | The proxy stopped answering | [Check 1](#check-1-read-the-two-statuses) | | The account keeps asking for verification | The apparent location moved, or LinkedIn is challenging the account | [Check 4](#check-4-run-the-location-check) | | The browser stops overnight | Usually nothing. Idle browsers suspend on purpose | [Is this even broken?](#is-this-even-broken) | ## Is this even broken? **Idle browsers power off after 15 minutes without activity** and wake on the next due sync or action, and recurring syncs only start inside the account's [sync window](/kb/sync-windows-and-auto-suspend). The power-off and a closed sync window write nothing to the browser event log; the wake writes a `started_by_user` row, `Starting by user: system`. ## Check 1: read the two statuses The browser and the proxy carry separate statuses, and a dead proxy does **not** show up as a proxy-named browser status. Read both on the **Browser** tab of the account drawer (**Senders**, click the row): the proxy block shows residential or datacenter, country, latency and a **Proxy uptime** row. Hover the **Proxy uptime** row to read `Proxy status: active | no connection | country mismatch | fraud flagged | replaced`. Only two browser statuses point at the proxy: **running issue**, a runtime fault and where a proxy failure lands, and **start issue**, frequently a proxy that timed out. **login issue** is a lost session (Check 6); `idle` is auto-suspend; **subscription required** is billing; **shared out** leaves the proxy with the owning team ([full list](/kb/browser-troubleshooting)). A browser escalates to **investigating** after more than 10 failures with more than 10 errors in 48 hours, and that state emails the account owner. The failure counter is cleared only when the browser comes back up and reports `running`, never by time; the 48 hour window applies to the error rows, so once old errors age out the browser stops escalating even though the counter still stands. Proxy failures do **not** increment it, so proxy errors alone never escalate a browser. One restart attempt is worth it; if it escalates again, open a ticket. | Proxy status | Meaning | Who can clear it | | - | - | - | | **active** | The last probe went through | Nothing to do | | **no connection** | The last probe failed, or the browser reported a proxy error | Clears itself on the next successful probe | | **country mismatch** | The proxy's real exit country is not the country it is registered as | Support only | | **fraud flagged** | The exit IP scored at or above 75 on a fraud database | Support only | | **replaced** | Retired. It will never be handed out again | Terminal | ## Check 2: read the event log Scroll to **Browser event log** at the bottom of the same tab, or search it at `POST /api/antidetect-browser-logs/search`. Match what you see against the exact strings: | Code | event\_type | Message you will see | What it is | | - | - | - | - | | `407` | `proxy_error` | `Proxy error` | The proxy refused or dropped the connection. The real proxy fault | | `100` | `start_failure` | `Error: Proxy Error. Request timeout after 13000ms` | The browser could not start, usually because the proxy did not answer in time | | `401` | `login_issue` | `LinkedIn logged out. Please log back in to your LinkedIn account to resume running this profile.` | The session is gone. A proxy swap will not fix it | | `777` | `server_unreachable` | varies | Our automation host, not your proxy | | `666` | `runtime_error` | `The browser is shutting down.` | Planned host restart | | `-3` | `runtime_error` | `browser was disconnected` | The profile lost its connection to the host | | `-2` | `runtime_error` | `failed to evaluate initial script` | Profile startup fault, not the network path | | `502`, `503`, `504`, `520`, `522` | `runtime_error` | gateway envelope | Upstream fault, not the proxy | | none, level `info` | `proxy_replaced` | `Proxy replaced after failure` or `Proxy changed by operator` | The proxy was already swapped for you | The `666`, `-3`, `-2` and `5xx` rows all land as `runtime_error`: the numeric `Code` is the only axis that tells them apart, so filter on `code`, not on a finer `event_type`. If the newest entry is `proxy_replaced` with `Proxy replaced after failure`, the platform already moved the browser onto a healthy proxy but did not restart it: use **Start** in the row menu. ## Check 3: run the connectivity check This is the answer to "is the proxy actually dead". It performs a real HTTPS request through the proxy and times it. Take the proxy sid (`ab_px_...`) off the browser row by searching `antidetect-browsers` with `"include": ["antidetect_browser_proxy"]`. This check and the location one run over the API and MCP only, with no button in the app, and both require the `can_change_proxy` permission: a member without it gets a `403`. ```bash curl theme={null} curl -X POST "https://app.gtm-api.com/linkedin/v4/api/antidetect-browser-proxies/check-proxy-connectivity" \ -H "Authorization: Bearer gtm_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"sid": "ab_px_YOUR_PROXY"}' ``` The result carries `total_checked`, `ok`, `avg_connection_time_ms` (the mean over the rows that answered, `null` when none did) and `failed[]`, where each entry has an `error_kind`: | `error_kind` | Meaning | | - | - | | `timeout` | The proxy accepted nothing within the probe budget | | `tls` | The TLS handshake through the proxy failed | | `network` | The connection could not be established at all | | `http_error` | The proxy itself rejected the request, typically HTTP 407 authentication | The probe times out at **30 seconds** for both connect and read. Only an HTTP `407` from the proxy counts as a failure; any other status, including a `5xx` from the probe target, counts as success, because it proves the request was routed. A failed probe flips the proxy to `no_connect` and a later successful one flips it back to `active`, but rows already in `country_mismatch`, `fraud_flagged` or `replaced` only get their latency refreshed. Send **exactly one** of `sid` or `filter`. Both or neither returns `422` with `field_errors.sid_or_filter` set to `Provide exactly one of sid or filter.` In filter mode a single call probes at most **50 rows**; the response reports the true match count in `total_matched` and sets `truncated: true` when the cap was hit. ## Check 4: run the location check Run this when the account keeps hitting verification challenges rather than failing to load. It asks several geo vendors and several fraud databases where the exit IP really is. Same request as above with the endpoint swapped for `check-proxy-location`, same `sid` or `filter` rule, same 50-row cap. Two things can trip: * **Country mismatch.** The verdict is the real exit country, measured through the proxy, not a vote among IP databases. If it disagrees with the country the proxy is registered as, the proxy is flagged `country_mismatch`. The `disagreement_pct` is context only. * **Fraud flag.** The aggregated score is the highest score any responding database returned. A proxy is flagged `fraud_flagged` at an aggregated score of **75 or above**. Rows in `country_mismatch` or `fraud_flagged` are never handed out to a new browser, and no automatic process returns them to `active`. Move the affected browser onto another proxy yourself (Check 5) and tell support about the flagged one. Inconclusive rows are skipped, never flagged: the check needs the through-proxy reading plus at least one other vendor that answered, and a skipped row is not counted in `total_checked`. ## Check 5: swap the proxy Two different operations, one dialog. On **Senders**, open the account's row menu, click **Change proxy**, pick a mode and confirm. | Mode in the dialog | What it does | API | | - | - | - | | **Rotate the IP, stay in ** | Moves the browser to the fastest active proxy in the same country, excluding the current one. The apparent location does not change | `POST /api/antidetect-browsers/replace-proxy` | | **Move to another country** | Assigns a proxy from the country you pick. The apparent location changes | `POST /api/antidetect-browsers/update-proxy` | Rotating is the safe default. The country dropdown lists only countries that currently have an active proxy in the pool, and blocks the country the browser is already in. Changing the **country** flips where LinkedIn thinks the sender is, and doing that mid-campaign is a classic trigger for a verification challenge. Pick the country the account genuinely operates from and keep it. ### What each swap restarts Both verbs stop the live session, push the new upstream into the profile, commit, then start the browser again. A running browser keeps egressing through the old proxy until it respawns. | Browser status at swap time | Restarted? | | - | - | | `queued_to_start`, `initializing`, `running` | Yes, stopped and started again | | `idle` | No. It is already powered off and wakes on demand on the new proxy | | `queued_to_stop` | No. A stop is already in flight | | `running_issue`, `start_issue`, `login_issue`, `client_error_investigation`, `support_error_investigation` | No. Their own handlers already powered the browser off, so start it yourself | The response carries `restarted: true|false` and `restart_error`, the literal string `restart_failed` when the browser could not come back up. In the app that shows the toast *"The proxy was changed, but the browser could not be started again. Start it from the row menu."* The swap itself still committed. ### Swap refusals Switch on `error.field_errors` for `422` and on `error.context.reason` for `409` and `503`: the top-level `error.code` is only `validation_failed`, `conflict` or `service_unavailable`. | HTTP | Value | Meaning | | - | - | - | | `422` | `proxy_not_managed` | This browser has no managed proxy to rotate. Pick a country instead to give it one | | `422` | `proxy_pool_empty` | No active proxy exists in the country you asked for | | `422` | `proxy_replacement_unavailable` | The current country has no other active proxy to rotate onto | | `422` | `proxy_country_mismatch` | The proxy you pinned is in a different country than the browser | | `422` | `managed_proxy_forbidden_for_owner` | This browser runs on a profile you bound by its id; its proxy is set in GoLogin, not here | | `409` | `proxy_managed_by_owner` | This is a shared copy. Proxy management stays with the owner | | `503` | `vendor_proxy_update_failed` | Nothing was committed and the browser is back on its old proxy. Retry | Only a profile gtm-api minted takes a proxy change here. A profile you bound by its id keeps its proxy in the GoLogin account that holds it, whoever that is: the menu item is disabled, with the reason on hover, and the dialog is never offered. Change the proxy in the GoLogin app, then **Restart** the browser. ## Check 6: if it is the session, not the proxy A browser in **login issue** is not a network problem. LinkedIn invalidated the session, the status is sticky, and no proxy swap, restart or wait will clear it. During the initial sync the drawer shows **Sync paused, action needed** with the reason `browser:logged_out`; after the initial sync it shows **Signed out of LinkedIn** with a **Log in via cloud browser** button. Re-login is the only fix, in the app or through **Generate re-login link** for the account's owner ([how](/kb/browser-troubleshooting)). Meanwhile a sync run parked on a broken browser re-checks every **30 minutes** and holds its place in the queue, and a run waiting on an `idle` browser to cold-start re-checks every **60 seconds**. Only `running` and `idle` browsers are woken by a sync run itself. A browser in `running_issue` or `start_issue` is restarted by the platform every ten minutes until it either runs or is parked. A parked browser (`client_error_investigation`, `support_error_investigation`) is tried again on its own when the cause is one a retry can outlive: every three hours when the fault is on our side, and, when it is the proxy inside your profile, after three hours, then six, then twelve, then once a day for two weeks. A parking on any other cause of yours (the session, a deleted profile, the account connected in another workspace), a `login_issue` and a `maintenance` wait for an explicit start. ## What the platform already does on its own * **Every active and no-connection proxy is probed on a 15 minute cycle**, 100 rows per tick, coldest first, and flagged or replaced rows are skipped entirely. **Geo and fraud are re-checked daily**, also 100 rows per tick. * **A browser that reports a proxy error is rotated automatically**, but never restarted (Check 2). * **Integrations get told** by the `antidetect-browsers.proxy-issue` and `antidetect-browsers.proxy-back-alive` webhook events, both carrying `antidetect_browser_proxy_sid`. There is no separate proxy-level webhook. ## When to contact support | Case | Why you cannot fix it | | - | - | | Proxy status **country mismatch** or **fraud flagged** | Nothing user-facing returns a proxy to `active`. Rotate onto another proxy to unblock yourself, then report the flagged one so it can be retired | | `proxy_pool_empty` or `proxy_replacement_unavailable`, or the **Change proxy** dialog offers no country at all | The pool has no other active proxy there. Widening it is an internal, money-spending operation. Name the country in the ticket | | Code `777` `server_unreachable`, or status **maintenance** | Our automation host, not your proxy | | Verification challenges keep coming back right after a genuine re-login | LinkedIn challenging the account, especially after a country change. The remedy is account-side, not network-side | A ticket resolves in one round trip when it carries the account name, a screenshot of the drawer's **Browser** tab showing both statuses, the literal `Code` and `Message` of the newest error row in the event log, and the raw JSON of any `422`, `409` or `503` a swap returned. Request shapes for every endpoint above are in the [API reference](/api-reference/overview). ## Related * [Antidetect browsers and proxies](/kb/antidetect-browsers-and-proxies) * [Browser troubleshooting](/kb/browser-troubleshooting) * [Sync windows and auto-suspend](/kb/sync-windows-and-auto-suspend) # LinkedIn Recruiter Source: https://docs.gtm-api.com/kb/recruiter Recruiter is supported on accounts that hold a Recruiter seat: candidate search, the talent inbox, hiring projects and Recruiter InMail. What the seat gives you, and why the session expires every 30 days. Recruiter is supported. An account that holds a LinkedIn Recruiter seat can run candidate searches, sync the talent inbox, read its hiring projects and send Recruiter InMail. Everything on this page needs that seat. Without one, every Recruiter call answers `422 recruiter_required`. ## What the seat gives you | Surface | Where it is documented | | - | - | | Candidate search, with the full Recruiter facet panel | [People and company search](/kb/people-and-company-search) | | The talent inbox, on its own sync cadence | [Inbox and message sync](/kb/inbox-and-message-sync) | | Hiring projects | API reference | | Recruiter InMail | API reference | Recruiter is a **third surface**, separate from the basic LinkedIn engine and from Sales Navigator. Its threads carry `messenger_type: "recruiter"`, its candidates are addressed by `talent_id`, and its search facet ids come from a lookup of their own: none of the three id spaces are interchangeable. ## How the seat is detected The premium check stamps the seat onto the account (`recruiter_seat_id`). Run it with `checks: ['recruiter']` to refresh just that one. Some calls need the stamped seat number, not only the seat itself. If it is missing they answer `422 recruiter_seat_unresolvable`, and the premium check resolves it. ## Several Recruiter contracts A member who holds seats on several Recruiter contracts is asked by LinkedIn to pick one before Recruiter opens, and holds no seat until they do. The premium check handles the common case on its own: when only one corporate contract is on offer (or the account is already bound to one of them) it binds the browser to that contract and stamps the seat. When several corporate contracts are on offer the check does not guess, because the wrong pick binds the browser to the wrong inbox: the seat stays unstamped until someone picks. An agent lists the contracts with `get_linkedin_account_my_recruiter_contracts` (the corporate rows are Recruiter seats; the individual row is a Job Posting contract on the personal account) and binds one with `select_linkedin_account_recruiter_contract`, passing `contract_id` exactly as listed; the seat holder can pick it in the account's browser instead. Either way the seat is stamped on the spot or by the next check. `recruiter_contract_id` on the account names the contract the seat belongs to. ## The Recruiter session expires every 30 days **This is the failure everyone hits, and it is not a fault.** Recruiter keeps its own session, roughly 30 days long, separate from the account's normal LinkedIn session. The account can be perfectly healthy and still have a dead Recruiter session. When it runs out, LinkedIn asks the seat holder for their password again before Recruiter opens. The account's `recruiter_status` says where it stands: `active` (the seat answers), `signed_out` (the seat needs a sign-in) or `null` (no Recruiter seat). It flips to `signed_out` on the first refused Recruiter call, and the platform also checks the session clock every 15 minutes so you hear about it before a sync runs into it. The moment it flips you get one webhook, `linkedin-accounts.recruiter-signed-out`, and the person who connected the account gets one email; nothing repeats until the next time, and never more than once a day per account. The webhook's `reason` tells the two cases apart. `session_expired` and `wire_401` mean LinkedIn ended a session that existed. `session_unconfirmed` means the seat is there but the browser has never been signed in to Recruiter (or the member is still on LinkedIn's contract chooser): nothing was ended, and the email says "sign in to switch it on" rather than "you were signed out". A freshly connected account with a Recruiter seat starts in exactly this state, so the first thing to do after connecting a seat holder is the sign-in below. While the seat is signed out, every Recruiter call answers `409 recruiter_reauth_required` and the recruiter inbox sync waits. The error carries `context.account_url`, the account's page in the app, and a plain suggestion an agent can relay. **Only the seat holder can sign in again.** Open the account's page, press **Sign in to Recruiter**: the account's cloud browser opens straight on Recruiter's sign-in, you enter the LinkedIn password there and confirm. The platform restarts the browser, re-checks the seat and flips the status back to `active` (and sends `linkedin-accounts.recruiter-signed-in`). Nobody else can do it for you: not another teammate, not support, not an agent, and the platform never stores the password. Retrying the call does not help until you have signed in. `recruiter_session_expires_at` on the account tells you when the session will run out, so you can sign in before it does rather than after. The platform reads it from the account's own running browser. It is null when the platform has not seen the session cookie, which is not a verdict: a browser that is still on LinkedIn's contract chooser has no session cookie at all, and the fix there is picking a contract, which the sign-in above ends on. ## InMail credits Recruiter InMails come out of the contract's own pool, which LinkedIn keeps apart from the Sales Navigator seat's grant and from the Premium plan's balance. The account carries it as `recruiter_inmail_credits`, what Recruiter's Usage Overview shows as InMail credits. The premium check reads it for an `active` seat as part of its `inmail_credits` step, every 6 hours in the background or on demand with `check_linkedin_account_premium_subscription` and `checks: ["inmail_credits"]`. Null means the platform has not read it yet, never zero. A new thread on a balance known to be zero is refused before anything reaches LinkedIn (rule `no_inmail_credits`); a reply into an existing thread is never guarded. A signed-out seat keeps its last number until the seat holder signs in again. `search_linkedin_accounts` filters on the field, so `recruiter_inmail_credits: {eq: 0}` lists the seats that cannot open a thread until the monthly refresh. ## Limits that come from LinkedIn, not from us **One InMail per candidate per 24 hours.** Sending a second one answers `429 recruiter_inmail_cooldown` with `cooldown_ends_at`. If a reply may have landed in between, refresh the thread before assuming the send failed. **A per-seat search throttle.** Heavy searching answers `429 recruiter_search_usage_limit`. Waiting a minute clears it. Neither is a platform limit and neither is configurable here. They are LinkedIn's, applied to the seat. # Smart limits and warmup Source: https://docs.gtm-api.com/kb/smart-limits-and-warmup Per-action daily budgets protect every LinkedIn account, and a warmup ramp grows them as a new account proves itself. How budgets, holds and warmup work. Smart limits budget how many actions of each kind a LinkedIn account may perform per day. They are on by default for every connected account and every outbound call checks them server-side before dispatch: work that would exceed a budget is parked, not lost. ## How budgets work Every action type (connection requests, messages, profile visits, endorsements, and so on) has its own daily bucket. When a bucket is spent, further actions of that kind wait until the budget refreshes. Parked work resumes automatically as soon as the limit releases; nothing needs re-queuing. ## Pacing: one call at a time Inside the day, calls of one bucket leave one at a time with a gap between them. The gap is the bucket's `delay_in_seconds`, varied by up to 20% per call; there is no burst, two calls of one bucket never leave together. Reads are paced in single seconds, actions in tens of seconds; connection requests and posts are deliberately slower. The number is per account and editable (`delay_in_seconds` 1..3600), and the platform's own value for the account comes back on every row as `recommended_delay_in_seconds`. | Action type | Per day | One call about every | | - | - | - | | Connection requests | 25 | 30 s | | Accept / ignore / withdraw / follow | 40 | 4 s | | Messages, voice notes | 50 | 10 s | | InMails, Sales Navigator and Recruiter messages | 30 | 23 s | | Inbox housekeeping (read / unread, mute, attachments) | 100 | 4 s | | Comments | 30 | 30 s | | Reactions, endorsements | 30 | 7 s | | Profile visits | 50 | 10 s | | Profile edits | 10 | 30 s | | Searches, typeaheads, engagement lists | 100 | 5 s | | Profile, company and post reads | 80 | 5 s | | Posts | 20 | 40 s | | Own-account sync reads | 200 | 1 s | | Custom requests | 60 | 5 s | An undo (unreact, delete a comment or a post, unendorse, unfollow, withdraw a request, recall a message) waits 2 seconds instead of the bucket's gap, so an action and its undo fit back to back. The slot is reserved before the call leaves, so two calls fired in the same second never reach LinkedIn together: the second one queues behind the first. What the caller sees depends on how long the queue is: | Wait for the slot | What happens | | - | - | | Up to 20 seconds | The server sleeps it. The call simply takes longer and answers normally | | Longer | `429 rate_limited`, `context.reason` `bucket_saturated`, `cause` `delay_not_elapsed`, and `retry_after`: the moment the same call goes through | Parallel calls on one account therefore buy nothing. Run them one after another, or spread the work over several accounts: every account paces on its own. A success names the same clock, so there is no need to run into the gap to learn it. Every response whose call reached LinkedIn carries a `pacing` block: ```json theme={null} "pacing": { "linkedin_account_sid": "ln_ac_YOUR_ACCOUNT", "limit_type": "send_messages", "next_call_after": "2026-09-18T12:00:10+00:00", "remaining_today": 46 } ``` `next_call_after` is the moment the same call goes through again without waiting: the next slot while the day has budget left, the daily reset when it has none. `remaining_today` is what the bucket has left after this call. A call that spends two buckets (a reaction on a post given by link reads the post first) names the bucket of the action itself and the later of the two clocks. On top of every bucket, one account starts at most 10 calls a minute. Smart Limits tab showing the warmup percentage, six health signals and per-action daily caps ## Warmup A freshly connected account starts with reduced budgets that ramp up gradually. The ramp is driven by six live signals about the account, shown in the drawer: recent activity, concurrent sessions and IPs, blocks, limit hits, account age, and profile completeness. They multiply into the day's allowance. Warmup counts real sends: activity the account actually performed matters, not just time passed. A dormant account ramps slower than one that is actively and successfully working. ## Limit statuses | Status | Meaning | | - | - | | `active` | Budgets available, actions flow normally | | `held` | The current budget is spent; outbound actions are parked until the refresh. The normal daily-cap state, not an error | | `linkedin_blocked` | LinkedIn itself restricted the account; actions stay parked until the restriction clears | ## Smart limits on or off: one switch per account Smart limits are a single switch on the account, not a setting per bucket. While they are on, the warmup owns every daily cap and pacing number: a request that types one of them by hand is refused with `409` and `context.reason` `smart_limits_governed`, and the answer names the fields it refused and the two ways forward. Inside the protected mode you can still bound a bucket with `target_limit` (the cap follows `min(smart, target)` at once) and pin the learner with `learning_enabled`. Turning the switch off hands every number to you: the caps run exactly as typed and the warmup never touches the account again. That is where accounts get lost, so the drawer asks in words before switching off, and the MCP tool previews before it commits. Turning it back on re-derives every cap from the latest snapshot on the spot. ```bash set-smart-limits theme={null} # Take an account off the warmup (or put it back with "enabled": true) curl -X POST "https://app.gtm-api.com/linkedin/v4/api/linkedin-accounts/ln_ac_YOUR_ACCOUNT/set-smart-limits" \ -H "Authorization: Bearer gtm_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"enabled": false}' ``` ## Raise a limit, release a hold With smart limits off, limits are editable per account in the drawer. Raising a limit offers **reset hold**, which atomically raises the budget and resumes everything parked on it in one step. Without reset hold, the raised budget takes effect on the next refresh cycle. ```bash curl theme={null} # Read an account's current buckets and what is spent today curl -X POST "https://app.gtm-api.com/linkedin/v4/api/linkedin-account-smart-limits/search" \ -H "Authorization: Bearer gtm_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"filter": {"linkedin_account_sid": {"eq": "ln_ac_YOUR_ACCOUNT"}}}' ``` ```bash reset-hold theme={null} # Raise a bucket and release its hold in one step curl -X POST "https://app.gtm-api.com/linkedin/v4/api/linkedin-account-smart-limits/lm_sl_YOUR_LIMIT/reset-hold" \ -H "Authorization: Bearer gtm_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{}' ``` Hitting a budget, a hold or a pacing gap longer than 20 seconds over the API answers `429 rate_limited` with `context.reason` `bucket_saturated`, the `cause` (`daily_saturation`, `held`, `delay_not_elapsed` or `linkedin_quota_hit`) and a `retry_after` timestamp. Inside a mass action the item is parked on that clock and resumes on its own; a direct call retries at `retry_after`, not earlier: an earlier retry is refused again with the same timestamp. The account is not penalized for your retry logic. ## Related * [Sync windows and auto-suspend](/kb/sync-windows-and-auto-suspend) * [Antidetect browsers and proxies](/kb/antidetect-browsers-and-proxies) * [Run a mass action](/guides/run-a-mass-action) # Sync windows and auto-suspend Source: https://docs.gtm-api.com/kb/sync-windows-and-auto-suspend Recurring syncs run inside each account's configured hours, idle browsers suspend themselves, and due work wakes them. What is window-gated and what is not. Each LinkedIn account has a **sync window**: the days and hours during which recurring synchronizations (inbox, connections, invitations, followers) are created and picked up. Outside the window no new recurring sync starts. A newly connected account starts on business hours, Monday to Friday 09:00-18:00, in your workspace timezone. Change it whenever you like on the Account Sync tab. An account with no window at all syncs **around the clock**, every day. That is what accounts connected before this default existed do, and it is what you get if you switch every day off. Emptying the window opens the account up rather than pausing it; to stop an account from working, stop its browser instead. Account Sync tab showing the sync days chips, the sync window hours and per-kind sync intervals ## What the window gates, and what it does not * **Recurring syncs are window-gated.** Conversations, connections, invitations, followers and profile refreshes are scheduled only inside the configured hours. * **Actions are not.** Sending a message or a connection request runs whenever you or an automation triggers it, at any hour, subject to [smart limits](/kb/smart-limits-and-warmup). * **The first import ignores the window.** The initial backfill of a freshly connected account runs to completion whatever the hours say, so onboarding never stalls overnight. Smart limits and pacing still apply. Only the recurring syncs that follow it are gated. Edit the window and the per-kind intervals on the **Account Sync** tab of the account drawer. Each sync kind shows its interval, the last run and the next due time. ## Browser auto-suspend and wake A running antidetect browser costs resources, so idle browsers suspend automatically: after about 15 minutes without activity the platform stops the browser. Waking is automatic too: * an **action** waits for the browser to start, then runs; * a due **sync** wakes the browser in the background. You never need to start browsers manually for scheduled work. A browser you stopped yourself stays stopped for manual use, but due syncs and actions still wake it. ## Health checks The platform periodically verifies that each running browser is actually responsive. A browser whose LinkedIn tab is dead is marked with an issue status and powered off rather than left running blind; the account's sync state shows the reason. ## Change the schedule over the API The window is a list of per-weekday entries in the account's timezone, minutes from local midnight. Weekdays 1 to 5 (Monday to Friday), 09:00 to 18:00: ```bash curl theme={null} curl -X POST "https://app.gtm-api.com/linkedin/v4/api/linkedin-accounts/ln_ac_YOUR_ACCOUNT/update-sync-config" \ -H "Authorization: Bearer gtm_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "sync_config": { "timezone": "Europe/Amsterdam", "window": [ {"day_of_week": 1, "start_minute": 540, "end_minute": 1080}, {"day_of_week": 2, "start_minute": 540, "end_minute": 1080}, {"day_of_week": 3, "start_minute": 540, "end_minute": 1080}, {"day_of_week": 4, "start_minute": 540, "end_minute": 1080}, {"day_of_week": 5, "start_minute": 540, "end_minute": 1080} ] } }' ``` ```typescript TypeScript theme={null} const window = [1, 2, 3, 4, 5].map((day_of_week) => ({ day_of_week, // ISO weekday: 1=Mon … 7=Sun start_minute: 9 * 60, // minutes from local midnight end_minute: 18 * 60, })); await fetch( "https://app.gtm-api.com/linkedin/v4/api/linkedin-accounts/ln_ac_YOUR_ACCOUNT/update-sync-config", { method: "POST", headers: { Authorization: "Bearer gtm_live_YOUR_KEY", "Content-Type": "application/json", }, body: JSON.stringify({ sync_config: { timezone: "Europe/Amsterdam", window }, }), }, ); ``` Omit `window`, or send it as an empty list, to sync around the clock. Both mean the same thing, and neither switches syncing off. Per-kind intervals ride the same request's `entries` object. Field names and the full request shape are in the [API reference](/api-reference/overview); to change the schedule for every account on the team in one confirmation, use a [mass action](/guides/run-a-mass-action). ## Related * [Antidetect browsers and proxies](/kb/antidetect-browsers-and-proxies) * [Smart limits and warmup](/kb/smart-limits-and-warmup) # Workspaces and team members Source: https://docs.gtm-api.com/kb/workspaces-and-team-members What a workspace owns, how switching between them works, how member access is expressed, what an invite does, and why team members are off on the free plan. A workspace is the tenant: it owns the accounts, the keys, the plan and the member list. This page covers switching between them, member access, invitations from both sides, and the free-plan rule. **Give this to your AI agent** to audit who can reach a workspace: "Connect to gtm-api: add the MCP connector at `https://mcp.gtm-api.com/mcp` (OAuth). Then search team-members in the workspace my token is scoped to: split active members from pending invitations (`user_sid` is null), show each row's email, permission count and account slice, and flag invitations older than 7 days. Do not change anything." ## What a workspace owns A workspace id starts with `ts_tm_`, and each of these belongs to exactly one of them: * Connected LinkedIn accounts, their browsers, conversations, searches, mass actions, auto scrapes. * API keys, login sessions and OAuth grants. **A key is workspace-wide**: it reaches every account in that workspace, and no key can be scoped to one account. * The plan, the account-slot cap and the billing history. * The member list, including invitations nobody has accepted yet. Exactly one person is the **owner** (`owner_user_sid`), the only structural role on the platform. Everything else is a permission list, which is why the app shows an **Access** column rather than a role column. Two workspace fields are editable: `name` (1 to 255 characters) and `config.timezone` (an IANA zone such as `Europe/Berlin`); `status`, `plan`, `limits` and the owner are derived, and the API drops them silently if you send them. Membership does not share accounts: being a member of workspace A and B does not let you drive A's LinkedIn account from B. Moving an account across workspaces is a share or a transfer, both in [account sharing and handover](/kb/account-sharing-and-handover). ## Create and switch workspaces Click the workspace chip under the logo in the sidebar to switch: the current one carries a check icon, and the list holds every workspace you own plus every one where you are an active member. **Create workspace** sits at the bottom of that dropdown, and confirming the name switches you into the new workspace immediately, as its owner. Your first workspace starts on [Sandbox, the forever free plan](/kb/billing-and-plans): one connected-account slot, no card, no time limit. Sandbox is one workspace per account, so a further workspace you create comes up without capacity until you buy a plan for it. Plans are per workspace: a paid plan on one workspace grants nothing to another. Switching reloads the page: an access token names exactly one workspace and carries its home cluster, so the app calls `POST /auth/switch-team`, gets a new access and refresh token, blacklists the old pair, and reloads. Your other devices, API keys and OAuth tokens are untouched. Which workspace opens when you load the app is the first valid candidate, in order, of: the `?team=ts_tm_...` query parameter, which makes any app URL a shareable deep link; the workspace open in this tab; the one last used in this browser; `config.latest_team_sid` on your user; your working team (`default_team_sid`, **Working team** in the UI, assigned when you join your first workspace, and where accounts shared with you land); then the first workspace you have. An invalid candidate is dropped and the next one is tried. Over the API there is no switching, because a token is already scoped. The `Team-SID` header is read before the token's own claim, but it has to name the workspace the token was minted for: anything else answers `403 forbidden`, reason `wrong_team`. See [authentication](/authentication). ## How member access is expressed There is no role column in the data. Access is a flat list of permission tokens shaped `can_{verb}_{noun}`, plus an optional account slice. The catalog holds **43 tokens** (21 platform-level, 18 LinkedIn, 4 orchestration), and the wildcard `*` means all of them. The picker offers three named presets over that list: **Superadmin** (everything, stored as `*`), **Operator** (the default for a new invite: run outreach on every account, no billing, keys, sessions or admin surfaces) and **View only** (read everything the product shows, act on nothing). A preset is a starting point, not a stored role: after picking one you can still tick or untick individual tokens. One token, `can_view_teams`, is part of every membership and cannot be unticked - without it a member could not even resolve their own workspace. | Token | Grants | | - | - | | `can_view_teams` / `can_manage_teams` | Read the workspace, rename it. Manage also covers transfer and delete | | `can_view_team_members` / `can_manage_team_members` | Read the member list; invite, re-scope, resend and remove | | `can_view_api_keys` / `can_manage_api_keys` | Read keys; create, rotate and revoke them | | `can_view_billing` / `can_manage_billing` | Read the subscription and invoices; change the plan | | `can_view_sessions` / `can_manage_sessions` | Read login sessions; revoke them | | `can_view_account_shares` / `can_manage_account_shares` | Read account shares; create and recall them | | `can_manage_account_transfers` | Transfer an account to another workspace. No read-only tier by design | The `account_sids` field is the slice: `null` is every account in the workspace (rendered **All**), a list of `ln_ac_` sids is only those (** of **). An explicit empty list is refused with a `422` - a member scoped to zero accounts would see a workspace that looks empty rather than forbidden, which is a support ticket, not a grant. Both scoping surfaces are in the app. The invite dialog carries the permission editor, so an invitation goes out with exactly the tokens you tick, and **Access** on a member row reopens the same editor for somebody already in the workspace. Either one writes the full list, so the editor's state is the grant: unticking is how you narrow. `PATCH /api/team-members/{sid}` does the same over the API. A change applies immediately: the member's live tokens are invalidated and silently re-minted with the new grant on their next request - no re-login, and no waiting for a token to expire. You can only grant what you hold yourself: a request that exceeds your own access answers `403 grant_exceeds_ceiling`. ## Invite a teammate Open **Settings**, then **Teammates** under the **Workspace** group in the left rail. The panel is titled **Members and invites**, and its path is in the URL, so the page is linkable. Click **Invite**, enter the email (max 255 characters) and send: a row appears immediately with status **Invited**, carrying a permission chip that pops the member's full token list. The invitee receives an email with subject **"You are invited to `{workspace name}`"** and an **Accept invitation** button carrying a single-use code. Accepting requires a signed-in session, because a membership binds to the authenticated user and never to an address in a request body, so somebody without an account signs up first. On acceptance the row flips from `invited` to `active`, the code is cleared, and if the person had no working team yet this workspace becomes it. An invitation link is valid for **7 days**, after which a daily job clears the stored code and the old link stops working. The row stays on the list: press **Resend** to re-roll the code and give another 7 days, which only works on rows in status `invited`. Inviting an email already on the list is not an error and sends no second email, so use **Resend** to genuinely re-send; inviting an address you removed earlier creates a fresh row. | Status | Meaning | | - | - | | `invited` | Invitation sent, not accepted. `user_sid` is still null, and the row shows only the email | | `active` | Accepted and in the workspace | | `revoked` | Written by the workspace-delete cascade as it soft-deletes every membership. No per-member action writes it | The panel loads the first **100 members** sorted by join date and has no pagination control. A workspace with more members than that is read over the API, which pages with a cursor. ## Remove, hand over, delete Removing a member and cancelling an invitation are the same row action, both safe for the data: * **Cancelling a pending invitation** kills the code. The link stops working immediately. * **Removing an active member** revokes their login sessions and agent tokens **in this workspace only**. Their sessions elsewhere survive, and so does the durable OAuth grant behind any connected app. Accounts, browsers, conversations and history stay where they are. If this workspace was their working team, that pointer moves to another membership of theirs, or clears. Removal is a soft delete: the row is kept as an access audit trail with no purge job, and deleting a row twice answers `already_deleted: true`. The owner's membership cannot be removed at all, and that button is disabled with the tooltip **Transfer ownership first**. To hand the workspace over, open **Settings** -> **General**, find the **Owner** row and click **Transfer**. Only the current owner can do this, not an admin holding `can_manage_teams`. The picker lists active members only, the target is re-checked as `active` at commit time, transferring to yourself answers `changed: false`, and the previous owner stays a member with the same permissions. Deleting a workspace is owner-only, blocked while a paid plan is live, and cannot be undone. API keys are revoked and members lose access the moment it is deleted, and its data is erased **30 days** later. ## The free plan is single-user On the free plan both inviting and accepting answer **HTTP 402** `payment_required`, with `context.reason = "team_members_not_available_on_free"` and `context.suggested_action = "upgrade_subscription"`. The error is recoverable: upgrade and the same call succeeds. Because the gate runs on acceptance as well as on the invite, a workspace that was paid, invited people, and then fell to the free plan keeps its pending invitations **frozen** rather than losing them: the invitee gets the 402, and the same link works again once a plan is applied. Paid and partner plans are multi-user: see [billing and plans](/kb/billing-and-plans). ## Over the API Members live in the ID service. Invite with `POST /api/team-members`, list members and pending invitations with `POST /api/team-members/search` (there is no `GET /api/team-members/{sid}`, member lists are small), narrow one with `PATCH /api/team-members/{sid}`, remove or cancel with `DELETE` on that path, re-roll a code with `POST /api/team-members/{sid}/resend-invitation`, and accept with `POST /api/team-members/accept-invitation`, which needs a signed-in user token rather than a key. ```bash theme={null} curl -X POST "https://app.gtm-api.com/id/v4/api/team-members" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"email": "teammate@company.com", "permissions": ["*"]}' ``` Search filters on `{"user_sid": {"is_null": true}}` for pending invitations only. Field names and the full request shape are in the [API reference](/api-reference/overview). ## Error to fix | What you sent | Answer | Fix | | - | - | - | | Invite or accept on a free-plan workspace | `402 payment_required`, reason `team_members_not_available_on_free` | Apply a paid plan, then repeat the call | | An email already on the member list | `200` with `already_exists: true` | Nothing failed. Use `resend-invitation` to send another email | | `DELETE` on the owner's membership | `409 conflict`, reason `owner_cannot_be_removed` | Transfer ownership first | | `resend-invitation` on an active member | `409 conflict`, reason `invalid_transition` | Only rows in status `invited` can be resent | | `accept-invitation` with an unknown or used code | `200` with `already_accepted: true` if you are an active member of any workspace, `404 not_found` if you are not | The `200` does not mean you joined the invited workspace. Ask for a fresh invitation. The code is never echoed back | | `accept-invitation` after 7 days | `409 conflict`, reason `invitation_expired` | Press **Resend** in the app, which gives another 7 days | | `PATCH` with neither field | `422 nothing_to_update` | Send `permissions` or `account_sids` | | An `account_sids` entry that is not an `ln_ac_` sid | `422 validation_failed` | Use LinkedIn account sids, not user or member sids | | A sid with the wrong prefix | `422 validation_failed`, `field_errors.sid[0].message` reads `Expected ts_mb_ prefix, got ...` | Member sids start with `ts_mb_` | | `switch-team` to a workspace you do not belong to | `403 forbidden`, reason `not_a_member` | The same code covers a deleted workspace and a revoked membership, on purpose | | `switch-team` with an OAuth installation token | `403 forbidden`, reason `not_a_login_session` | Only login sessions switch. Agent tokens are minted per workspace | | A call with no `Team-SID` on a workspace-less token | `422 validation_failed`, with `field_errors.team_sid[0].rule` = `team_sid_required` | Get a token minted for a workspace. The header cannot supply one to a token that has none | | `Team-SID` naming a workspace the token was not minted for | `403 forbidden`, reason `wrong_team` | Send the header the token was minted with, or drop it | ## Related * [Billing and plans](/kb/billing-and-plans) * [Account sharing and handover](/kb/account-sharing-and-handover) * [Authentication](/authentication) # Agent-native signup and billing Source: https://docs.gtm-api.com/mcp/agent-signup How an AI agent registers its operator, connects itself, and handles the moment the workspace needs a paid plan. An agent does not need its operator to have an account before it can start. The signup ladder has four rungs; the higher two are API surfaces built for agents. ## Start a signup for your operator The agent submits the operator's email plus its own OAuth client identity and a PKCE challenge. The human gets one email and clicks one link; that click is the consent screen and the abuse boundary at once. Nothing provisions before it. Standard RFC 7591 dynamic client registration: ```bash theme={null} curl -X POST "https://app.gtm-api.com/id/v4/oauth/register" \ -H "Content-Type: application/json" \ -d '{"client_name": "My Agent", "redirect_uris": ["http://127.0.0.1:8976/callback"]}' ``` Keep the returned `client_id`. Clients born this way are stamped `actor_kind: agent`, so every token they mint is attributable as agent activity. ```bash theme={null} curl -X POST "https://app.gtm-api.com/id/v4/auth/register-agent" \ -H "Content-Type: application/json" \ -d '{ "email": "operator@company.com", "client_id": "YOUR_CLIENT_ID", "redirect_uri": "http://127.0.0.1:8976/callback", "code_challenge": "BASE64URL_S256_OF_YOUR_VERIFIER", "code_challenge_method": "S256" }' ``` The answer carries `registration_sid` and a one-time `poll_secret`. The operator receives a verification email; tell them it is coming. ```bash theme={null} curl -X POST "https://app.gtm-api.com/id/v4/auth/register-agent/poll" \ -H "Content-Type: application/json" \ -d '{"registration_sid": "id_ar_...", "poll_secret": "..."}' ``` `pending` until the operator finishes the page; then `{"status": "verified", "code": "...", "team_sid": "ts_tm_..."}`. The code is single-use and short-lived; poll again for a fresh one if it expires before you exchange it. The standard token endpoint, nothing custom: ```bash theme={null} curl -X POST "https://app.gtm-api.com/id/v4/oauth/token" \ -H "Content-Type: application/json" \ -d '{ "grant_type": "authorization_code", "code": "...", "code_verifier": "YOUR_VERIFIER", "client_id": "YOUR_CLIENT_ID", "redirect_uri": "http://127.0.0.1:8976/callback" }' ``` From here you hold the normal grant credential and installation token: connect to `https://mcp.gtm-api.com/mcp` or call REST directly. The operator lands in a Sandbox workspace, the forever free plan: one sender slot, no card, no time limit. Rate limits apply per IP on every endpoint above, the emailed link expires in 24 hours, and an email that already has an account is told so on the verification page, not in the API answer. One live registration per (email, client): re-posting the same pair re-sends the email and answers `already_exists: true`. If the operator already has an account, skip this flow: connect through OAuth from any client ([Connect a client](/mcp/connect)) and the consent screen does the rest. ## When Sandbox is not enough Sandbox never expires, but verbs answer `402` with `subscription_required` the moment the workspace needs more than the free plan grants, for example a second connected account or a second webhook. That moment is handleable by the agent, not just reportable: 1. Read the state: search `billing-subscriptions` for the workspace and see `status`. 2. Mint a payment link: create a checkout for the plan through the billing tools (or a Paddle portal link for an existing subscription). Both return a URL a human can open. 3. Hand the URL to your operator with the one-line reason, and retry the blocked action after the `billing-subscriptions.activated` webhook, or on your next run. The agent never touches payment credentials: the link opens Paddle's checkout in the operator's browser, and the platform unblocks on the webhook. # Connect a client Source: https://docs.gtm-api.com/mcp/connect Add the gtm-api MCP server to Claude, ChatGPT, Cursor, VS Code, Claude Code, Codex or any MCP-compatible runtime. The server speaks streamable HTTP at `https://mcp.gtm-api.com/mcp` and authenticates with OAuth. Your client discovers the flow on its own: the first call comes back with a `WWW-Authenticate` challenge pointing at the server's metadata, the client registers itself, and you approve the consent screen in your browser. Nothing to paste, no key to store. ## Add to your agent One action per client, then approve the consent screen that opens. That is the whole setup. Open Settings, Connectors, then "Add custom connector". Paste `https://mcp.gtm-api.com/mcp` and approve access. Works for claude.ai and Claude Desktop. One-click install (the button opens Cursor). Sign in on the consent page that follows. One-click install (the button opens VS Code). Sign in on the consent page that follows. Settings, Connectors, Advanced settings, enable Developer mode. Add a connector with the URL above and complete the OAuth sign-in. ### Claude Code ```bash theme={null} claude mcp add --transport http gtm-api https://mcp.gtm-api.com/mcp ``` Then run `/mcp` in any session and authenticate when prompted. `claude mcp list` shows `gtm-api` as connected once consent is done. ### Codex ```bash theme={null} codex mcp add gtm-api --url https://mcp.gtm-api.com/mcp codex mcp login gtm-api ``` The first line registers the server in `~/.codex/config.toml`, the second runs the OAuth flow and opens the consent page. No client id to pass: the server registers the client on its own. ### Any other MCP client Point it at `https://mcp.gtm-api.com/mcp` over the streamable HTTP transport and let it run the OAuth flow. Clients that support MCP OAuth need nothing else: the server advertises its authorization server, and dynamic client registration means your client does not need to be pre-registered with us. ### Clients that only spawn a command Some clients cannot take a URL at all, only a command to run over stdio. For those, the [`@gtm-api/linkedin-mcp`](https://www.npmjs.com/package/@gtm-api/linkedin-mcp) launcher bridges stdio to the same hosted endpoint and runs the same OAuth flow, opening the consent page on first use and caching the tokens under `~/.mcp-auth`. ```json theme={null} { "mcpServers": { "gtm-api": { "command": "npx", "args": ["-y", "@gtm-api/linkedin-mcp"] } } } ``` Launcher versions up to 1.1.0 sent `GTM_API_KEY` against an endpoint that rejected keys, and 1.2.x ignored the variable and did OAuth only. Key auth works from 1.3.0 on. Anything pinned to 1.1.0 or older keeps failing on the first call; upgrade the pin. ## Headless: authenticate with an API key The same key that authenticates the [REST API](/api-reference/overview) works on this server as a plain bearer token. No browser, no consent screen, so it fits CI, servers and any client where the OAuth round-trip is not an option. Mint a key in the app (Settings, API keys) and attach it as a header: ```bash theme={null} claude mcp add --transport http gtm-api https://mcp.gtm-api.com/mcp --header "Authorization: Bearer gtm_live_..." ``` For command-spawning clients, the launcher takes it from the environment instead: ```json theme={null} { "mcpServers": { "gtm-api": { "command": "npx", "args": ["-y", "@gtm-api/linkedin-mcp"], "env": { "GTM_API_KEY": "gtm_live_..." } } } } ``` Pick one mode per connection: with a key attached, the OAuth flow never starts. The key is scoped to its workspace and its permission set, revocable in the app, and treated like a password (see [Authentication](/authentication)). Interactive clients such as claude.ai connectors have no header field and always use OAuth. ## Check it from a terminal To confirm the endpoint and walk the OAuth flow by hand: ```bash theme={null} npx @modelcontextprotocol/inspector ``` Choose the streamable HTTP transport, enter `https://mcp.gtm-api.com/mcp`, connect, and complete the consent screen. The tool list appears once you are authorized. ## What you get Three meta-tools rather than hundreds of individual ones: `list_toolsets` to see the domains, `get_toolset_tools` to inspect one, and `call_tool` to run any tool by name. Dangerous tools still run their preview and confirm step, so an agent cannot commit a bulk send without showing you what it is about to do. ## First prompt to try > "Check the smart limits on my LinkedIn account, then draft, preview and send a connection request to the profile at this URL." The agent chains the tools on its own; the preview step and the server-side limit check run before anything is sent. # MCP server Source: https://docs.gtm-api.com/mcp/overview The same platform, exposed as Model Context Protocol tools an AI agent can call. GTM API ships a managed MCP server at `https://mcp.gtm-api.com/mcp`. It exposes 281 typed tools across 18 toolsets, so Claude, Cursor, ChatGPT or a custom agent runtime can search, connect, message and enrich on a LinkedIn account you own. MCP and REST are the same contract. Every tool maps 1:1 to an endpoint in the [API reference](/api-reference/overview): one Zod schema defines the tool, the endpoint and this documentation, so an agent and your backend code see identical shapes. If you can do it over REST, an agent can do it over MCP, under the same server-side safety layer. ## What the tools cover | Area | Examples | | - | - | | Messaging | send a message, voice message or InMail, search and sync the inbox | | Network | send, accept, withdraw or ignore connection requests, list connections and followers | | Content | track posts and their metrics, comment, react, pull engagers | | Enrichment | lite and full profiles, experience, skills, education, company data | | Search | people, companies and posts, similar profiles, employees, decision-makers | | Account health | smart limits, health snapshots, quota, block and activity logs | | Infrastructure | anti-detect cloud browsers, dedicated proxies, webhooks | ## What to ask Once a client is connected, the agent picks the tools itself from a plain request. A few that work on day one, each mapping onto the areas above: | Ask | What the agent does | | - | - | | "Show my latest LinkedIn conversations" | reads the synced inbox across your connected accounts, newest first | | "Find decision-makers at fintech companies in Berlin" | runs a people search with the title, industry and location filters, returns the profiles | | "Send connection requests to these 20 people, a few a day" | previews the batch, then dispatches it as a paced mass action under the account's daily budget | | "Reply to everyone who wrote back this week" | lists the threads with a new inbound message, drafts a reply per thread and sends after your confirmation | | "Who liked my last post? Enrich them and keep the founders" | pulls the post's engagers, enriches each profile, filters by title | | "Which of my connections changed jobs this month?" | compares the synced connections against fresh profile data and lists the movers | | "Draft a follow-up for everyone who did not answer in 5 days" | finds the outbound threads with no reply after five days and drafts the next message for each | | "How is my account health, am I close to any limit?" | reads the account's health snapshot and today's spend against every smart limit | Anything that sends (a message, an invitation, a bulk run) goes through the preview and confirm step below; reads run straight away. The wording does not matter, the tools do: every row here is one or two tools from the [API reference](/api-reference/overview), chosen by the agent. ## Guardrails that matter for agents * **Preview then confirm.** Outward actions (sends, bulk dispatches) return a preview and require an explicit confirmation step before anything reaches LinkedIn, so an over-eager agent cannot burn an account on its own. * **Server-side limits.** Per-action daily budgets are checked before dispatch, under every tool call. The agent does not need to count; the server refuses when a budget is spent. * **Typed errors.** Tools fail with the same 16-code taxonomy as REST, with `recoverable` and a `suggestion` field written for an LLM to act on. ## Where to go Claude, Cursor, and anything else that speaks MCP. The documentation's own MCP endpoint. The public interface, connect examples and safety notes also live in the open repository: [github.com/gtm-api/linkedin-mcp](https://github.com/gtm-api/linkedin-mcp). # Use these docs from your AI tools Source: https://docs.gtm-api.com/mcp/use-docs-with-ai The documentation has its own MCP endpoint, plus llms.txt and per-page Markdown. Two different MCP servers exist, and it helps not to mix them up: * `https://mcp.gtm-api.com/mcp` is the **product**: it calls gtm-api on your behalf. See [Connect a client](/mcp/connect). * `https://docs.gtm-api.com/mcp` is the **documentation**: it searches and reads these pages. It cannot call the API, needs no API key, and is the right thing to wire into a coding assistant that should answer questions about the platform. ## Add the docs server ```bash theme={null} claude mcp add --transport http gtm-api-docs https://docs.gtm-api.com/mcp ``` Or in any client's MCP config: ```json theme={null} { "mcpServers": { "gtm-api-docs": { "url": "https://docs.gtm-api.com/mcp" } } } ``` ## Plain-text access * `https://docs.gtm-api.com/llms.txt` indexes the site for AI ingestion; `llms-full.txt` carries the full content. * Append `.md` to any page URL to get that page as Markdown. * The contextual menu on each page copies the page as Markdown or opens it in Claude or ChatGPT. # Quickstart Source: https://docs.gtm-api.com/quickstart Mint an API key and make your first authenticated call. This page takes you from nothing to a successful API response. **Give this to your AI agent** and it will handle the API side while you sign up and log the LinkedIn account in: "Connect to gtm-api: add the MCP connector at `https://mcp.gtm-api.com/mcp` (OAuth) or call the REST API at app.gtm-api.com with my key. Then search my linkedin-accounts, tell me each account's status, and once one reaches active, show what its smart limits allow today. Read-only for now: do not send or post anything." Sign up at [app.gtm-api.com](https://app.gtm-api.com/signup). Your workspace starts on Sandbox, the forever free plan: no card, no time limit. Connect a LinkedIn account you own: it opens in a dedicated anti-detect cloud browser with its own proxy, and the platform starts an initial sync of your inbox, connections and profile. You can make read calls right away. Outbound actions (connection requests, messages) stay blocked with an `invalid_transition` error until the initial sync finishes. In the app, create an API key. The full secret looks like `gtm_live_` followed by 40 characters and is shown exactly once: store it now, because the API only ever returns the prefix and last 4 characters afterwards. If you lose it, rotate the key to get a new secret. Keys can also be minted over the API with [`POST /api/api-keys`](/api-reference/id/api_keys/create-api-key) once you have a first key or a session token to call it with. Search your connected LinkedIn accounts. The key goes in the `Authorization` header as a bearer token. ```bash curl theme={null} curl -X POST "https://app.gtm-api.com/linkedin/v4/api/linkedin-accounts/search" \ -H "Authorization: Bearer gtm_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"page_size": 5}' ``` ```typescript TypeScript theme={null} const res = await fetch( "https://app.gtm-api.com/linkedin/v4/api/linkedin-accounts/search", { method: "POST", headers: { Authorization: `Bearer ${process.env.GTM_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ page_size: 5 }), }, ); const data = await res.json(); ``` ```python Python theme={null} import os, requests res = requests.post( "https://app.gtm-api.com/linkedin/v4/api/linkedin-accounts/search", headers={"Authorization": f"Bearer {os.environ['GTM_API_KEY']}"}, json={"page_size": 5}, ) data = res.json() ``` Every success is the same envelope: `success`, the `operation` shape, and a `meta` block with a `trace_id` for support. Fields below are trimmed for the example. ```json theme={null} { "success": true, "operation": "search", "items": [ { "item": { "sid": "ln_ac_Hx7kQ3mN2pL4", "status": "active", "full_name": "Jane Cooper" }, "included": {} } ], "counts": { "total_count": 1, "groups": { "status": { "active": 1 } } }, "pagination": { "next_cursor": null, "has_more": false, "total_count": 1 }, "applied_filters": {}, "includes": [], "meta": { "trace_id": "0198f2ab-7c11-7e32-9a41-d2b64f2a91c3" } } ``` The `sid` is the account's stable identifier. Every entity in the platform has one, with a type-specific prefix (`ln_ac_` for LinkedIn accounts, `id_ak_` for API keys, `wh_hk_` for webhooks). ## Next steps Key rotation, permissions and the Team-SID header. The response contract and the 16 error codes. Preview, commit and monitor a bulk dispatch. The same tools, callable by an AI agent.