Direct actions

Send connection requests, messages, InMails and other one-off LinkedIn actions from a sender account, outside campaigns, with idempotent retries and an approvals queue.

Direct actions drive a connected sender account one action at a time: connection requests, messages, InMails, profile visits, follows, post likes, post comments and connection lookups, all outside a campaign. They are the building blocks for custom outreach tooling: your own scheduler, a CRM-triggered follow-up, a one-off send from an internal dashboard. If you want Swarmhit to run a multi-step sequence for you, use Campaigns instead.

Every direct action runs from one sender account, so the endpoints live under /accounts/{id}/.... Get the account id from List accounts. A paused sender rejects direct sends with 409 code: sender_paused; resume it first (see Sender accounts).

Each write counts against the sender's daily cap for that action type, and connection requests also count against a weekly limit. Read LinkedIn safety before building on these endpoints.

Endpoints

ActionMethod and pathReference
Send a connection requestPOST /accounts/{id}/connectionssendConnectionRequest
Withdraw a pending requestDELETE /accounts/{id}/connections/{identifier}withdrawConnectionRequest
Message a connectionPOST /accounts/{id}/messagessendDirectMessage
Send an InMailPOST /accounts/{id}/inmailssendInmail
Visit a profilePOST /accounts/{id}/profile-viewsvisitProfile
Follow a leadPOST /accounts/{id}/followsfollowProfile
Like the lead's last postPOST /accounts/{id}/post-likeslikeLastPost
Comment on the lead's last postPOST /accounts/{id}/post-commentscommentLastPost
Check connection statusGET /accounts/{id}/connections/{identifier}checkConnection
Look up a profileGET /accounts/{id}/profiles/{identifier}lookupProfile
List direct actionsGET /accounts/{id}/actionslistOutreachActions

The two GET endpoints are silent reads: no profile view is registered on LinkedIn and no cap is used.

How targets are resolved

Write endpoints take the target as a profileUrl in the JSON body. Swarmhit resolves the URL to a live LinkedIn profile before acting; if it matches no profile, the request fails with 404 and code: profile_not_found (distinct from a plain 404, which means the Swarmhit resource in the path does not exist).

Read endpoints take the profile's public identifier in the path instead: the username in linkedin.com/in/{identifier}, for example john-doe.

On sendDirectMessage and sendInmail you can also pass providerId, the recipient's stable LinkedIn id from a connection.accepted or message.received webhook payload. When supplied, the send skips the profile lookup, which is the slow round-trip on this path. Store it when the webhook arrives and reuse it for follow-up sends.

Sending an invite

curl -X POST "https://app.swarmhit.com/api/v1/accounts/ACCOUNT_ID/connections" \
  -H "Authorization: Bearer swh_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 5f0c2a4e-9d3b-4c1a-8e7f-2b6d9a1c3e5f" \
  -d '{
    "profileUrl": "https://www.linkedin.com/in/john-doe",
    "note": "Hi John, enjoyed your post on onboarding flows. Would love to connect."
  }'

The response data is an action object with a status of pending, sent, accepted, withdrawn or failed. The profile is tracked as a lead in your workspace, and a connection.accepted webhook fires when the invitation is accepted.

Notes are a premium feature: up to 300 characters (longer notes are trimmed), and free sender accounts cannot include one, so their invites go out without the note rather than failing. If an invitation to the profile is already pending, the send returns 409. To cancel a pending request, call withdrawConnectionRequest; it is safe to call when nothing is pending and returns { withdrawn, alreadyGone }.

Messages and InMails

sendDirectMessage messages a profile the sender is already connected to. To reply inside an existing conversation, use the Inbox endpoint (sendChatMessage) instead.

curl -X POST "https://app.swarmhit.com/api/v1/accounts/ACCOUNT_ID/messages" \
  -H "Authorization: Bearer swh_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 8a1d3f5b-2c4e-4a6d-9b8c-7e5f1a3d2c4b" \
  -d '{
    "profileUrl": "https://www.linkedin.com/in/john-doe",
    "providerId": "PROVIDER_ID_FROM_WEBHOOK",
    "text": "Thanks for connecting, John!"
  }'

sendInmail reaches non-connections, and requires a premium LinkedIn plan on the sender (free senders cannot send any; you get 409 code: inmail_not_available). Sales Navigator and Recruiter senders InMail open profiles free up to a monthly allowance (about 800 per month, plan-dependent); every other send, including classic Premium, draws the sender's InMail credit balance, visible as inmailCredits on the account. When credits run out you get 429 code: inmail_credits_exhausted.

Not sure which channel applies? checkConnection returns the sender's relationship with a profile: status (not_connected, pending, connected), connection degree, openProfile, and whether this sender canMessage or canInvite them, without using any cap.

Idempotent sends

Every POST direct action accepts an Idempotency-Key header: a unique key per action (a UUID works), up to 255 characters. Reusing the key replays the first response instead of sending again, so a retry cannot double-send. It is recommended on all writes.

The failure mode it exists for: your request times out at the network level, but the send actually went through. Without a key, retrying would message the person twice. With a key, retry the exact same request with the same key and you get the original response back.

Two idempotency-specific 409 codes can come back on a retry:

CodeMeaningWhat to do
idempotency_in_progressA request with the same key is still in flightRetry shortly
idempotency_failedThe original errored after it may have taken effectRetry with a new key

Error handling

Branch on error.code, not on error.message (the wording may change, codes will not).

StatusMeaning
404 code: profile_not_foundThe profile URL or identifier matched no LinkedIn profile
409A conflict: an invitation to this profile is already pending, a workspace limit was reached, or the sending account was rejected by LinkedIn and may need reconnecting. Codes: sender_paused, inmail_not_available, idempotency_in_progress, idempotency_failed
422LinkedIn could not complete the action on this profile: cannot invite, already connected, the recipient cannot be messaged by this sender, or (for post likes and comments) the lead has no recent post
429A sender limit was hit. Codes: inmail_credits_exhausted for InMail credits, otherwise a cap

A 422 is about the target, not the request, so retrying it unchanged will usually fail again; skip the target or switch action type instead.

A 429 covers the daily cap for the action type, the weekly connection-request limit (tier-based: about 130 invites per week for free sender accounts, 150 for paid), and the pending-invitation backlog. Its details object carries { limit, used, scope, resetAt }, where scope is day, week, or pending. resetAt is the UTC time the window frees; it is absent for pending, which clears as invitations are accepted or withdrawn. Schedule your retry for resetAt rather than hammering the endpoint.

Tracking what was sent

listOutreachActions returns the sender's direct actions newest first, filterable by type (invitation, message, inmail, profile_view, follow, post_like, post_comment) and status (pending, sent, accepted, withdrawn, failed). Each action records the target (profileUrl, publicIdentifier, leadId), the note or comment text, an error string when it failed, and sentAt / acceptedAt / withdrawnAt timestamps. For push-style tracking, subscribe to webhooks instead of polling.

Approvals: AI comment drafts

Approvals are the human-review queue for AI-written post comments. They are produced by campaigns, not by the direct-action endpoints: a comment_post sequence step with requireApproval: true drafts a comment on the lead's latest post and holds it as a pending comment instead of posting it. The API lets you build that review flow into your own tools.

Each draft carries the post context (postUrl, postText), the AI prompt, the drafted comment, the lead's identity, and a status: pending, approved, rejected, posted, skipped or failed.

curl -X POST "https://app.swarmhit.com/api/v1/pending-comments/PENDING_COMMENT_ID/approve" \
  -H "Authorization: Bearer swh_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "text": "Great breakdown, especially the point on activation metrics." }'

To comment with exact text you control, outside any campaign, use the direct commentLastPost endpoint; no approval step is involved.

Reference

On this page