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 needsproducts:write,payments:writeandcustomers: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.
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 Repeat with
billing_cycle is recurring, and its cadence cannot change after you create it, so a monthly and a yearly plan are two products."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.2
Create a checkout session from your server
When a signed-in user clicks Subscribe, your server creates a checkout session and returns its The same call as a route in your app:In the browser, send the user to
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.app/api/checkout/route.js
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:The CLI prints a signing secret (
Terminal
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.Then decide access from the saved status, everywhere in your app: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
app/api/webhooks/bachs/route.js
lib/billing.js
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.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 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.
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
6
Run the whole loop in the sandbox
With your app and
bachs listen running:- Sign in to your app, click Subscribe, and pay with the test card shown on the sandbox checkout page.
- Watch the CLI:
customer.subscription.created,invoice.paidandcustomer.subscription.updatedarrive, and your route answers200. - Check that the user now has access and that their
customerIdis saved. - 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.updatedwithcancel_at_period_end: true, and the user keeps access untilcurrent_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 customer pays and closes the tab
The customer pays and closes the tab
The webhook still arrives. That is why access comes from the webhook and your success page reads from your own database.
The same event arrives twice
The same event arrives twice
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.Your webhook endpoint was down
Your webhook endpoint was down
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.A renewal payment fails
A renewal payment fails
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.The customer cancels
The customer cancels
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.The customer upgrades, downgrades or switches to yearly
The customer upgrades, downgrades or switches to yearly
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.
You change your prices
You change your prices
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.
A user who already subscribes clicks Subscribe again
A user who already subscribes clicks Subscribe again
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 andhttps://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
- Subscriptions: statuses and how renewals work.
- Offer a free trial: add a trial before the first charge.
- Manage subscriptions: change plans, update metadata and cancel through the API.
- Customer portal: what customers can do and how to configure it.
- Charge in any currency: sell one-time products in your customers’ currencies.
- Build a platform for businesses: let your own customers take payments through you with Connect.

