Account
The account commands manage LinkedIn account connections: linking new accounts, re-authenticating an existing one in place, resolving the verification checkpoints LinkedIn issues during sign-in, and managing already-connected accounts. All account commands are root-scoped: they operate on your workspace and do not require --account.
Before you start. You need an API key (CURVIATE_API_KEY, or curviate login). This is the one command group that does not require an already-connected LinkedIn account; it is how you get one. Every other command group does, so start here.
Changed in 0.15.0:
account connect-link,account reconnect-link, andaccount reconnectare removed; the hosted-link flow has no v2 endpoint. In-place re-authorization now lives onaccount linkitself, behind--account-id(see Reconnecting an account).account connect-session pollis retained, but its--sessionvalue changed meaning: it is now theaccount_idreturned byaccount link's checkpoint response (a plain status poll), not a separate hosted-link session id.
account list and get
# List all connected accounts (paginated)
curviate account list [--limit <n>] [--cursor <token>] [--all] [--max-pages <n>]# Get a single account by ID (verbatim account ID, not a LinkedIn identifier)
curviate account get <account_id>account get returns the account's current status, its quotas (one entry per tracked quota family, with remaining/total/recommended_throttle_hint), and, with --verbose, the cached enrichment fields (username, premium_id, public_identifier, signatures, groups). Those five enrichment fields are best-effort: for accounts connected going forward they may be null or empty, so treat them as optional. full_name and substrate_created_at remain populated.
Account status is one of active, reconnect_needed, restricted, connecting, or disconnected.
account link
account link does two jobs, decided by whether you pass --account-id:
- omit
--account-id: connect a new LinkedIn account into an empty seat. - pass
--account-id <acc_...>: re-authenticate that existing account in place, keeping its id, its seat, its webhooks, and its history. See Reconnecting an account.
--seat-id and --auth-method are always required by the CLI. On a reconnect the server ignores --seat-id (the account keeps the seat it already holds), but the CLI still asks for it, so pass the account's current seat.
Where to get a seat ID. Open Billing (/billing) in the dashboard and look at the seat table. Every seat you own is listed under Seat ID as a seat_... chip; click a chip to copy the full ID. For a new connect, pick a seat whose Attached account column reads Empty; if none does, use Add Seats on the same page first.
# Link a new LinkedIn account (write)
curviate account link \
--seat-id <seat_id> \
--auth-method <credentials|cookie> \
[--account-id <acc_...>] \
[--email <email>] \
[--password <password>] \
[--password-stdin] \
[--li-at <cookie>] \
[--li-at-stdin] \
[--li-a <cookie>] \
[--user-agent <ua>] \
[--country <code>] \
[--ip <address>] \
[--proxy-protocol <http|https|socks5>] \
[--proxy-host <host>] \
[--proxy-port <port>] \
[--proxy-username <user>] \
[--proxy-password <pass>] \
[--recruiter-contract-id <id>] \
[--no-interactive]Cookie auth requires --user-agent. When --auth-method cookie, pass the exact browser User-Agent the li_at cookie was captured with; cookie sessions are pinned to it. Omitting --user-agent on a cookie link fails with INVALID_REQUEST. --user-agent stays optional for credentials auth.
Prefer --password-stdin / --li-at-stdin (or the CURVIATE_LINKEDIN_PASSWORD / CURVIATE_LINKEDIN_LI_AT environment variables) over passing a secret on the command line, where it is visible to other processes and saved in shell history.
On success the command prints the connected account:
{ "object": "account", "account_id": "acc_...", "status": "active" }If LinkedIn issues a verification challenge, the response is a checkpoint instead of an account:
{
"object": "checkpoint",
"status": "checkpoint_required",
"account_id": "acc_...",
"challenge_type": "two_factor_app",
"expires_at": "2026-06-08T10:10:00Z"
}On an interactive terminal, account link then guides you through the checkpoint in-process, prompting for the code or waiting out a mobile-app approval, and exits once the account connects. Pass --no-interactive to always render the checkpoint envelope and exit instead of prompting, even on a TTY. On a non-interactive session it prints the checkpoint envelope and exits 12 so a script can resolve it later with the checkpoint commands below. The account_id in the checkpoint is the handle you pass to those commands.
If the account is already connected, account link returns ACCOUNT_ALREADY_LINKED (a LinkedIn identity maps to one account per workspace). When your workspace already owns that account, the error names your own account_id. A checkpoint resolution that turns out to reactivate an account you had previously disconnected returns that account with "recovered": true.
account connect-session poll
Poll the status of an in-progress connect. It is a pure status read that makes no external call and does not itself resolve a checkpoint. --session takes the account_id from account link's checkpoint (202) response, not a separately-minted session id.
# Single poll (prints the session status, exits 0 regardless of status)
curviate account connect-session poll --session <account_id>
# Block until the session resolves (or the wait window elapses)
curviate account connect-session poll --session <account_id> --wait [--timeout <ms>]status is checkpoint_required, done, expired, or failed; account_id is populated only once status is done. Use this as a lightweight way to check on a link attempt without solving the checkpoint yourself. To actually resolve a pending checkpoint, use the checkpoint commands below.
account update and disconnect
# Update an account's metadata / proxy configuration (write, --preview accepted)
curviate account update <account_id> \
[--metadata <json>] \
[--proxy-protocol <http|https|socks5>] \
[--proxy-host <host>] \
[--proxy-port <port>] \
[--proxy-username <user>] \
[--proxy-password <pass>] \
[--clear-proxy]--metadata sets a flat key/value map of your own tags on the account and replaces the stored map wholesale (keys you omit are removed); pass it as a JSON object, e.g. --metadata '{"team":"growth","region":"eu"}'. The --proxy-* flags set a custom proxy; --clear-proxy removes a custom proxy and reverts to automatic proxy protection (mutually exclusive with --proxy-host). A managed location is set only at connect time (on account link); account update does not accept --country or --ip.
# Permanently disconnect an account (write, --preview accepted)
curviate account disconnect <account_id>Checkpoint flow
When account link returns a checkpoint_required status, LinkedIn is waiting for a verification code or a mobile-app approval. The checkpoint commands are account-scoped: pass the account_id from the 202 response as a positional argument:
# Solve a code-based checkpoint (OTP / 2FA) (write)
curviate account checkpoint solve <account_id> --code <code># Re-request the verification notification (e.g. the user never got the code) (write)
curviate account checkpoint request <account_id># Poll a mobile-app-approval checkpoint (write)
curviate account checkpoint poll <account_id> [--wait] [--timeout <ms>]solvesubmits a code and, on success, returns the connectedaccount. A chained challenge (for example OTP then 2FA) returns anothercheckpoint_required; solve the next code at the same account. A wrong code returnsCHECKPOINT_INVALID_CODE.requestre-issues the challenge notification. Itsresentfield is honest:resent: falsemeans there was nothing to re-send (for example atwo_factor_appcode is generated on the device), not a failure. It does not extend the checkpoint's expiry.pollis formobile_app_approvalcheckpoints, where the user approves the sign-in on their phone. While pending it returns{ "object": "checkpoint", "status": "pending", "expires_at": "..." }; once approved it returns the connectedaccount. Add--waitto block until it resolves. A code-based checkpoint returns422here, so usesolveinstead.
The mobile-approval window is short (a few minutes; poll until the returned expires_at). If it elapses before the user approves, the checkpoint reaches a terminal expired state with a recovery_hint explaining the next step:
{
"object": "checkpoint",
"status": "expired",
"challenge_type": "mobile_app_approval",
"recovery_hint": "This sign-in wasn't completed in time. If you approved it in the LinkedIn app, this identity may already be connected; try Reconnect, sign in with the cookie method, or check your connected accounts."
}Reconnecting an account
When an account's status turns reconnect_needed or disconnected, LinkedIn has invalidated the stored session. Re-authenticate it in place with account link --account-id. Do not disconnect and re-link: that destroys the account record, releases its seat, and orphans anything bound to the old id.
# 1. Find the account that needs attention
curviate account list --json --fields items.account_id,items.status
# => {"account_id":"acc_01J8Z3K9P0Q1R2S3T4V5W6X7Y8","status":"reconnect_needed"}
# 2. Read its current seat (the CLI still requires --seat-id; the API ignores it on a reconnect)
curviate account get acc_01J8Z3K9P0Q1R2S3T4V5W6X7Y8 --json
# 3. Re-authenticate that same account id in place
curviate account link \
--account-id acc_01J8Z3K9P0Q1R2S3T4V5W6X7Y8 \
--seat-id seat_abc123 \
--auth-method cookie \
--li-at-stdin <<< "$LI_AT" \
--user-agent "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
# => { "object": "account", "account_id": "acc_01J8Z3K9P0Q1R2S3T4V5W6X7Y8", "status": "active" }A reconnect can raise a checkpoint just like a fresh link. If it does, resolve it with the same account checkpoint commands above, using the account_id from the response.
The account keeps its id, its seat, its metadata, and its webhook subscriptions. Nothing downstream has to change.
--account-idis what makes it a reconnect. Omit it and the same command opens a brand-new connection into--seat-idinstead, which is not what you want for an account you already have. If the identity you are linking is already connected to this workspace,account linkreturnsACCOUNT_ALREADY_LINKEDnaming your existingaccount_id; pass that id back as--account-idto reconnect it. A checkpoint resolution that reactivates an account you had previously disconnected returns"recovered": true.
Challenge types
The challenge_type on a 202 checkpoint is one of otp, two_factor_sms, two_factor_app, two_factor_whatsapp, mobile_app_approval, otp_or_mobile_app_approval (the user may either enter a code or approve in the app), or contract_selection (a Recruiter or Sales Navigator seat picker; the response also carries contracts with { id, name } entries, and you solve with the chosen id). Two challenges cannot be resolved automatically, a CAPTCHA and a phone-number registration; those return 422 with a machine-readable challenge_type of captcha or phone_register.
Examples
List connected accounts
curviate account listLink an account with credentials
curviate account link \
--seat-id seat_abc123 \
--auth-method credentials \
--email user@example.com \
--password-stdin <<< "$LINKEDIN_PASSWORD"Link an account with a cookie
# Cookie auth requires --user-agent (the browser the li_at was captured with)
curviate account link \
--seat-id seat_abc123 \
--auth-method cookie \
--li-at-stdin <<< "$LI_AT" \
--user-agent "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"Complete an OTP checkpoint manually
# 1. A non-interactive link returns a checkpoint and the account_id to resolve it
curviate account link --seat-id seat_abc123 --auth-method credentials \
--email user@example.com --password-stdin <<< "$LINKEDIN_PASSWORD" --no-interactive
# => { "object": "checkpoint", "status": "checkpoint_required", "account_id": "acc_X", "challenge_type": "otp", "expires_at": "..." }
# 2. Solve the code from the user's email or authenticator app
curviate account checkpoint solve acc_X --code 123456
# => { "object": "account", "account_id": "acc_X", "status": "active" }
# 3. If the user never received the code, re-request the notification
curviate account checkpoint request acc_XWait out a mobile-app-approval checkpoint
# The user approves the sign-in on their phone; --wait blocks until it resolves
curviate account checkpoint poll acc_X --waitPoll a link attempt's status without solving it
curviate account connect-session poll --session acc_X --waitPreview a disconnect
# See what would be sent without actually disconnecting
curviate account disconnect acc_YOUR_ACCOUNT_ID --previewErrors you may hit
| Code | Exit | Cause | Fix |
|---|---|---|---|
UNAUTHORIZED | 3 | Missing or invalid API key. | Set CURVIATE_API_KEY, or run curviate login. |
ACCOUNT_NOT_FOUND | 4 | The acc_... id is not owned by this API key. | Re-read it from curviate account list. |
SEAT_NOT_FOUND | 4 | The --seat-id does not exist, or is not yours. | Copy it from the seat table on the dashboard's Billing page. |
ACCOUNT_ALREADY_LINKED | 8 | That LinkedIn identity is already connected to this workspace. | Pass the id it names as --account-id to reconnect it instead. |
CONNECTION_IN_PROGRESS | 8 | A connection attempt for this LinkedIn account is already open. | Wait for it to finish or expire before starting another. |
CHECKPOINT_INVALID_CODE | 9 | Wrong verification code. | Re-read the code, or run account checkpoint request <account_id>. |
CHECKPOINT_EXPIRED | 9 | The checkpoint window elapsed. | Start the link again. |
PAYMENT_REQUIRED | 11 | No paid or trial seat is available. | Use Add Seats on the dashboard's Billing page. |
INVALID_REQUEST | 2 | A missing or malformed argument, most often --user-agent on a cookie link. | Read the message on stderr, it names the field. |
Full envelope shapes are in the error reference.