Skip to main content
You sell something people download: an ebook, a template pack, a course or a software licence. In this guide you’ll create your products, send a buyer to checkout without asking them to sign up, and release the download only after Bachs confirms the payment. By the end you’ll have a store that charges a fixed price or lets the buyer name their price, and that delivers each order exactly once.

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_...) with products:write and payments: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.
Every request goes to 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 amount. A pay-what-you-want product uses price_type: "custom", with a minimum_amount and a preset_amount that the checkout shows first.
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.
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 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
A 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
Put the signing secret it prints (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:
  1. Verify the signature on the raw body.
  2. Find the order from data.reference, or from data.checkout_id.
  3. If the order is already paid, stop.
  4. Retrieve the checkout from Bachs. Continue only if its status is completed. If it isn’t yet, answer 503 so Bachs sends the event again later.
  5. Check that the checkout’s currency and amount match 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.
  6. Mark the order paid and record the event ID, in one database transaction.
app/api/webhooks/bachs/route.ts
The checkout’s 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.
Never deliver from the success_url page. The buyer can open it without paying. The order page should only read the order your webhook saved, and show the download once it is paid.
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

  1. Start your store and bachs listen.
  2. 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.
  3. Watch checkout.completed and collection.succeeded arrive. The order turns paid and the download appears.
  4. Pay with 4000 0000 0000 0002 to see a declined card. The checkout stays open and nothing is delivered.
See Test payment outcomes for the other test cards.

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 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.
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 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.
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.
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.
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 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 and https://api.bachs.io.
  • success_url points to a public page on your store.
  • You registered your production webhook endpoint for checkout.completed, collection.succeeded and checkout.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