Integrations / Migration

Migrating from Lemlist

Bring your Lemlist lead identifiers across when you migrate outreach to Crispy. Then use a webhook to push declined invitations back to a Lemlist email-only sequence so you don't lose long-tail prospects.

Why this matters

Migrations break things when identifiers don't survive the move. Keeping the Lemlist lead ID on the Crispy contact means analytics, dedup, and re-targeting all keep working across both systems during the transition window.

Step 1: Import with custom_attrs

Export your Lemlist leads to CSV, then import to Crispy with the Lemlist ID and email mapped into custom_attrs. Use any column prefix matching custom_attrs.<key>; the import_contacts tool maps these directly.

email,first_name,last_name,linkedin_url,custom_attrs.lemlist_lead_id,custom_attrs.lemlist_email,custom_attrs.industry
[email protected],Alex,Rivera,https://www.linkedin.com/in/alex-rivera,lem-9f8a7b6c,[email protected],SaaS
[email protected],Sam,Kim,https://www.linkedin.com/in/sam-kim,lem-1a2b3c4d,[email protected],FinTech

The same data flows through every webhook payload, so downstream automations always have access to payload.data.contact.custom_attrs.lemlist_lead_id.

Step 2: Subscribe to invitation.declined

Decline-on-LinkedIn is a strong signal that the connection-request channel is dead for that prospect, but email might still work. Send them back to Lemlist for email-only follow-up.

curl -X POST https://crispy.sh/api/v1/subscriptions \
  -H "Authorization: Bearer crispy_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "event": "invitation.declined",
    "target_url": "https://hook.us2.make.com/your-make-webhook-id"
  }'

Step 3: Wire to Lemlist's API

  1. In Make or n8n, create a scenario with a webhook trigger pointing at Crispy.
  2. Verify the signature using the HMAC verification snippet.
  3. Add an HTTP → Make a request module:
    PATCH https://api.lemlist.com/api/leads/{{ payload.data.contact.custom_attrs.lemlist_lead_id }}
    Authorization: Basic <base64 of api-key:>
    Content-Type: application/json
    
    {
      "campaignId": "your-email-only-campaign-id",
      "email": "{{ payload.data.contact.custom_attrs.lemlist_email }}"
    }
  4. If a Lemlist lead ID is missing (newer Crispy contact never imported from Lemlist), short-circuit gracefully: filter the route on payload.data.contact.custom_attrs.lemlist_lead_id being non-empty before the Lemlist call.

Sample payload

The Lemlist identifiers you imported are surfaced under contact.custom_attrs:

{
  "payload_version": "1",
  "id": "00000000-0000-0000-0000-000000000903",
  "event": "invitation.declined",
  "timestamp": "2026-04-29T18:00:00.000Z",
  "webhook_id": "00000000-0000-0000-0000-0000000009h4",
  "account_id": "00000000-0000-0000-0000-0000000009a1",
  "data": {
    "contact": {
      "id": "00000000-0000-0000-0000-0000000009c3",
      "workspace_id": "00000000-0000-0000-0000-0000000009w1",
      "connection_status": "declined",
      "current_campaign_id": "00000000-0000-0000-0000-0000000009p1",
      "tags": ["q4-target"],
      "custom_attrs": {
        "industry": "SaaS",
        "lemlist_lead_id": "lem-9f8a7b6c",
        "lemlist_email": "[email protected]"
      },
      "external_ids": {}
    },
    "source_event": "invitation.declined",
    "triggered_at": "2026-04-29T18:00:00.000Z"
  }
}

Troubleshooting

custom_attrs are missing from the payload.
Check the import. CSVs that use a header like lemlist_lead_id instead of custom_attrs.lemlist_lead_id won't map. Re-export with the prefix and re-import the affected contacts.
Lemlist returns 404 on the lead ID.
The lead may have been removed from Lemlist after export. Add a fallback path that creates the lead by email instead of patching by ID.