You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
We want to build a new adaptor for the lemlist REST API (v2). lemlist is a multichannel sales outreach tool (email, LinkedIn, WhatsApp, SMS, calls).
Driving use case: "Create or update Copper leads from new lemlist activities" (Zapier template for reference). When a lemlist activity happens (email sent/opened/replied/bounced/unsubscribed, LinkedIn profile visited, call ended, etc.), OpenFn receives it and creates or updates the matching Lead in Copper. This issue covers the lemlist side; the Copper side is tracked in #1807.
How the trigger works in OpenFn: Zapier's "New Activity" is an instant trigger, which is just a lemlist webhook. In OpenFn the equivalent is a webhook-triggered workflow: register the workflow's webhook URL with lemlist (POST /hooks), and each activity arrives as state.data. As a fallback for users who can't use webhooks, a cron-triggered workflow can poll GET /activities with minDate set from a cursor. The adaptor should support both.
To start, this adaptor should:
Handle authentication. lemlist uses HTTP Basic auth with an EMPTY username and the API key as the password (i.e. base64 of :<apiKey>, note the leading colon). Not Bearer. Set the Authorization: Basic ... header automatically from state.configuration.apiKey.
Base URL:https://api.lemlist.com/api (default; allow override via configuration.baseUrl).
Generic HTTP helpers:request(method, path, body, options) plus get, post, patch, put, delete so any unwrapped endpoint can be called. Several endpoints require ?version=v2; default this on where the docs mark it required.
listActivities(query, options) wrapping GET /activities?version=v2.
Filters: type (the ActivityType enum, e.g. emailsReplied, linkedinVisitDone, aircallEnded; note the plural emails* spelling), campaignId, leadId, isFirst, minDate / maxDate (unix seconds or ISO 8601).
Paging is offset/limit (max 100), not cursor. Provide a paginate/auto-fetch option that loops offset += limit until fewer than limit results come back, respecting rate limits.
Document the polling pattern (store last createdAt as a cursor, pass as minDate next run).
Webhook management helpers so users can set up the instant trigger from a job: createWebhook({ targetUrl, type, campaignId, isFirst, secret }) (POST /hooks), listWebhooks() (GET /hooks), deleteWebhook(hookId) (DELETE /hooks/{hookId}).
Handle the 409 cases with clear messages: 200-webhook cap per team (disabled webhooks count) vs. targetUrl already registered (uniqueness is on URL only, regardless of type).
Document that the optional secret is echoed in every webhook body so the workflow can verify origin, and that the payload also carries current contact fields (email, firstName, lastName, companyName, ...) besides the lead* snapshot fields. Non-campaign events have no lead* keys, so mapping should fall back to email / firstName / lastName.
Lead lookup:getLead({ email | id }) wrapping GET /leads?version=v2 (used to enrich the activity before pushing to Copper). Optionally getContact(idOrEmail) for the contact record.
Campaign lookup:listCampaigns(options) (GET /campaigns) and getCampaign(id) so users can resolve campaignId to a name and scope webhooks to one campaign.
Rate limits:20 requests per 2 seconds per API key. Read Retry-After / X-RateLimit-Remaining and back off on 429 (note X-RateLimit-Reset is a human-readable date string, not a timestamp).
Results in state.data, consistent paging, and errors that pass through lemlist's message without logging the API key, webhook secret, or lead PII.
Unit tests with mocks for every operation, plus JSDoc examples for each.
Out of scope for v1 (follow-up issues if needed): creating/updating leads in campaigns, campaign management, sequences, enrichment, inbox/messages, unsubscribes, lemwarm, signal agents.
Credentials
Login credentials: No shared OpenFn account yet. Sign up for lemlist's 14-day free trial (no card needed; trial gives full Multichannel plan access including the API; there is no permanent free tier with API access). Save the login + API key to LastPass as lemlist Trial - Adaptor Dev and note the expiry date here.
Test record(s): Create a test campaign, add 2 to 3 leads using addresses you control (e.g. +alias OpenFn emails), launch it, then open/reply to generate real emailsSent / emailsOpened / emailsReplied activities. Use webhook.site or an OpenFn webhook trigger to capture real payloads for test fixtures.
Authentication method(s): API key via Basic auth (empty username). Generate the key in lemlist under Settings > Integrations > Generate a new API key (shown only once). Auth docs. lemlist also offers OAuth for its MCP/CLI; not required for v1.
Suggested configuration-schema.json: apiKey (required, sensitive), baseUrl (optional, defaults to the URL above).
Sample Code
// One-off setup job: register the OpenFn webhook trigger with lemlistcreateWebhook({targetUrl: 'https://app.openfn.org/i/<workflow-webhook-id>',type: 'emailsReplied',// omit to receive every eventsecret: $.configuration.webhookSecret,});// Polling alternative (cron-triggered workflow)listActivities({type: 'emailsReplied',minDate: $.lastRun??'2026-10-01T00:00:00Z'},{paginate: true});fn(state=>{state.lastRun=newDate().toISOString();returnstate;});// Enrich a webhook payload before handing it to the Copper stepgetLead({email: $.data.leadEmail??$.data.email});// generic escape hatchget('/campaigns',{query: {version: 'v2',limit: 100}});
Request
We want to build a new adaptor for the lemlist REST API (v2). lemlist is a multichannel sales outreach tool (email, LinkedIn, WhatsApp, SMS, calls).
Driving use case: "Create or update Copper leads from new lemlist activities" (Zapier template for reference). When a lemlist activity happens (email sent/opened/replied/bounced/unsubscribed, LinkedIn profile visited, call ended, etc.), OpenFn receives it and creates or updates the matching Lead in Copper. This issue covers the lemlist side; the Copper side is tracked in #1807.
How the trigger works in OpenFn: Zapier's "New Activity" is an instant trigger, which is just a lemlist webhook. In OpenFn the equivalent is a webhook-triggered workflow: register the workflow's webhook URL with lemlist (
POST /hooks), and each activity arrives asstate.data. As a fallback for users who can't use webhooks, a cron-triggered workflow can pollGET /activitieswithminDateset from a cursor. The adaptor should support both.To start, this adaptor should:
:<apiKey>, note the leading colon). Not Bearer. Set theAuthorization: Basic ...header automatically fromstate.configuration.apiKey.https://api.lemlist.com/api(default; allow override viaconfiguration.baseUrl).request(method, path, body, options)plusget,post,patch,put,deleteso any unwrapped endpoint can be called. Several endpoints require?version=v2; default this on where the docs mark it required.listActivities(query, options)wrappingGET /activities?version=v2.type(theActivityTypeenum, e.g.emailsReplied,linkedinVisitDone,aircallEnded; note the pluralemails*spelling),campaignId,leadId,isFirst,minDate/maxDate(unix seconds or ISO 8601).paginate/auto-fetch option that loopsoffset += limituntil fewer thanlimitresults come back, respecting rate limits.createdAtas a cursor, pass asminDatenext run).createWebhook({ targetUrl, type, campaignId, isFirst, secret })(POST /hooks),listWebhooks()(GET /hooks),deleteWebhook(hookId)(DELETE /hooks/{hookId}).409cases with clear messages: 200-webhook cap per team (disabled webhooks count) vs.targetUrlalready registered (uniqueness is on URL only, regardless oftype).secretis echoed in every webhook body so the workflow can verify origin, and that the payload also carries current contact fields (email,firstName,lastName,companyName, ...) besides thelead*snapshot fields. Non-campaign events have nolead*keys, so mapping should fall back toemail/firstName/lastName.getLead({ email | id })wrappingGET /leads?version=v2(used to enrich the activity before pushing to Copper). OptionallygetContact(idOrEmail)for the contact record.listCampaigns(options)(GET /campaigns) andgetCampaign(id)so users can resolvecampaignIdto a name and scope webhooks to one campaign.Retry-After/X-RateLimit-Remainingand back off on429(noteX-RateLimit-Resetis a human-readable date string, not a timestamp).state.data, consistent paging, and errors that pass through lemlist's message without logging the API key, webhook secret, or lead PII.Out of scope for v1 (follow-up issues if needed): creating/updating leads in campaigns, campaign management, sequences, enrichment, inbox/messages, unsubscribes, lemwarm, signal agents.
Credentials
lemlist Trial - Adaptor Devand note the expiry date here.+aliasOpenFn emails), launch it, then open/reply to generate realemailsSent/emailsOpened/emailsRepliedactivities. Use webhook.site or an OpenFn webhook trigger to capture real payloads for test fixtures.configuration-schema.json:apiKey(required, sensitive),baseUrl(optional, defaults to the URL above).Sample Code
Resources
Logo Links