--- title: "How to schedule subscription downgrades with Upstash QStash" sidebarTitle: "Schedule downgrades with QStash" description: "Learn how to automatically schedule subscription downgrades at the end of the current billing period using the Polar API and Upstash QStash." --- ## Overview When managing subscriptions, you may want to schedule downgrades to take effect at the end of the current billing period rather than immediately. This ensures customers receive the full value of their current subscription while automatically transitioning to another tier when their billing period ends. ## Prerequisites - A [Polar API access token](/integrate/authentication) - An [Upstash QStash](https://upstash.com/qstash) account - A server endpoint that can receive HTTP requests - [Node.js](https://nodejs.org/en/blog/announcements/v20-release-announce) or your preferred backend language installed ## Step 1: Set Up Environment Variables Store your API credentials securely in environment variables. ```bash # Polar API credentials POLAR_MODE="sandbox" # can be "production" POLAR_ACCESS_TOKEN="polar_pat_..." # Upstash QStash credentials QSTASH_URL="https://qstash.upstash.io" QSTASH_TOKEN="ey...=" QSTASH_CURRENT_SIGNING_KEY="sig_..." QSTASH_NEXT_SIGNING_KEY="sig_..." # Your application URL APP_URL="https://localhost:3000" ``` You can find your QStash token in the [Upstash Console](https://console.upstash.com/qstash) under the **QStash** section. ## Step 2: Fetch Subscription Details Use the Polar API to get the subscription's current period end date. This determines when the downgrade should be scheduled. ### Get Subscription by ID ```tsx import { Polar } from "@polar-sh/sdk"; const polar = new Polar({ accessToken: process.env.POLAR_ACCESS_TOKEN }); // Fetch subscription details const subscription = await polar.subscriptions.get({ id: "sub_xxxxxxxxxxxxx", }); console.log("Current period ends at:", subscription.currentPeriodEnd); console.log("Subscription status:", subscription.status); console.log("Current product:", subscription.product.name); ``` ## Step 3: Schedule the Downgrade with QStash Use Upstash QStash to schedule an HTTP request to your downgrade endpoint at the end of the billing period. ### Create the Scheduling Function ```tsx import { Client } from "@upstash/qstash"; const qstash = new Client({ token: process.env.QSTASH_TOKEN! }); async function scheduleDowngrade( subscriptionId: string, newProductId: string, executeAt: Date ) { // Schedule the downgrade request const result = await qstash.publishJSON({ // Add retries in case of failures retries: 1, body: { subscriptionId, newProductId }, // Schedule for the end of the current period notBefore: Math.floor(executeAt.getTime() / 1000), url: `${process.env.APP_URL}/api/execute-downgrade`, }); console.log("Downgrade scheduled:", result.messageId); return result.messageId; } ``` ### Complete Example: Schedule Downgrade Endpoint Here's a complete Next.js API route that schedules a downgrade: ```tsx title="app/api/schedule-downgrade/route.ts" import { Polar } from "@polar-sh/sdk"; import { Client } from "@upstash/qstash"; import { NextRequest, NextResponse } from "next/server"; const polar = new Polar({ server: process.env.POLAR_MODE, accessToken: process.env.POLAR_ACCESS_TOKEN, }); const qstash = new Client({ token: process.env.QSTASH_TOKEN, }); export async function POST(req: NextRequest) { try { const { subscriptionId, newProductId } = await req.json(); // Step 1: Fetch current subscription details const subscription = await polar.subscriptions.get({ id: subscriptionId, }); if (!subscription.currentPeriodEnd) { return NextResponse.json( { error: "Subscription has no current period end date" }, { status: 400 } ); } // Step 2: Calculate when to execute the downgrade const executeAt = new Date(subscription.currentPeriodEnd); // Step 3: Schedule the downgrade with QStash const result = await qstash.publishJSON({ retries: 1, body: { subscriptionId, newProductId, customerId: subscription.customerId, }, url: `${process.env.APP_URL}/api/execute-downgrade`, delay: Math.floor((executeAt.getTime() - (new Date()).getTime()) / 1000), }); // Step 4: Optionally store the scheduled task // You might want to store this in your database console.log(`Scheduled downgrade for subscription ${subscriptionId}`); console.log(`Will execute at: ${executeAt.toISOString()}`); console.log(`QStash message ID: ${result.messageId}`); return NextResponse.json({ success: true, messageId: result.messageId, scheduledFor: executeAt.toISOString(), }); } catch (error) { console.error("Failed to schedule downgrade:", error); return NextResponse.json( { error: "Failed to schedule downgrade" }, { status: 500 } ); } } ``` Store the QStash `messageId` in your database along with the subscription ID. This allows you to track or cancel scheduled downgrades if needed. ## Step 4: Create the Downgrade Execution Endpoint Create an endpoint that QStash will call at the scheduled time to execute the downgrade. Here's a completed Next.js API route for downgrade: ```tsx title="app/api/execute-downgrade/route.ts" import { Polar } from "@polar-sh/sdk"; import { NextRequest, NextResponse } from "next/server"; import { verifySignatureAppRouter } from "@upstash/qstash/nextjs"; import { SubscriptionProrationBehavior } from "@polar-sh/sdk/models/components/subscriptionprorationbehavior.js"; const polar = new Polar({ server: process.env.POLAR_MODE, accessToken: process.env.POLAR_ACCESS_TOKEN, }); async function handler(req: NextRequest) { try { const { subscriptionId, newProductId, customerId } = await req.json(); console.log(`Executing downgrade for subscription ${subscriptionId}`); // Fetch the subscription to verify it's still active const subscription = await polar.subscriptions.get({ id: subscriptionId, }); // Verify subscription is still active if (subscription.status !== "active" && subscription.status !== "trialing") { console.log(`Subscription ${subscriptionId} is not active, skipping downgrade`); return NextResponse.json({ success: false, reason: "Subscription is not active", }); } // Check if already on the target product if (subscription.productId === newProductId) { console.log(`Subscription already on product ${newProductId}`); return NextResponse.json({ success: true, reason: "Already on target product", }); } // Execute the downgrade const updatedSubscription = await polar.subscriptions.update({ id: subscriptionId, subscriptionUpdate: { productId: newProductId, prorationBehavior: SubscriptionProrationBehavior.Invoice, }, }); console.log(`Successfully downgraded subscription ${subscriptionId}`); console.log(`New product: ${updatedSubscription.product.name}`); return NextResponse.json({ success: true, subscription: updatedSubscription, }); } catch (error) { console.error("Failed to execute downgrade:", error); // Return 200 to prevent QStash retries for certain errors if (error instanceof Error && error.message.includes("not found")) { return NextResponse.json( { success: false, error: "Subscription not found" }, { status: 200 } ); } // Let QStash retry for other errors return NextResponse.json( { error: "Failed to execute downgrade" }, { status: 500 } ); } } // Verify QStash signature to ensure requests come from QStash export const POST = verifySignatureAppRouter(handler); ``` **Security**: Always verify QStash signatures to ensure requests are coming from QStash and not malicious actors. The `verifySignatureAppRouter` function handles this automatically. ## Step 5: Test Your Integration Test the complete flow to ensure downgrades are scheduled and executed correctly. Use Polar's [sandbox environment](/integrate/sandbox) to test without affecting production data. ```tsx const polar = new Polar({ // Use sandbox for testing server: process.env.POLAR_MODE, accessToken: process.env.POLAR_ACCESS_TOKEN, }); ``` Create a test subscription and schedule a downgrade for a few minutes in the future. ```tsx // Schedule downgrade 5 minutes from now for testing const executeAt = new Date(Date.now() + 5 * 60 * 1000); const result = await fetch("/api/schedule-downgrade", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ subscriptionId: "sub_test_xxxxx", newProductId: "prod_basic_xxxxx", }), }); ``` Check the [Upstash Console](https://console.upstash.com/qstash) to see scheduled messages and their status. After the scheduled time, verify the subscription was downgraded: ```tsx const subscription = await polar.subscriptions.get({ id: "sub_test_xxxxx", }); console.log("Current product:", subscription.product.name); ```