Comped Users Not Syncing to Stripe When Added via Ghost Admin but Sync When Added via API

I’ve noticed a strange inconsistency when adding complimentary (comped) users in Ghost. When I create a comped user via the Ghost Admin API, they appear in Stripe, and I can access their Stripe account info via the Ghost admin panel. However, when I add a comped user manually through Ghost Admin, they do not appear in Stripe, and the only action available for them in the admin panel is to remove their complimentary status.

I’m attaching two screenshots:

• The first shows a comped user added via the API, which includes more options in the menu (like viewing their Stripe account).

• The second shows a comped user added via the Admin UI, which lacks these options and does not appear in Stripe.

Has anyone else encountered this issue? Is this expected behavior?
According some forum posts from past it looks like Ghost were syncing comped users to Stripe when added via the Admin UI, does anyone know the reason why it works now only via Ghost admin API?

Thanks in advance!


That’s interesting. The admin dashboard is just calling the API, so you are likely calling the API with some parameter different than the dashboard is using. Snooping the network call (F12 and then network tab in your browser) while doing the action in the admin dashboard will probably be informative.

1 Like

Hi Cathy,

Thank you for your response! I’ll definitely compare those requests and let you know what I find.

I’m still a bit confused about the expected behavior—should comped users be created in Stripe at all? Since Ghost Admin doesn’t do this by default, I assume the expected behavior is that they shouldn’t appear in Stripe. Is that correct?

It seems to me that there’s no reason they should be in Stripe. They don’t have payment info, and you can’t bill them, so…?

If you can share the api call causing stripe user creation for comp users, that sounds like a bug!

1 Like

Thank you, Cathy.

I’ve inspected both scenarios, and here’s what I found:

In the Ghost Admin Dashboard, the process is slightly different. When creating a comped user, we first enter their name and email, which initially creates them as a free user. After that, we manually add them to a comped subscription.

Ghost Admin then sends a PUT request with the specific user ID in the URL to update that specific user.


//PUT http://127.0.0.1:2368/ghost/api/admin/members/67a1f41eff65e1337ab11a24/?include=tiers

{
  "members": [
    {
      "id": "67a1f41eff65e1337ab11a24",
      "email": "johndoe@example.com",
      "tiers": [
        {
          "id": "66f404a39362c0554a20b350",
          "expiry_at": "2025-08-06T00:00:00.000Z"
        }
      ]
    }
  ]
}

My scenario is to create a comped user directly via the API, without first creating a free member and then updating them.

I created a new user via a POST request using this payload:

//POST http://127.0.0.1:2368/ghost/api/admin/members/
{
    "members": [
      {
        "email": "johndoe@example.com",
        "name": "John Doe",
        "subscribed": true,
        "labels": [
          {
            "name": "from script",
            "slug": "from-script"
          }
        ],
        "subscriptions": [
          {
            "id": "",
            "tier": {
              "name": "The Deep Thinker Plus",
              "active": true
            },
            "plan": {
              "id": "",
              "nickname": "Complimentary",
              "interval": "year",
              "currency": "USD",
              "amount": 0
            },
            "status": "active",
            "current_period_end": "2026-02-07T00:00:00.000Z"
          }
        ],
        "comped": true,
        "status": "comped"
      }
    ]
}

This correctly creates the user as comped, but I noticed that a Stripe webhook is fired, and the user appears among Stripe subscribers, paying $0 per month.

I tried modifying the request to match the Ghost Admin PUT request, using Tiers instead.

//POST http://127.0.0.1:2368/ghost/api/admin/members/   
{
    email: "petr@example.com",
    name: "petr example",
    subscribed: true,
    labels: [{ name: "script", slug: "script" }],
    tiers: [
      {
        id: "66f877dd73c3c7db470fc69f", //tier id
        expiry_at: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).getTime(),
      },
    ],
    comped: true, 
    status: "comped"
  },

This approach almost worked, but for some reason, it subscribed the user to the requested tier as comped while also subscribing them to the first paid tier synced with Stripe. As a result, the user is now in two tiers—one correctly comped without Stripe and another mistakenly synced with Stripe.

This version is finally working, though I have no clue why. Simply commenting out comped: true somehow fixed the issue.

 //POST http://127.0.0.1:2368/ghost/api/admin/members/  
 {
    email: "petr@example.com",
    name: "petr example",
    subscribed: true,
    labels: [{ name: "script", slug: "script" }],
    tiers: [
      {
        id: "66f877dd73c3c7db470fc69f", //tier id
        expiry_at: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).getTime(),
      },
    ],
    //comped: true, -> this is causing subscribing user into first paid tier and sync user with stripe
    status: "comped"
  },

Is this really a bug, or just a relic from the past? I came across a discussion from 2022 mentioning that creating a comped user in the admin panel also added them to Stripe. If that was true back then, I wonder why the behavior has changed—and why it still works differently when done via code.

I’m particularly interested in this because I’m still trying to find a way to track the expiration of comped users, especially since the webhook for user updates doesn’t trigger when a comped subscription expires. I was hoping Stripe might offer a workaround.

Your reasoning about why it should work this way makes sense, but I’m just curious. I’ll leave this here in case someone can shed more light on it later. And thank you for your time Cathy.

1 Like

Consider adding an Ideas post about getting a webhook when a comp plan expires. That makes a lot of sense to me. :)

1 Like

I’ve encountered another unfortunate issue: all comped users created via the API, even with correctly set expiration times, expire approximately 15 hours after creation. It seems like I am relly hitting a wall here.
Any help would be apprecited.

So that’s … weird. If you mimic the two step admin panel flow (create them as free first), does it still happen? Here’s the exact comp request I’m seeing:

PUT TO https://cathys-second-demo-site.ghost.io/ghost/api/admin/members/67ab8XXXXe8/?include=tiers

{"members":[{
  "id":"67ab8eXXXXX8",
  "email":"YYYYY@gmail.com",
  "tiers":[{
    "id":"66a5236fe87f2c0001e025b6",
    "expiry_at":"2025-03-12T00:00:00.000Z"
  }]
}]}
1 Like

Hi Cathy,

Thank you again for your time. Yes, that came to my mind as well. I even inspected the SQLite database I’m using, and I don’t see any difference between a user created manually as comped and one created via the API.

There is a table called “member_products”, which contains an expiry_at field with a Unix timestamp set to the correct expiration time. However, on the Ghost instance where users were converted to FREE after 15 hours, this table (member_products) was emptied. I have no idea how or why that happened.

Another issue is that I can’t properly test this scenario. When I try changing my local PC’s time to simulate the future, nothing happens in Ghost. Even though the Ghost console picks up the correct time from my OS, the expiration process doesn’t trigger. Does this mean I really have to wait approx.15 hours between each attempt? That seems crazy.

Question: Is your Stripe in test mode? Stripe has subscription clocks where you can nominally move the clock forward on a subscription, but I’ve never gotten them to work quite right with Ghost. (Although I’ll fess up that I don’t think I’ve ever changed my local PC’s time.)

Oh, and if you don’t have webhooks set up going back to your local install, you’re not going to get updates from Stripe, so all sorts of weirdness may be going on. Directions for doing that are here: Stripe checkout not working in local development installation - #3 by daniel1

My general observation about Stripe in test mode is that subscription updates are glitchy at best. Stripe deletes subscriptions made in test mode at some point in the future, and there might be other functionality missing. If this is something you need to test, you probably want to spin up a pikapod install or something else fully routable, link it to a stripe account (/not/ your main one) in REAL mode, and see what happens there. Yeah, you’re still going to be waiting 15 hours, but if the problem doesn’t recur, you’ll know tomorrow that it’s a test-mode gremlin.

1 Like

The Stripe connection didn’t initially come to mind, but I just realized this issue is happening on a staging instance connected to test Stripe.

I just created a comped account on the client’s live website, which is connected to live Stripe. We’ll see if the same issue occurs there.

I initially thought there was no connection to Stripe because when I create a comped user via API on my local install (which is also connected to test Stripe), I don’t see any webhook events coming from Stripe at all.

The only difference I noticed in SQLite—though I doubt it’s related—is in the member_status_events table. It shows that comped users were initially created as free because Ghost Admin forces them to be created this way first. They are then updated to comped via a PUT request.

Stripe clocks works for me usually but I thought time change is valid only for particular stripe user, not comped ones or other users, I might try that though.

Yeah, if you don’t set up to get them (running stripe CLI and passing the wh_secret into Ghost as an environment variable), you don’t get them running locally.

1 Like

I mean - I use the WH_SECRET environment variable along with Stripe CLI, and I can see the standard Stripe subscription webhooks. However, these only apply to Stripe-paid users. When I add a comped user, there is no communication between Stripe and Ghost at all. So that is why I think it is not related. But I might try to move with clocks in Stripe to see if that will have any impact on comped users.

Right, sorry. I was remembering back to your earlier post where your comped users were showing up in stripe, and wondering if stripe was then trying to cancel their non-subscriptions or something weird…

1 Like

Yeah that was another issue which I don’t want to bring into discussion, but it seems to be solved, my comped users created via API looks same as those created via Ghost Admin now.

I’ve tried change clock at one stripe subscriber, but it has impact only on that particular member.