lemlist sends across both channels, so a candidate who ignored the email gets a LinkedIn touch inside the same campaign. Your agent loads the leads and reports on who engaged. Nothing starts sending until you say so.
lemlist is activation. It takes a validated list and turns it into conversations.
clean addresses before a multichannel sequence starts
more LinkedIn volume than lemlist alone will carry
replies belong on the record, not in an inbox
the enrichment that fills the sequence
A skill is a whole pipeline your agent already knows. These ones call Lemlist as part of the run.
use whenyou have years of placements in your database and no habit of asking those people who else is good
use whenyou have a spec and want search strings you can paste into LinkedIn, Google or your ATS, without hand-typing forty title variants
use whenone database keeps handing you the same names and you suspect the rest of the market is sitting in the others
use whenyou are sourcing in a German-speaking market and the search returns fewer people than you know are there
use whenan industry event has published its speakers and the people on stage look like the hire your client wants
use whenyou have a live role and want a shortlist you would actually send to the client, not a raw export
Every tool ships with a written playbook, and the agent loads it before the first call. Auth, rate limits, what each call costs, which actions need your approval, and the mistakes worth avoiding. It is the difference between an agent that knows the tool and one that guesses at it.
Read the Lemlist docs →HOW in lib/lemlist.py. Endpoint set cached at reference/docs/lemlist/raw/llms.txt.
https://api.lemlist.com/api · Auth: HTTP Basic — API key as the password, empty username (client sets auth=('', key)). Env LEMLIST_API_KEY; never hardcode. get_team() = safe read-only pilot..md + llms.txt, 2026-06-01). QUIRK: version=v2 query param required on get_campaigns (defaulted) and other v2 reads. Add-lead is POST /api/campaigns/{id}/leads/ with email in the body (not the path). Rate limit 20 req/2s. Full index: reference/docs/lemlist/raw/endpoints.md; request() covers the long tail (companies, inbox, schedules, enrich, people-database, etc.).start_campaign begins sending; add_lead_to_campaign enrolls a lead into active sending — approval-gate (pilot, confirm, then batch).Campaigns: get_campaigns, get_campaign, get_campaign_stats, start_campaign, pause_campaign. Leads: add_lead_to_campaign, remove_lead_from_campaign. Contacts: get_contacts, upsert_contact. Plus get_activities, enrich_lead, get_team. Else → generic request().
Activation layer (multichannel). Enriched+validated leads → add to campaign → review → start. get_activities feeds reply/interest tracking back to the ATS.
Field-shape note: these are vendor-native operational notes. The client returns the raw vendor JSON and uses the method names in this file — read field shapes accordingly (no normalized-wrapper /
result.data.prefix).
Flow: create/list campaign -> add sequence steps -> add contacts in small batches (10-25) -> review in the Lemlist UI (web_url returned on create/update) -> monitor activities/inbox.
email, linkedinInvite, linkedinSend, linkedinVisit, manual, phone, api, whatsappMessage, conditional, sendToAnotherCampaign.172800 ("seconds") triggers a > 1500 days API error.all, contacted, interested, notInterested, emailsBounced, paused, emailsSent, emailsOpened, emailsReplied.emailsSent, emailsOpened, emailsClicked, emailsReplied, emailsBounced, emailsUnsubscribed.send_email requires send_user_id, send_user_email, send_user_mailbox_id - list the inbox first to discover them.linkedin_url within a batch are rejected; cross-batch dupes can 409.lib/lemlist.pyImport: from lib.lemlist import Lemlist → instantiate Lemlist() (reads key from env). Base: https://api.lemlist.com/api. Generic passthrough: request(method, path, *, params, json).
add_conditional_step(sequence_id: str, condition_key: str, *, delay_type: str = 'within', delay: int | None = None, index: int | None = None) -> Any — POST /api/sequences/{sequenceId}/steps with type='conditional' — adds a branch point and RETURNS conditions: [{sequenceId, label, key, delayType, delay}, {sequenceId, fallback:true}] — i.e. an accepted-branch sequenceId AND a fallback (not-met) sequenceId. Add follow-up steps into those branch sequence ids via add_sequence_step(branch_seq_id, ...). condition_key e.g.add_lead_to_campaign(campaign_id: str, *, email: str | None = None, linkedin_url: str | None = None, payload: dict | None = None, variables: dict | None = None, **query) -> Any — POST /api/campaigns/{campaignId}/leads/ — add a SINGLE lead (CONFIRMED). This is a single-lead call with top-level fields — there is NO contacts:[…] array shape (that 400s). email is OPTIONAL — a LinkedIn-only lead works with just linkedin_url (verified live 2026-06-02). Provide at least one of email / linkedin_url. Standard fields go in payload: firstName, lastName, companyName, jobTitle, phone, companyDomain, icebreaker, timezone, contactOwner, picture.add_lead_variables(lead_id: str, variables: dict) -> Any — POST /api/leads/{leadId}/variables?{{name}} renders in the sequence. Custom field names + values are passed as QUERY params, NOT a JSON body (CONFIRMED from developer.lemlist.com). ⚠️ CREATE-only: a name that collides with a lemlist default var (email, firstName, lastName, picture, phone, linkedinUrl, companyName, companyDomain, icebreaker) OR an already-set custom var ERRORS — pass only NEW custom keys. Needs the leadId (the _id from add_lead_to_campaign's response). variables: {name: value, …}. Returns {ok: true}.add_sequence_step(sequence_id: str, type: str = 'email', *, subject: str | None = None, message: str | None = None, delay: int | None = None, index: int | None = None, **fields) -> Any — POST /api/sequences/{sequenceId}/steps — add a step. type ∈ email|manual|phone|api| linkedinVisit|linkedinInvite|linkedinSend|linkedinVoiceNote|conditional|whatsappMessage|sms|… Email step: subject (≤400), message (HTML body), delay (days, 0–1500; default 0 first/1 after), index (position, -1=end). No sender required. Extra fields (altMessage, url, method, conditionKey, images, videos…) via **fields. CONFIRMED live.create_campaign(name: str, *, timezone: str | None = None, auto_review: bool | None = None, auto_review_conditions: list[str] | None = None) -> Any — POST /api/campaigns — create a campaign; returns state: running, NOT draft, so pause it before building the sequence.delete_sequence_step(sequence_id: str, step_id: str) -> Any — DELETE /api/sequences/{sequenceId}/steps/{stepId} — remove a step. CONFIRMED live 2026-06-02 (note: needs the sequenceId in the path — /sequences/steps/{id} returns 405).enrich_lead(payload: dict) -> Any — POST /enrich — enrich a lead (see api-reference/endpoints/enrich).get_activities(**params) -> Any — GET /activities — outreach activity events (opens, replies, etc.).get_campaign(campaign_id: str) -> Anyget_campaign_leads(campaign_id: str, *, state: str | None = None, limit: int | None = None, **params) -> Any — GET /api/campaigns/{campaignId}/leads/ — read the leads currently in a campaign (synchronous; the practical counterpart to the async CSV export). Returns an array of lead objects with their fields, INCLUDING any custom merge variables set via add_lead_variables — so it's how you VERIFY variables persisted. Query: state (scanned|contacted|interested|notInterested|paused|…), limit (default 100, max 500). Sorted newest-first; no cursor paging. CONFIRMED from developer.lemlist.com.get_campaign_sequences(campaign_id: str) -> Any — GET /api/campaigns/{id}/sequences — the campaign's sequence(s) (each has _id = sequenceId).get_campaign_stats(campaign_id: str, **params) -> Anyget_campaigns(*, version: str = 'v2', **params) -> Any — GET /api/campaigns — list campaigns. version=v2 is REQUIRED (defaulted here); supports offset/limit (max 100)/page, sortBy=createdAt, sortOrder, status, createdBy.get_contacts(**params) -> Anyget_people_database_filters() -> Any — GET /database/filters — available filterIds for the People DB (e.g. currentTitle, location (lead's city/state = CONTACT location), country, seniority, department, currentCompany*…). Each is autocomplete/select typed.get_people_schema() -> Any — GET /schema/people — the People-DB record schema (available fields). Read-only pilot.get_team() -> Any — GET /team — team info (safe read-only pilot).pause_campaign(campaign_id: str) -> Anyremove_lead_from_campaign(campaign_id: str, email: str) -> Anysearch_people_database(filters: list[dict], *, search: str | None = None, page: int = 1, size: int = 25, excludes: list[str] | None = None) -> Any — POST /database/people — search lemlist's People DB. filters: list of {filterId, in:[...], out:[...]} — BOTH in and out are REQUIRED per filter (omit out → 400 "Parameter filters is invalid"); this method injects out:[] if you leave it off.start_campaign(campaign_id: str) -> Any — POST /campaigns/{id}/start — starts sending (outreach!). Approval-gate.update_sequence_step(sequence_id: str, step_id: str, **fields) -> Any — PATCH /api/sequences/{sequenceId}/steps/{stepId} — update a step (subject, message, delay, …).upsert_contact(payload: dict) -> Any — Create/update a contact (see api-reference/endpoints/contacts/upsert-contact).