BillKit/DocsConsole
SDK

React

React bindings for the embedded checkout element. SSR-safe, shaped like @stripe/react-stripe-js.

v1.1.0React 18 or 19, and @billkit-eu/js 1.1 or newerOfficialSource

Install

npm install @billkit-eu/react @billkit-eu/js

@billkit-eu/js (1.1 or newer within 1.x), react and react-dom (18 or 19) are peer dependencies, so install the loader alongside this package. Both React majors are tested on every change.

The package follows semantic versioning from 1.0.0: nothing in a 1.x release breaks code written against an earlier 1.x. Coming from 0.x, read Upgrading from 0.x.

This is the browser half. No secret key ever belongs in a browser; for server calls use @billkit-eu/sdk. All of the security model and postMessage handling lives in @billkit-eu/js, and this package is the React surface over it.

Quick start

Mint an ephemeral client_secret on your server, then hand it to the element.

tsx
import { BillKitProvider, CheckoutElement } from "@billkit-eu/react";

export function Checkout({ clientSecret }: { clientSecret: string }) {
return (
  <BillKitProvider>
    <CheckoutElement
      clientSecret={clientSecret}
      theme={{ colorPrimary: "#2f6bff", borderRadius: "10px" }}
      onSuccess={({ sessionId }) => router.push(`/thanks?cs=${sessionId}`)}
      onError={({ message }) => toast.error(message)}
    />
  </BillKitProvider>
);
}

No credential goes on the provider. The client_secret already names the tenant, the mode and the session. <BillKitProvider> takes iframeOrigin, apiBase and logger, applied to every element beneath it. Rendering any of the three elements outside a provider throws, and the error names all three.

The element props are the @billkit-eu/js options (clientSecret, theme, locale, title, loadTimeoutMs, logger, onReady, onChange, onProcessing, onEscape (both since 1.1), onSuccess, onError, onRedirect), plus className and style for the container the iframe mounts into. title is the iframe’s accessible name; it defaults to English, so set it when your page is not. See @billkit-eu/js for what each one does and for the error codes.

No publishable key

Migrating from @stripe/react-stripe-js? The shapes line up, with one difference. There is no publishableKey. BillKit only mints secret keys, which must never reach a browser, so elements authenticate with the ephemeral client_secret instead.

Saved payment methods

tsx
import { BillKitProvider, PaymentMethodElement } from "@billkit-eu/react";

<BillKitProvider>
<PaymentMethodElement clientSecret={clientSecret} />
</BillKitProvider>;

The client_secret comes from an embedded checkout session created for that customer, and the wallet always acts on the customer that session belongs to, so no customerId is needed. A new clientSecret remounts the element.

One-off payments

<OneShotPaymentElement/> takes the same props as <CheckoutElement/>, for a single charge with no subscription behind it. Pass the client_secret from POST /v1/checkout/one_shot with ui_mode: "embedded"; onSuccess receives { oneShotPaymentId, paymentStatus }. A decline is final for that payment, so to let the buyer try again create a new one-off payment and pass its secret, which remounts the element. See Take the payment on your own page.

Server rendering

All three components (<CheckoutElement/>, <PaymentMethodElement/> and <OneShotPaymentElement/>) render null on the server and on the first client render, then mount the iframe after hydration. No hydration mismatch, no window access during render. Next.js App Router, Remix and Astro islands work with no dynamic(..., { ssr: false }) wrapper.

Your own pay button

Take a ref and call submit(). Gate the button on onChange’s complete, and re-enable it from onError: a declined card fires onError({ code: "payment_declined" }) while the element shows its own retry panel, so that is the only signal the attempt is over.

tsx
import { useRef, useState } from "react";
import { BillKitProvider, CheckoutElement, type BillKitElementRef } from "@billkit-eu/react";

function Checkout({ clientSecret }: { clientSecret: string }) {
const element = useRef<BillKitElementRef>(null);
const [complete, setComplete] = useState(false);
const [submitting, setSubmitting] = useState(false);

return (
  <BillKitProvider>
    <CheckoutElement
      ref={element}
      clientSecret={clientSecret}
      onChange={({ complete }) => setComplete(complete)}
      onError={() => setSubmitting(false)}
      onSuccess={({ sessionId }) => router.push(`/thanks?cs=${sessionId}`)}
    />
    <button
      disabled={!complete || submitting}
      onClick={() => {
        setSubmitting(true);
        element.current?.submit();
      }}
    >
      Pay
    </button>
  </BillKitProvider>
);
}

The ref also exposes focus(), which moves keyboard focus into the element for a page that reveals it in a later step, and updateTheme(tokens). The declarative theme prop is hot-applied without a remount and is usually what you want. <PaymentMethodElement/>’s ref exposes focus() and updateTheme() and no submit(): it has no form to submit, its actions are per-row buttons inside the iframe.

Callbacks and re-renders

Pass inline arrow functions freely. Callbacks are read through a ref at event time, so a fresh closure on every render does not tear down a live payment iframe. The same goes for logger.

Only clientSecret, customerId (when you pass one), locale, title and the origin overrides remount the element, because a remount destroys an in-progress payment.

Content Security Policy

text
frame-src https://js.billkit.eu;

If the iframe never boots, onError fires with load_timeout. A missing frame-src and an ad blocker look identical from the page, so check both.

Logging

Silent by default. Opt in on the provider with logger={console}, or per element. The client_secret, message payloads and full redirect URLs are never logged.

Upgrading from 0.x

1.0 is a deliberate break, and every change is listed in the package’s CHANGELOG.md. In short:

  • Peer ranges: @billkit-eu/js ^1.0.0 (^1.1.0 since 1.1), and react / react-dom ^18 || ^19. A future React major is not assumed to work until it is tested.
  • customerId on <PaymentMethodElement/> is optional.
  • New: the title prop, and focus() on every element’s ref.
  • The outside-a-provider error names every element, not only <CheckoutElement/>.
  • Everything listed under @billkit-eu/js applies too, since the types are re-exported from it.