Why defer a downgrade?
A customer clicks “switch to a cheaper plan.” If you apply that change instantly, you either claw back what they already paid or hand them service they didn’t pay for. Neither feels fair. The cleaner model: let the customer keep everything they paid for until the current period ends, then flip them to the lower tier at renewal. The hard part is the timing — you need something to fire an API call days or weeks from now, exactly when the period closes. This guide wires up that timer with Upstash QStash, a serverless message queue that holds a delayed HTTP request and delivers it on schedule. No always-on worker, no cron box to babysit. Two endpoints and a signed callback do the whole job.Because Macropay is the merchant of record, the plan change you trigger here is also reflected on the customer’s invoice and statement — and any sales tax or VAT on the new price is recalculated and remitted by Macropay automatically. You schedule the downgrade; the tax math takes care of itself.
How the flow works
- Your
schedule-downgraderoute reads the subscription’scurrentPeriodEndand asks QStash to call you back at that moment. - When the time arrives, QStash POSTs to
execute-downgradewith a signed request. - That route re-checks the subscription is still active, then applies the new product.
What you’ll need
- A Macropay access token
- An Upstash QStash account and token
- A publicly reachable HTTP endpoint (QStash must be able to reach it to deliver the callback)
- A backend runtime — the examples use Node.js with Next.js, but any language works
Step 1: Configure credentials
Keep secrets in environment variables, never in source.Terminal
Grab your token and signing keys from the QStash section of the Upstash Console.
Step 2: Schedule the callback
This route fetches the subscription, computes the delay until the period closes, and hands QStash a message to deliver to your execution endpoint.app/api/schedule-downgrade/route.ts
Step 3: Execute the downgrade on the callback
QStash calls this route at the scheduled moment. Before mutating anything, it re-validates the subscription — a lot can change in a billing cycle, and a stale message shouldn’t clobber a subscription that the customer already cancelled or re-upgraded.app/api/execute-downgrade/route.ts
Status codes are your retry contract
The status code you return tells QStash what to do next. Get this wrong and you’ll either drop downgrades or replay them forever.Step 4: Test the full loop
1
Run against sandbox
Point the SDK at the sandbox environment so test downgrades never touch live customers or real money.
2
Queue a downgrade a few minutes out
You don’t want to wait a full billing cycle to test. Trigger the scheduler against a test subscription and let it compute a short delay.
3
Watch it in the QStash console
Open the Upstash Console to confirm the message is queued, then watch it transition to delivered when the delay elapses.
4
Confirm the plan switched
After the callback fires, read the subscription back and check the product.
Adapting the pattern
The same schedule-then-execute shape covers more than tier downgrades:- End-of-trial conversions — queue the callback for the trial’s end and swap the customer onto the paid product.
- Scheduled cancellations — call
subscriptions.update(or your cancel flow) at period end instead of a product swap. - Usage-based plan moves — pair the boundary with Macropay’s metered billing so the customer’s meters reset cleanly on the new plan.