Hi. I’m Nathan Tankus a new and happy user of Ghost for my large Newsletter Notes on the Crises. I sent out my first piece by email today and I noticed that when free members unsubscribe from emails, they don’t get removed from the members list. I’m wondering if there’s a way I can do that, either with an integration or if there’s a Ghost update coming with this feature? Thanks again.
Hello @Nathan_Tankus, nice to see you making the move to Ghost from Substack.
Regarding your question of removal of unsubscribed members, there’s been no official word from Ghost that I know of, the best I can offer being this other thread with various reasoning for and against such an option:
As an aside, I just read your latest post where you mention that
Over the longer term, I am planning on building my own website where I will have room for many different types of content (including a podcast!)
I hope that implies sticking with Ghost, seeing how Ghost is slowly becoming more and more amenable to such things.
Cheers.
Thanks I’ll check out that thread!
re: website building- it did not originally mean sticking with Ghost but the more I use Ghost the more I like it and the more I think I’ll stick around.
also, I wanted to say I would be fine with manual deletion if I could… find… the unsubscriptions. being able to at least sort members by whether they’re subscribed to emails or not would go a long way.
I haven’t updated my site yet to 4.x from 3.x (I’ve only played around with it on a test site I’ve set up), but as far as I’d noticed that isn’t possible (yet?).
As you’re a Ghost(Pro) user it might be useful to know that you can always email support@ghost.org, they being the ones more familiar with all this than anybody else.
Thanks, I will (and have already). my inclination is always to go to forums first since I hate using up that kind of personalized service (and I have like 9-10 questions at any given time).
Ditto. Ask away then, lots of knowledgeable people here
Hi I need help I subscribed my boss to Notes on the crises and billed USD10 a month, I would like to know how can I unsubscribe it and looking forward to help in the new areas.
Hi Nathan, Ryan Singel here from Outpost, which does membership management for Ghost sites.
There are legitimate reasons that people might unsubscribe and still want to be a member (e.g. less email but still read+comment on membership-walled posts), but there’s also the case where people could think they have canceled by unsubscribing and then get upset when they are charged later.
Our solution is to email people who have unsubscribed from newsletters and let them know they are still a member, but won’t get newsletters. That email includes a link to Portal for those that want to re-subscribe or to cancel properly.
You don’t have to use Outpost to do this. You can use an external service/self-hosted solution that listens on a webhook for that event. You create an integration, define a webhook for it, then configure the external service to listen for the event and then send the email.
How do you know if someone unsubscribed from your newsletter? Does Ghost tell you? If not is there a 3rd party service that you have to connect to be able to get those alerts?
That is so helpful, thank you!
Have you considered an tiny API script ?
I haven’t! I’m tech proficient but not a developer so I actually don’t know what that means.
Ah… well it implies installing some python (programing language) and dependencies, then running a script like the one below.
A more practical solution can be using Make to make the automation without needing to code:
EXAMPLE SCRIPT
import requests
import jwt
from datetime import datetime
import time
from tqdm import tqdm
# Configuration
GHOST_ADMIN_API_URL = "https://yoursite.com/ghost/api/admin"
GHOST_ADMIN_API_KEY = "YOUR_ADMIN_API_KEY"
MEMBERS_PER_PAGE = 100
DRY_RUN = True # Set to False to actually delete members
def generate_ghost_token():
"""Generate token following Ghost's official documentation"""
try:
id, secret = GHOST_ADMIN_API_KEY.strip().split(':')
iat = int(datetime.now().timestamp())
header = {
'alg': 'HS256',
'typ': 'JWT',
'kid': id
}
payload = {
'iat': iat,
'exp': iat + 5 * 60,
'aud': '/admin/'
}
secret_bytes = bytes.fromhex(secret)
token = jwt.encode(payload, secret_bytes, algorithm='HS256', headers=header)
return token
except Exception as e:
print(f"Error in token generation: {str(e)}")
raise
def fetch_members(page=1):
"""Fetch members with their subscription status"""
token = generate_ghost_token()
api_url = f"{GHOST_ADMIN_API_URL}/members/"
headers = {
'Authorization': f'Ghost {token}',
'Accept-Version': 'v5.104'
}
params = {
'page': page,
'limit': MEMBERS_PER_PAGE,
'include': 'newsletters' # Include newsletter subscriptions
}
try:
response = requests.get(api_url, headers=headers, params=params)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"Error fetching members: {str(e)}")
return None
def delete_member(member_id):
"""Delete a member by ID"""
token = generate_ghost_token()
api_url = f"{GHOST_ADMIN_API_URL}/members/{member_id}/"
headers = {
'Authorization': f'Ghost {token}',
'Accept-Version': 'v5.104'
}
try:
response = requests.delete(api_url, headers=headers)
response.raise_for_status()
return True
except requests.exceptions.RequestException as e:
print(f"Error deleting member {member_id}: {str(e)}")
return False
def main():
print("Starting to process members...")
# Get total number of members first
result = fetch_members(1)
if not result or 'meta' not in result:
print("Error: Could not fetch members")
return
total_members = result['meta']['pagination']['total']
print(f"Found {total_members} total members")
# Calculate total pages
total_pages = (total_members + MEMBERS_PER_PAGE - 1) // MEMBERS_PER_PAGE
# Initialize counters
unsubscribed_count = 0
deleted_count = 0
failed_count = 0
# Create CSV for logging deletions
from datetime import datetime
import csv
log_filename = f"deleted_members_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
with open(log_filename, 'w', newline='') as log_file:
log_writer = csv.writer(log_file)
log_writer.writerow(['Email', 'Name', 'ID', 'Created At', 'Last Seen At'])
# Create progress bar for all members
with tqdm(total=total_members, desc="Processing members", unit="member") as pbar:
# Process each page
for page in range(1, total_pages + 1):
result = fetch_members(page)
if not result or 'members' not in result:
print(f"\nError fetching page {page}")
continue
members = result['members']
# Process each member
for member in members:
try:
newsletters = member.get('newsletters', [])
# Check if member has no newsletter subscriptions
if not newsletters:
unsubscribed_count += 1
# Log member details before deletion
log_writer.writerow([
member.get('email'),
member.get('name'),
member.get('id'),
member.get('created_at'),
member.get('last_seen_at')
])
if not DRY_RUN:
# Delete member
if delete_member(member['id']):
deleted_count += 1
else:
failed_count += 1
# Add a small delay to avoid rate limiting
time.sleep(0.5)
except Exception as e:
print(f"\nError processing member '{member.get('email', 'Unknown')}': {str(e)}")
failed_count += 1
pbar.update(1)
# Print summary
print("\nProcess completed!")
print(f"Total members scanned: {total_members}")
print(f"Members without newsletter subscriptions: {unsubscribed_count}")
if not DRY_RUN:
print(f"Successfully deleted: {deleted_count}")
print(f"Failed to delete: {failed_count}")
else:
print("This was a dry run - no members were actually deleted")
print(f"Deletion log saved to: {log_filename}")
if __name__ == "__main__":
if DRY_RUN:
print("RUNNING IN DRY RUN MODE - No members will be deleted")
print("Set DRY_RUN = False to perform actual deletions")
print("Press Ctrl+C to cancel or Enter to continue...")
input()
main()
Make is similar to Zapier?
You would inject this code into the code injection section of the site? And then it would email you every time someone unsubscribes?
Yes make is similar to zapier.
Yet a bit more technical ( less user friendly) but more powerful, and cheaper
I run a similar script manually every week locally in my computer.
But I like your idea.
You can have make or zapier run an API call or webhook running python scripts.
You would not put this code into ghost’s code injection. Code injection is for things you want website visitors to run (and must be JavaScript or css).