LinkedIn Campaign Contacts
li_campaign_contactsContacts in a campaign with both stages, per-step sequence progress, the next planned action, what the CONVERSATION is doing, and the failure — filed under the stage it came from.
PAGED. Without limit this returns the first 50 contacts (max 200), and a ROWS reply always carries totalContacts, returned, offset and truncated — a page is never the whole campaign, and the rest are unread rather than absent. The summary view carries totalContacts and counts over every contact, so it has nothing to truncate and reports no page. Walk on with offset (nextOffset). FILTERS run over the whole campaign before the page is cut: connectionStatus ("none" = imported already connected), conversationState, hasInbound; the reply then adds matchingContacts, and truncated is about the matches. For "how is this campaign doing", pass view="summary": counts over EVERY contact by contact status, message status, connection status, conversation state and failure category, with no per-contact rows at all.
THREE statuses, each NAMED for its stage because they share four words. contactStatus is the CONTACT roll-up (pending | active | replied | handed_off): it only moves forward and says nothing about what is queued. messageStatus is the MESSAGE lifecycle (awaiting_connection | pending | in_progress | replied | handed_off | completed | failed), where pending really does mean a message is queued to send. connectionStatus is the INVITE lifecycle (not_sent | invited | accepted | withdrawn | declined | failed). Read the one whose name says the stage you mean.
messageStatus is CORRECTED: a failed message sitting on a LIVE invite reads awaiting_connection, because acceptance revives it. messageStatusRaw is what the database stores and messageStatusSuperseded is true when the two differ.
TWO failures, never both at once. connectionFailure is the INVITE stage (the connection ended failed/declined/withdrawn — the retry is a fresh invite, and a withdrawn one is blocked for about three weeks). messageFailure is the MESSAGE stage (the send failed on a connection we already have — the retry is a re-send to someone who may already have the text). Never offer one as the other. Both carry category/isPermanent/retryable; retryable is narrower than !isPermanent, because an invite LinkedIn quietly ignored is not permanent but is time-gated.
COMPACT BY DEFAULT: a row carries no steps array and no copy, only stepCounts (its steps by status) and connectionNoteLength with connectionNoteOmitted: true. Pass includeSteps=true, or any text mode, for every step and its copy. With steps, text picks how much copy: "preview" (the default) clips the note and each step to 120 characters and says so per field (textLength/textTruncated, connectionNoteLength/connectionNoteTruncated), "full" returns them whole (to review what will send), "none" keeps only lengths (textOmitted: true). ⚠️ THE TWO "truncated"S ANSWER DIFFERENT QUESTIONS: the reply's truncated is about CONTACTS; a field's textTruncated is about that string. The reply echoes copyMode and stepsIncluded. format: "csv" returns the rows as one CSV string (csv) with dotted columns (conversation.state, steps.0.id).
With steps included, every step carries its id and position, so li_edit_step and li_cancel_steps can be driven straight off this read rather than through a dossier per person.
conversation.state folds both stages and the thread into one answer (invite_pending, awaiting_reply, needs_reply, send_failed, no_reply …), and a human-marked outcome (won / meeting_booked / interested / not_interested / no_outcome) always wins over it.
conversation.seenByThemAt is when the other side read the LATEST message we sent — the LinkedIn read receipt, captured whenever the stored thread is read. Null means NO RECEIPT — they have not opened the thread, or read receipts are switched off on either side (a mutual LinkedIn setting); the two are indistinguishable, so never read a null as unseen or not-read — only the positive claim (seen, with a time) is honest.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| text | No | How much COPY each row carries — the connection note and every step text. Setting it includes the steps. "preview" (the default with steps) clips each to 120 characters and says so per field; "full" returns them complete (to review what will send); "none" keeps only lengths. | |
| view | No | "summary" returns the counts for the WHOLE campaign — every status, conversation state and failure category, with no per-contact rows. It is the cheap way to ask "how is this campaign doing"; "rows" (the default) is for acting on named people. | |
| limit | No | How many contacts to return (default 50, max 200). The reply always says the real total and whether it was cut. | |
| format | No | "csv" = one header + rows string instead of the `contacts` array (rows view only) | |
| offset | No | Skip this many contacts — how to walk past the first page. | |
| campaignId | Yes | Campaign id from li_list_campaigns | |
| hasInbound | No | true = only contacts who have written to you; false = only those who have not | |
| includeSteps | No | true = each row carries its `steps` (ids for li_edit_step / li_cancel_steps). Default false: rows carry `stepCounts` only. | |
| connectionStatus | No | Only contacts whose invite is in one of these statuses ("none" = imported already connected) | |
| conversationState | No | Only contacts whose `conversation.state` is one of these (a marked outcome wins) |