# Checkout

Accept one-time and recurring payments with `<rebill-checkout>`. This page covers the difference between product and plan checkouts, what you get back from the `success` event, and the edge cases you'll hit in production.

For the install + first render, see the [Quickstart](/sdk/quickstart). For every available prop and display toggle, see [Customization](/sdk/customization).

## Product vs plan: what changes

You pick the mode by setting exactly one of `product-id` or `plan-id` on the component. The internal flow is almost identical; the outputs and the follow-up behavior are not.

| Attribute              | What gets charged                               | What gets created                                                              | success event `data.result`         |
| ---------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------ | ----------------------------------- |
| `product-id="prd_..."` | A single charge of the product's default price. | A `payment` record. No subscription.                                           | `paymentId`, `subscriptionId: null` |
| `plan-id="pln_..."`    | The initial charge of the plan.                 | A `payment` **plus** a `subscription` that auto-renews on the plan's schedule. | `paymentId`, `subscriptionId`       |

If you need dynamic pricing (setup fees, pay-what-you-want, usage-based amounts) neither attribute fits — use [Instant Checkout](/sdk/instant-checkout) with `instant-product` or `instant-plan` instead.

## What arrives on success

Hook the `success` event to fulfill access. Its `event.detail` looks like this:

```javascript
element.addEventListener('success', (event) => {
  const { paymentMethod, data } = event.detail
  // paymentMethod: 'card' | 'cash' | 'bank_transfer'
  // data.result: the backend checkout response
  const { paymentId, subscriptionId } = data.result
  // plan checkout → both are set
  // product checkout → subscriptionId is null
})
```

**Never rely on the event alone to grant access.** Always confirm server-side by either (1) calling `GET /v3/payments/:paymentId` with your secret key and checking `status === "approved"`, or (2) waiting for the `payment.created` webhook. The SDK fires `success` as soon as the processor authorizes, but bank-transfer and cash methods remain `pending` until the customer actually completes payment.

## Other events you'll want in production

Beyond `success`, the component emits five more events:

```javascript
// Fires once the component finishes mounting, loads the session, and
// is ready to accept input. Useful if you need to show a skeleton until
// the checkout is interactive.
element.addEventListener('ready', () => { /* event.detail is void */ })

// Customer submitted but the charge was rejected (insufficient funds,
// fraud rules, expired card, 3DS failure).
element.addEventListener('error', (event) => {
  const { paymentMethod, data } = event.detail
  // data.result?.statusDetail holds the normalized reason (see Error Messages)
  // data.error?.error?.message holds the Rebill-side message
})

// Fires on every field change. Use it to enable/disable your own CTA
// if you're driving the submit externally.
element.addEventListener('formChange', (event) => {
  const { data, isValid, errors } = event.detail
})

// Fires when the customer signs out of the one-click session
// (e.g., they click "Use a different card").
element.addEventListener('sessionClosed', (event) => {
  const { customerId, reason } = event.detail
})

// Fires when a processor-owned redirect URL is about to take over
// (APMs that bounce to a bank page, 3DS challenges that navigate).
element.addEventListener('successRedirect', (event) => {
  // Save state before the navigation — the customer returns to a
  // fresh page load, not back to this component's memory.
})
```

## Multi-currency and installments

If the plan or product has prices in more than one currency, the checkout renders a currency selector above the card fields. The customer picks, and the component re-fetches fees and installments for the chosen currency. You don't configure this — it's driven entirely by the prices you set up on the resource.

Installments appear when they are available for the card's country, as in Argentina and Brazil. The component surfaces the installment count the customer picked inside `data.installments` on the `success` event.

## Styling and section visibility

`<rebill-checkout>` accepts `display`, `css`, and `customer-information` attributes that let you hide billing fields, pre-fill identification, or theme the component to match your site. Those props belong to the customization layer — see [Customization](/sdk/customization) for the full list with examples.

## Common mistakes

* **Granting access on `success` without server confirmation.** Bank-transfer and cash flows fire `success` when the *request* is accepted, not when the money lands. Reconcile via webhook or API call.
* **Handling `error` as terminal.** The customer can retry with a different card without unmounting the component. Keep the listener attached and wait for either a `success` or a manual unmount.
* **Ignoring `sessionClosed`.** If you surface "Welcome back, email\@..." copy elsewhere on the page based on the one-click session, you need to clear that UI when this event fires.
* **Logging `event.detail.data` to your server without redaction.** The backend response can include card metadata like BIN and last four — fine to store, but never log the full `data` blob to a third-party analytics tool without auditing what it contains.

## Next

* [Instant Checkout](/sdk/instant-checkout) — when pricing is decided at runtime.
* [Customization](/sdk/customization) — every `<rebill-checkout>` prop and how to theme the UI.
* [Test payment methods](/sdk/test-payment-methods) — sandbox cards that force each outcome.
