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"
},