# Source: https://docs.rebill.com/index.md
# Rebill Documentation
Accept payments and manage subscriptions across Latin America with a single integration.
---
# Source: https://docs.rebill.com/api.md
# API Documentation
Use the [Rebill](https://rebill.com/) REST API to build custom payment flows, automate billing, and manage your entire payment operation programmatically. Create products, configure subscription plans, process payments, and handle refunds with standard HTTP requests.
### Getting started
Create a new application , then make requests to the resources you need using the HTTP API or client SDKs.
---
# Source: https://docs.rebill.com/api/quickstart.md
# API Quickstart
Create your first payment link and start accepting payments in **3 steps**.
To render payment forms inside your own app instead of redirecting to a hosted page, use the [JavaScript SDK](/sdk/introduction).
> You need a [secret key](/api/reference/authentication) to run the calls below.
>
> If you're logged in, your sandbox secret key auto-populates in the code snippets where applicable.
## Step 1: Create a plan or product
Create a plan or product that represents what you sell. The payment link is associated with it.
To create a one-time payment link without a plan or product, skip to [Instant Payment Links](#instant-payment-links).
Create a new plan:
> Customize the plan details in the request body as necessary for your specific offering.
Rebill returns the unique `id` of the created plan. Use this ID in the next step.
```json
{
"id": "pln_4b970527a2fe4923ba7477c0c86ec933",
"name": [
{ "language": "en", "text": "Monthly Premium Plan" }
],
"description": [
{ "language": "en", "text": "Premium subscription billed monthly" }
],
"status": "active",
"frequency": {
"period": "month",
"count": 1
},
"prices": [
{ "amount": 1990.00, "currency": "ARS", "isDefault": true, "createdAt": "2026-04-21T18:06:49.266Z", "updatedAt": "2026-04-21T18:06:49.266Z" },
{ "amount": 1790.00, "currency": "CLP", "isDefault": false, "createdAt": "2026-04-21T18:06:49.266Z", "updatedAt": "2026-04-21T18:06:49.266Z" }
]
}
```
> For more details on the plan object, see the [API reference](/api/reference/plans).
## Step 2: Create a payment link for the plan/product
With a plan or product ID, generate a payment link associated with it. See the [Payment Links](/api/reference/payment-links) API reference for all available parameters.
The currencies available for the payment link are limited to those defined in the associated plan or product.
Create a payment link for a plan:
> Replace `{planId}` in the request body with the actual ID of the plan you created in Step 1.
## Step 3: Share the payment link
The API returns the full payment link object. The `url` field is the one you share with customers.
```json
{
"id": "pl_5555fd46072648408c127833aabbccdd",
"organizationId": "org_8fa0d70f8ab54c7d8e54bc356422b2bd",
"url": "https://pay.rebill.com/my-business-alias/pl_5555fd46072648408c127833aabbccdd",
"status": "active",
"title": [{ "text": "Test Payment Link", "language": "en" }],
"type": "plan",
"paymentMethods": [
{ "methods": ["card", "bank_transfer"], "currency": "ARS" },
{ "methods": ["card", "bank_transfer"], "currency": "CLP" }
],
"isSingleUse": false,
"showCoupon": false,
"createdAt": "2026-04-20T14:27:06.135Z",
"updatedAt": "2026-04-20T14:27:06.135Z"
}
```
Share the `url` with your customers via email, SMS, social media, or embed it on your site. The customer lands on a [hosted checkout page](/api/reference/checkout) to pay.
## Instant payment links
To charge a one-off amount without a plan or product, skip Step 1 and send `prices` directly in the payment link body. See the [Payment Links reference](/api/reference/payment-links#create-a-payment-link) for the full request shape.
## Integrating payment forms directly
Use the [JavaScript SDK](/sdk/introduction) to render payment forms inside your own application. The SDK keeps card data off your servers, so you stay out of PCI scope.
## Testing your integration
Before going live, simulate successful payments, rejections, and expired cards. Use the [test payment methods](/api/test-tools-and-resources#test-payment-methods) reference for card numbers and outcome triggers.
To exercise the API end-to-end—including edge cases—import the official collection into Postman, Bruno, or Insomnia. The collection ships with variables and auth notes in its description.
## What's next?
* Grab your Secret Key from the Rebill Dashboard if you haven't already.
* Explore the full [API Reference](/api/reference/authentication) for every endpoint.
* Read the [SDK documentation](/sdk/introduction) for client-side integration.
* Set up [webhooks](/api/reference/webhooks) to receive real-time payment events.
***
---
# Source: https://docs.rebill.com/api/reference.md
# API Documentation
Rebill REST API allows developers to interact programmatically with their Rebill account and services using HTTP requests. Manage payments, products, plans, and more.
### Getting started
To get started, create a new application in your [developer settings](#), then read about how to make requests for the resources you need to access using our HTTP APIs or dedicated client SDKs. When your integration is ready to go live, publish it to our [integrations directory](#) to reach the Rebill community.
---
# Source: https://docs.rebill.com/api/reference/addresses.md
# Addresses
Store billing and shipping addresses tied to a customer. Use them to pre-fill checkout flows, satisfy card network requirements on cross-border transactions, and keep tax-relevant data on record.
Every endpoint requires a secret key (`sk_`). Addresses are scoped to the organization that owns the key.
***
## The address object
Rebill auto-generates this on creation. Prefixed with `addr_`.
The customer this address belongs to. Prefixed with `cus_`. Rebill omits this field when the address isn't linked to a customer (for example, a card billing address created at tokenization).
Either `billing` or `shipping`.
Two-letter country code you provided on creation. Rebill stores the value verbatim — it does not validate against ISO 3166-1 or a supported-country list.
State or province. Pass the full name — Rebill doesn't normalize abbreviations.
City name.
Postal or ZIP code. Format depends on the country.
Primary street line — number and street name.
Secondary line — apartment, suite, floor, or reference. Rebill omits this field when not set.
```json
{
"id": "addr_12aa31cdfb574e4191e96e67523a3560",
"customerId": "cus_786176c9d39f40d8af98428edb2aa3db",
"type": "billing",
"country": "AR",
"state": "Buenos Aires",
"city": "Buenos Aires",
"zipCode": "1043",
"lineOne": "Av. Corrientes 1234",
"lineTwo": "Piso 5, Depto A"
}
```
***
## Create an address
Attaches a new address to an existing customer. Create the customer first via the [Customers API](/api/reference/customers) if it doesn't exist yet — Rebill rejects addresses pointing to unknown customers.
### Request body
Existing customer ID. Must match `cus_` + 32 hex chars (or `test_cus_` + 32 hex chars in sandbox).
Two-letter country code. Rebill stores it as-is without validation.
State or province.
City name.
Postal or ZIP code.
Primary street line — number and street name.
Secondary line — apartment, suite, floor, or reference.
Either `billing` or `shipping`. Defaults to `billing` when omitted.
### Response
`201 Created` with `{ "address": { ... } }`.
### Errors
| Status | Cause | Usually happens when |
| ------------------ | ------------------------------ | ---------------------------------------------------------------------- |
| `400 Bad Request` | Invalid `customerId` format | You passed a raw UUID instead of a prefixed `cus_` ID |
| `400 Bad Request` | Invalid `type` value | You passed anything other than `billing` or `shipping` |
| `400 Bad Request` | Missing required field | `country`, `state`, `city`, `zipCode`, or `lineOne` wasn't in the body |
| `401 Unauthorized` | Missing or invalid `x-api-key` | You forgot the header, or the key was rotated |
| `404 Not Found` | Customer doesn't exist | The `customerId` doesn't match a customer in your organization |
A customer can own multiple addresses of the same type. Rebill doesn't enforce uniqueness or a "default" address — your application decides which one to use at checkout.
```json
{
"address": {
"id": "addr_12aa31cdfb574e4191e96e67523a3560",
"customerId": "cus_786176c9d39f40d8af98428edb2aa3db",
"type": "billing",
"country": "AR",
"state": "Buenos Aires",
"city": "Buenos Aires",
"zipCode": "1043",
"lineOne": "Av. Corrientes 1234",
"lineTwo": "Piso 5, Depto A"
}
}
```
***
## Retrieve an address
Fetches a single address by ID. Use it to verify the address on record before pre-filling a checkout form or to audit what a customer stored.
### Path parameters
The address ID. Prefixed with `addr_`.
### Response
`200 OK` with `{ "address": { ... } }`.
### Errors
| Status | Cause | Usually happens when |
| ------------------ | ------------------------------ | ---------------------------------------------------------------- |
| `401 Unauthorized` | Missing or invalid `x-api-key` | |
| `404 Not Found` | No address with that ID exists | The ID was deleted, mistyped, or belongs to another organization |
```json
{
"address": {
"id": "addr_12aa31cdfb574e4191e96e67523a3560",
"customerId": "cus_786176c9d39f40d8af98428edb2aa3db",
"type": "billing",
"country": "AR",
"state": "Buenos Aires",
"city": "Buenos Aires",
"zipCode": "1043",
"lineOne": "Av. Corrientes 1234",
"lineTwo": "Piso 5, Depto A"
}
}
```
***
## List addresses
Returns every address in your organization. Most of the time you'll want to filter by customer via the `customerId` query parameter — useful for showing a customer's saved addresses at checkout.
### Query parameters
Return only addresses belonging to this customer. Prefixed with `cus_`.
### Response
`200 OK` with `{ "addresses": [ ... ] }`. Empty array if no addresses match.
### Errors
| Status | Cause | Usually happens when |
| ------------------ | ------------------------------ | -------------------- |
| `401 Unauthorized` | Missing or invalid `x-api-key` | |
```json
{
"addresses": [
{
"id": "addr_12aa31cdfb574e4191e96e67523a3560",
"customerId": "cus_786176c9d39f40d8af98428edb2aa3db",
"type": "billing",
"country": "AR",
"state": "Buenos Aires",
"city": "Buenos Aires",
"zipCode": "1043",
"lineOne": "Av. Corrientes 1234",
"lineTwo": "Piso 5, Depto A"
},
{
"id": "addr_2a94dfaf407c4f84a41578aad0a6122b",
"customerId": "cus_786176c9d39f40d8af98428edb2aa3db",
"type": "shipping",
"country": "AR",
"state": "Buenos Aires",
"city": "Buenos Aires",
"zipCode": "1414",
"lineOne": "El Salvador 5678"
}
]
}
```
***
## Update an address
Updates any subset of fields on an existing address. Omitted fields stay unchanged.
The `customerId` is fixed at creation. To move an address to a different customer, delete it and create a new one under the target customer.
### Path parameters
The address ID. Prefixed with `addr_`.
### Request body
Any field from [the address object](#the-address-object) except `customerId`. All fields are optional — include only the ones you want to change.
### Response
`200 OK` with `{ "address": { ... } }` containing the full updated address.
### Errors
| Status | Cause | Usually happens when |
| ------------------ | ------------------------------ | ---------------------------------------------------------------- |
| `400 Bad Request` | Invalid `type` value | You passed anything other than `billing` or `shipping` |
| `401 Unauthorized` | Missing or invalid `x-api-key` | |
| `404 Not Found` | No address with that ID exists | The ID was deleted, mistyped, or belongs to another organization |
```json
{
"address": {
"id": "addr_12aa31cdfb574e4191e96e67523a3560",
"customerId": "cus_786176c9d39f40d8af98428edb2aa3db",
"type": "billing",
"country": "AR",
"state": "Buenos Aires",
"city": "Buenos Aires",
"zipCode": "1043",
"lineOne": "Av. Cordoba 5678",
"lineTwo": "Office 12"
}
}
```
***
## Delete an address
Permanently removes an address. Use it when a customer updates their billing info and you want to clean up the stale record. The customer and their other addresses stay intact.
This action can't be undone.
### Path parameters
The address ID. Prefixed with `addr_`.
### Response
`200 OK` with an empty body.
### Errors
| Status | Cause | Usually happens when |
| ------------------ | ------------------------------ | ------------------------------------------------------------------------- |
| `401 Unauthorized` | Missing or invalid `x-api-key` | |
| `404 Not Found` | No address with that ID exists | The address was already deleted or the ID belongs to another organization |
---
# Source: https://docs.rebill.com/api/reference/authentication.md
# API Keys
Every request to the Rebill API must include an API key in the `x-api-key` header. Requests without a valid key return `401 Unauthorized`.
***
## API key types
Each organization gets two keys: a secret key and a public key.
### Secret key (`sk_`)
Full server-side access — create charges, manage subscriptions, issue refunds, configure webhooks, manage customers and cards. Never expose it in client-side code, mobile apps, or public repositories.
### Public key (`pk_`)
Safe for client-side environments — browsers, mobile apps, and the [Rebill SDK](/sdk/introduction). Grants a limited subset of operations:
* **Checkout**: create SDK, hosted, and save-card sessions
* **Cards**: tokenize payment methods
* **Customers**: create customers, check existence by email
* **Subscriptions**: read status
All other resources require a secret key.
A secret key also works on any endpoint that accepts a public key. If you're building a server-side integration, the secret key is all you need.
```text {{ title: 'Secret key' }}
sk_ea1db6a164bb4fd6bb208d0b86655657
```
```text {{ title: 'Public key' }}
pk_50a6bfb37f474fa2995c32f21ee13d1c
```
***
## Get your keys
Keys are managed on [my.rebill.com](https://my.rebill.com): open **Developer Center**, then **API Keys**.
1. Click **Create API KEY** — Rebill creates a secret/public pair.
2. The **public key** stays visible in the dashboard. The **secret key** is always masked and never shown in full.
Your account includes a paired sandbox organization for testing. Switch to it from the top bar in the Dashboard to access its keys — all transactions are simulated and never charge real cards.
***
## Make an authenticated request
Pass your key in the `x-api-key` header on every call. The example below fetches your organization details — a reliable way to confirm your key is working before making other calls.
Your API key. Use `sk_` for server-side calls; `pk_` only works on public endpoints.
```json
{
"id": "org_4f7b2c8e1a3d9f6e2b5a8c3d1e9f7b2c",
"name": "Acme Corp",
"alias": "acme",
"status": "active",
"environment": "productive"
}
```
***
## Authentication errors
| Status | Cause |
| ------------------ | ------------------------------------------------------ |
| `401 Unauthorized` | Missing `x-api-key` header or invalid key. |
| `401 Unauthorized` | Public key (`pk_`) used on a secret-key-only endpoint. |
| `403 Forbidden` | Insufficient permissions for the requested operation. |
***
## Security
**Store keys in environment variables.** Never hard-code an API key in source code. Use `.env` files locally and your platform's secrets manager in production.
```bash
# .env
REBILL_SECRET_KEY=sk_ea1db6a164bb4fd6bb208d0b86655657
```
**Rotate keys regularly.** Generate a new key, update your environment variables, then delete the old one. Both keys can coexist during the transition window.
**Revoke exposed keys immediately.** If a key appears in a log, a repo, or a screenshot — revoke it from the Dashboard and generate a fresh pair.
**Never log the full key value.** Structured logs should include at most the last four characters: `key ending in ...5657`. Enable secrets scrubbing in your APM or logging tool.
***
## Next steps
* [API Quickstart](/api/quickstart) — make your first API call in minutes
* [Going live](/guides/going-live) — checklist before switching to production keys
---
# Source: https://docs.rebill.com/api/reference/cards.md
# Cards
Tokenize a card once and reuse the token for future charges, recurring subscriptions, and card-on-file flows. Rebill stores card data PCI-compliantly — your servers see only the last four digits, BIN, brand, and expiration.
Every endpoint requires a secret key (`sk_`). Cards are scoped to the organization that owns the key.
**For most integrations, don't call these endpoints directly.** Use the [Rebill SDK](/sdk/introduction) to tokenize cards on the client side — your servers never touch raw card numbers, and you stay outside PCI scope. The endpoints below exist for organizations already certified PCI DSS Level 1 or Level 2 that need server-side tokenization.
**`POST /v3/cards/{customerId}` requires raw card data access.** It is off for every organization, in production and in sandbox, and the endpoint returns `403` until Rebill enables it. To request access with your PCI DSS evidence, see [Raw card data access](/guides/raw-card-data).
***
## The card object
Wrapped card details. See fields below.
Rebill auto-generates this on tokenization. Prefixed with `crd_`.
ISO 8601 timestamp of tokenization.
Cardholder name as entered at tokenization.
Card network. One of `visa`, `mastercard`, `maestro`, `amex`, `discover`, `diners`, `jcb`, `unionpay`, `cencosud`, `cmr`, `argencard`, `cabal`, `cordobesa`, `naranja`, `other`. Rebill detects this from the BIN — you don't pass it on creation.
One of `credit`, `debit`, `prepaid`.
First six digits of the card number. Safe to store and log.
Last four digits. Safe to display.
`{ month, year }` — both strings. `month` is two digits (`01`–`12`), `year` is four digits (`2099`).
The customer this card belongs to. Prefixed with `cus_`.
IDs of related records created at tokenization: `{ identification: { id }, address: { id } }`. Dereference them via the [Addresses API](/api/reference/addresses) or the identifications endpoint when you need the full data.
```json
{
"card": {
"id": "crd_d16eb336861744e3aadc6d29874bd9a1",
"createdAt": "2026-04-18T14:39:56.747Z",
"name": "Ana Lopez",
"brand": "visa",
"type": "credit",
"bin": "453914",
"lastFour": "6467",
"expiration": {
"month": "11",
"year": "2099"
}
},
"customerId": "cus_786176c9d39f40d8af98428edb2aa3db",
"billingDetails": {
"identification": {
"id": "id_54c0a9c9a07249f8987e237f214fde9b"
},
"address": {
"id": "addr_c73768f9551d4b43b6dae703e19d7891"
}
}
}
```
***
## Tokenize and save a card
Tokenizes raw card data and attaches it to an existing customer. The request accepts raw `number` and `cvv` — only call it from a PCI-compliant environment, never from a browser or mobile client. Your organization needs [raw card data access](/guides/raw-card-data).
### Path parameters
Existing customer ID. Prefixed with `cus_`.
### Request body
`{ number, name, cvv, expiration: { month, year } }` — all fields required. Numbers and CVV go as strings.
Billing information used for AVS and card network checks.
* `address` (required): `{ country, state, city, zipCode, lineOne, lineTwo? }`. `country` is a two-letter code.
* `identification` (optional): either `{ id }` to reference an existing record, or `{ type, value }` to create one (e.g., `{ type: "DNI", value: "12345678" }`).
Two-letter ISO country code that determines the payment processor and currency. Must be one of the countries Rebill supports — sending an unsupported code returns `400 Bad Request`.
### Response
`200 OK` with the [card object](#the-card-object).
### Errors
| Status | Cause | Usually happens when |
| ------------------ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `400 Bad Request` | Invalid card data | Card number fails Luhn, expiration is in the past, CVV is malformed |
| `400 Bad Request` | Unsupported `processingCountry` | You passed a country Rebill doesn't route for (outside AR, BR, CL, CO, MX, PE, UY, US, CA) |
| `400 Bad Request` | Missing required field | `card.number`, `card.name`, `card.cvv`, `expiration`, `billingDetails.address`, or `processingCountry` wasn't in the body |
| `401 Unauthorized` | Missing or invalid `x-api-key` | |
| `403 Forbidden` | Raw card data access not enabled | Your organization doesn't have [raw card data access](/guides/raw-card-data). Use the SDK to tokenize cards, or request access |
| `404 Not Found` | Customer doesn't exist | The `customerId` in the path doesn't match a customer in your organization |
Rebill doesn't deduplicate cards. Tokenizing the same PAN twice for the same customer produces two independent `crd_` IDs. Your application decides whether to check for duplicates before calling this endpoint.
```json
{
"card": {
"id": "crd_d16eb336861744e3aadc6d29874bd9a1",
"createdAt": "2026-04-18T14:39:56.747Z",
"name": "Ana Lopez",
"brand": "visa",
"type": "credit",
"bin": "453914",
"lastFour": "6467",
"expiration": { "month": "11", "year": "2099" }
},
"customerId": "cus_786176c9d39f40d8af98428edb2aa3db",
"billingDetails": {
"identification": { "id": "id_54c0a9c9a07249f8987e237f214fde9b" },
"address": { "id": "addr_c73768f9551d4b43b6dae703e19d7891" }
}
}
```
***
## Retrieve a card
Fetches a single card by ID. Use it to confirm a card is still on file before attempting a charge, or to display the saved card details (BIN, last four, brand) in your UI.
### Path parameters
The card ID. Prefixed with `crd_`.
### Response
`200 OK` with the [card object](#the-card-object).
### Errors
| Status | Cause | Usually happens when |
| ------------------ | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------- |
| `401 Unauthorized` | Missing or invalid `x-api-key` | |
| `400 Bad Request` | No card with that ID exists | The card was deleted, mistyped, or belongs to another organization — Rebill returns `400 Card not found` rather than `404` |
```json
{
"card": {
"id": "crd_d16eb336861744e3aadc6d29874bd9a1",
"createdAt": "2026-04-18T14:39:56.747Z",
"name": "Ana Lopez",
"brand": "visa",
"type": "credit",
"bin": "453914",
"lastFour": "6467",
"expiration": { "month": "11", "year": "2099" }
},
"customerId": "cus_786176c9d39f40d8af98428edb2aa3db",
"billingDetails": {
"identification": { "id": "id_54c0a9c9a07249f8987e237f214fde9b" },
"address": { "id": "addr_c73768f9551d4b43b6dae703e19d7891" }
}
}
```
***
## List cards by customer
Returns every card tokenized under a given customer. Use it to let customers pick a saved card at checkout, or to audit what's on file before running a renewal.
### Query parameters
The customer ID. Prefixed with `cus_`.
### Response
`200 OK` with an array of [card objects](#the-card-object). Empty array if the customer has no cards.
### Errors
| Status | Cause | Usually happens when |
| ------------------ | ------------------------------------ | --------------------------------------------------------------------------------------- |
| `400 Bad Request` | Missing `customerId` query parameter | You called `/cards` without the filter — this endpoint always requires a customer scope |
| `401 Unauthorized` | Missing or invalid `x-api-key` | |
```json
[
{
"card": {
"id": "crd_d16eb336861744e3aadc6d29874bd9a1",
"createdAt": "2026-04-18T14:39:56.747Z",
"name": "Ana Lopez",
"brand": "visa",
"type": "credit",
"bin": "453914",
"lastFour": "6467",
"expiration": { "month": "11", "year": "2099" }
},
"customerId": "cus_786176c9d39f40d8af98428edb2aa3db",
"billingDetails": {
"identification": { "id": "id_54c0a9c9a07249f8987e237f214fde9b" },
"address": { "id": "addr_c73768f9551d4b43b6dae703e19d7891" }
}
}
]
```
---
# Source: https://docs.rebill.com/api/reference/checkout.md
# Checkout
Charge a customer in one API call. The checkout endpoint handles the whole flow — validates the card (or alternative payment method), creates or reuses the customer, and records the payment or starts the subscription. Use it for card-present, card-on-file, and redirect-based APM flows.
Every endpoint requires a secret key (`sk_`).
**For client-side integrations, don't send raw card data to these endpoints.** Use the [Rebill SDK](/sdk/introduction) to tokenize cards on the browser or mobile app — you receive a `cardTokenId` that you can pass to `/v3/checkout` without putting your servers in PCI scope.
**The full `card` object requires raw card data access.** It is off for every organization, in production and in sandbox, and requests that include it return `403` with `raw_card_data_not_enabled`. `cardTokenId` and `cardId` are not affected. To request access, see [Raw card data access](/guides/raw-card-data).
**If you send the full `card` object, integrate the device fingerprint.** Your page collects the card, so it also has to run the [device ID](#get-device-id) `script` and send `customAttributes.deviceId` in the checkout — otherwise the antifraud engine screens the payment without device data. See [Fraud prevention](/guides/fraud-prevention#api).
***
## Transaction types
A single checkout can charge for one of four things. The fields you include in `transaction` or `subscription` determine the type:
| Type | Top-level field | Required inside |
| ----------------------------------------- | --------------- | ------------------------------------------------------------- |
| Existing product (one-time) | `transaction` | `productId`, `quantity` |
| Existing plan (subscription) | `subscription` | `planId`, `quantity` |
| Instant payment (one-time, custom amount) | `transaction` | `amount`, `currency`, `name`, `quantity` |
| Instant subscription (custom plan) | `subscription` | `amount`, `currency`, `name`, `frequency`, `type`, `quantity` |
Include exactly one of `transaction` or `subscription` per request — they're mutually exclusive.
***
## Card checkout
Charge a customer's card. Provide exactly one of: full `card` object, `cardId` (saved card), or `cardTokenId` (SDK token).
Pass an `x-idempotency-key` header so Rebill treats retries as a single charge. See [Best Practices](/api/test-tools-and-resources/best-practices#idempotency-keys).
### Request body
One-time payment details. Mutually exclusive with `subscription`.
* `productId` (string) — Existing product ID.
* `amount` (number) — Decimal in the currency's major unit (e.g., `250.00` for $250 ARS). Required for instant payments.
* `currency` (string) — Three-letter ISO code. Required for instant payments.
* `quantity` (number, required) — How many units to charge.
* `name` (array) — `[{ language, text }]` where `language` is one of `en`, `es`, or `pt`. Required for instant payments.
* `description` (array) — Same shape as `name`.
* `couponCode` (string) — Coupon code recorded on the payment. Discount application lives in the subscriptions/billing engine; the checkout endpoint stores the code but doesn't reprice the transaction.
Subscription details. Mutually exclusive with `transaction`.
* `planId` (string) — Existing plan ID.
* `amount`, `currency`, `name`, `description` — Same as `transaction`. Required for instant subscriptions.
* `quantity` (number, required) — Units per billing cycle.
* `frequency` (object) — `{ period, count }`. `period` is one of `month` or `year` in production (the enum also includes `minute`, `hour`, `day` but those are reserved for internal testing). Required for instant subscriptions.
* `type` (string) — `units` or `usage`. Required for instant subscriptions.
* `repetitions` (number) — Total billing cycles. Omit for open-ended.
* `trialPeriodDays`, `debitDay`, `debitStrategy`, `initialFee`, `couponCode` — Optional plan overrides.
`{ firstName, lastName, email }` required. Optional `phone: { countryCode, number }`, `identification: { type, value } | { id }`, `shippingAddress`.
`firstName` and `lastName` can't contain digits — a name with numbers returns `400` with `customer.firstName: firstName must not contain numeric characters`.
Rebill matches customers by `email` within your organization. When the email already exists, `firstName` and `lastName` get overwritten with the values you send; new phone numbers append (deduplicated by `countryCode + number`); identifications and shipping addresses merge into the existing record instead of replacing it.
Full card details. Mutually exclusive with `cardId` and `cardTokenId`. Requires [raw card data access](/guides/raw-card-data).
* `number`, `name`, `cvv` (all string)
* `expiration: { month, year }` (both strings — typically `01`–`12` for `month` and four digits for `year`)
ID of a card previously tokenized for this customer (prefixed `crd_`). Mutually exclusive with `card` and `cardTokenId`.
Single-use card token returned by the [Rebill SDK](/sdk/introduction). Mutually exclusive with `card` and `cardId`.
`{ address, identification }`. Required when using full `card` or `cardTokenId` — Rebill needs it for AVS and card network checks. Addresses and identifications can reference an existing record by `id` or be created inline.
Number of installments. Availability depends on the card brand, issuer, and country.
When `true`, Rebill stores the card token and returns a `cardId` you can pass on future checkouts. Default: `false`.
Free-form key-value map attached to the payment. Survives refunds and chargebacks.
Free-form key-value map attached to the subscription. Only applies when `subscription` is present.
Free-form context, notably `deviceId` from the [device ID endpoint](#get-device-id) for fraud scoring.
### Response
`201 Created` with:
```json
{
"traceId": "...",
"date": "2026-04-19T14:22:10.000Z",
"result": {
"paymentId": "pay_...",
"status": "approved",
"subscriptionId": "sub_...",
"cardId": "crd_...",
"cardLastFour": "6467",
"customerId": "cus_...",
"statusDetail": "...",
"errorType": "..."
}
}
```
`subscriptionId` appears only when the request included a `subscription`. `cardId` appears whenever a card was used on the payment (stored, tokenized, or raw). `statusDetail` and `errorType` are always in the response shape but usually `null` on approved payments — they carry processor context on declines, partial approvals, or when the provider returned extra detail.
On any error, the response body keeps the same shape with `error` instead of `result`:
```json
{
"traceId": "...",
"date": "...",
"error": {
"message": "rejected",
"type": "insufficient_funds",
"rawMessage": "The card has insufficient funds..."
}
}
```
`error.message` reflects the payment outcome (typically `rejected`). `error.type` is the specific error code — see [Error messages](/api/test-tools-and-resources/error-messages) for the full list. The HTTP status code varies by cause (see Errors below).
### Errors
| Status | Cause | Usually happens when |
| --------------------------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400 Bad Request` | Validation failed | You included both `transaction` and `subscription`, sent multiple of `card`/`cardId`/`cardTokenId`, missed a required field, or passed an invalid amount/currency |
| `401 Unauthorized` | Missing or invalid `x-api-key` | You forgot the header or the key was rotated |
| `402 Payment Required` | Card declined by issuer | `insufficient_funds`, `bank_rejected`, `expired_card`, `invalid_card_cvc`, or `blacklisted` — the API call succeeded, the payment didn't |
| `403 Forbidden` | Fraud or card restriction | `fraud_high_risk`, card disabled by issuer, or card type not allowed for this merchant |
| `403 Forbidden` | `raw_card_data_not_enabled` | You sent the full `card` object and your organization doesn't have [raw card data access](/guides/raw-card-data). Use `cardTokenId` from the SDK or request access |
| `408 Request Timeout` | Payment timed out | Provider took too long, or the session's max attempts were reached |
| `409 Conflict` | Duplicate payment | Rebill already processed a one-time payment with the same `x-idempotency-key`. Subscription renewals with a matching key return the original payment instead of erroring — the engine treats duplicates as idempotent success |
| `417 Expectation Failed` | Additional auth needed | Issuer requires 3DS or `call_bank_for_authorize` before approving |
| `422 Unprocessable Entity` | Business rule violation | Invalid installments configuration for this card, or other processor-side rejection |
| `429 Too Many Requests` | Rate limit exceeded | Too many checkout attempts for this session — back off before retrying |
| `502 Bad Gateway` | Upstream processor failure | The payment processor returned an error or was unavailable — retry with backoff |
| `500 Internal Server Error` | Internal error | Contact support with the `traceId` |
A rejected subscription checkout still creates the subscription in a paused state. Use the [Subscriptions API](/api/reference/subscriptions) to retry or cancel it.
```json {{ title: 'Approved' }}
{
"traceId": "checkout_1709058830240_kj8n2x5",
"date": "2026-04-19T14:22:10.000Z",
"result": {
"paymentId": "pay_987f8a061d2c3da6ab044711e51f163d",
"status": "approved",
"subscriptionId": "sub_3k1568576a364e10aa96a149c753093",
"cardId": "crd_251568576a364e10aa96a149c753krut",
"cardLastFour": "6467",
"customerId": "cus_327f8a061d2c3da6ab044711e510sder"
}
}
```
```json {{ title: 'Declined' }}
{
"traceId": "checkout_1709058830240_kj8n2x5",
"date": "2026-04-19T14:22:10.000Z",
"error": {
"message": "rejected",
"type": "insufficient_funds",
"rawMessage": "The card has insufficient funds to complete this transaction"
}
}
```
***
## APM checkout
Create a redirect-based APM payment — bank transfer, cash voucher, or QR code. The response includes a `redirectUrl` to send the customer to, or payment instructions (barcode, reference number, QR code) you display in your own UI.
APM payments are asynchronous. The checkout starts in `pending_customer_charge` and reaches a terminal state when the customer completes payment or the window expires. Subscribe to [webhooks](/api/reference/webhooks) or poll the payment — see [Tracking APM checkout outcome](#tracking-apm-checkout-outcome) — to detect the outcome.
### Supported payment methods
| Method | Country | Type |
| ------------------------ | --------- | --------------------------- |
| `br-bank_transfer-pix` | Brazil | Instant bank transfer (PIX) |
| `br-cash-boleto` | Brazil | Cash voucher (Boleto) |
| `ar-bank_transfer-qr` | Argentina | QR-code bank transfer |
| `co-bank_transfer-pse` | Colombia | Bank transfer (PSE) |
| `mx-bank_transfer-spei` | Mexico | Bank transfer (SPEI) |
| `cl-bank_transfer-etpay` | Chile | Bank transfer (Etpay) |
### Request body
Same shape as card checkout. Mutually exclusive with `subscription`.
Same shape as card checkout. Mutually exclusive with `transaction`.
One of the values in the table above. The currency must match the method's country (e.g., `co-bank_transfer-pse` requires `COP`).
Same shape as card checkout.
`{ address, identification }`. APMs require billing details for compliance.
`{ approved, rejected }`. URLs Rebill redirects the customer to after the payment is resolved or cancelled. Use HTTPS in production — payment processors expect secure redirects for anything involving customer credentials.
Free-form key-value map attached to the payment.
Free-form key-value map attached to the subscription.
Free-form context.
### Response
`201 Created` with:
```json
{
"traceId": "fQ1785280056215",
"date": "2026-07-28T23:07:36.797Z",
"result": {
"checkoutRequestId": "che_req_...",
"status": "pending_customer_charge",
"data": {
"qrCode": "00020101021226350010com.rebill...",
"expiresAt": "2026-07-28T23:37:36.801Z"
}
}
}
```
`data` carries the payment instrument, and its contents vary by method: PSE and Etpay return a `redirectUrl`; Boleto returns a `barcode`; PIX and the Argentine transfer return a `qrCode` payload. `expiresAt` is orthogonal to that — it can accompany any of those payloads.
Absolute expiry of the payment instrument, ISO 8601 with timezone (`"2026-07-28T23:37:36.801Z"`). It's an instant, not a duration — compare it against your own clock and use the top-level `date` (Rebill's timestamp for the same response) as the reference to correct clock drift.
Only present on methods with a known expiry window. When the method has none, the key is **absent** — it never comes back as `null`, so read it defensively instead of assuming it exists.
| Method | Returns `expiresAt` | Typical window |
| ------------------------ | ------------------- | ------------------------------------ |
| `br-bank_transfer-pix` | Yes | 30 min – 1 h, depending on the rail |
| `br-cash-boleto` | Yes | 7 days (24 h on some configurations) |
| `ar-bank_transfer-qr` | Yes | 30 min |
| `co-bank_transfer-pse` | No | — |
| `mx-bank_transfer-spei` | No | — |
| `cl-bank_transfer-etpay` | No | — |
**`expiresAt` is informative, not a hard cutoff.** Reaching that timestamp doesn't guarantee the instrument stopped working — an Argentine QR paid after it expires still settles, and the webhook fires normally. `status` stays the only source of truth for the payment outcome: don't derive "expired" or "failed" from `expiresAt`, and don't stop polling or cancel the order the moment the clock runs out. Leave margin.
`expiresAt` is additive and optional — nothing else in the response changed, and current integrations need no action. The main use case is rendering a real countdown on your own checkout instead of hardcoding a window. See [Instrument expiration](/guides/apm#instrument-expiration) for the integration pattern.
### Errors
| Status | Cause | Usually happens when |
| ------------------ | --------------------------------------- | ----------------------------------------------------------------------------------- |
| `400 Bad Request` | Unsupported method or currency mismatch | You passed `method: "br-cash-boleto"` with `currency: "ARS"` |
| `400 Bad Request` | Missing required field | `method`, `customer`, `billingDetails`, `redirect`, or the body itself is malformed |
| `401 Unauthorized` | Missing or invalid `x-api-key` | |
```json {{ title: 'PSE — no expiry' }}
{
"traceId": "checkout_1709058830244_r8h3k5m",
"date": "2026-04-19T14:22:10.000Z",
"result": {
"checkoutRequestId": "che_req_2f5a8d1c4e7b9a3d6f8c1e4a7b2d5c8e",
"status": "pending_customer_charge",
"data": {
"redirectUrl": "https://pse.bancolombia.com/98765432"
}
}
}
```
```json {{ title: 'QR transfer — with expiry' }}
{
"traceId": "fQ1785280056215",
"date": "2026-07-28T23:07:36.797Z",
"result": {
"checkoutRequestId": "che_req_651c1d9fb1e84218a280348545d3bddb",
"status": "pending_customer_charge",
"data": {
"qrCode": "00020101021226350010com.rebill...",
"expiresAt": "2026-07-28T23:37:36.801Z"
}
}
}
```
***
## Tracking APM checkout outcome
APM payments resolve asynchronously. Rather than polling, subscribe to the `payment.created`, `payment.updated`, and `payment.updated.refunded` [webhooks](/api/reference/webhooks) — Rebill emits them as soon as the customer finishes the off-site flow (or the window expires).
If your integration must support polling, the [Retrieve a payment](/api/reference/payments#get-payment-by-id) endpoint gives you the live payment status once a `payment.created` event fires for the checkout session.
The session-based variants used by the SDK and hosted checkout — `POST /v3/sessions/checkout/request` and `GET /v3/sessions/checkout/request/{checkoutRequestId}` — return the same `data` object, `expiresAt` included. On the `GET`, `expiresAt` sits at `data.expiresAt` and survives the status change, so reading a checkout request that already reached `approved` still shows the window the instrument had:
```json
{
"checkoutRequestId": "che_req_651c1d9fb1e84218a280348545d3bddb",
"status": "approved",
"type": "bank_transfer",
"data": {
"amount": 100,
"qrCode": "00020101021226350010com.rebill...",
"currency": "ARS",
"expiresAt": "2026-07-28T23:37:36.801Z"
}
}
```
A payment approved after `expiresAt` is a normal outcome, not an inconsistency — see the warning above.
***
## Get device ID
Generates a device ID and a fingerprint script for fraud prevention. Inject the returned `script` in your checkout page and send the same `deviceId` in `customAttributes.deviceId` on your checkout request — it gives the antifraud engine the customer's device data and raises approval rates on high-risk transactions. See [Fraud prevention](/guides/fraud-prevention) for the full flow.
No authentication required. The endpoint is public because your checkout page calls it from the browser before the customer enters card data.
### Path parameters
Two-letter ISO country code where the payment will be processed.
### Response
Device ID for this checkout session. Send it in `customAttributes.deviceId` on `POST /v3/checkout`.
JavaScript to run in your checkout page. It's plain code, not an HTML `
```
## Attributes
| Attribute | Type | Required | Notes |
| ---------------------- | ------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `public-key` | string | Yes | Your organization's `pk_` key. |
| `processing-country` | string | Yes | ISO alpha-2 (`AR`, `BR`, `CL`, `CO`, `MX`, `US`). Determines which processor tokenizes the card. **Changing this later requires re-tokenization** — you can't use a card tokenized in Argentina to charge through a Mexican processor. |
| `customer-information` | object (JSON) | No | Pre-fills or supplies fields when the equivalent sections are hidden. Shape below. |
| `redirect-urls` | object (JSON) | No | `{ approved: "https://your.app/card-saved" }`. When set, the component navigates to the URL after success instead of showing its built-in confirmation screen. |
| `language` | string | No | `'en' \| 'es' \| 'pt'`. Defaults to the country's language. |
| `display` | object (JSON) | No | Section visibility toggles. |
| `css` | string | No | Custom stylesheet (inline CSS or URL). |
## `customer-information` shape
Flat structure — different from ``, which nests the address. `email` and `fullName` are always required in the save-card flow. In addition, the `/preview/checkout-fields` contract marks identification, billing address and phone as required for card tokenization in every country. `identification` is auto-filled by the SDK only for USD and MXN; everywhere else the customer must provide a real document. All of address, identification and phone are required to charge the card afterwards.
```javascript
{
email: "ana.perez@rebill.com",
fullName: "Ana Perez",
phoneNumber: "1123456789",
countryCodePhoneNumber: "AR", // ISO alpha-2, resolved to the dial prefix
billingAddress: {
country: "AR",
state: "Buenos Aires",
city: "Mar del Plata",
zipCode: "7600",
address: "Av. Córdoba 123",
addInformation: "Apt 4B" // optional
},
document: {
type: "DNI", // country-dependent: DNI, CURP, CPF, RUT, CC, etc.
number: "12345678"
}
}
```
`fullName` can't contain digits — a name with numbers returns a `400` (`customer.firstName: firstName must not contain numeric characters`).
The customer is matched by email if one exists in your organization; otherwise a new `cus_` record is created at tokenization time.
### Required fields by processing-country
| Country | Currency | Identification at tokenization | Address / Phone | Charge with cardId |
| ------- | -------- | ------------------------------------------------------------------------ | --------------- | ------------------------------- |
| AR | ARS | Real document, validated (DNI/CUIT/CUIL); also enforced by the processor | Required | address + id + phone (required) |
| BR | BRL | Real document (CPF/CNPJ) + online tax-id validation | Required | address + id + phone (required) |
| CL | CLP | Real document (RUT/RUN) | Required | address + id + phone (required) |
| CO | COP | Real document (NIT/CC) | Required | address + id + phone (required) |
| PE | PEN | Real document (no format check, min 6) | Required | address + id + phone (required) |
| UY | UYU | Real document (no format check, min 6) | Required | address + id + phone (required) |
| MX | MXN | Auto-filled by the SDK (placeholder), not asked | Required | address + id + phone (required) |
| US | USD | Auto-filled by the SDK (placeholder), not asked | Required | address + id + phone (required) |
Address and phone are required at tokenization in every country. The placeholder for MX/US only covers the identification document; address and phone must still be captured to charge the card.
## Requirements to charge a saved card
A `cardId` by itself is not enough to charge a saved card. The customer linked to the card must have: a billing address, an identification document (type + value), and a phone number. These three are required for every country and every processor, no exceptions. If any is missing, the charge is rejected with a `400` before it reaches the processor. Saving the card may succeed with incomplete data, but the charge will not — always capture real address, identification and phone at tokenization time.
## `display` flags
All default to `true`. Hide the corresponding section by setting to `false`:
| Flag | Controls |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `submitButton` | The built-in save button. Drive submission externally with `submit()`. |
| `logo` | Your organization logo header. |
| `footer` | The Rebill-branded footer. |
| `billingAddress` | The billing address section. Pass data via `customer-information.billingAddress` when hidden. |
| `phoneNumber` | The phone number field. |
| `documentId` | Controls the identification-document field. An identification document is required to tokenize a card in every country. In AR, BR, CL, CO, PE, UY the customer must enter a real document (validated client-side; BR also validates the tax-id online). In MX (MXN) and US (USD) the SDK auto-fills a placeholder, so you may hide it there. Hiding it anywhere else sends an empty document and both tokenization (country-dependent) and any later charge will fail — pass identification via `customer-information` instead. |
| `sandboxMode` | Sandbox-only helper UI (test card shortcuts). Hide in production previews. |
**Important.** `billingAddress`, `documentId` and `phoneNumber` are displayed by default. Hiding a field does not remove it from the request — the SDK sends empty values (address) or a placeholder (identification in USD/MXN). If you hide any of them, you must provide that data through `customer-information`. Otherwise the card is saved with empty data and any later charge using the `cardId` will fail, because charging a saved card requires a real billing address, identification document and phone number.
## Events
The component emits four events:
| Event | `event.detail` | Fires when |
| ----------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ready` | `void` | The component finished mounting, loaded countries and required fields, and is ready for input. |
| `success` | `{ card: { id, brand, lastFour, expiration }, customer: { id, email, firstName, lastName } }` | The card was tokenized and stored. `card.id` is the `crd_...` you charge against later; `customer.id` is the `cus_...` (new or matched by email). |
| `error` | `{ error }` | Validation, tokenization, or network failure. |
| `successRedirect` | `{ url, data }` | Fires **in addition to** `success` when `redirect-urls.approved` is set. The SDK is about to navigate the browser to `url`; save any state you need first. `data` carries the same shape as `success.detail`. |
### React example
```javascript
"use client"
import { useEffect, useRef } from 'react'
import 'rebill'
export default function SaveCard() {
const ref = useRef(null)
useEffect(() => {
const el = ref.current
if (!el) return
const onSuccess = (e) => {
const { card, customer } = e.detail
// Store card.id on your user record to charge later.
}
const onError = (e) => {
const { error } = e.detail
// Surface a retry prompt; common causes: Luhn failure, expired card.
}
el.addEventListener('success', onSuccess)
el.addEventListener('error', onError)
return () => {
el.removeEventListener('success', onSuccess)
el.removeEventListener('error', onError)
}
}, [])
return (
)
}
```
## Common mistakes
* **Confusing save with charge.** Tokenization moves $0. Until you call `POST /v3/payments` (or attach the card to a subscription), the customer is not charged.
* **Using a sandbox card on a production org.** Sandbox cards (`4539 1488 0343 6467`, etc.) return `400 Sandbox cards are only available for sandbox organizations` when the `pk_` belongs to a production org.
* **Mismatching `processing-country` and card origin.** Tokenizing a Mexican card with `processing-country="AR"` works, but the charge later will route through the Argentine processor and may be declined for cross-border rules. Pick the country that matches where you plan to charge.
* **Assuming `successRedirect` replaces `success`.** Both events fire when `redirect-urls.approved` is set — `success` always, and `successRedirect` right after. Don't duplicate fulfillment logic; put side effects in `success` and use `successRedirect` only to save state before the browser navigates.
## Next
* [Checkout](/sdk/checkout) — tokenize AND charge in the same flow.
* [Renew subscription card](/sdk/renew-subscription-card) — replace the card on an existing subscription.
* [Cards API reference](/api/reference/cards) — the full `card` object shape returned to the `success` event.
---
# Source: https://docs.rebill.com/sdk/checkout.md
# Checkout
Accept one-time and recurring payments with ``. 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 processor's reason
// 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 the country's processor supports them (Argentina via Decidir, Brazil via the card processor). The component surfaces the installment count the customer picked inside `data.installments` on the `success` event.
## Styling and section visibility
`` 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 `` prop and how to theme the UI.
* [Test payment methods](/sdk/test-payment-methods) — sandbox cards that force each outcome.
---
# Source: https://docs.rebill.com/sdk/customization.md
# Customization
Configure `` through attributes. Swap sections on or off, inject custom CSS, pre-fill customer data, pre-apply a coupon, and drive submission from your own UI. An interactive SDK playground is coming soon.
## Attributes
| Attribute | Type | Required | Notes |
| ---------------------- | ------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `public-key` | string | Yes | Your organization's `pk_` key from the dashboard. Safe to expose in the browser. |
| `product-id` | string | See note | ID of a stored product for a one-time charge. |
| `plan-id` | string | See note | ID of a stored plan for a subscription. |
| `instant-product` | object (JSON) | See note | Inline product for dynamic pricing. See [Instant Checkout](/sdk/instant-checkout). |
| `instant-plan` | object (JSON) | See note | Inline plan for dynamic recurring pricing. See [Instant Checkout](/sdk/instant-checkout). |
| `customer-information` | object (JSON) | No | Pre-fills or overrides customer data. Required when you hide `customerInformation` or `billingAddress` sections. Shape below. |
| `coupon-code` | string | No | Pre-applies a coupon code before the form renders. Customer still sees the applied discount in the summary. |
| `language` | string | No | `'en' \| 'es' \| 'pt'`. Defaults to the session's country language. |
| `one-click-checkout` | boolean | No (default `true`) | Offers one-click payment to returning customers with a saved card. |
| `display` | object (JSON) | No | Section visibility. Shape below. |
| `css` | string | No | Custom CSS (inline string or URL to a stylesheet). |
| `js` | string | No | Custom JavaScript to inject (string or URL). Use with care. |
One of `product-id`, `plan-id`, `instant-product`, or `instant-plan` is required. Pick the one that matches your flow.
## `display` — show or hide checkout sections
All flags default to `true`. Flip to `false` to hide.
| Flag | Controls |
| ----------------------- | ---------------------------------------------------------------------------------------------------- |
| `submitButton` | The default submit button. Set to `false` to drive submission via `submit()`. |
| `checkoutSummary` | The right-hand summary panel (amount, item name, coupon recap). |
| `billingAddress` | The billing address section. Data must come from `customer-information` when hidden. |
| `customerInformation` | Name, email, phone, identification. Data must come from `customer-information` when hidden. |
| `discountCode` | Coupon code input. Hiding it doesn't block pre-applied `coupon-code`. |
| `logo` | Your organization logo at the top of the checkout. |
| `footer` | The Rebill-branded footer. |
| `processingPayment` | The in-progress loader while the processor responds. |
| `successPage` | The built-in "thank you" page after `success`. Turn off to redirect or render your own. |
| `errorMessage` | The built-in error banner. Turn off to render errors from the `error` event. |
| `useAddressSearch` | The autocomplete search input for the billing address. When `false`, shows the manual form directly. |
| `sandboxMode` | Sandbox-only helper UI (test card shortcuts, doc links). Hide in production previews. |
| `excludePaymentMethods` | Array of methods to hide. Accepts `'card'`, `'cash'`, `'bank_transfer'`. |
### Default `display` values
```javascript
{
submitButton: true,
checkoutSummary: true,
billingAddress: true,
customerInformation: true,
discountCode: true,
logo: true,
footer: true,
processingPayment: true,
successPage: true,
errorMessage: true,
useAddressSearch: true,
sandboxMode: true
// excludePaymentMethods is undefined by default — no methods are filtered
}
```
## Pre-filling customer data
When you hide `customerInformation` or `billingAddress`, pass the equivalent data through `customer-information`. The component reads from this object at submit time.
```javascript
customerInformation={{
// Required when display.customerInformation is false
email: "ana.perez@rebill.com",
fullName: "Ana Perez",
phoneNumber: {
number: "1123456789",
countryCode: "AR"
},
// Optional: prefills identification fields when the country needs them
documentType: "DNI",
documentNumber: "12345678",
// Required when display.billingAddress is false
address: {
billingAddress: {
address: "Av. Córdoba 123",
city: "Mar del Plata",
state: "Buenos Aires",
country: "AR",
zip: "7600"
}
}
}}
```
`fullName` can't contain digits — a name with numbers returns a `400` at submit time (`customer.firstName: firstName must not contain numeric characters`).
`countryCode` inside `phoneNumber` is the ISO alpha-2 country (`"AR"`, `"US"`, `"BR"`); the SDK resolves it to the dial prefix. `documentType` depends on the country — `DNI` for AR, `CURP` or `RFC` for MX, `CPF` for BR, `CC` or `CE` for CO, `RUT` for CL.
## Driving submission externally
Hide the built-in button and trigger the checkout from your own CTA. Useful for multi-step forms where Rebill is one of several final steps.
```javascript
// From your own button handler:
async function handleSubmit() {
try {
await checkoutRef.current.submit()
} catch (error) {
// submit() rejects when the form is invalid — the error event
// will fire separately with a reason if the charge itself fails.
}
}
```
## Styling with `css`
```html
```
Pass a string of CSS or a URL to a stylesheet. The styles apply inside the component's shadow DOM; they don't leak to your page and your page's global CSS doesn't leak into the component.
## Events
The component emits seven events. Attach listeners with `addEventListener` — the event's detail payload goes in `event.detail`.
| Event | `event.detail` | Fires when |
| ----------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `ready` | `void` | The component finished mounting, fetched the session, and is ready for input. |
| `success` | `{ paymentMethod, data }` | Charge was approved by the processor. See [Checkout](/sdk/checkout#what-arrives-on-success) for `data.result` shape. |
| `error` | `{ paymentMethod, data }` | Charge was rejected. `data.result?.statusDetail` holds the reason. |
| `formChange` | `{ data, isValid, errors? }` | Any field value changes. Useful for syncing your external submit button's enabled state. |
| `paymentMethodSelected` | `'card' \| 'cash' \| 'bank_transfer'` | Customer switches the selected method tab. |
| `sessionClosed` | `{ customerId?, reason }` | Customer exits the one-click session (e.g., clicks "Use a different card"). |
| `successRedirect` | Processor-specific payload | A redirect URL is about to take over (APMs that bounce to a bank, 3DS challenges). Fires in addition to `success`, not instead of it. |
### Listener example
```javascript
const checkout = document.querySelector('rebill-checkout')
checkout.addEventListener('success', (event) => {
const { paymentMethod, data } = event.detail
// Fulfill access, then confirm server-side via GET /v3/payments/:id
})
checkout.addEventListener('error', (event) => {
const { data } = event.detail
const reason = data.result?.statusDetail ?? data.error?.error?.message
showRetryPrompt(reason)
})
checkout.addEventListener('formChange', (event) => {
const { isValid } = event.detail
myExternalCta.disabled = !isValid
})
checkout.addEventListener('paymentMethodSelected', (event) => {
track('checkout_method_switched', { method: event.detail })
})
```
## Common mistakes
* **Hiding a section without providing the equivalent data.** If you hide `billingAddress` but don't pass `customer-information.address.billingAddress`, the session fails with a validation error when the customer submits.
* **Injecting untrusted strings into `js`.** The `js` attribute runs arbitrary code inside the component. Only use it with code you authored.
* **Listening only for `success`.** `paymentMethodSelected` and `sessionClosed` are essential for analytics and one-click UX — without them you lose visibility into why customers drop off or switch methods.
* **Assuming `submitButton: false` disables the form.** It only hides the button. The form is still interactive; drive submission with `submit()` or the form stays idle.
## Next
* [Checkout](/sdk/checkout) — full `success` payload and multi-currency behavior.
* [Card tokenization](/sdk/card-tokenization) — when you want to save a card without charging.
* [Renew subscription card](/sdk/renew-subscription-card) — dedicated component for card renewal.
---
# Source: https://docs.rebill.com/sdk/implementation-examples.md
# Implementation example
The [Quickstart](/sdk/quickstart) shows the minimum render. This page walks through a production-grade integration in Next.js: plan selection on the client, `` rendering the payment UI, and a server route that verifies the outcome against Rebill's API before fulfilling access. The pattern applies to any framework — Next.js is used here only to keep the example concrete.
## Architecture
Three moving parts work together:
* **Client (Next.js page)** — renders your plan picker and the `` component with the `pk_` key.
* **Rebill** — hosts the tokenization iframe, creates the session, runs the charge, and fires webhooks.
* **Server (Next.js route handler)** — verifies the payment with `sk_` and grants access. The client never trusts the `success` event alone.
The `pk_` is public and safe to ship to the browser. The `sk_` stays server-side.
## Step 1 — Install and set environment variables
```bash
npm install rebill
```
```bash
# .env.local
NEXT_PUBLIC_REBILL_PK=pk_your_public_key
REBILL_SK=sk_your_secret_key
```
The `NEXT_PUBLIC_` prefix exposes the variable to the browser bundle — use it only for the public key. The secret key stays without the prefix so Next.js keeps it server-only.
## Step 2 — Render the checkout
Let the customer pick a plan, then render the component with the selected `plan-id`. The component listens for its own events; your React state only tracks selection.
```jsx
// app/checkout/page.jsx
'use client'
import { useEffect, useRef, useState } from 'react'
import 'rebill'
const plans = [
{ id: 'pln_basic_123', name: 'Basic', price: 9.90 },
{ id: 'pln_pro_456', name: 'Pro', price: 19.90 },
{ id: 'pln_elite_789', name: 'Elite', price: 49.90 },
]
export default function Checkout() {
const [selected, setSelected] = useState(plans[1])
const [status, setStatus] = useState('idle') // idle | verifying | granted | failed
const ref = useRef(null)
useEffect(() => {
const el = ref.current
if (!el) return
const onSuccess = async (event) => {
const { data } = event.detail
const paymentId = data?.result?.paymentId
if (!paymentId) return setStatus('failed')
setStatus('verifying')
const res = await fetch('/api/rebill/verify-payment', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ paymentId })
})
setStatus(res.ok ? 'granted' : 'failed')
}
const onError = () => setStatus('failed')
el.addEventListener('success', onSuccess)
el.addEventListener('error', onError)
return () => {
el.removeEventListener('success', onSuccess)
el.removeEventListener('error', onError)
}
}, [])
if (status === 'granted') return
}
)
}
```
The `key={selected.id}` prop re-mounts the component when the customer switches plans, which resets the session and keeps the form consistent with the selected price.
## Step 3 — Verify on the server
The `success` event tells you Rebill *authorized* the charge, not that the money moved (bank transfer and cash APMs fire success on request acceptance, not on settlement). Confirm status server-side before fulfilling access.
```javascript
// app/api/rebill/verify-payment/route.js
export async function POST(request) {
const { paymentId } = await request.json()
if (!paymentId) return new Response('missing paymentId', { status: 400 })
const res = await fetch(`https://api.rebill.com/v3/payments/${paymentId}`, {
headers: { 'x-api-key': process.env.REBILL_SK }
})
if (!res.ok) return new Response('payment not found', { status: 404 })
const payment = await res.json()
// Approved: settle access immediately.
// Pending (APMs): mark provisional; wait for the webhook.
// Rejected: bail.
if (payment.status !== 'approved') {
return new Response('payment not approved', { status: 409 })
}
// Your fulfillment logic here:
// await grantAccessToUser(payment.customer.id, payment.subscriptionId)
return new Response(JSON.stringify({ ok: true }), {
headers: { 'content-type': 'application/json' }
})
}
```
The secret key only ever lives in this route handler's process — never passed back to the browser.
## Step 4 — Handle webhooks
The client-side flow covers the happy path. Webhooks cover everything else: delayed APM settlements, renewals, failed retries, cancellations. Register one endpoint and branch on the event type.
```javascript
// app/api/webhooks/rebill/[secret]/route.js
import crypto from 'crypto'
export async function POST(request, { params }) {
if (params.secret !== process.env.REBILL_WEBHOOK_SECRET) {
return new Response('', { status: 404 }) // don't leak the endpoint shape
}
// Read the raw bytes — the HMAC is over what Rebill sent, not over a
// re-serialized object. Parse only after the signature checks out.
const raw = await request.text()
const expected = crypto
.createHmac('sha256', process.env.REBILL_SIGNING_SECRET)
.update(raw)
.digest()
const received = Buffer.from(request.headers.get('x-rebill-signature') ?? '', 'hex')
if (received.length !== expected.length ||
!crypto.timingSafeEqual(received, expected)) {
return new Response('', { status: 401 })
}
// Rebill wraps every event as:
// { data: , webhook: { id, event, url, logId } }
const { data, webhook } = JSON.parse(raw)
switch (webhook.event) {
case 'payment.created':
case 'payment.updated':
// Re-confirm against the API, then reconcile your records.
await reconcilePayment(data.payment.id)
break
case 'subscription.updated':
// Handle status transitions: active → retrying → defaulted → cancelled.
await syncSubscription(data) // data is the subscription-shaped payload
break
case 'subscription.renewal_soon':
// Send a card-update link if the card is about to expire.
await maybeSendRenewalLink(data.id)
break
}
return new Response('', { status: 200 })
}
```
The signature check is what proves the POST came from Rebill. `REBILL_SIGNING_SECRET` is the `whsec_` value from `POST /v3/webhooks` — see [Verify the signature](/guides/webhooks#verify-the-signature) for the full scheme and its limits. The secret path segment and the API re-confirmation stay useful on top of it: the signature has no timestamp, so it can't tell a fresh event from a replayed one.
## What this example doesn't cover
* **Checkout customization** — section visibility, coupons, pre-filled customer data. See [Customization](/sdk/customization).
* **Card-on-file collection without a charge** — see [Card tokenization](/sdk/card-tokenization).
* **Card renewal on existing subscriptions** — see [Renew subscription card](/sdk/renew-subscription-card).
* **Dynamic pricing** — if amount/currency depends on user input, switch from `plan-id` to [Instant Checkout](/sdk/instant-checkout).
## Common mistakes
* **Trusting `success` to grant access.** APM methods (PIX, Boleto, SPEI, PSE, bank transfers) fire `success` on request acceptance, not settlement. Always re-check via API or webhook.
* **Shipping `REBILL_SK` to the browser.** If the secret key shows up in the client bundle, any visitor can charge cards. Audit your `.env` prefixes (no `NEXT_PUBLIC_*` on secret keys) and your build output.
* **Mounting the component before the plan is selected.** Without a `plan-id` or `product-id`, `` can't create a session. Render it conditionally, or use the `key` prop to re-mount on change as shown above.
* **Forgetting the webhook endpoint.** If your only confirmation is `success`, you miss delayed APM settlements, renewal outcomes, and failed retries. Webhooks are not optional in production.
---
# Source: https://docs.rebill.com/sdk/instant-checkout.md
# Instant Checkout
Charge an amount that you decide at runtime instead of referencing a stored product or plan. Use it when pricing depends on the user (custom quotes, setup fees, usage roll-ups) or when the resource is short-lived and you don't want it cluttering your dashboard.
For install and first render, see the [Quickstart](/sdk/quickstart). For the standard plan/product flow, see [Checkout](/sdk/checkout).
## When to pick instant over product/plan
* The **amount** isn't known until the user picks add-ons, quantities, or a quote lands.
* You charge something **once** and don't need it as a permanent record.
* You're building a marketplace where every seller has bespoke prices.
If the prices are stable and you want the resource to show up in the dashboard with historical reporting, create a regular product or plan and pass its ID instead.
## `instant-product` — one-time charge
A complete `instant-product` object describes a single, non-recurring charge:
| Field | Type | Required | Notes |
| ------------- | ---------------------- | -------- | --------------------------------------------------------------------------------------------------- |
| `name` | `{ language, text }[]` | Yes | One entry per language. `language` is `'en' \| 'es' \| 'pt'`. |
| `description` | `{ language, text }[]` | No | Same shape as `name`. |
| `amount` | number | Yes | Decimal in the **major unit** (`1990.00` = $1,990 ARS, `25.50` = $25.50 USD). Not cents. |
| `currency` | string | Yes | ISO 4217. One of `ARS`, `BRL`, `CLP`, `COP`, `MXN`, `USD`. |
| `metadata` | object | No | Arbitrary key-value pairs. Stored on the resulting payment; retrievable via `GET /v3/payments/:id`. |
### Embed with static JSON
```html
```
The attribute must be a JSON string. Inside the component it's parsed, and **`metadata` is stripped before the request** (the SDK keeps it client-side and re-attaches it to the resulting product/plan in the session response). Everything else is forwarded to `POST /v3/sessions/sdk`.
### Set dynamically from JavaScript
```javascript
import 'rebill'
const el = document.querySelector('rebill-checkout')
el.setAttribute('instant-product', JSON.stringify({
name: [{ language: 'en', text: 'Custom Service Package' }],
description: [{ language: 'en', text: 'Tier A + priority support' }],
amount: 24.99,
currency: 'USD',
metadata: { selectedTier: 'A', support: 'priority' }
}))
```
## `instant-plan` — runtime-priced subscription
Use `instant-plan` when the subscription's price or cadence changes per customer:
| Field | Type | Required | Notes |
| ------------- | ---------------------- | -------- | -------------------------------------------------- |
| `name` | `{ language, text }[]` | Yes | |
| `description` | `{ language, text }[]` | No | |
| `amount` | number | Yes | Decimal major unit. The amount charged each cycle. |
| `currency` | string | Yes | ISO 4217. |
| `plan` | `PlanConfig` | Yes | Cadence + lifecycle, see below. |
| `metadata` | object | No | Attached to the subscription. |
The nested `plan` object:
| Field | Type | Required | Notes |
| ------------------ | -------------- | -------- | ---------------------------------------------------------------------------------------------------------------------- |
| `frequency.period` | string | Yes | `'minute' \| 'hour' \| 'day' \| 'month' \| 'year'`. Shorter cadences are for testing only. |
| `frequency.count` | number | Yes | `1` with `period: "month"` bills monthly. `3` with `period: "month"` bills every three months. |
| `repetitions` | number \| null | No | Total billing cycles before the subscription ends. Minimum `2` when set. Omit or set to `null` for open-ended renewal. |
| `type` | string | No | `'units' \| 'usage'`. Defaults to `units`. Use `usage` only when billing is metered (consumption reports). |
| `metadata` | object | No | |
```html
```
## Dynamic pricing pattern
A common shape: compute the amount from the customer's selection, serialize it, and hand it to the component before it renders.
```javascript
import 'rebill'
function buildInstantProduct(selections) {
const basePrice = 19.90
const addons = selections.includes('priority-support') ? 5.00 : 0
const total = basePrice + addons
return {
name: [{ language: 'en', text: 'Custom Package' }],
description: [{ language: 'en', text: `Includes: ${selections.join(', ')}` }],
amount: Number(total.toFixed(2)),
currency: 'USD',
metadata: { selections: selections.join(',') }
}
}
const el = document.querySelector('rebill-checkout')
el.setAttribute('instant-product', JSON.stringify(buildInstantProduct(['priority-support'])))
```
Set the attribute **before** the customer interacts with the form. Changing `instant-product` after the session has been created requires re-mounting the component.
## Common mistakes
* **Treating `amount` as cents.** `amount: 1990` means $1,990, not $19.90. Use decimals: `19.90`.
* **Forgetting `currency`.** Without it the session creation returns `400 currency must be a valid enum value`.
* **Sending `metadata` with non-string values hoping for filtering.** Metadata round-trips through JSON — the payment-service stores what you send, but filtering on nested objects isn't supported. Keep values flat strings or numbers.
* **Assuming `instant-plan` replaces `plan-id` forever.** The resulting subscription is real; it appears in the dashboard and renews on schedule. "Instant" refers to creation, not lifetime.
* **Using `period: "minute"` or `"hour"` in production.** Those cadences exist for testing subscription retries. Use `day`, `month`, or `year` for live customers.
## Next
* [Checkout](/sdk/checkout) — the stored-resource flow (`product-id` / `plan-id`).
* [Customization](/sdk/customization) — `display`, `css`, `one-click-checkout`, and the rest of the attribute list.
* [Test payment methods](/sdk/test-payment-methods) — cards and outcomes for sandbox runs.
---
# Source: https://docs.rebill.com/sdk/introduction.md
# Rebill SDK
Drop a checkout into any page and your customers pay without leaving your site. Cards are tokenized inside a PCI-compliant iframe hosted by Rebill, so your servers never see the PAN, CVV, or expiration date.
The package is published as [`rebill`](https://www.npmjs.com/package/rebill) on npm. For the install + first-render walkthrough, jump to the [Quickstart](/sdk/quickstart) — you can have a working checkout in under five minutes. This page explains what's in the box and why it's designed this way.
## The four components
Every component is a standard web component — `` tags work in React, Vue, Angular, Svelte, and plain HTML without framework-specific wrappers.
| Component | What it does | When to use it |
| ---------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------ |
| `` | Full checkout for products and plans (one-time + subscriptions). | Primary checkout on a product or signup page. |
| `` | Tokenizes a card and attaches it to a customer without charging. | Onboarding a card for later charges, or collecting a card on file. |
| `` | Prompts a customer to update the card on an existing subscription. | Dunning pages, "your card expired" flows. |
| `` | Lets an active customer swap the card on a subscription. | Account settings, billing portals. |
## Payment methods the components cover
The SDK renders whichever methods are enabled for your organization and configured for the session's country. Today that includes:
* **Cards** — Visa, Mastercard, Amex, Cabal, Argencard, Cencosud, CMR, Naranja, and Diners, across AR, BR, CL, CO, MX, and US.
* **Brazil** — PIX (instant bank transfer), Boleto (cash voucher).
* **Argentina** — Transferencias 3.0 (QR bank transfer).
* **Colombia** — PSE (direct bank transfer).
* **Mexico** — SPEI (interbank transfer).
* **Chile** — ETpay (bank transfer).
Installments for credit cards are surfaced automatically when the processor supports them (AR Decidir and BR providers).
## Why web components
Rebill ships Stencil-compiled custom elements instead of a JS client library for three reasons:
1. **Framework-agnostic by design.** A web component is a browser-native primitive. React wraps it with a ref, Vue binds it as a DOM element, and plain HTML consumes it directly. There's no version lock with your framework's release cycle.
2. **No secret key in the bundle.** The `public-key` attribute is safe to ship to the browser. Rebill authenticates the session server-side from that key plus the customer-controlled form data.
3. **PCI-compliant by construction.** Card fields live in an iframe served from `sdk-iframe.rebill.to`, not your origin. The card number never crosses into your page's JavaScript, which keeps your site out of PCI DSS scope.
## Common pitfall
Do not confuse the public key with the secret key. `pk_...` belongs in `` on the client. `sk_...` belongs on your backend in the `x-api-key` header when you call `/v3/payments`, `/v3/subscriptions`, or `/v3/refunds`. Shipping `sk_` to the browser exposes your entire organization.
## Next steps
* [Quickstart](/sdk/quickstart) — wire up the component in React, Vue, or plain HTML in under five minutes.
* [Checkout](/sdk/checkout) and [Instant Checkout](/sdk/instant-checkout) — pick between plan/product-based flows and dynamic pricing.
* [Customization](/sdk/customization#sdk-playground) — theme the component, hide sections, and pre-fill customer data.
* [Test payment methods](/sdk/test-payment-methods) — the sandbox card numbers that trigger each outcome.
---
# Source: https://docs.rebill.com/sdk/quickstart.md
# Quickstart
Install the SDK and render a working checkout in minutes.
## Prerequisites
Before you start, make sure you have:
* A Rebill account with products or plans created
* Your public key from the [Rebill Dashboard]($\{process.env.V3_DASHBOARD_URL})
* A modern web development environment
## Step 1: Install the SDK
```bash
npm install rebill
```
## Step 2: Import the SDK
Import the checkout component in your main project file:
```javascript
import 'rebill';
```
## Step 3: Get your public key
Find your public key, plan ID, and product ID in the [Rebill Dashboard]($\{process.env.V3_DASHBOARD_URL}).
## Step 4: Add the checkout component
### Next.js
In React, use refs to access the component and attach event listeners:
```javascript
"use client"
import 'rebill';
export default function Checkout() {
const checkoutRef = useRef(null);
const handleSuccess = (event) => {
const { paymentMethod, data } = event.detail;
// Your code here
};
const handleError = (event) => {
const { paymentMethod, data } = event.detail;
// Your code here
};
const handleFormChange = (event) => {
const { data, isValid } = event.detail;
// Your code here
};
useEffect(() => {
const element = checkoutRef.current;
if (!element) return;
element.addEventListener('success', handleSuccess);
element.addEventListener('error', handleError);
element.addEventListener('formChange', handleFormChange);
return () => {
element.removeEventListener('success', handleSuccess);
element.removeEventListener('error', handleError);
element.removeEventListener('formChange', handleFormChange);
};
}, [checkoutRef]);
return (
);
}
```
### HTML
```html
```
## Updating the SDK
Rebill ships bug fixes, new payment methods, and performance improvements continuously. Always use `@latest`.
To update to the latest version:
```bash
npm install rebill@latest
```
To install a specific version:
```bash
npm install rebill@x.x.x
```
## Next steps
Now that your checkout is working:
* **[Checkout](/sdk/checkout):** Configure product and plan checkouts
* **[Instant Checkout](/sdk/instant-checkout):** Create payments with dynamic pricing
* **[Customization](/sdk/customization):** Style the checkout to match your brand
---
# Source: https://docs.rebill.com/sdk/renew-subscription-card.md
# Renew subscription card
When a customer's card expires, gets blocked, or just needs to change, you need a way to collect the new card without losing the subscription state (cycle counters, discounts, dates). Rebill handles this through a **hosted renewal page** that you surface to your customer via a short-lived URL.
The page is pre-bound to the subscription, so when the customer tokenizes their new card, Rebill attaches it, resumes recurring charges, and fires a `subscription.updated` webhook.
## The flow
1. **Your backend** asks Rebill for a renewal URL for a given subscription.
2. **Rebill** returns a signed URL valid for a limited window (JWT-backed).
3. **You deliver the URL** to the customer — redirect them, email it, send it over WhatsApp, put it in your in-app dunning page.
4. **The customer** opens the URL and completes the form on Rebill's hosted page. No integration work on your side.
5. **Rebill** updates the subscription, fires `subscription.updated`, and resumes billing on the normal cycle.
## Generate the URL
Call the private endpoint with your secret key from your backend:
```bash
curl -X POST https://api.rebill.com/v3/subscriptions/sub_YOUR_SUBSCRIPTION_ID/card/update-link \
-H "x-api-key: $REBILL_SK"
```
Response:
```json
{
"url": "https://pay.rebill.com/your-org-alias/card-renewal/sub_YOUR_SUBSCRIPTION_ID?token=eyJhbGciOi..."
}
```
The URL carries a JWT that expires after the window configured on the subscription (default a few hours). Generating a new URL invalidates the previous one, so don't cache them — request a fresh one when you need it.
## Deliver the URL
Pick whichever channel fits the moment:
```javascript
// Redirect at dunning time
app.get('/billing/update-card', async (req, res) => {
const { url } = await rebillApi.post(`/v3/subscriptions/${req.user.subId}/card/update-link`)
res.redirect(url)
})
// Email on the "renewal soon" webhook
app.post('/webhooks/rebill', async (req, res) => {
if (req.body.event === 'subscription.renewal_soon') {
const { url } = await rebillApi.post(
`/v3/subscriptions/${req.body.data.subscription.id}/card/update-link`
)
await sendEmail({
to: req.body.data.customer.email,
subject: 'Update your card',
body: `Keep your plan active: ${url}`
})
}
res.status(200).end()
})
```
Any defense-in-depth around your webhook endpoint applies — see the webhook security guidance in [Going Live](/guides/going-live#webhooks).
## What the customer sees
Rebill's hosted page renders:
* The subscription name and current plan amount.
* A card form inside a PCI-compliant iframe (same tokenization path as ``).
* A success screen on completion.
The UI uses your organization's logo and language defaults. You don't configure anything per-renewal on the SDK side.
## After the customer submits
Rebill fires these webhooks:
* **`subscription.updated`** — `cardUpdated` is `true` and `card` points to the new `crd_...`. The status is untouched, so `status` and `lastStatus` both stay `active`: branch on `cardUpdated`, not on the statuses. See [Detecting a card change](/guides/webhooks#detecting-a-card-change).
* The next scheduled charge runs on the original `nextChargeDate` with the new card.
If the customer abandons the page (closes the tab mid-flow, lets the token expire), nothing changes — the subscription stays on the old card. Generate a fresh URL and retry.
## Advanced: embed the renewal form on your own domain
The hosted URL is the supported path for most cases. If you need to keep customers on your domain, Rebill ships two web components you can embed: `` (for ACTIVE or RETRYING subscriptions) and `` (for any status, including PAUSED or DEFAULTED). Both live in the [`rebill`](https://www.npmjs.com/package/rebill) npm package.
The embed flow has three steps.
### 1. Get the token from your backend
Use the same endpoints as the hosted flow. The returned URL contains the JWT as a base64-encoded token — strip it out:
```javascript
// Backend route
import { Buffer } from 'node:buffer'
app.get('/api/renewal/:subscriptionId', async (req, res) => {
const { url } = await rebillApi.post(
`/v3/subscriptions/${req.params.subscriptionId}/card/update-link`
)
// url looks like: https://pay.rebill.com/{alias}/card-renewal/{subId}?token={base64Jwt}
const base64Token = new URL(url).searchParams.get('token')
const jwt = Buffer.from(base64Token, 'base64').toString('utf-8')
res.json({ token: jwt })
})
```
### 2. Exchange the token for session data
The component needs the full `RenewalSession` object. Fetch it by calling Rebill's public session endpoint with the JWT as a bearer token — no `sk_` needed here, the JWT carries the authorization:
```javascript
const res = await fetch('https://api.rebill.com/v3/sessions/subscription-renewal', {
method: 'POST',
headers: {
'Authorization': `Bearer ${jwt}`,
'Content-Type': 'application/json',
'x-forwarded-for': clientIp // required — the endpoint rejects requests without it
},
body: JSON.stringify({ deviceId: generateDeviceId() })
})
const sessionData = await res.json()
```
The response matches the `RenewalSession` type exported by the SDK: `{ id, subscription, organization, paymentMethods, price, country, installments, requires3DS, createdAt, updatedAt }`.
### 3. Render the component
```jsx
'use client'
import { useEffect, useRef } from 'react'
import 'rebill'
export default function Renewal({ jwt, sessionData }) {
const ref = useRef(null)
useEffect(() => {
const el = ref.current
if (!el) return
const onSuccess = () => { /* show confirmation, reconcile via webhook */ }
const onError = () => { /* surface retry prompt */ }
el.addEventListener('success', onSuccess)
el.addEventListener('paymentError', onError) // note: paymentError, not error
return () => {
el.removeEventListener('success', onSuccess)
el.removeEventListener('paymentError', onError)
}
}, [])
return (
)
}
```
Attribute names differ per component. `` uses `renewal-data` + `renewal-token`. `` uses `change-card-data` + `change-card-token`. The payload shape is the same.
### Events
Both components emit `ready` (void) when mounted, `success` when the new card is attached, and `paymentError` on failure. Note: the error event is `paymentError`, **not** `error` — this differs from ``. `` also emits `paymentMethodSelected` when the customer switches payment method. Reconcile server-side via the `subscription.updated` webhook before granting final confirmation.
## Common mistakes
* **Caching the URL.** Tokens are single-use within a window. Treat every renewal moment as a fresh URL request.
* **Sending the URL over unencrypted channels.** The JWT authorizes card replacement — anyone who opens it can swap the card. HTTPS only, email and secure messaging only, no SMS short-links that preview the token.
* **Trying to `POST /v3/subscriptions/:id` with a new `cardId`.** The public update endpoint does not accept a card swap — the renewal URL is the only supported path for changing the card on an active subscription.
* **Listening for a synchronous result.** The URL is a handoff. Your code finds out the card was updated via the webhook, not a return value.
## Next
* [Card tokenization](/sdk/card-tokenization) — save a card without charging (net-new customer flow, not renewal).
* [Subscriptions API reference](/api/reference/subscriptions) — request-card-change, change-card, and other subscription lifecycle endpoints.
* [Going Live](/guides/going-live) — webhook setup to drive the renewal flow automatically.
---
# Source: https://docs.rebill.com/sdk/test-payment-methods.md
# Test payment methods
Drive the SDK through every outcome before going live — approved, declined, and a renewal that fails after the first charge — so you can validate your `success`, `error`, and webhook paths without moving real money.
Cards work only when the `pk_` belongs to a **sandbox organization** (`isSandbox: true`). Using a sandbox card against a production org returns `400 Sandbox cards are only available for sandbox organizations`.
The outcome is decided by the **card number alone**. Expiry date and CVV are ignored in sandbox, so any values work; the ones listed below are simply the canonical ones.
To run the SDK end-to-end in sandbox, drop `` on a page with your sandbox `pk_` and one of the cards below, or hit the API directly with the [Postman collection](/api/test-tools-and-resources#postman-collection).
## Testing a subscription end to end
Declines are not all equivalent, and the difference decides whether you see a dunning cycle at all:
* A **hard** decline (`bank_rejected`, and the generic ones) marks the cycle `defaulted` on the first attempt. No retries follow. Use it to test the immediate cut-off.
* A **soft** decline (`insufficient_funds`) and `invalid_card_cvc` start the retry ladder.
To walk a subscription through its whole life:
1. Create the subscription with the card that **approves the first charge and declines every renewal**. The first charge succeeds and the subscription goes active.
2. Call `POST /v3/subscriptions/{id}/sandbox/advance` to run the renewal immediately instead of waiting for it. It is declined with `insufficient_funds`, and the retry ladder starts.
3. Call the same endpoint again for each retry. After the fourth, the cycle ends as `defaulted`.
The endpoint runs the same charge the scheduler would run, so you observe real payments, events and webhooks — it only skips the waiting. It is available for sandbox organizations only.
If you would rather watch it happen on its own, the sandbox retry ladder runs at 2, 2, 3 and 3 **hours**, so the full cycle finishes within a working day instead of the 10 days it takes in production.
To see repeated successful renewals instead, create the plan with an `hour` frequency, which sandbox organizations can use and production organizations cannot.
## Argentina
| Brand | Card Number | CVV | Outcome |
| :----------- | :------------------ | :--- | :----------------------------------------------------------------------------- |
| Visa | 4539 1488 0343 6467 | 123 | Approved |
| Mastercard | 5511 0002 2141 4072 | 123 | Approved |
| Visa (debit) | 4280 6300 0000 7713 | 123 | Approved |
| Visa | 4485 3647 3952 7352 | 123 | Declined — generic (hard, no retries) |
| Amex | 3744 009328 89231 | 1234 | Declined — generic (hard, no retries) |
| Visa | 4242 4247 6602 5862 | 123 | Declined — `invalid_card_cvc` (retries) |
| Mastercard | 5511 0194 3632 1948 | 123 | Declined — `bank_rejected` (hard, no retries) |
| Visa | 4573 0152 9630 7413 | 123 | Approves the first charge, declines every renewal (`insufficient_funds`, soft) |
| Mastercard | 5457 0152 9630 7411 | 123 | Declined — `insufficient_funds` (soft, retries) |
## Brasil
| Brand | Card Number | CVV | Outcome |
| :--------- | :------------------ | :--- | :----------------------------------------------------------------------------- |
| Visa | 4532 6381 9543 1230 | 123 | Approved |
| Mastercard | 5555 1224 2222 4444 | 123 | Approved |
| Visa | 4486 3462 9547 8236 | 123 | Declined — generic (hard, no retries) |
| Amex | 3745 007985 69882 | 1234 | Declined — generic (hard, no retries) |
| Visa | 4243 4384 1852 5292 | 123 | Declined — `invalid_card_cvc` (retries) |
| Mastercard | 5105 1051 0510 5100 | 123 | Declined — `bank_rejected` (hard, no retries) |
| Visa | 4573 0252 9630 7412 | 123 | Approves the first charge, declines every renewal (`insufficient_funds`, soft) |
| Mastercard | 5457 0252 9630 7410 | 123 | Declined — `insufficient_funds` (soft, retries) |
## México
| Brand | Card Number | CVV | Outcome |
| :--------- | :------------------ | :--- | :----------------------------------------------------------------------------- |
| Visa | 4539 6722 9047 6389 | 123 | Approved |
| Mastercard | 5555 0020 4444 0000 | 123 | Approved |
| Visa | 4485 5500 9000 0001 | 123 | Declined — generic (hard, no retries) |
| Amex | 3746 004321 71150 | 1234 | Declined — generic (hard, no retries) |
| Visa | 4244 4482 8478 2108 | 123 | Declined — `invalid_card_cvc` (retries) |
| Mastercard | 5512 0022 3704 7986 | 123 | Declined — `bank_rejected` (hard, no retries) |
| Visa | 4573 0352 9630 7411 | 123 | Approves the first charge, declines every renewal (`insufficient_funds`, soft) |
| Mastercard | 5457 0352 9630 7419 | 123 | Declined — `insufficient_funds` (soft, retries) |
## Chile
| Brand | Card Number | CVV | Outcome |
| :--------- | :------------------ | :--- | :----------------------------------------------------------------------------- |
| Visa | 4539 9978 0343 0007 | 123 | Approved |
| Mastercard | 5513 0064 2588 8480 | 123 | Approved |
| Visa | 4485 1070 3952 0019 | 123 | Declined — generic (hard, no retries) |
| Amex | 3747 008169 54658 | 1234 | Declined — generic (hard, no retries) |
| Visa | 4245 4517 6949 2184 | 123 | Declined — `invalid_card_cvc` (retries) |
| Mastercard | 5513 0138 0747 2123 | 123 | Declined — `bank_rejected` (hard, no retries) |
| Visa | 4573 0452 9630 7410 | 123 | Approves the first charge, declines every renewal (`insufficient_funds`, soft) |
| Mastercard | 5457 0452 9630 7418 | 123 | Declined — `insufficient_funds` (soft, retries) |
## Colombia
| Brand | Card Number | CVV | Outcome |
| :--------- | :------------------ | :--- | :----------------------------------------------------------------------------- |
| Visa | 4539 5422 0343 0015 | 123 | Approved |
| Mastercard | 5555 9856 4444 0026 | 123 | Approved |
| Visa | 4485 6749 3952 0027 | 123 | Declined — generic (hard, no retries) |
| Amex | 3748 009837 05170 | 1234 | Declined — generic (hard, no retries) |
| Visa | 4246 4654 1876 2341 | 123 | Declined — `invalid_card_cvc` (retries) |
| Mastercard | 5514 0055 0087 2558 | 123 | Declined — `bank_rejected` (hard, no retries) |
| Visa | 4573 0552 9630 7419 | 123 | Approves the first charge, declines every renewal (`insufficient_funds`, soft) |
| Mastercard | 5457 0552 9630 7417 | 123 | Declined — `insufficient_funds` (soft, retries) |
## United States
| Brand | Card Number | CVV | Outcome |
| :--------- | :------------------ | :--- | :----------------------------------------------------------------------------- |
| Visa | 4111 1122 3344 5551 | 123 | Approved |
| Mastercard | 5301 0012 3456 7892 | 123 | Approved |
| Visa | 4488 0000 1111 2227 | 123 | Declined — generic (hard, no retries) |
| Amex | 3749 001234 56781 | 1234 | Declined — generic (hard, no retries) |
| Visa | 4247 4712 3456 7897 | 123 | Declined — `invalid_card_cvc` (retries) |
| Mastercard | 5515 0012 3456 7894 | 123 | Declined — `bank_rejected` (hard, no retries) |
| Visa | 4573 0652 9630 7418 | 123 | Approves the first charge, declines every renewal (`insufficient_funds`, soft) |
| Mastercard | 5457 0652 9630 7416 | 123 | Declined — `insufficient_funds` (soft, retries) |
Use these cards to verify your debit and credit card payment flows.
## PIX
* PIX payments in sandbox mode are automatically approved **30 minutes** after creation.
* Requires a **valid zip code** and **valid CPF** for successful BRL payments.
* Generate a test CPF [here](https://www.4devs.com.br/gerador_de_cpf). The address must match the CPF's state.
## Nequi
Nequi sandbox outcomes depend on the payment amount:
1. **Approved:** 0--99,999 COP or more than 200,000 COP
2. **Not found:** 150,000--200,000 COP
3. **Canceled:** 100,000--150,000 COP
Confirm you are in sandbox mode before testing.
---
# Source: https://docs.rebill.com/sdk/web-components.md
# Web components reference
Rebill ships four web components through the [`rebill`](https://www.npmjs.com/package/rebill) npm package. Each is a standard `` — no framework wrapper, no build step beyond the single `import 'rebill'`. This page is an index; each component has its own page with props, events, and common mistakes.
```bash
npm install rebill
```
```javascript
import 'rebill'
```
## ``
Full checkout flow for products and plans. Handles payment method selection, card tokenization (iframe), 3DS, identification, installments, coupons, and one-click for returning customers.
```html
```
Required: `public-key` plus one of `product-id`, `plan-id`, `instant-product`, or `instant-plan`.
Events: `success`, `error`, `formChange`, `paymentMethodSelected`, `sessionClosed`, `successRedirect`.
See [Checkout](/sdk/checkout) and [Instant Checkout](/sdk/instant-checkout).
## ``
Tokenize a card without charging it. Returns a `crd_...` ID you use later to bill the customer via the API.
```html
```
Required: `public-key`, `processing-country`.
Events: `success`, `error`, `successRedirect`.
See [Card tokenization](/sdk/card-tokenization).
## ``
Replaces the card on an **ACTIVE or RETRYING** subscription. Requires a short-lived renewal token (JWT) issued by `GET /v3/subscriptions/:id/generate-renewal-url`.
```html
```
Required: `renewal-data` (JSON stringified `RenewalSession`), `renewal-token` (JWT).
Events: `ready`, `success`, `paymentError`, `paymentMethodSelected`.
See [Renew subscription card](/sdk/renew-subscription-card) for the end-to-end flow, including how to exchange the token for `renewal-data` via `POST /v3/sessions/subscription-renewal`.
## ``
Replaces the card on a subscription in **any status** (including PAUSED, CANCELLED, DEFAULTED, FINISHED). Uses a longer-lived recovery token issued by `POST /v3/subscriptions/:id/card/update-link`.
```html
```
Required: `change-card-data` (JSON stringified `RenewalSession`), `change-card-token` (recovery JWT).
Events: `ready`, `success`, `paymentError`.
Attribute names differ from ``, but the data shape is the same. The exchange flow to obtain the data is documented in [Renew subscription card](/sdk/renew-subscription-card#advanced-embed-the-renewal-form-on-your-own-domain).
**When to pick which**: call `/generate-renewal-url` for healthy subscriptions; call `/card/update-link` when the sub has been paused/defaulted/cancelled and you want to recover it.
## Framework compatibility
Every component is a standard web component, so React, Vue, Angular, Svelte, and plain HTML all consume them the same way. There are no `rebill-react-sdk` or `rebill-angular-sdk` wrapper packages — the single `rebill` package is the only thing you install.
* **React / Next.js** — attach listeners with `useRef` + `useEffect`. See the pattern in [Implementation example](/sdk/implementation-examples).
* **Vue** — bind attributes with `:public-key="..."`, listen with `@success="..."`.
* **Angular** — add `CUSTOM_ELEMENTS_SCHEMA` to your module, then bind attributes as usual.
* **Plain HTML** — drop the tag, call `addEventListener('success', ...)` after the page loads.
## Versioning
The package follows semver. Check the [npm page](https://www.npmjs.com/package/rebill) for the latest version and changelog. Breaking changes to attribute names or event shapes land on a major version bump.
## Next
* [Quickstart](/sdk/quickstart) — install + first render.
* [Customization](/sdk/customization) — every attribute and display flag on ``.
* [Test payment methods](/sdk/test-payment-methods) — sandbox cards for each country.
---
# Source: https://docs.rebill.com/what-is-rebill.md
# What is Rebill?
Rebill is the payment infrastructure built for Latin America. Charge in local currency, sell subscriptions, and operate with or without a local entity in each market — all through a single integration and one API for every country you process in.
***
## Supported payment methods
| Country | Payment methods |
| ----------------- | ---------------------------------------------------------------------------------- |
| **Argentina** | Cards, QR transfers from any wallet (MercadoPago, Ualá, Modo, Brubank, and others) |
| **Brazil** | Cards, Pix, Boleto |
| **Chile** | Cards, transfers (ET Pay) |
| **Colombia** | Cards, transfers (PSE) |
| **Mexico** | Cards, transfers (SPEI) |
| **United States** | Cards |
Cards (debit and credit) are available in every country.
***
## What you can do with Rebill today
**Operate without a local entity in every market**
1. Sell locally with an entity in the same country, or cross-border with an entity in a different country. No local entity required per market.
2. Charge in USD with global reach.
**Accept the right method for every market**
3. Connect to the leading local payment methods in each country — bank transfers, Pix, and digital wallets.
4. Offer installments across every LATAM country so your customers can finance their purchases.
5. Achieve acceptance rates above 90% through direct processor connections.
**Manage recurring revenue**
6. Run subscriptions with flexible billing cycles and automatic retries that recover failed payments.
7. Tokenize cards securely to enable subscriptions and on-demand charges without storing card data yourself.
**Sell with payment links**
8. Generate payment links with multi-currency and multi-language support — no integration required to start collecting.
**Engage your customers**
9. Send automatic payment notifications by WhatsApp, email, or SMS.
10. Display a soft descriptor on customer bank statements that identifies your brand.
**Run your business**
11. Track key metrics — volume, approval rate, churn, revenue by country — from a single dashboard.
12. Reach a dedicated support team whenever you need it.
---
# Source: https://docs.rebill.com/why-rebill.md
# Why Rebill?
Transparent pricing. Dedicated human support. No hidden fees.
Pay only for what you use. No hidden fees, no surprises. Rebill gives you full visibility into your payment operation costs so you can plan as you grow.
Get support from dedicated, real people, not bots, not automated queues. Your team gets direct access to engineers and account managers who know your integration.
Rebill works for companies across EdTech, e-learning, SaaS, and MedTech. Manage payments, automate billing, tokenize cards, and scale subscriptions, all from a single platform.
### What can you do with Rebill
* Collect payments in local currencies across 10+ countries, with deep coverage in Latin America.
* Accept the region's most popular payment methods: debit cards, credit cards, bank transfers, and cash deposits.
* Approval rates above 90% through direct processor connections.
* Full transparency on payment operation costs. No hidden fees.
* Dedicated support from real people. No bots. No automated replies.
***
# FAQs