React
React bindings for the embedded checkout element. SSR-safe, shaped like @stripe/react-stripe-js.
Install
@billkit-eu/js, react 18+ and react-dom 18+ are peer dependencies, so install the loader alongside this package.
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.
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.
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
import { BillKitProvider, PaymentMethodElement } from "@billkit-eu/react";
<BillKitProvider>
<PaymentMethodElement clientSecret={clientSecret} customerId="cus_9XKp2vQ1" />
</BillKitProvider>; Server rendering
Both components 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.
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 updateTheme(tokens). The declarative theme prop is hot-applied without a remount and is usually what you want. <PaymentMethodElement/>’s ref exposes updateTheme() only: 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, locale and the origin overrides remount the element, because a remount destroys an in-progress payment. customerId has to be on that list: the wallet’s set-default and remove actions act on whichever customer the iframe was initialised with, so a stale frame would point them at the wrong person.
Content Security Policy
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.
BillKit