Using the API to change someone's status from comped to free

Hi, all. My newsletter (Bamboo Weekly), running on Ghost(Pro), has a free tier and a paid tier. On the Ghost(Pro) site, that works like a charm.

But people can also get my newsletter when they subscribe to my online Python+Data course membership. I use Zapier to accomplish this; when someone signs up for a membership at LernerPython.com, Zapier signs the person up for a comped Bamboo Weekly subscription. That is, they don’t have to pay, but they get the benefits of a paid subscription.

That’s all great, until/unless they cancel their membership. I’ve been in touch with the Ghost(Pro) folks, and they say that Zapier only makes it possible to do comped subscriptions for one year. And those subscriptions auto-renew. And there’s no Zapier way to remove someone’s comped status. That’s in part because the subscription is handled by Stripe.

For now, I go to my Ghost dashboard, find the user manually, use the provided link to go to the Stripe subscription, and cancel that. Which is… quite a pain.

I’ve been trying to write a Python program that’ll allow me to enter an e-mail address and then have the person’s subscription changed from comped to free. But it doesn’t seem to work at this point. The API call succeeds, but the comped status remains.

I’m enclosing my code here. I’d like to think that I’m trying to do something relatively simple, but this is really driving me crazy, and eating up a fair amount of time. Any thoughts?

#!/usr/bin/env python3

import json
import requests
import jwt
import time
from datetime import datetime as dt
from io import BytesIO

class GhostAdmin():
    def __init__(self, siteName):
        self.siteName = siteName
        self.site = {'name': 'bambooweekly.com',
                     'url': 'https://bamboo-weekly.ghost.io/',
                     'AdminAPIKey': 'ADMIN_KEY_GOES_HERE',
                     'ContentAPIKey': 'CONTENT_KEY_GOES_HERE'}
        self.token = self.create_token()
        self.headers = {'Authorization': f'Ghost {self.token}'}

    def create_token(self):
        key = self.site['AdminAPIKey']
        kid, secret = key.split(':')
        iat = int(dt.now().timestamp())
        header = {'alg': 'HS256', 'typ': 'JWT', 'kid': kid}
        payload = {
            'iat': iat,
            'exp': iat + (5 * 60),
            'aud': '/v3/admin/'
        }
        return jwt.encode(payload, bytes.fromhex(secret), algorithm='HS256', headers=header)

    def get_subscriber(self, email):
        url = f"{self.site['url']}ghost/api/v3/admin/members/?filter=email:{email}"
        result = requests.get(url, headers=self.headers)
        if result.ok:
            data = json.loads(result.content)
            members = data['members']
            if members:
                return members[0]
        return None

    def update_subscriber_status(self, member_id, updates):
        url = f"{self.site['url']}ghost/api/v3/admin/members/{member_id}/"
        data = {"members": [{"id": member_id, **updates}]}
        result = requests.put(url, headers=self.headers, json=data)
        print(f"API Response Status Code: {result.status_code}")
        print(f"API Response Content: {result.content}")
        if result.ok:
            updated_member = json.loads(result.content)['members'][0]
            print(f"Updated member data: {json.dumps(updated_member, indent=2)}")
            return updated_member
        else:
            print(f"Error updating member: {result.content}")
            return None

    def convert_comped_to_free(self, email):
        subscriber = self.get_subscriber(email)
        if not subscriber:
            print(f"No subscriber found with email: {email}")
            return None

        if not subscriber['comped']:
            print(f"Subscriber {email} is not currently comped. No action needed.")
            return subscriber

        # Try updating comped status first
        updates = {"comped": False}
        print(f"Attempting to update subscriber {email} with the following changes: {updates}")
        updated_subscriber = self.update_subscriber_status(subscriber['id'], updates)

        if updated_subscriber and not updated_subscriber['comped']:
            print("Successfully removed comped status.")
        else:
            print("Failed to remove comped status.")

        # Now try updating the status
        updates = {"status": "free"}
        print(f"Attempting to update subscriber {email} with the following changes: {updates}")
        updated_subscriber = self.update_subscriber_status(subscriber['id'], updates)

        if updated_subscriber:
            if not updated_subscriber['comped'] and updated_subscriber['status'] == 'free':
                print(f"Successfully converted {email} from comped to free status.")
            else:
                print(f"Update call succeeded, but subscriber status did not change as expected.")
                print(f"Current status: comped={updated_subscriber['comped']}, status={updated_subscriber['status']}")
            return updated_subscriber
        else:
            print(f"Failed to convert {email} from comped to free status.")
            return None

if __name__ == '__main__':
    ga = GhostAdmin('bambooweekly.com')

    email = input("Enter the email address of the subscriber: ")
    subscriber = ga.get_subscriber(email)

    if subscriber:
        print(f"\nSubscriber information for {email}:")
        for field in ['id', 'name', 'email', 'status', 'subscribed', 'comped']:
            print(f'{field}: {subscriber.get(field, "N/A")}')

        if subscriber['comped']:
            convert = input("This subscriber is comped. Do you want to convert them to a free subscription? (y/n): ")
            if convert.lower() == 'y':
                updated_subscriber = ga.convert_comped_to_free(email)
                if updated_subscriber:
                    print("\nUpdated subscriber information:")
                    for field in ['id', 'name', 'email', 'status', 'subscribed', 'comped']:
                        print(f'{field}: {updated_subscriber.get(field, "N/A")}')
    else:
        print(f"No subscriber found with email: {email}")

My best tip when dealing with the (undocumented) members API is to do the desired action in the Ghost admin panel and watch the network call.

When I delete a complimentary subscription, I see a PUT request to https://cathys-second-demo-site.ghost.io/ghost/api/admin/members/{{memberid}}/ with the payload:

{"members":[{
   "id":"that-same-memberid",
   "email":"someone@somewhere",
   "tiers":[]}
]}

So I’d suggest replicating that in your script. But do be sure to check that they’re not a paying member first, because you’re going to wipe all tiers doing that. :slight_smile:

1 Like

I tried your suggestion, and I got the following error back in trying to remove one of my comped subscriptions:

Error updating member: b'{"errors":[{"message":"Request not understood error, cannot edit member.","context":"Cannot delete a non-comped Product from a Member, because it has an active Subscription for the same product","type":"BadRequestError","details":null,"property":null,"help":null,"code":null,"id":"039235c0-857b-11ef-9b45-f51d5201c27b","ghostErrorCode":null}]}'

I’m getting the feeling that I can’t do this via the Ghost API, because the Ghost dashboard doesn’t let me remove comped subscriptions, either. Rather, I have to delete the subscription via Stripe. The good news is that Ghost provides a link to the appropriate Stripe dashboard page, but the bad news would seem to be that this requires some work with Stripe. (So there isn’t even any network interaction to see happen, because Ghost doesn’t handle this.)

Is that a reasonable assumption? If so, maybe I’ll have to use the Stripe API, too…

Thanks again!

I just tried removing a comped subscription in the dashboard, and did so with no problems. So something is odd here…

What version of Ghost are you on? Anything unusual about your setup? Oh… I see you said Ghost Pro? Or are you testing on a localhost setup?

Cannot delete a non-comped Product from a Member, because it has an active Subscription for the same product

What’s the dashboard showing for that member? What’s the GET network request return when you go to that member’s page in the dashboard?

The comment where that error message occurs says

 // Only allow to delete comped products without a subscription attached to them
 // Other products should be removed by canceling them via the related stripe subscription

There could be a bug in the API logic, but I think checking whether that member you’re testing with actually has an active subscription is a good start…

Thanks again for your help.

I’m indeed running on Ghost(Pro), so I’m using whatever version of things they run – which, according to the settings I got back from the API, is 5.96.

The user in question is one of my test accounts (reuven@mandarinweekly.com). I’m enclosing a screenshot, but that user is definitely subscribed, and has a comped subscription that renews on September 10th, 2025. The GET request I’m using (in my browser) is

https://bamboo-weekly.ghost.io/ghost/#/members/66e03a0aa867a10001f3cbb7

I’m enclosing a screenshot of the page for this user.

There is a “cancel subscription” option in the “subscriptions” window. That offered options of cancelling, but also of seeing the user’s Stripe subscription. I clicked on “cancel,” just to see what would happen, and I’m enclosing the result here:

As you can see (I hope), the user still has a comped subscription. And the subscription still exists in Stripe. Cancelling seems to cancel at the end of the subscription period, rather than right now. To do so right now, I have to go to Stripe and cancel things there… which seems weird to me, to be honest, but maybe that’s the best I can hope to do?

Indeed, maybe that’s the source of the trouble: I cannot change the user’s comped status, because they still have a subscription on Stripe. Maybe, just maybe, there’s a way to cancel a renewal via the API, but even that doesn’t seem likely at this point.

So something odd is perhaps afoot here. My comped users don’t have Stripe subscriptions. Here’s how one of mine looks:

I think you’ve gotten that user into an odd state. Can you make another test user and try your flow again?

So, I just completely deleted the reuven@mandarinweekly.com e-mail address. No subscription in Stripe, nothing at all on my site or dashboard.

I then (re-)added them via my dashboard. And whadaya know, it’s precisely like yours, with just the “remove complimentary subscription” on there. No mention of Stripe anywhere.

So… you were totally right!

Maybe the problem is that I’m adding people via Zapier? Somehow, that’s not only giving them a comped subscription, but a Stripe one as well, which is making life difficult for me.

In Zapier, I check to see if the person is already in my user database. If so, then I update them:

But if not, then I create them:

In both cases, you can see that I’m telling Zapier (which is obviously a simplified version of the API) that I want to give them a “complimentary premium plan.” I suspect that the key is I’m giving them not just a “complimentary” plan, but also a “premium plan,” which is Stripe related.

1 Like

Yeah, so maybe you ditch Zapier and write your own?

1 Like

You may also want to have a thorough look (reveal ALL fields) through Zapier. I think there’s a setting for attempting to match emails to Stripe subscribers (which might be to blame), but it’s possible I’m mis-remembering.

1 Like