Membership Upgrades
An active user moves to a higher tier mid-cycle by paying the prorated difference until their next billing date. Error bodies are the ErrorResponse schema (error_code 8 with an error_string) in the API Specification; the M… names below identify the branch that produced them.
Tier Configuration
Tiers come from the GrowthBook feature global.tiers.config (Feature Flags), a map of lower-case tier name to configuration:
{
"plus": {
"current_version": "v1",
"versions": [
{ "version_name": "v1", "display_name": "Plus", "price": { "monthly": 9.99 } }
]
}
}
Requests name the tier in lower case (plus); membership records store it upper-cased (PLUS) with the version_name in force at the time. The BASE tier costs $4.99 per month and is the baseline of every proration.
Proration
GET /{user_id}/user/membership/upgrade/proration?upgrade_tier=<tier> returns the exact charge for an upgrade so the app can show it before confirming.
-
Resolve the tier and its current version (
400 M8_INVALID_TIER/M9_TIER_VERSION_NOT_FOUND). -
Fetch the next billing date from the Subscription Service (
500 M7_BILLING_DETAILS_FETCH_FAILED). -
days= billing date minus today, both truncated to midnight UTC.days ⇐ 0yields a charge of 0.days > 65yields the error sentinel (400 M10_PRORATION_CALCULATION_FAILED): the subscription was pushed far into the future by MX and cannot be upgraded through this flow. -
cents=round_half_even((monthly_price - 4.99) / 30 * days, 2), computed in decimal arithmetic.
The response carries the amount in dollars and in cents (proration_amount_int), the tier, the billing date and days_until_billing. Metric user.membership.upgrade.proration.
Source: pkg/api/membership_tiers.go (CalculateProrationAmountAt)
Upgrade
POST /{user_id}/user/membership/upgrade with upgrade_tier and the proration amount (upgrade_amount_int in cents, preferred; otherwise upgrade_amount in dollars).
-
Load the tier configuration (
500 M2_CONFIG_FETCH_FAILED). -
The user must exist (
404 M3_USER_NOT_FOUND) and beACTIVE(403 M4_USER_NOT_ACTIVE); a membership must exist (404 M5_MEMBERSHIP_NOT_FOUND). -
A membership pending a downgrade whose current tier equals
upgrade_tiertakes the retraction path instead. -
Fetch the primary debit card from the Payments Service (
500 M6_DEBIT_CARD_NOT_FOUND) and the billing details from the Subscription Service (500 M7_BILLING_DETAILS_FETCH_FAILED). -
Resolve the tier version (
400 M8/M9), compute the proration (400 M10when negative) and require the request amount to equal it (400 M11_PRORATION_AMOUNT_MISMATCH). -
Charge the card through the Payments Service with a card override for the primary card, a new
prorated_subscription_idandbypass_retry_limit: true(Retry-Limit Bypass): a call failure is500 M12_PAYMENT_SUBMISSION_FAILED, a failed payment is402 M13_PAYMENT_DECLINED. -
Move the subscription to the tier and version in the Subscription Service. On an error or a non-201 the charge is refunded:
500 M17_UPGRADE_FAILED_REFUND_ISSUEDwhen the refund succeeds,500 M16_REFUND_FAILEDwhen it does not (the user was charged and nothing was upgraded). -
Write the membership record:
<TIER>/MONTHLYat the version, eventMEMBERSHIP_UPGRADE, the caller-derived source, the confirmation id (500 M18_MEMBERSHIP_RECORD_CREATE_FAILED— the charge and subscription change stand and need reconciliation). -
Write the prorated subscription row and send the Iterable receipt
subscription-prorated-digital-receipt; both are best effort. -
201withconfirmation_idand the new membership. Metricuser.membership.upgradewithfrom_tier:andto_tier:.
Upgrading Back During a Pending Downgrade
When the current membership has is_pending_downgrade set and the requested tier is the tier the user still holds, the upgrade cancels the downgrade instead of charging: the Subscription Service upgrade is called with an empty confirmation id, and the membership is re-written with event MEMBERSHIP_RETRACT, is_pending_downgrade false and downgrade_date cleared. The response is 201 with an empty confirmation_id. The check reads the durable is_pending_downgrade flag, so a pause and unpause between the downgrade and the retraction does not hide it.
Retry-Limit Bypass
The Payments Service enforces a pinless-debit retry limit on automated collections and refuses blocked submissions with status: FAILED and a block_reason. The upgrade charge is member-initiated and is submitted with bypass_retry_limit: true, so a FAILED response is always a processor decline and maps to M13_PAYMENT_DECLINED; any block_reason that does appear is logged with the decline. Bypassed attempts are still recorded in the Payments Service’s retry-limit ledger, so a declined upgrade charge consumes decline budget for automated float and subscription collections on the same card and amount.