Skip to main content
In this guide you’ll create a checkout session, redirect a customer to the hosted checkout page, and confirm the payment through a webhook. By the end you’ll have a working one-time purchase flow that you can drop into your app. A checkout session ties a price to a customer and returns a hosted checkout_url. Bachs resolves pricing, handles currency conversion, selects payment methods, and processes the payment. You send the customer to the URL and listen for the result. customer is optional. Pass one and it’s attached at creation; omit it and the hosted page collects the buyer’s email and name before they can pay. See Attach a customer. You price a session in one of two ways, and supply exactly one:
  • product_cart: one or more catalog products (each item may override its price with pricing).
  • pricing: a raw amount and currency, with no product. See Charge a raw amount below.

Before you start

Everything below uses the sandbox base URL, so you can run the whole flow without moving real money.

Steps

1

Create the checkout session

Call POST /v1/checkout-sessions with the products the customer is buying and their details. You get back a checkout_url.
The total charged is the sum of unit_amount × quantity across the cart. All products in the cart must share the same primary currency.
2

Redirect the customer

Send the customer to the checkout_url from the response. Bachs renders the hosted page, collects payment, and handles currency conversion.
Redirect
When the customer finishes, Bachs redirects them to your success_url with ?checkout_id= appended. If they cancel or abandon the checkout, they are sent to your cancel_url.
3

Confirm the payment with a webhook

Don’t rely on the redirect alone. When the payment resolves, Bachs sends a collection.succeeded event to your webhook endpoint. Use it to fulfill the order.
collection.succeeded
Redirects can be lost if the customer closes the tab. Treat the webhook as the source of truth for fulfillment, and verify its signature before trusting it. See Set up webhooks.
Provide exactly one pricing source: product_cart or pricing. Never both, and never neither.

Charge a raw amount

When your pricing is computed at order time and you have no product to reference, pass a pricing object instead of a cart. Bachs creates the checkout from the amount alone, with no products and no catalog. It’s the right choice when your pricing is dynamic, computed at order time, or when you want to accept a payment without defining products in Bachs first.
The rest of the flow is identical: you get a checkout_url, redirect the customer, and confirm with the collection.succeeded webhook.

Pricing intent

Set pricing.currency to USD and Bachs converts to the customer’s local currency at the prevailing FX rate, letting them pay in any currency you support. Set it to any other supported fiat currency instead and the customer pays in that currency only. Add currency_options to lock in exact amounts for specific currencies instead of relying on the live FX rate. Bachs uses the override when the customer selects that currency, and falls back to FX for any currency not listed. Keys must be fiat currency codes you support, and cannot include currency itself.
See the create checkout session reference for the full pricing field list, including per-currency minimums.

Override a product price

To sell a catalog product at a different price for one checkout, set pricing on the cart item. Bachs charges the override instead of the catalog price, without creating a new product. It works on any product, including fixed-price ones, and supports the same price types as catalog prices (fixed, custom, and free). The override is in the product’s primary currency and applies to that checkout only.
A custom override lets the buyer pick the amount on the hosted page, within your bounds. On a custom ui_mode (server-to-server) checkout there is no hosted page, so supply the buyer’s chosen amount as the cart item’s amount.
For a one-time product the override is snapshotted on the checkout, so it never enters your price list. For a recurring product it becomes the subscription’s price for its whole life; Bachs mints an archived, one-off price the subscription ties to when it activates, kept out of your catalog price list.

Attach a customer

Pass a new customer by email (Bachs creates or matches one), an existing customer by ID, or omit customer entirely and let the buyer identify themselves on the hosted page.
customer is required for recurring products. A cart containing a product with a billing_cycle becomes a subscription checkout, which needs a durable identity to bill on renewal, and there is no opportunity to collect one later. Omitting customer returns 400.

Guest checkout

Omit customer on a standard hosted checkout and Bachs collects the buyer’s email and name on the checkout page itself. By default they do not join your customer directory: you receive their email and name on customer_details, and customer stays null. Send customer_creation: "always" if you want them on file as a customer. Until the buyer supplies their identity, GET /v1/checkout-sessions/{checkout_id} and the list endpoint return "customer": null and "customer_details": null, and the hosted page’s own pricing and payment calls return 400, since there is no one to bill yet. A malformed email returns 400 with error_code: "VALIDATION_ERROR".

Whether a guest becomes a customer: customer_creation

customer_creation decides whether collecting an identity on the hosted page also creates a customer record. Send it when you create the checkout.
The default keeps your directory to people you have an ongoing relationship with, which is usually what you want for one-time sales. Reach for always when you want every buyer on file, for example to look them up through the API or to bill them again by hand.
Under always, a buyer whose email already matches a customer you hold attaches to that record rather than creating a second one, so their payment history stays in one place. Matching is on the email alone, and an email typed on the checkout page is not verified, so anyone who knows one of your customers’ addresses can have their purchase recorded against that customer. Under if_required this cannot happen: a buyer who shares an email with one of your customers stays entirely separate from them.
customer_creation is ignored for a subscription or a setup-mode checkout. Both always create a customer, whatever you pass, because recurring billing needs a durable record to keep the renewal card on. If you set customer_creation: "if_required" everywhere and still see new customers appearing, this is why.
See customer vs customer_details for exactly which of the two you get in each case.

Recurring products

You don’t create subscriptions directly. If a product in the cart has a billing_cycle, Bachs turns the checkout into a subscription checkout automatically: when the customer pays, it saves their card, bills the first cycle (or defers it for a trial), and creates the subscription. On completion you receive collection.succeeded and customer.subscription.created + invoice.paid. Subscriptions are USD card only today. See Subscriptions for how they renew and how to manage them.

Common options

Restrict payment methods

By default a checkout offers every payment method your account is enabled for. Pass payment_method_types to show less than that. Each entry is an exact payment-method corridor, not a payment type. Card, bank transfer, and mobile money are each split into one corridor per currency: USD_CARD and NGN_CARD are separate corridors, as is each of the nine mobile money corridors. You restrict to a currency by choosing which corridors to list, not by filtering a shared card entry. A corridor you leave out is not offered at all.
That checkout offers USD cards and NGN bank transfer, and nothing else. NGN cards are not offered, because NGN_CARD is not listed. Mobile money and crypto do not appear at all, because no corridor for them is named. A restriction only ever narrows. It cannot offer a corridor your account is not enabled for, and listing only corridors you cannot accept leaves the checkout with no way to pay — the request fails with CHECKOUT_RESTRICTION_LEAVES_NO_PAYMENT_METHOD.

The corridors

Do not guess which corridors your account can accept. Payment method support lists every corridor and whether it is enabled for you. CRYPTO is one corridor covering every asset and network we support. The customer picks the one they want to pay with; whichever they choose, you are credited in USD.
Restricting only ever removes options. It cannot add a corridor or a currency your account is not already enabled for, and it cannot bring back something another rule has already ruled out. If you restrict a checkout to a corridor your account is not enabled for, that corridor stays absent and the request still succeeds.If the restriction leaves nothing payable, the request fails with 400 rather than creating a checkout the customer cannot complete.
Naming a currency the corridor cannot process is rejected with 422, as is an empty currencies array. To turn a corridor off, leave its key out rather than passing an empty list. The same field works on payment links, where it restricts every checkout the link creates.

Testing

Use a sandbox key (sk_sandbox_...) to run the whole flow against test-mode products without moving real funds. See Sandbox.

Next steps