Skip to main content

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

  1. Your schedule-downgrade route reads the subscription’s currentPeriodEnd and asks QStash to call you back at that moment.
  2. When the time arrives, QStash POSTs to execute-downgrade with a signed request.
  3. 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
Save the returned messageId next to the subscription in your database. It’s your handle for cancelling a scheduled downgrade if the customer changes their mind, and a useful audit trail when support asks “why did this plan switch?”

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
Your execution endpoint is public, so treat any unsigned request as hostile. verifySignatureAppRouter checks the QStash signature against your signing keys and rejects anything else — without it, anyone who guesses the URL could downgrade a customer at will.

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.
In every case the principle holds: honor the period the customer already paid for, then make the change land precisely at the boundary.