ShotPay Button
Early access
A drop-in, branded button for your storefront: it asks your server to open a checkout session, guards against double clicks, and sends the customer to the hosted checkout with a top-level redirect. It is the one browser piece of the SDK family, and it works precisely because it holds nothing secret — your API key stays on your server, where the session is opened.
On BigCommerce, none of this is yours to wire up — the plugin mounts and configures this button for you.
What it does — and does not
The button owns the first leg of the
checkout flow: open a session, redirect. It
mints an idempotency key per attempt so a retried click cannot open two
sessions, disables itself while the session opens, and surfaces failures in
an aria-live region.
It does not open the session itself (that is your server, with the
server SDKs or a direct call to
POST /v1/sessions), and it does
not replace the handshake — your webhook handler
and return_url page still decide when an order is paid.
Install
@shotpay/button — view on npm.
npm install @shotpay/button
React is an optional peer dependency — install it only if you import
@shotpay/button/react. The package is 0.x, so breaking changes can land in
a minor bump (0.1.x → 0.2.0); a default ^0.1.0 range resolves only
0.1.x.
No build step is required to try it — the ESM build runs from any bundler, and CJS and type definitions ship alongside it.
Usage
Point it at an endpoint on your own server that opens the session and responds with it:
import { mount } from '@shotpay/button';
mount(document.querySelector('#shotpay'), {
endpoint: '/shotpay/create-session',
});
Your endpoint receives POST {} with an Idempotency-Key header — pass that
through to ShotPay — and responds with the session, bare or wrapped in a
data envelope. checkout_url is the only field the button reads; on
success it redirects with window.location.assign(session.checkout_url).
When the request needs your own payload, or your framework's fetch wrapper,
take over the call with createSession:
mount(element, {
createSession: async ({ idempotencyKey }) => {
const response = await fetch('/shotpay/create-session', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify({ cart: cartId }),
});
return response.json();
},
});
Resolving with null aborts quietly — the button resets without an error,
which is how your own validation says "not yet".
In React
import { ShotPayButton } from '@shotpay/button/react';
<ShotPayButton createSession={openSessionOnYourServer} />;
The wrapper renders the same button and forwards every prop below.
Options
| Option | Default | What it does |
|---|---|---|
createSession |
— | Opens the session on your server. Exactly one of this or endpoint. |
endpoint |
— | URL the button POSTs to instead of createSession. |
onSession |
redirect | Receives the opened session. Omit it to get the checkout redirect. |
onError |
built-in text | Receives failures instead of the button's own error line. |
theme |
'dark' |
'dark' or 'light'. |
label |
Pay with ShotPay |
The resting label. |
loadingLabel |
Opening… |
The label while the session opens. |
size |
'md' |
Padding scale: 'sm', 'md', or 'lg'. |
radius |
'md' |
Corner rounding: 'none', 'sm', 'md', or 'pill'. |
fullWidth |
true |
false shrinks the button to its content instead of filling the host. |
showLogo |
true |
false hides the ShotPay mark, leaving the label alone. |
disabled |
false |
Renders the button but refuses clicks. |
type |
'button' |
'submit' joins a surrounding form; the click is intercepted either way. |
The label, theme, loading text, size, shape, and logo visibility are configurable; the palette and the mark's geometry are not — that is the point of shipping a button.
Page requirements
The button injects one <style> element, so a Content-Security-Policy must
allow inline styles for style-src. Everything it renders is plain light-DOM
markup — an ordinary <button> your tests, analytics, and focus management
can see.