`s stay **empty** in your HTML - the SDK owns their
contents. Write only the tag and the attribute.
- Do not read, replace, or attach behavior to the contents the SDK mounts.
Those descendants belong to the SDK or payment provider and may change.
Customize checkout through your own surrounding HTML, the public
`CfSdk.checkout.*` API, CSS design tokens, and the supported class hooks
documented below.
- Labels, IDs, native `required`, and `autocomplete` tokens belong to the host
page's controls. The SDK-generated address form supplies those semantics for
its own controls and marks the state/region required only when the selected
country has a region list.
- **Best practice: always include the `shipping` element, even when today's
product is digital.** The SDK shows `shipping`/`billing` only while the cart
and gateway require them (a digital-only cart never shows an address form;
Stripe collects billing inside its own payment UI), so the element costs
nothing while hidden - and a product later changed to physical in the
funnel settings just works, with no page change. Omit it and that same
product change breaks checkout with a shipping-address error and nowhere
to enter one. The same always-include rule applies on OTO pages (see
"Physical-product OTOs"). They show/hide without any of your JS - wrap
them in sections whose visibility is pure CSS:
```css
#shipping-section { display: none; }
#shipping-section:has([data-cf-element="shipping"] *):not(:has([data-cf-element="shipping"][hidden])) { display: block; }
#billing-section { display: none; }
#billing-section:has([data-cf-element="billing"] *):not(:has([data-cf-element="billing"][hidden])) { display: block; }
```
- Adding `data-cf-visible` to the element keeps the address form visible at
all times, regardless of product type - for merchants who want to collect
shipping information on a digital product that doesn't require it:
`
`
- Contact identity fields (`email`, `first-name`, `last-name`, `name`,
`phone`) prefill automatically for a visitor moving through the funnel in
the same session - what they typed on an earlier opt-in step fills any
still-empty inputs here. Values you set (or the visitor types) always win.
Separate first/last values are combined for a single `name` input; a single
`name` value is not split into separate first/last inputs.
- On success there is **no success event** - the browser follows the server's
redirect to the funnel's next step. Failures surface through `onError`
(below) and the pay button re-enables.
## Put the product in the cart
The form markup gives the SDK somewhere to mount and submit payment, but the
SDK does not guess which attached product or price to sell. For a
single-product order form, the only custom JavaScript required is selecting
that line after the catalog loads:
```html
```
Stop here unless the page needs a product picker, quantities, order bumps,
custom totals, coupons, or shipping-option selection. The SDK owns payment
mounting, charging, 3-D Secure, and redirects.
## Advanced: customize the cart with JavaScript
`CfSdk` is the supported host-page API, not access to SDK internals. Use it to
change cart intent or render server-computed checkout state; do not manipulate
the SDK's mounted payment or address DOM.
`window.CfSdk` is callable as soon as the `sdk.js` script tag has executed
(calls queue until the SDK finishes booting). With `defer` on the script tag,
run your code on `DOMContentLoaded` or later.
| Call | Returns | Purpose |
| ---- | ------- | ------- |
| `CfSdk.products()` | `Promise<{products, bumpProducts}>` | The step's catalog - main products and order bumps |
| `CfSdk.checkout.setQuantity(variantId, priceId, qty)` | `Promise
` | Set a cart line (0 removes it) |
| `CfSdk.checkout.updateState({...})` | `Promise` | Partial update: `coupon_codes: []`, `shipping_address`, `billing_address` |
| `CfSdk.checkout.setShipping(optionId \| null)` | `Promise` | Pick a shipping option from `state.shipping.options` |
| `CfSdk.checkout.currentState()` | `Promise` | Read the current state |
| `CfSdk.checkout.onStateChange(fn)` | unsubscribe `fn` | Fires on **every** committed state - your single render path |
| `CfSdk.checkout.onPending(fn)` | unsubscribe `fn` | A mutation or charge is in flight |
| `CfSdk.checkout.onError(fn)` | unsubscribe `fn` | A state carrying errors was committed |
| `CfSdk.checkout.onOtoOutcome(fn)` | unsubscribe `fn` | OTO pages only - see "One-time offers" |
`CfSdk.products()` resolves with the catalog (empty arrays - never a
rejection - when checkout isn't configured for the page, alongside the console
warning above):
```jsonc
{
"products": [
{ "id": "JWkEqJ", "name": "The Course",
"variants": [
{ "id": "nJdDJd", "name": "Default", "product_type": "digital",
"prices": [
{ "id": "BNAqxN", "name": null, "amount": "149.0", "currency": "usd",
"payment_type": "one_time", "interval": null, "interval_count": null }
] }
] }
],
"bumpProducts": [] // same Product shape; a bump may carry "bump_preheadline"
}
```
Every mutator resolves with the same `CheckoutState` record, priced by the
server:
```jsonc
{
"line_items": [
{ "variant_id": "nJdDJd", "price_id": "BNAqxN", "quantity": 1,
"name": "The Course", "unit_amount": 14900, "line_amount": 14900 }
],
"totals": { "subtotal": 14900, "discount": 0, "tax": 0, "shipping": 599,
"total": 15499, "currency": "USD" },
"shipping": { "address_required": true, // an address must be collected on this form
"options": [ { "id": "hyNOQZ", "name": "Standard", "amount": 599 } ],
"selected_option_id": "hyNOQZ" },
"errors": [] // { code, message, field?, source: "validation"|"gateway"|"sdk" }
}
```
Two money formats to keep straight:
- **`CheckoutState` amounts are integer minor units** (cents; zero-decimal
currencies like JPY are x1). These are the authoritative numbers to render.
- **Catalog prices** (`products()`) carry `amount` as a decimal string
(e.g. `"149.0"`) - fine for displaying an offer, but totals come from state.
Render from `onStateChange`, not from each mutator's resolved value - it also
fires for SDK-internal repricing (e.g. an address change re-quoting shipping),
so it is the one place your totals can't go stale.
An empty cart reports a `no_products` validation error; treat it as the
normal starting state rather than something to alarm the buyer with.
## Advanced example - custom summary and order bump
This expands the basic form with a custom summary and optional order bump. Use
it only when the page needs those controls; the shorter snippet above is enough
for a basic single-product checkout.
```html
```
An order bump is not a special API: it's a catalog product served in
`bumpProducts`, and selecting it is one more `setQuantity` line. The server
prices it into totals - no client-side math.
Note the bump checkbox lives **outside** the `