# 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, `<rebill-checkout>` 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.&#x20;

## Architecture

Three moving parts work together:

* **Client (Next.js page)** — renders your plan picker and the `<rebill-checkout>` 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 <h1>Welcome to {selected.name}</h1>

  return (
    <main>
      <section>
        {plans.map(p => (
          <button key={p.id} onClick={() => setSelected(p)}>
            {p.name} — ${p.price}
          </button>
        ))}
      </section>

      <rebill-checkout
        ref={ref}
        key={selected.id}
        public-key={process.env.NEXT_PUBLIC_REBILL_PK}
        plan-id={selected.id}
      />

      {status === 'verifying' && <p>Confirming your payment…</p>}
      {status === 'failed' && <p>Payment could not be confirmed. Please try again.</p>}
    </main>
  )
}
```

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: <event-specific payload>, 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 → paused (statusDetail: 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`, `<rebill-checkout>` 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.
