How it fits together
Your store owns the orders and the files. Bachs owns the payment. The store releases a file only when Bachs reports the checkout as completed, for the amount the order expects.What you’ll use
Before you start
- A sandbox API key (
sk_sandbox_...) withproducts:writeandpayments:write. See Authentication and Permissions. - The Bachs CLI, to forward webhooks to your machine. See Install the CLI.
- A server-side app. The examples use Node.js with the official Bachs SDK; every call is also a plain API request.
https://sandbox-api.bachs.io, so nothing moves real money while you build.
Steps
1
Create your products
Create one product for each thing you sell. A fixed-price product needs an Keep each product ID, and keep its price or minimum in your own catalog too. Your store checks the amount paid against it before it delivers anything. See Products.
amount. A pay-what-you-want product uses price_type: "custom", with a minimum_amount and a preset_amount that the checkout shows first.2
Save the order, then create the checkout
When a buyer clicks Buy, save an order first, with its own ID and an Idempotency-Key. Then create the checkout with the order ID as its A
reference. The browser sends only which product it wants; your server chooses the product ID.Leave out customer. The checkout page asks for the buyer’s email, so nobody needs an account with your store.app/api/checkout/route.ts
reference is unique for good on your account, even after its checkout expires, so one order has exactly one checkout. If the request times out, keep the order and its key: if the buyer paid, the webhook still names the order. See Idempotency.Bachs refuses
localhost and private network addresses for success_url and cancel_url, in the sandbox too. While you build on your own machine, leave them out and go back to your store yourself after paying.3
Forward webhooks to your machine
Terminal
whsec_...) in your environment as BACHS_WEBHOOK_SECRET. See Test webhooks locally.4
Deliver from the webhook, after checking with Bachs
checkout.completed is sent once a checkout is paid, and collection.succeeded once its payment succeeds. Either can arrive first, and either can arrive more than once. Handle both the same way, and deliver each order once:- Verify the signature on the raw body.
- Find the order from
data.reference, or fromdata.checkout_id. - If the order is already paid, stop.
- Retrieve the checkout from Bachs. Continue only if its
statusiscompleted. If it isn’t yet, answer503so Bachs sends the event again later. - Check that the checkout’s
currencyandamountmatch the order: the exact price for a fixed-price product, at least the minimum for pay what you want. If they don’t, hold the order for review instead of delivering it. - Mark the order paid and record the event ID, in one database transaction.
app/api/webhooks/bachs/route.ts
amount is the total in the product’s currency. For pay what you want, it is the price the buyer chose. Compare amounts as whole minor units, not floating-point numbers.5
Release the download
Give each order a long random secret, and serve the file only to a request that carries the order’s secret, for an order that is paid. Keep the files outside your public folder, so the only way to them is through that check.Email the buyer a link to their order page after payment. The buyer’s email is on the checkout’s
customer_details.6
Run it in the sandbox
- Start your store and
bachs listen. - Buy each product. Pay with any test card number, for example
4242 4242 4242 4242. For pay what you want, change the price on the checkout page. - Watch
checkout.completedandcollection.succeededarrive. The order turns paid and the download appears. - Pay with
4000 0000 0000 0002to see a declined card. The checkout stays open and nothing is delivered.
Webhooks to handle
You don’t need
collection.failed. It is sent when one payment attempt fails, but the checkout stays open and the buyer can try again.
Edge cases
The buyer pays and closes the tab
The buyer pays and closes the tab
The webhook still arrives and the order is marked paid. Email the buyer their order link, so they can get the file without returning to the same browser.
The same event arrives twice, or both events arrive at once
The same event arrives twice, or both events arrive at once
Delivery is at least once and not in order. Record each event ID with the order change in one transaction, and stop when the order is already paid. Whichever event arrives first delivers the order; the other changes nothing.
A payment arrives after the checkout expired
A payment arrives after the checkout expired
A buyer can still pay a checkout after you receive
checkout.expired, for example with a bank transfer sent before it expired. The checkout then completes and checkout.completed follows. Don’t treat expiry as final, and don’t reuse the order for a new checkout.The buyer pays less than the price
The buyer pays less than the price
A card always charges the full amount. A bank transfer can arrive short. The checkout then does not complete, so no fulfilment event is sent and the order stays unpaid. You can refund the amount received. For crypto, a short payment sends
collection.underpaid, and the buyer can send the rest.Someone tries to pay less than your minimum
Someone tries to pay less than your minimum
Bachs refuses a pay-what-you-want price below the product’s
minimum_amount. Your store checks the amount again anyway, and holds any order that does not match for review.A buyer wants a refund
A buyer wants a refund
Refund the payment with Issue a refund, using the
charge_id from collection.succeeded or from the checkout’s charge. A payment can carry one refund, so decide on a partial refund before you send it. Revoke the download when the refund is paid.The request to create the checkout times out
The request to create the checkout times out
The checkout may exist. Keep the order and its Idempotency-Key, and don’t create another checkout for it. If the buyer paid, the webhook names the order through its
reference.Go-live checklist
- Your account is verified. See Go live.
- You created your products again in production and updated their IDs. Sandbox and production share nothing.
- Your catalog prices match the products in Bachs.
- Your server uses an
sk_live_key andhttps://api.bachs.io. -
success_urlpoints to a public page on your store. - You registered your production webhook endpoint for
checkout.completed,collection.succeededandcheckout.expired, and put its signing secret in your environment. - Orders, event IDs and download secrets are in a real database, not a local file.
- Buyers receive an email with the link to their order.
Start from working code
The Next.js digital products starter is this guide as a working store: a fixed-price product, a pay-what-you-want product, guest checkout, delivery checked against Bachs, and secret download links, with tests for each rule on this page.Build it with an AI assistant
The one-time payment prompt builds this flow into your own app.Next steps
- Accept a payment: every checkout option, including restricting payment methods.
- Add an overlay checkout: keep buyers on your page while they pay.
- Sell in local currencies: show buyers prices in their own currency.
- Issue a refund: refund a purchase in full or in part.

