LinkedIn safety and rate limits
The daily and weekly per-sender limits Swarmhit enforces, how to pace your API calls like a human, and exactly how to handle 429, 409 and 401 responses.
Everything in this guide exists to keep sender accounts safe. A restricted LinkedIn account is worth far more than any single day's sends, so Swarmhit enforces limits server-side and expects your integration to add human-like timing on top.
The one rule
Swarmhit applies LinkedIn's recommended daily and weekly limits automatically and caps the number of outstanding invitations. What your flow controls is timing: since your code decides when each call fires, space the calls like a human would. A tight loop, even while under the limits, is what gets accounts restricted. Add the delays and working hours below.
Daily limits (per sender, reset at UTC midnight)
Limits are counted per sender account, not per API key or workspace. The same counters apply whether an action runs through the REST API, the MCP server, or a campaign (in a campaign, if_connected and if_open_profile checks also draw the profile-view cap, and voice notes the message cap).
| Action | Endpoint | MCP tool | Cap (default / max) | Use per day | Hard rule |
|---|---|---|---|---|---|
| Connection request | POST /accounts/{id}/connections | send_connection_request | 25 / 40 | ≤ 20 | also capped over a rolling 7 days (130 for free senders, 150 for paid) and at 500 pending by default; stay near 100/week to be safe |
| Message (1st-degree) | POST /accounts/{id}/messages | send_direct_message | 40 / 40 | ≤ 40 | connections only |
| InMail (non-connection) | POST /accounts/{id}/inmails | send_inmail | 40 / 40 | ≤ 30 | premium senders only; open profiles are free on Sales Navigator and Recruiter (about 800 / month), everything else uses InMail credits |
| Profile view | POST /accounts/{id}/profile-views | visit_profile | 40 / 40 | ≤ 40 | real and visible to the target |
| Follow | POST /accounts/{id}/follows | follow_profile | 30 / 40 | ≤ 30 | |
| Post like | POST /accounts/{id}/post-likes | like_last_post | 30 / 40 | ≤ 30 | the lead's latest post |
| Post comment | POST /accounts/{id}/post-comments | comment_last_post | 15 / 40 | ≤ 15 | posts right away, as written |
| Search import | POST /lead-imports | create_lead_import | 1000 / 2500 | ≤ 1000 | default 2500 on Sales Navigator; LinkedIn also limits searches on free accounts each month |
A sender is paid when its account has isPremium: true or a linkedinPlan of sales_navigator or recruiter, and free otherwise. The defaults apply until someone changes a sender's caps in Swarmhit (its sender limits), up to the max; the weekly limit already sits at its max, so it can only be lowered. The API reads the caps but does not change them: limits on the account holds the caps saved for that sender (null while it runs on the defaults), and GET /accounts/{id}/stats returns every cap in force under limits.daily and limits.rolling, each with its limit and how much is used.
The search-import figures are the daily per-sender import volume. They are separate from the maxLeads parameter on a single import job (default 100, max 2500), covered in Leads, lists and imports. An import that reaches its sender's daily cap is not refused: it stays running and carries on after the next UTC midnight.
A direct action over its cap returns 429 with details: { limit, used, scope, resetAt }, where scope is day, week, or pending. Stop that action for that sender until resetAt.
Reads and withdrawals draw no cap: GET connection status, GET profile lookup, and DELETE (withdraw) are uncapped. Each one still runs on the sender's LinkedIn session (a read loads the profile, a withdrawal is an action), so space them out too and never loop them.
LinkedIn tolerates more on paid accounts (roughly 100 invites/day, 100 to 150 messages/day, 100 profile views/day), but Swarmhit's daily caps on these actions stop at 40 on every plan, and the weekly invite cap is what you will actually hit: 130 a week for free senders, 150 for paid, enforced server-side. Stay conservative (around 100 invites per week); do not chase the daily maximum.
Pace your calls (required)
- 8 to 15 minutes between write actions per sender, randomized (never a fixed interval).
- Working hours only, in the account's local time zone (about 9:00 to 18:00), weekdays.
- Spread the day's volume across 2+ sessions; vary the daily total by ±20%.
- Keep writes under about 5 to 10 per hour.
Campaigns handle this scheduling for you. These rules matter when you drive senders yourself through the direct actions endpoints: Swarmhit runs a direct action the moment you call it. Each one also pushes the sender's next campaign action back by a fresh gap, so a sender you share with campaigns stays paced.
Hygiene
- Pre-check with the uncapped
GETs (connection status, profile lookup) and skip anyone already pending or connected. Fewer wasted sends and409s. Theirpendingstatus only reflects invitations this sender sent through the API or MCP: one sent by a campaign or from LinkedIn itself shows asnot_connected. - Withdraw stale pending invites (3 to 4 weeks old) via
DELETE(uncapped); keep total pending under ~500. Too many open invites is itself a restriction trigger. Swarmhit counts an invitation as pending until it is accepted or withdrawn, so a declined one keeps counting until you withdraw it. A campaign's invite step withdraws its own unaccepted requests (after 25 days by default:withdrawEnabled,withdrawDays); invitations you send through the API are never withdrawn for you. - Re-inviting the same person too soon after a withdraw is rejected; they must wait about 3 weeks.
- Personalize first touches. Invitation notes are a premium sender feature of up to 300 characters (a longer
noteis rejected with400); invites from free senders are sent without the note, and the returned action'snoteisnull(LinkedIn gives free accounts only a handful of note-invites per month). Never send identical templated messages at volume. - Connect each sender with the
countryits owner signs in from, or with your ownproxy, so LinkedIn sees the account in a consistent place. See Sender accounts.
Error handling
| Signal | What to do |
|---|---|
429 with details.scope day / week / pending | Stop that action for that sender until resetAt; for pending, withdraw some invites first. No retry loop. |
429 with code: inmail_credits_exhausted | Stop InMails from that sender until its credits refill. |
429 with no details ("rate-limiting, slow down") | Back off hard and widen your gaps. |
409 (already pending) | Skip and move on. |
401 / 403 | Stop; do not retry. A 401 is your API key; a rejected sender surfaces as 409 or an account webhook. |
422, or 404 with code: profile_not_found | Skip the profile. |
502 | LinkedIn did not answer; retry later, with a new Idempotency-Key. |
429: a sender limit was hit
A 429 with details means the sender hit one of Swarmhit's limits: its daily cap for that action, its weekly connection-request limit (tier-based: 130 invitations over the last 7 days for free sender accounts, 150 for paid), the pending-invitation backlog, or its InMail credits.
{
"error": {
"status": 429,
"message": "Daily limit reached for this action on this account (25/25).",
"details": { "limit": 25, "used": 25, "scope": "day", "resetAt": "2026-06-19T00:00:00.000Z" }
}
}Branch on details.scope:
day: pause that action for that sender untilresetAt, the next 00:00 UTC. Other actions and other senders are unaffected.week: pause connection requests from that sender untilresetAt, when the oldest invitation of the last 7 days leaves the window and frees a slot.pending: there is noresetAt; the backlog clears as invitations are accepted or withdrawn. Withdraw stale invites, then resume.inmail_credits(code: inmail_credits_exhausted): the sender is out of InMail credits. Stop sending InMails from it until the balance refills (visible asinmailCreditson the account).
A 429 without details comes from LinkedIn itself: see "rate-limiting, slow down" below.
Never sit in a retry loop on a 429. Retrying does not free the window and the burst pattern itself endangers the account.
409: a conflict
Branch on error.code where present:
- Invitation already pending to this profile: skip the profile and move on. This is normal attrition, not a failure.
sender_paused: the sender is paused. Either resume it or route the send to another sender.inmail_not_available: the sender has no premium InMail access. Do not retry; InMail needs a premium LinkedIn plan on the sender.- Sender rejected by LinkedIn, or its LinkedIn session is gone: the account may need reconnecting. Stop sending from it and reconnect.
idempotency_in_progress: a request with the sameIdempotency-Keyis still in flight; retry shortly with the same key.idempotency_failed: the original errored after it may have taken effect; retry with a new key.
401 and 403: stop and alert
A 401 means the bearer key is missing, invalid or revoked, or the X-Swarmhit-Workspace header names a workspace the key cannot act for. A 403 means the workspace is not allowed to perform the action. Neither resolves on retry: stop the integration and alert a human.
A sender that loses its LinkedIn session is a separate case. Subscribe to the account.status_changed and account.checkpoint_required webhook events so you learn about it immediately, stop all sends from that sender, and reconnect it before resuming.
422 and 404: skip the profile
A 422 means LinkedIn could not complete the action on this target (cannot invite, already connected, the recipient cannot be messaged by this sender, or, for likes and comments, the lead has no recent post). A 404 with code: profile_not_found means the profile URL matched nothing. Both are per-profile outcomes: log them, skip the profile, and never retry them in a loop. A 404 without a code means the sender id is not in this workspace.
502: LinkedIn did not answer
The action may or may not have gone through. Retry later rather than at once. If you sent an Idempotency-Key, the retry needs a new one: the old key now answers 409 idempotency_failed.
"rate-limiting, slow down"
When LinkedIn itself throttles the sender, the call fails with a 429 that has no details and a message telling you to slow down. Treat it as a serious signal even when you are under every documented cap. Back off hard and widen your gaps.
Example: a safe send loop
Use an Idempotency-Key on every send so a timeout retry can never double-send, and check connection status first.
# Free pre-check: skip anyone already pending or connected
curl "https://app.swarmhit.com/api/v1/accounts/ACCOUNT_ID/connections/john-doe" \
-H "Authorization: Bearer swh_live_xxxxxxxxxxxxxxxxxxxxxxxx"
# Send, spaced 8 to 15 minutes after the sender's previous write
curl -X POST "https://app.swarmhit.com/api/v1/accounts/ACCOUNT_ID/connections" \
-H "Authorization: Bearer swh_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 8b1f2c3d-a4e5-4f60-9b71-2c3d4e5f6a7b" \
-d '{
"profileUrl": "https://www.linkedin.com/in/john-doe",
"note": "Hi John, enjoyed your post on outbound benchmarks. Would be glad to connect."
}'On 429, persist resetAt per sender and action, and gate future sends on it locally instead of rediscovering the limit with failed calls.