Skip to content

Sync Substack Unsubscribes to Your CRM (Kit, HubSpot)

By the newsletterapi team · Published · Updated · 7 min read

The problem: “When someone unsubscribes on Substack, they stay in my CRM and keep getting our other emails, and I can't get a list of who left.”

Cover image: Sync Substack Unsubscribes to Your CRM (Kit, HubSpot)

Short answer

Substack has no unsubscribe webhook or list of who left, as of October 2026. To sync unsubscribes, export your full subscriber list on a schedule, compare it with the previous export, and treat every email that disappeared as an unsubscribe. Then suppress those people in Kit (bulk Unsubscribe or the API) or HubSpot (opt-out list import or the API). Your latency equals your export cadence.

The problem

"Any ideas on how to sync Unsubscribes between Substack and the crm?" (r/Substack, July 2024)

"Hey guys, I have a client and she is asking us to build a scenario that will unsubscribe people inside GHL once they unsubscribe from Substack." (Make Community, July 2025)

"Want a list of people who unsubscribe? You can't get one." (@BelangerTrading on X, October 2026)

If your newsletter runs on Substack but your sales, onboarding or event emails run from Kit, HubSpot or GoHighLevel, the same person lives in two places. When they leave Substack, your CRM doesn't know. They keep getting your other emails, complaints rise, and you may be emailing someone who just told you to stop.

In our hand-coded review of public forum and social posts (October 2026), 5 distinct sources asked for exactly this job, including an agency whose client already pays for a CRM. This guide shows the reliable way to do it with Substack's own export, and where a script saves you the clicking.

Does Substack have an unsubscribe webhook or list?

No public one, as of October 2026. Here is what Substack does give you:

What Substack offers What it tells you Limits
"Canceled free subscription" notification An email "that their email has been disabled and removed from your list" (help) Optional; we haven't confirmed it still includes the address after Substack removed addresses from new-subscriber emails in March 2026 (what changed)
Stats → Unsubscribes tab "the email, feedback, date, and reason for each paid subscriber who cancelled" (metrics guide) Paid publications only; covers paid cancellations, not free unsubscribes
Subscriber export (CSV) Everyone currently on your list (export help) A snapshot; you work out who left by comparison
Publisher API, where enabled Can look up one known email Can't list subscribers or changes (what the Substack API can do)

So the export is the source of truth. A free reader who unsubscribes from all emails is removed from your list (reader help), which means they disappear from your next export. That disappearance is your unsubscribe event.

How do I find who unsubscribed from Substack?

  1. Export the full list. Subscribers → All subscribers → the three dots → Export (export help). Clear every filter first. If filters are on, Substack asks whether you want the whole list or the filtered one (dashboard help); pick the whole list every time.
  2. Save it with the date in the file name, for example substack-2026-10-24.csv.
  3. Compare it with the last export. Anyone in the old file but not in the new one has left, was removed, or had their email disabled.
  4. Review the list, then suppress those people in your CRM (next section).

This short Python script does step 3. It uses only the standard library, finds the email column by name, and refuses to run if an unusual share of the list vanished, which almost always means one export was filtered.

import csv, sys

# Usage: python3 diff_exports.py last-week.csv this-week.csv
# Both files must be FULL exports (no filters), or "missing" means nothing.
MAX_LEFT_SHARE = 0.05  # stop if more than 5% vanished: probably a filtered or broken export

def load(path):
    with open(path, newline="", encoding="utf-8-sig") as f:
        reader = csv.DictReader(f)
        email = next((h for h in reader.fieldnames if "email" in h.lower()), None)
        if not email:
            sys.exit(f"{path}: no email column in {reader.fieldnames}")
        return {r[email].strip().lower(): r for r in reader if r[email].strip()}

old, new = load(sys.argv[1]), load(sys.argv[2])
left = sorted(set(old) - set(new))
joined = sorted(set(new) - set(old))

if old and len(left) / len(old) > MAX_LEFT_SHARE:
    sys.exit(f"{len(left)} of {len(old)} emails missing. Check both exports are full, unfiltered lists.")

with open("left.csv", "w", newline="") as f:
    w = csv.writer(f)
    w.writerow(["email"])
    w.writerows([e] for e in left)

print(f"left: {len(left)}  new: {len(joined)}  -> left.csv")

Tune MAX_LEFT_SHARE to your normal weekly churn. Substack doesn't publish its export header names, so the script matches any column with "email" in its name; open your file once to check.

Paid cancellations are different. When you cancel a paid subscription from the dashboard, the reader is "downgraded to a free subscriber" (subscription help), so they stay in the export. Expect the same for most readers who cancel their own plan, and check your exports to confirm. If your CRM tags paying readers, compare the subscription-type column between exports as well, or use the Unsubscribes tab for paid cancellations.

How do I unsubscribe them in Kit or HubSpot?

Pick the no-code route for a weekly list of a few dozen people, and the API route when you want it scheduled.

Tool No-code route API route
Kit Filter or tag the people in left.csv, select them, use the Unsubscribe bulk action (Kit bulk actions) GET /v4/subscribers?email_address= to find the ID (list subscribers), then POST /v4/subscribers/{id}/unsubscribe (unsubscribe)
HubSpot Import left.csv as "List of contacts to opt out of emails"; HubSpot sets "Unsubscribed from all email" to true (opt-out import) POST /communication-preferences/v4/statuses/{email}/unsubscribe-all with the communication_preferences.write scope (v4 guide)

Kit warns that its unsubscribe call is effectively permanent and revokes consent, so only re-add someone who explicitly opts in again (Kit API). That's the behaviour you want here.

Here is a script for the API route. It's a dry run unless you pass --apply, so you can read what it will do first. Set KIT_API_KEY (a Kit v4 API key) or HUBSPOT_TOKEN (a HubSpot private app token with communication_preferences.write).

import csv, json, os, sys, time, urllib.error, urllib.parse, urllib.request

# Usage: python3 push_unsubscribes.py kit|hubspot left.csv [--apply]
# Without --apply it only prints what it would do.
tool, path, apply = sys.argv[1], sys.argv[2], "--apply" in sys.argv
KIT = "https://api.kit.com"
HUBSPOT = "https://api.hubapi.com"

def call(method, url, headers, body=None):
    data = json.dumps(body).encode() if body is not None else None
    req = urllib.request.Request(url, data=data, method=method,
                                 headers={**headers, "Content-Type": "application/json"})
    with urllib.request.urlopen(req) as resp:
        raw = resp.read()
        return json.loads(raw) if raw else {}

def kit(email):
    h = {"X-Kit-Api-Key": os.environ["KIT_API_KEY"]}
    q = urllib.parse.urlencode({"email_address": email})
    found = call("GET", f"{KIT}/v4/subscribers?{q}", h).get("subscribers", [])
    if not found:
        return "skip: no active Kit subscriber"
    if apply:
        call("POST", f"{KIT}/v4/subscribers/{found[0]['id']}/unsubscribe", h, {})
    return f"unsubscribe Kit subscriber {found[0]['id']}"

def hubspot(email):
    h = {"Authorization": f"Bearer {os.environ['HUBSPOT_TOKEN']}"}
    if apply:
        e = urllib.parse.quote(email, safe="@")
        call("POST", f"{HUBSPOT}/communication-preferences/v4/statuses/{e}/unsubscribe-all", h)
    return "opt out of all email in HubSpot"

with open(path, newline="", encoding="utf-8-sig") as f:
    emails = [r["email"] for r in csv.DictReader(f)]

for email in emails:
    try:
        result = {"kit": kit, "hubspot": hubspot}[tool](email)
    except urllib.error.HTTPError as err:
        print(f"error {err.code} for {email}: {err.read()[:200]!r}")
        continue
    print(("done: " if apply else "would ") + f"{result} ({email})")
    time.sleep(0.3)  # stay well inside rate limits

We tested both scripts against sample files and a local mock of the two APIs, not against live accounts. Run the dry run first, then --apply on a list of one.

For GoHighLevel, ActiveCampaign or another CRM, the shape is the same: feed left.csv into that tool's own unsubscribe or "do not email" action.

How often should I run the sync?

Your unsubscribe latency equals your export cadence. Weekly is a sensible default: it's a two-minute job and keeps the gap to days. If you send frequent marketing emails from the CRM, run it before each big send instead.

Keep the gap short because emailing people who opted out costs you. Google asks senders to keep spam rates reported in Postmaster Tools below 0.30% and to support one-click unsubscribe for marketing mail (Google sender guidelines). People who just left your newsletter are the likeliest to press "Report spam" on your next CRM email.

  • You're the controller. Substack says publishers "are the data controllers of that audience, and they should ensure that they are in compliance with applicable Data Regulations" (Substack help). Honouring an unsubscribe across your tools is part of that.
  • Decide what an unsubscribe means. Leaving your newsletter doesn't always mean leaving your product emails. Write the rule down: for most teams, leaving Substack means "opt out of marketing email" in the CRM, while receipts and account emails continue.
  • Suppress, don't delete. Keep the opted-out record so a later import can't re-add them.
  • Never push the other way without consent. Don't add CRM contacts to Substack to "fill the gap"; see syncing Substack to Kit or HubSpot for the full two-way rules.
  • Treat the CSVs as personal data. Store them somewhere private and delete old ones once the diff is done.

What not to do

  • Don't run a hosted browser with your Substack login to watch the dashboard. It conflicts with Substack's terms and risks your account.
  • Don't parse Gmail notifications as your only record. The new-subscriber email lost its address in March 2026, and the cancellation email could change the same way.
  • Don't diff a filtered export against a full one. You'd unsubscribe hundreds of people who never left.

Can newsletterapi do this for me?

newsletterapi's subscriber analytics reads the exports you upload: send a weekly CSV to the imports endpoint and review who left since the last upload, then remove them from your CRM. It doesn't log in to Substack, so latency is still your export cadence. Its growth and segment reports are also available to AI assistants through the newsletterapi MCP server. To get access, email hello@newsletterapi.io.

If you're also building a weekly growth sheet from the same exports, see the Google Sheets growth dashboard, and for the other direction (new subscribers into Kit) see exporting Substack subscribers.

Questions people ask

Does Substack tell me who unsubscribed?
Partly. You can turn on the "Canceled free subscription" notification, and the Stats Unsubscribes tab lists paid cancellations with email, date and reason. There is no list or webhook of free unsubscribes, so the reliable record is the difference between two full exports.
Can Zapier or Make sync Substack unsubscribes?
Not with an official trigger. Substack has no Zapier app and no webhooks as of October 2026. Zapier, Make or n8n can still run the second half: take your diff file and unsubscribe those people in Kit or HubSpot.
Should I delete the contact or just unsubscribe them?
Unsubscribe or opt them out rather than delete. A deleted contact can be re-imported by accident later; an opted-out contact stays suppressed. Delete only when the person asks you to erase their data.
Why did someone vanish from my export who didn't unsubscribe?
They may have been removed by you or a co-admin, had their email disabled, or your export was filtered. Always compare two full, unfiltered exports, and review the list before suppressing anyone in a paid tool.
Does a paid cancellation count as an unsubscribe?
No. A paid subscriber who cancels usually stays on your list as a free subscriber, so they don't disappear from the export. Track plan changes separately if your CRM tags paying readers.

Sources

  1. Substack Help: How do I export my email list on Substack? (accessed 2026-10-24)
  2. Substack Help: How do I use the subscriber dashboard on Substack? (accessed 2026-10-24)
  3. Substack Help: What does email disabled mean? (accessed 2026-10-24)
  4. Substack Help: A guide to Substack metrics (accessed 2026-10-24)
  5. Substack Help: How do I make changes to a reader's subscription on Substack? (accessed 2026-10-24)
  6. Substack Help: How do I unsubscribe from a free subscription? (accessed 2026-10-24)
  7. Substack Help: Is Substack a data controller or a data processor? (accessed 2026-10-24)
  8. Kit Help: How to use bulk actions (accessed 2026-10-21)
  9. Kit API v4: List subscribers (accessed 2026-10-24)
  10. Kit API v4: Unsubscribe subscriber (accessed 2026-10-24)
  11. HubSpot Knowledge Base: Import opted-out contacts (accessed 2026-10-21)
  12. HubSpot Developers: Subscription preferences v4 API guide (accessed 2026-10-24)
  13. Google Workspace Admin Help: Email sender guidelines (accessed 2026-10-24)
  14. r/Substack: Any work-around to integrate Substack with a CRM? (comment 2024-07-10) (accessed 2026-10-21)
  15. Make Community: Unsubscribe from Substack, is this possible? (2025-07-01) (accessed 2026-10-21)
  16. @BelangerTrading on X (2026-10-08) (accessed 2026-10-21)