Skip to main content

Migrate from the legacy unsubscribe endpoints to the v2 unsubscribe API

The old /unsubscribes routes stop working on November 1, 2026. Here's the replacement for each call, the response changes that break existing code, and the new contact-level and bulk options.

The Legacy Unsubscribe API endpoints are sunset from November 1, 2026. After that date, calls made to these endpoints no longer function, so every flow still using them — your own code, Zapier, Make, n8n — has to point at the v2 multichannel unsubscribe endpoints to keep working without disruption. This article maps each legacy route to its replacement, lists the response changes that can break your existing code, and covers the new contact-level and bulk routes.


Why this is changing

The unsubscribe feature was originally developed when email was the only channel available on lemlist. With several channels now available, the unsubscribe system was reworked to handle them.

That rework also introduced a way to unsubscribe at the Contact level, ensuring no future communication is sent from any channel — including any channel added to the platform later.


Before you start

  • Calls to the legacy endpoints stop working from November 1, 2026.

  • The legacy routes only accept emails and domains. LinkedIn URLs and phone numbers can only be unsubscribed through v2.

  • You cannot re-subscribe a variable whose source is lead or abuse. These sources are protected and return 409.

  • The limit parameter on the list route accepts a maximum of 100.

  • One bulk unsubscribe request accepts up to 10,000 values.

You need:

  • A lemlist API key — see Find and use the lemlist API.

  • A list of every script, integration, and automation that calls an /unsubscribes route today.


Key concepts

  • Variable: a contact value that you can unsubscribe. Four types exist: email, domain (starts with @), LinkedIn URL, phone number.

  • Contact: a person identified by a contactId (prefix con_). Unsubscribing a contact sets the doNotContact flag, which blocks all channels. The GET Contact route shows this flag in the boolean field unsubscribed.

  • Source: the origin of an unsubscription. Possible values: api, bounced, lead, user, abuse.

  • Idempotence: unsubscribing a variable that is already unsubscribed returns the existing record with code 200.


Phase 1: Swap each legacy call for its v2 replacement

These five endpoints are sunset and act as direct replacements for one another. Replace the route, keep the same intent.

  • POST /unsubscribes/{email}POST /v2/unsubscribes/variables/{value} (Unsubscribe Variable)

  • GET /unsubscribes/{email}GET /v2/unsubscribes/variables/{value} (Get Unsubscribed Variable)

  • GET /unsubscribesGET /v2/unsubscribes/variables (List Unsubscribed Variables)

  • DELETE /unsubscribes/{email}DELETE /v2/unsubscribes/variables/{value} (Re-subscribe Variable)

  • GET /unsubs/exportGET /v2/unsubscribes/exports/variables (Export Unsubscribed Variables)

Verify: a call to the v2 route returns 200 with the record, and the same call to the legacy route is no longer used anywhere in your flow.


Phase 2: Update your code for the response changes

These changes can break existing client code. Check each point against your current integration.

  • POST variable: the email field is removed. Read value instead. The response also adds variable and source.

  • GET variable: the response no longer includes createdAt. A missing value returns 404 with "Variable not found". An empty value returns 400 instead of 500.

  • DELETE variable: the response is the plain string "Variable subscribed", not a JSON object. A missing value returns 400 instead of 404. A protected source returns 409.

  • GET list: the format does not change. Note the limit parameter now accepts a maximum of 100.

  • CSV export: the _id column is added in the first position. Columns become _id, value, source, createdAt.

A future update will change the routes that return a plain string, for example "Variable not found" or "Variable subscribed". These routes will return JSON. This change standardizes the API. Do not depend on the exact string in your code.

Verify: nothing in your code reads email from a POST response, depends on the exact text of the DELETE response, or maps export columns by position.


Phase 3: Update your webhooks

Since March 30, 2026, a Contact unsubscription no longer emits emailsUnsubscribed.

Subscribe to entityUnsubscribed (whole contact) or variableUnsubscribed (single variable) instead.


Phase 4: Use what v2 adds (optional)

You can now unsubscribe in bulk:

  • POST /v2/unsubscribes/variables → Bulk Unsubscribe Variables

You also have access to new endpoints that manage unsubscribes at Contact level rather than by specific channel:

  • GET /v2/unsubscribes/contacts/{contactId} → Get Contact Subscription Status

  • POST /v2/unsubscribes/contacts/{contactId} → Unsubscribe Contact

  • DELETE /v2/unsubscribes/contacts/{contactId} → Re-subscribe Contact

  • GET /v2/unsubscribes/exports/contacts → Export Unsubscribed Contacts


Example

A form on your website unsubscribes people, and your script reads the email back from the response to log it.

Before: POST /unsubscribes/{email}, then read email from the response.

After: POST /v2/unsubscribes/variables/{value}, then read value. You also get variable and source back. If that value is already unsubscribed, you get the existing record with 200, so you don't need to check first.

If you want that person blocked on every channel and not just on one email address, call POST /v2/unsubscribes/contacts/{contactId} instead. This sets doNotContact on the contact.


Troubleshooting

Issue: 404 with "Variable not found" on a GET that used to succeed.

Root cause: the value is not unsubscribed, and v2 returns a 404 where your code expected the legacy response.

Fix: treat 404 as "this value is not unsubscribed" rather than as an error, and remove any read of createdAt from that response.

Issue: 409 when re-subscribing a variable.

Root cause: the variable was unsubscribed by a protected source — lead or abuse.

Fix: read the source field before attempting to re-subscribe, skip protected sources, and handle 409 without retrying.

Issue: your DELETE call fails when handling the response.

Root cause: the response is currently the plain string "Variable subscribed" rather than a JSON object, and it will become JSON in a future update.

Fix: write the response handling so it accepts both a plain string and a JSON object, and never branch on the exact text. Rely on the status code instead, and expect 400 rather than 404 when the value does not exist.

Issue: the email is missing from the response after an unsubscribe.

Root cause: the email field was removed from the POST variable response.

Fix: read value instead.


If your case is different

This change takes some work to put in place on your end, so reach out to support or your assigned Account Manager if you need extra help.

Did this answer your question?