Skip to content

New adaptor: lemlist #1808

Description

@jackohilts

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 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:

  1. 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.
  2. Base URL: https://api.lemlist.com/api (default; allow override via configuration.baseUrl).
  3. 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.
  4. 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).
  5. 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.
  6. 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.
  7. Campaign lookup: listCampaigns(options) (GET /campaigns) and getCampaign(id) so users can resolve campaignId to a name and scope webhooks to one campaign.
  8. 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).
  9. Results in state.data, consistent paging, and errors that pass through lemlist's message without logging the API key, webhook secret, or lead PII.
  10. 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 lemlist
createWebhook({
  targetUrl: 'https://app.openfn.org/i/<workflow-webhook-id>',
  type: 'emailsReplied', // omit to receive every event
  secret: $.configuration.webhookSecret,
});

// Polling alternative (cron-triggered workflow)
listActivities(
  { type: 'emailsReplied', minDate: $.lastRun ?? '2026-10-01T00:00:00Z' },
  { paginate: true }
);
fn(state => {
  state.lastRun = new Date().toISOString();
  return state;
});

// Enrich a webhook payload before handing it to the Copper step
getLead({ email: $.data.leadEmail ?? $.data.email });

// generic escape hatch
get('/campaigns', { query: { version: 'v2', limit: 100 } });

Resources

Logo Links

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Priority Level

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions