Sender accounts
Connect, monitor, pause and reconnect the LinkedIn accounts that execute your outreach.
A sender account is a connected LinkedIn account that Swarmhit acts from. Campaign steps, direct actions and inbox conversations all execute through a sender. The Accounts endpoints let you connect senders, watch their health, daily limits and usage, pause them, and recover them when LinkedIn interrupts a session.
Credentials are stored encrypted and are never returned by any endpoint. Each connected sender occupies a seat in its workspace: with an organization key and the X-Swarmhit-Workspace header set, the sender lands in that workspace and consumes a seat from its allocation (see Organization).
Hosted connect links
If your users connect their own LinkedIn accounts, you do not have to ask them for a LinkedIn password. POST /accounts/hosted-link mints a one-time URL where they sign in on a page Swarmhit serves, carrying your organization's name and logo. Send them to it, and they come back to your return URL when they are done.
Your workspace name and picture are used on the page by default. Override them, and set a default return URL, in your organization settings — so most calls only need to say which workspace the sender belongs to. Pass redirectUrl when a particular link should land somewhere else.
curl -X POST https://app.swarmhit.com/api/v1/accounts/hosted-link \
-H "Authorization: Bearer swh_live_..." \
-H "X-Swarmhit-Workspace: wsp_customer_42" \
-H "Content-Type: application/json" \
-d '{
"redirectUrl": "https://yourapp.com/settings/linkedin/done",
"externalId": "your-user-42"
}'{
"data": {
"id": "hlk_9f2...",
"status": "pending",
"url": "https://app.swarmhit.com/connect/nmkRnAG...",
"expiresAt": "2026-08-20T09:17:23.933Z"
}
}url is returned once, here: the token inside it is stored only as a hash and never comes back on a read. Links are single use and expire after 30 minutes by default (expiresIn, in seconds, between 300 and 604800).
redirectUrl is optional when your organization has a default set; without either, the call fails with 400 and code: invalid-redirect-url.
The seat is checked when the link is created, so a workspace with no seat left fails at this call rather than at the end of your user's sign-in.
Proxy and country
Pass proxy ({ host, port, username?, password? }) and country when creating the link. They describe the session the sender is connected with, and they belong on the link rather than on the page: the person signing in is your customer, a proxy form would be meaningless to them, and routing a sender near where its owner actually is is a judgement only you can make. The proxy password is stored encrypted and never returned — reads only tell you hasProxy. Setting country also stops the page asking the person for one.
Both apply to password sign-ins. A browser-session (liAt) connect carries the origin of the browser it came from, so it ignores them.
If your own users run their own proxies, you can instead let them enter one: turn on Let people use their own proxy in your organization settings and the connect page gains an optional proxy panel. It is off by default, because most people signing in would not know what to put in it. A link created with its own proxy always wins and hides the panel.
Knowing how it went
Three ways, and you can use whichever fits:
- The redirect. They come back to
redirectUrlwith?hosted_link_id=…&status=connected|failed|expired|cancelled&account_id=…&external_id=…. - Webhooks.
account.connectedandaccount.connection_failedfire as usual, withhostedLinkIdandexternalIdadded so you can match them to the link you created. - Polling.
GET /accounts/hosted-link/{id}returns the link's currentstatus, theaccountIdit produced, and anerrorwhen it failed.
Verification steps (authenticator code, email or SMS code, phone registration, in-app approval, captcha) are all handled on the page itself. You never drive the checkpoint endpoints for a hosted session; they remain for senders you connect directly with POST /accounts.
Reconnecting through a link
Pass accountId and the link repairs that sender instead of adding one. It keeps its id, its campaigns and its seat, and no seat is consumed:
curl -X POST https://app.swarmhit.com/api/v1/accounts/hosted-link \
-H "Authorization: Bearer swh_live_..." \
-H "Content-Type: application/json" \
-d '{
"redirectUrl": "https://yourapp.com/settings/linkedin/done",
"accountId": "aCc123"
}'On the page, users can also supply their authenticator setup key under "Keep it connected automatically". Doing so makes the sender an infinite-login one, which signs itself back in whenever LinkedIn ends the session — so nobody has to be asked for anything again. It is the outcome worth steering your users to.
Connecting a sender
POST /accounts takes the sender's LinkedIn username (email) and password, plus an optional proxy and 2-letter country. Use it when you already hold the credentials; otherwise prefer a hosted connect link. There are two modes:
- Automatic (infinite) login: include a
totpSecret(the account's 2FA authenticator setup key). The sign-in completes hands-free, solving a 2FA challenge from the secret, and the sender reconnects on its own later. Any other verification step fails with400and anaccount.connection_failedwebhook rather than leaving a half-connected sender holding a seat. - Interactive: omit
totpSecret. If LinkedIn asks for a verification step, the response is200withstatus: "checkpoint_required"and acheckpointobject describing it.
curl -X POST https://app.swarmhit.com/api/v1/accounts \
-H "Authorization: Bearer swh_live_..." \
-H "Content-Type: application/json" \
-d '{
"username": "jane@example.com",
"password": "her-linkedin-password",
"totpSecret": "JBSWY3DPEHPK3PXP",
"country": "US"
}'Connecting from a browser session
A sender can also be connected from a LinkedIn session that already exists in a browser: send liAt (the li_at cookie value) and the userAgent of the browser it came from, instead of username and password. The session is stored encrypted, connectionMethod comes back as cookie, and checkpoints and webhooks behave exactly the same.
curl -X POST https://app.swarmhit.com/api/v1/accounts \
-H "Authorization: Bearer swh_live_..." \
-H "Content-Type: application/json" \
-d '{
"liAt": "AQEDAT...",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ..."
}'A browser session cannot re-establish itself: when LinkedIn ends it, only a fresh li_at brings the sender back. These senders never reconnect on their own, so password sign-in with a totpSecret — or a hosted connect link — is the lower-maintenance path.
Connection failures carry a stable error.code to branch on (invalid-credentials, rate-limited, proxy-failed, provider-timeout, provider-unreachable, checkpoint-restricted, already-connected, no-plan, seat-limit-reached, and more). The full list is on the connect reference; the same code is included on the account.connection_failed webhook.
Sign-in checkpoints
A checkpoint is a verification step LinkedIn requires before it finishes signing the sender in. The checkpoint object tells you what kind and what answers it:
| Type | What LinkedIn wants | input |
|---|---|---|
2FA | A code from the account's authenticator app | code |
OTP | A one-time code sent by email or SMS | code |
IN_APP_VALIDATION | Approval in the LinkedIn mobile app | none |
PHONE_REGISTER | A phone number to register | phone |
CAPTCHA | A puzzle only LinkedIn's own page can show | none |
Answer with POST /accounts/{id}/checkpoint: send the verification code, the phone number with its dialling code in brackets (for example (+33)0612345678), or the literal TRY_ANOTHER_WAY to make LinkedIn offer a different method (the way out of an in-app approval nobody can reach). The response is status: "connected" once the sender is live, or checkpoint_required with the next checkpoint when LinkedIn chains another step.
Checkpoints expire five minutes after LinkedIn asks (expiresAt on the checkpoint object). Past that window the attempt is dropped and answering returns 400 with code: checkpoint_expired; reconnect the sender to start a fresh attempt.
IN_APP_VALIDATION and CAPTCHA are completed outside the API, so poll GET /accounts/{id}/checkpoint (or listen for account.status_changed): it re-reads the live session and returns resolved: true as soon as the sender is up. POST /accounts/{id}/checkpoint/resend asks LinkedIn for a fresh code or a new approval push when the pending type supports it (canResend on the checkpoint).
Status and health
GET /accounts and GET /accounts/{id} return each sender with its status, a more granular connectionStatus string, limits, usage, pause state, InMail credit balance, sign-in country and cached Social Selling Index.
status | Meaning | What to do |
|---|---|---|
active | The sender is signed in and available for outreach. | Nothing. |
pending | The sign-in has not completed yet (for example, a checkpoint is waiting). | Answer or poll the checkpoint. |
error | The LinkedIn session broke. | Reconnect the sender. |
suspended | LinkedIn has restricted the account. | Clear the restriction on linkedin.com, then connect it again. |
The account.status_changed webhook reports transitions between these states and can also report disconnected when the session is gone.
When a sender needs reconnecting
Send endpoints return 409 when the sending account was rejected by LinkedIn and may need reconnecting. When that happens, retry the saved sign-in with POST /accounts/{id}/reconnect. For a credentials or infinite-login sender it runs one attempt now. If the person changed their LinkedIn password or 2FA, pass the new password (and totpSecret) to replace the stored credentials first:
curl -X POST https://app.swarmhit.com/api/v1/accounts/aCc123/reconnect \
-H "Authorization: Bearer swh_live_..." \
-H "Content-Type: application/json" \
-d '{ "password": "the-new-password" }'The response status is reconnected, checkpoint_required (answer the returned checkpoint as above; a stored authenticator secret clears a 2FA challenge on its own) or failed. Either way the sender keeps its id, its campaigns and its seat.
For a cookie sender there is no saved password to retry, so pass a fresh liAt (and the userAgent it came from) to replace the browser session. Without one the call returns 400 with code: not-supported:
curl -X POST https://app.swarmhit.com/api/v1/accounts/aCc123/reconnect \
-H "Authorization: Bearer swh_live_..." \
-H "Content-Type: application/json" \
-d '{ "liAt": "AQEDAT...", "userAgent": "Mozilla/5.0 ..." }'When you do not hold the person's credentials at all, create a hosted connect link scoped to the sender and let them sign in themselves.
Reconnect also recovers a sender whose session no longer exists at all — for example one that was parked while the workspace had no active plan. As long as the stored credentials are intact it simply signs in fresh; this needs an available sender seat on the plan, and returns 400 when there is none.
If the account is up but its stored identity (name, headline, picture, paid-plan flag) looks stale, POST /accounts/{id}/sync refreshes it from LinkedIn.
Limits and usage
Each account carries two objects on the API:
limits: the sender's per-day sending caps.usage: today's action tally against those caps.
When an action would exceed a cap, the endpoint returns 429 with details: { limit, used, scope, resetAt }, where scope is day (the daily cap), week (the weekly connection-request limit: about 130 invites per week for free sender accounts, 150 for paid) or pending (the pending-invitation backlog, which clears as invitations are accepted or withdrawn). resetAt is the UTC time the window frees; it is absent for pending.
When invite limits park sends, the account's cooldownUntil is stamped with the time the cooldown ends (cooldownReason says why); it is null when no cooldown is active.
Sender tier also shapes what an action can include: connection-request notes are a premium feature capped at 300 characters, and free senders' invites go out without the note. See LinkedIn safety for how to stay within healthy volumes.
Pausing and resuming
POST /accounts/{id}/pause stops all activity from a sender: campaigns stop acting from it, and direct actions are rejected with 409 code: sender_paused until it is resumed with POST /accounts/{id}/resume. Both are no-ops when the sender is already in the target state, and both still return the account.
curl -X POST https://app.swarmhit.com/api/v1/accounts/aCc123/pause \
-H "Authorization: Bearer swh_live_..."On the account, paused is true, pausedAt is when it happened, and pausedReason says why:
user: paused manually (or via this endpoint).inmail_credits: auto-paused because its InMails could not be sent. Swarmhit resumes it automatically once InMail credits are available again (theaccount.resumedwebhook fires withby: system).
A pause does not change the sender's connection status, and its campaign leads stay assigned to it. To keep a campaign moving while a sender is down, move its not-yet-contacted leads to the rest of the pool (see rebalancing below).
InMail credits
inmailCredits on the account is the InMail credit balance of the sender's LinkedIn plan: balance (or null when unknown) and syncedAt. It is null when it has never been synced. Sending an InMail from a sender without premium InMail access fails with 409 code: inmail_not_available; a sender that is out of credits fails with 429 code: inmail_credits_exhausted (details.scope is inmail_credits).
Social Selling Index
LinkedIn scores every member 0 to 100 on how well their profile sells socially. The score is made of four pillars worth up to 25 each, and LinkedIn also reports where the profile sits against its industry and its own network.
GET /accounts/{id}/ssi returns it, and the same object rides along as ssi on the account itself:
curl https://app.swarmhit.com/api/v1/accounts/aCc123/ssi \
-H "Authorization: Bearer swh_live_..."{
"data": {
"score": 62.4,
"brand": 17.1,
"people": 14.8,
"insights": 12.2,
"relationships": 18.3,
"industryTop": 8,
"networkTop": 3,
"industryName": "Computer Software",
"unavailable": false,
"syncedAt": "2026-08-04T09:12:44.120Z"
}
}| Field | Pillar |
|---|---|
brand | Establish your professional brand |
people | Find the right people |
insights | Engage with insights |
relationships | Build relationships |
industryTop and networkTop are percentiles: 8 means the profile is in the top 8% of its industry.
The score is cached on the account and refetched once it goes stale, since LinkedIn recomputes it daily at most. Pass refresh=true to force a live read. It is also refreshed as part of POST /accounts/{id}/sync.
unavailable: true means LinkedIn is not exposing a score for that sender's session, and every number is null. That is a state, not an error: the endpoint still returns 200. Treat it the same way as an unsynced InMail balance rather than retrying.
Sender performance
GET /accounts/{id}/stats reports how a sender has actually performed, over a window of 7 to 90 days (days, default 30). Campaign sends and direct actions are both counted, so an API-driven workspace sees its full volume.
curl "https://app.swarmhit.com/api/v1/accounts/aCc123/stats?days=30" \
-H "Authorization: Bearer swh_live_..."The response carries:
cards: connection requests, acceptances, messages, replies and interested leads for the window, each withdeltaPctagainst the preceding window of the same length.series: one entry per calendar day, for charting.lifetimeandrates: all-time totals, acceptance rate and reply rate.byCampaignandpoolCampaigns: the per-campaign split, plus campaigns the sender is pooled into but has not sent from yet.actions: direct (non-campaign) actions in the window, by type.limits: every cap in force with how much is already used, both the daily caps and the rolling weekly ones.
limits is the endpoint to poll before a large batch: it tells you the remaining headroom on each action without having to hit a 429 first.
Daily series buckets are calendar days on the server's clock, while the daily caps in limits.daily reset at 00:00 UTC. The two can disagree by a few hours near midnight.
Senders and campaigns
A campaign's linkedinAccountIds is its sender pool: the LinkedIn sender accounts eligible to run its leads. Each lead is stickily assigned to one of these on its first action tick (the executor picks the sender with the most remaining daily quota for the upcoming step), and once the lead is contacted the sender is pinned: every subsequent step runs from it. LeadInCampaign.linkedinAccountId exposes the assignment (null until a sender is assigned). Add multiple ids to scale outreach beyond a single account's daily cap.
Editing the pool with PATCH /campaigns/{id}:
- An empty
linkedinAccountIdsis rejected with400while the campaign is (or is being set)active; pause it first. On a paused campaign it clears the pool and leads wait until a sender is added. - Removing a sender reassigns its in-flight leads to the remaining pool on their next run.
Rebalancing a paused or failed sender's leads
POST /campaigns/{id}/senders/{accountId}/rebalance moves a paused or failed sender's not-yet-contacted leads in that campaign onto the campaign's other available senders, evenly. Leads the sender already contacted stay pinned to it. A moved lead's heldBy is cleared and re-evaluated against its new sender on the next run; pausing a sender account does not release holds, because they are recorded against the membership rather than the account.
curl -X POST https://app.swarmhit.com/api/v1/campaigns/cMp456/senders/aCc123/rebalance \
-H "Authorization: Bearer swh_live_..."The response reports how many leads moved and how many receivers took them. receivers: 0 means no other sender was available and nothing moved (still 200); 422 means the account is not in that campaign's sender pool.
Disconnecting
DELETE /accounts/{id} revokes the sender's session, removes it from the workspace and frees its seat. It is permanent: reconnecting later means connecting the sender again from scratch. Note that deleting a workspace from an organization is rejected with 400 while it still has connected senders; disconnect them first.
Mailboxes
A mailbox is the email counterpart of a sender: the address a campaign's email steps send from, and where replies land. Mailboxes are connected from the app (Google with an app password, Microsoft 365, or any IMAP/SMTP account), never over the API, because connecting needs a password on the wire. Once connected they are managed here, with the same verbs as senders. Mailboxes take no seat: a seat covers one LinkedIn sender, and a workspace on a paid plan can connect as many mailboxes as it needs.
GET /email-accounts lists them with their status (pending while connecting, active, error when the sign-in broke, suspended), pause state, daily limits and usage, the tracking settings trackOpens and trackClicks, the sending domain's dnsHealth (SPF, DKIM and DMARC, each ok, missing, invalid, weak or unknown, re-read daily), and the counters (sent, bounced, complained, opened, clicked). Credentials are never returned. Tracking is set in the app, in the mailbox settings.
curl https://app.swarmhit.com/api/v1/email-accounts \
-H "Authorization: Bearer swh_live_..."POST /email-accounts/{id}/pauseand.../resumework like the sender pair: campaigns stop emailing from a paused mailbox, its leads stay assigned, and both emitaccount.paused/account.resumedwithchannel: "email".DELETE /email-accounts/{id}removes the mailbox and its threads, takes it out of every campaign pool and moves the leads it was emailing onto the remaining mailboxes.POST /email-accounts/{id}/check-dnsre-reads the domain's records now, after you change DNS. See Email deliverability for what each record does.POST /email-accounts/{id}/deliverability-testsends one email to our test inbox; the mailbox'sdeliverabilityTestthen reports where it landed and what the receiving server saw.GET /email-accounts/{id}/statsreports emails sent, replies and bounces over a window, plus opens and clicks for tracked emails, with the per-campaign split.
Writes need the email channel enabled for the workspace (403 otherwise). A lead whose address bounced hard or reported spam carries emailStatus (bounced / complained) and stops at any email step it reaches from then on; see Email deliverability.
Reacting to account events
Account lifecycle changes are pushed as webhooks: account.connected, account.connection_failed, account.status_changed, account.checkpoint_required, account.paused and account.resumed. Subscribing to account.status_changed and account.checkpoint_required is the reliable way to catch a sender going down and bring it back without polling. Mailboxes emit the same events with channel: "email" and the mailbox object in data.account.
Reference
List sender accounts
Health, daily limits and usage, pause state, cooldowns and InMail credits for every sender.
Connect a sender
Sign a LinkedIn account in, optionally hands-free with a 2FA setup key.
Reconnect a sender
Retry the saved sign-in after a session breaks, with optional new credentials.
Social Selling Index
LinkedIn's 0-100 score for a sender, its four pillars and its industry and network percentiles.
Sender performance
Sends, acceptances, replies, the per-campaign split and remaining limit headroom over a date range.
Rebalance a sender's leads
Move a paused or failed sender's uncontacted campaign leads onto the rest of the pool.
List mailboxes
Status, pause state, daily limits and gateway counters for every connected mailbox.
Mailbox performance
Emails sent, replies and bounces over a date range, with the per-campaign split.