Skip to main content
You run a software product and want customers to pay for it every month or every year. In this guide you’ll create your plans, send a signed-in user to checkout, give them access when Bachs tells you the subscription is active, and let them change or cancel their plan in the customer portal. By the end you’ll have the complete subscription loop running in the sandbox, with three routes on your server and no billing logic of your own.
Subscriptions bill USD cards today. Free trials are in beta. See Subscriptions and Trials.

How it fits together

Your app owns two things: who the user is, and whether they have access. Bachs owns everything about money: the card, the renewals, the retries, the receipts and the cancellation flow. The webhook is where the two meet.

What you’ll use

Before you start

  • A sandbox API key (sk_sandbox_...). See Authentication. If you restrict the key’s permissions, this build needs products:write, payments:write and customers:write.
  • The Bachs CLI, to forward webhooks to your machine. See Install the CLI.
  • An app with signed-in users. The examples use Next.js route handlers, but any server framework works the same way.
Every request below goes to https://sandbox-api.bachs.io, so you can run the whole flow without moving real money. Keep these values in your server’s environment, never in the browser:
.env

Steps

1

Create your plans

Create one product for each plan and cadence. A product with a billing_cycle is recurring, and its cadence cannot change after you create it, so a monthly and a yearly plan are two products.
Repeat with "name": "Pro (yearly)", "amount": "290.00" and "interval": "year". Put both IDs in your environment as BACHS_PRO_MONTHLY and BACHS_PRO_YEARLY. You can also create products in the dashboard; the IDs work the same way.
To offer a free trial, add "trial_period": { "interval": "day", "frequency": 14 } to the product. The card is saved at checkout and the first charge happens when the trial ends. A trial checkout sends customer.subscription.created with status trialing, and the first invoice.paid arrives when the trial ends. See Offer a free trial.
2

Create a checkout session from your server

When a signed-in user clicks Subscribe, your server creates a checkout session and returns its checkout_url. The browser sends only the plan name. Your server decides which product that means, so nobody can change the price from the browser.A subscription checkout needs a customer, because Bachs must know who to charge on every renewal. Send the user’s email the first time, and their saved customer_id after that. Put your own user ID in metadata: Bachs copies a subscription checkout’s metadata onto the subscription, so every subscription webhook tells you which of your users it belongs to.
The same call as a route in your app:
app/api/checkout/route.js
In the browser, send the user to checkout_url. To keep them on your page instead, open the same URL in the overlay checkout with Bachs.Checkout.open({ checkoutUrl }).
3

Forward webhooks to your machine

Start the CLI and forward the events this build uses to your local webhook route:
Terminal
The CLI prints a signing secret (whsec_...) for this session. Put it in BACHS_WEBHOOK_SECRET. Your verification code is the same code you run in production; only the secret is different. See Test webhooks locally.
4

Give access from the webhook

Your webhook route does three things, in this order: verify the signature against the raw body, skip events you have already processed, then save the subscription.
app/api/webhooks/bachs/route.js
Then decide access from the saved status, everywhere in your app:
lib/billing.js
Each subscription event carries the whole subscription, so save its full state instead of applying changes one by one. Events are not guaranteed to arrive in order, so keep each event’s created_at with the record and skip an event that is older than the one you saved. If you ever need to be certain of the current state, read it with Retrieve a subscription.If saving fails, let the route return a 5xx so Bachs retries the delivery. A 4xx response other than 408 or 429 is not retried, so use 400 only for a signature that does not match.
Never give access from the success_url redirect. The customer can close the tab before it loads, and anyone can open the URL. Your success page should read the user’s subscription from your own database and show “Activating your plan…” until the webhook has arrived.
5

Let customers manage their plan

Add a Manage billing button that calls a route on your server. The route creates a portal session for the user’s customer_id and redirects to its url. In the portal, customers see their subscriptions and invoices and can cancel. A customer whose renewal failed can always update their card there. Updating a card at any other time, and switching plans, are off until you turn them on in your portal settings. Create a new session on every click, because sessions are short-lived.
app/api/billing/portal/route.js
Whatever the customer changes in the portal reaches you as the same webhooks as before, so the route from the previous step already handles it.
6

Run the whole loop in the sandbox

With your app and bachs listen running:
  1. Sign in to your app, click Subscribe, and pay with the test card shown on the sandbox checkout page.
  2. Watch the CLI: customer.subscription.created, invoice.paid and customer.subscription.updated arrive, and your route answers 200.
  3. Check that the user now has access and that their customerId is saved.
  4. Click Manage billing and cancel the plan in the portal. By default the portal cancels at the end of the period: you receive customer.subscription.updated with cancel_at_period_end: true, and the user keeps access until current_period_end.
bachs trigger does not emit subscription events yet. To test this flow, complete a real sandbox checkout as above.

Webhooks to handle

A subscription checkout also emits collection.succeeded and checkout.completed. You don’t need them for this build.

Edge cases

The webhook still arrives. That is why access comes from the webhook and your success page reads from your own database.
Delivery is at least once. Store each event’s id and skip IDs you have already processed. Saving the full subscription state also makes a repeated event harmless.
Bachs retries a failed delivery several times over about 80 minutes, then stops. If your endpoint was down for longer, read the current state of your subscriptions with List subscriptions, or redeliver past events with bachs events replay.
The subscription moves to past_due and Bachs retries three times: 1 day after the failure, then 3 days later, then 5 days later. It emails the customer after each failed attempt, and from the second email on, the email includes a link to update their card. If a retry succeeds, the subscription is active again. If all retries fail, the subscription is canceled, or marked unpaid if you choose that in your subscription settings. See Payment recovery.
A cancellation from the portal, or from Cancel a subscription with cancel_at_period_end: true, keeps the subscription working until current_period_end. You receive customer.subscription.updated now and customer.subscription.deleted when it ends. Send cancel_at_period_end: true explicitly when you cancel through the API: without it, the cancellation is immediate. An immediate cancellation sends customer.subscription.deleted at once and does not refund automatically. See Manage subscriptions.
Customers can switch plans in the portal once you turn on plan switching and list the products they may move to in your portal settings. You can also change the plan through the API. The price difference is handled for you. See Proration.
Existing subscribers keep the amount they signed up with. A new price applies to new subscriptions only. To move an existing subscriber, change their plan.
Check your own records before you create a checkout. If the user already has a subscription with access, send them to the portal instead.

Go-live checklist

  • Your account is verified. See Go live.
  • You created your products again in production and updated the product IDs. Sandbox and production share nothing.
  • Your server uses an sk_live_ key and https://api.bachs.io.
  • You registered your production webhook endpoint for the five events above and put its signing secret in your environment. See Set up webhooks.
  • You chose what happens when payment retries run out. See When recovery is exhausted.
  • You configured what customers can change in the portal, including plan switching if you offer it. See Configuring the portal.

Start from working code

The live demo runs this flow end to end on the sandbox: a server route creates checkout sessions for a fixed set of products, the overlay opens them, and webhooks drive fulfilment. See how it is built.

Build it with an AI assistant

Give your assistant this prompt. It points the assistant at this page; if your assistant cannot open web pages, use Copy page at the top of this page and paste it in as well. For more prompts and the rules to add to your project, see Build with AI.
Prompt

Next steps