@shoppexio/checkout-js
v0.7.0
Published
Official JavaScript SDK for Shoppex checkout: embed the hosted checkout modal, or build a fully custom (headless) checkout via @shoppexio/checkout-js/headless
Maintainers
Readme
@shoppexio/checkout-js
Official JavaScript SDK for embedding Shoppex hosted checkout as a modal on any website.
Install
npm install @shoppexio/checkout-jsVanilla JS
import { open, on } from '@shoppexio/checkout-js';
on((event) => {
if (event.type === 'success') console.log('Paid', event.payload?.invoiceId);
});
document.querySelector('#buy')?.addEventListener('click', () => {
void open({
shopId: 'your-shop',
items: [{ productId: 'PROD_123', quantity: 1 }],
theme: 'auto',
locale: 'de',
returnUrl: 'https://your-site.com/thanks',
});
});Events
on(listener) receives every checkout event and returns an unsubscribe function.
| event.type | event.payload | When |
|---|---|---|
| ready | — | A checkout step finished mounting. Fires again for each step. |
| invoice-created | { invoiceId } | An invoice exists. Not paid yet. |
| redirect / external-redirect | { url } | The buyer is going to a payment provider. |
| success | { invoiceId, completionAccessGrant } | Paid. Fire your conversion here. |
| error | { error } | Checkout failed — reported by the checkout, or by the modal when the checkout could not be started at all. |
| close | { completed, invoiceId } | The modal closed — by the close button, Escape, the backdrop, close(), or the checkout itself. completed: false means the buyer left without paying. |
| resize | { height, width } | Handled by the modal; rarely useful. |
| rewards-updated | { invoiceId, rewards } | Rewards were recalculated. |
| setLocale | { locale } | The buyer switched language inside the checkout. |
rewards-applied, rewards-error, and style-update are typed and forwarded
but are not emitted by a live checkout today.
close and error reach this SDK on two channels — the checkout's own messages
and the modal script's page events — because the modal script is served
unversioned and can be an older cached build than this package. Each occurrence
is reported once regardless: close fires exactly once per opened modal, and an
error the checkout reports is not counted twice.
on((event) => {
if (event.type === 'success') analytics.track('purchase', event.payload);
if (event.type === 'close' && !event.payload?.completed) {
analytics.track('checkout_abandoned');
}
});Full reference, including the CSP directives your site needs: docs.shoppex.io/developers/embeds/reference.
React
import { ShoppexCheckout } from '@shoppexio/checkout-js/react';
export default function BuyButton() {
return (
<ShoppexCheckout
shopId="your-shop"
items={[{ productId: 'PROD_123', quantity: 1 }]}
locale="de"
onSuccess={(invoiceId) => console.log('paid', invoiceId)}
trigger={<button>Buy now</button>}
/>
);
}Headless (build your own checkout UI)
For full control of the checkout step on your own domain, use the headless
primitives. You build every pixel; Shoppex runs the invoice, payment session,
webhooks, and fulfillment. Add your site's origin under Dashboard → Checkout →
Headless and use your publishable key (pk_live_…).
import {
ShoppexCheckoutProvider,
useCheckoutSession,
useStripePaymentSession,
ShoppexPoweredBy,
} from '@shoppexio/checkout-js/headless/react';
function Checkout() {
const { session } = useCheckoutSession({ product_id: 'PROD_123', email });
const stripe = useStripePaymentSession(session?.id, {
returnUrl: 'https://your-site.com/thanks',
});
return (
<form onSubmit={(e) => { e.preventDefault(); stripe.confirm(); }}>
{/* your layout, your styles */}
<div ref={stripe.mountRef} />
<button disabled={!stripe.ready}>Pay</button>
<ShoppexPoweredBy />
</form>
);
}
export default function App() {
return (
<ShoppexCheckoutProvider publishableKey="pk_live_…">
<Checkout />
</ShoppexCheckoutProvider>
);
}Render against the session kind (embed | redirect | address | balance |
manual), not the provider name — that keeps redirect and crypto gateways
provider-independent. Stripe has a React hook. Square, NMI, SumUp, and PayPal
have framework-agnostic provider adapters exported from /headless:
import {
HeadlessCheckoutClient,
mountSquareCard,
mountNmiCardFields,
mountSumupCard,
mountPaypalButtons,
} from '@shoppexio/checkout-js/headless';Card fields stay inside the provider SDK/iframe. Shoppex never receives the PAN.
mountSumupCard and PayPal order capture call on_complete only after Shoppex
projects COMPLETED; use on_pending to show provider processing. For PayPal subscriptions,
on_approved means the buyer approved the subscription at PayPal. Keep polling
the checkout session until its webhook-backed payment_status becomes
COMPLETED; subscription approval is not payment completion.
ready is bound to the session id you passed on that render. Hand
useStripePaymentSession a different session and ready is false from that
render on, until the new payment form is mounted — so disabled={!stripe.ready}
is a complete guard. confirm() refuses (throws) if it is called while the
mounted form still belongs to a previous session, rather than confirming one
session against another's return URL.
<ShoppexPoweredBy /> is required unless the server returns an active White Label entitlement.
The SDK refuses to confirm a payment unless the badge is visible whenever
session.attribution.required is true. The hidden White Label plan is
assigned by Shoppex operators and includes Business; it is not publicly
purchasable. A gateway's presentation.hide_provider_attribution preference only
affects that payment provider's attribution; it never hides Shoppex branding.
The framework-independent client does not use React:
import { HeadlessCheckoutClient } from '@shoppexio/checkout-js/headless'.
Without React hooks, you must enforce the badge. Register the visible badge
with registerAttributionBadge(el, session.attribution). Before you confirm a
payment, call assertAttribution(session.attribution.required). Both functions
are exported from /headless. The server directive protects the required label
and Shoppex link from DOM or CSS changes.
The session includes authoritative gateway fee previews, payment-method labels and requirements, buyer identity state, Customer Balance availability, product custom-field definitions, and delivery-instruction definitions. Use the client methods instead of writing directly to invoice endpoints:
await client.updateSession(session.id, {
email: '[email protected]',
payment_method_terms_accepted: true,
billing_address: {
name: 'Buyer Name',
line1: '1 Main Street',
city: 'Berlin',
country: 'DE',
postal_code: '10115',
},
});
if (!session.payment_required) await client.completeFreeCheckout(session.id);EU consumer law lets buyers of digital goods waive their 14-day withdrawal
right in exchange for immediate delivery. When the shop has enabled this under
Dashboard → Settings → Checkout, the session tells you whether the waiver is
outstanding for this buyer. Render withdrawal_consent.text verbatim as an
unchecked checkbox and record it before starting the payment — otherwise the
payment start is refused with 409 withdrawal_consent_required:
const consent = session.withdrawal_consent;
if (consent.required && !consent.recorded_at) {
// buyer ticked the checkbox showing consent.text
session = await client.recordWithdrawalConsent(session.id, consent.text_version);
}
// React: const { recordWithdrawalConsent } = useCheckoutSession(...)useStripePaymentSession starts the Stripe attempt as soon as the session id
arrives. When that start is refused because the consent is outstanding, the
hook re-issues it by itself once recordWithdrawalConsent() succeeds; other
gateways start on your call, so start them after the consent.
await client.requestBalanceOtp(session.id, '[email protected]');
const verified = await client.verifyBalanceOtp(session.id, '[email protected]', '123456');
await client.payWithBalance(session.id, verified.session_token);Crypto progress and underpayments are returned from the authoritative payment projection. Do not calculate the remainder from floating-point numbers:
const detail = session.payment_detail;
if (typeof detail?.confirmations_needed === 'number') {
console.log(`${detail.confirmations ?? 0}/${detail.confirmations_needed}`);
}
if (detail?.remaining && detail.buyer_actionable !== false) {
console.log(`Send ${detail.remaining} ${detail.crypto_currency ?? 'crypto'}`);
}
if (detail?.buyer_actionable === false) {
console.log(detail.buyer_action_reason);
}
if (detail?.received && detail.received_fiat_estimate) {
console.log(
`Received on-chain: ${detail.received} ${detail.crypto_currency} (~${detail.received_fiat_estimate} ${detail.fiat_currency})`,
);
console.log(`Credited by provider: ${detail.credited_fiat ?? '0'} ${detail.fiat_currency}`);
}Manual gateways expose require_proof and proof_type in their payment
session. Submit proof with client.submitManualProof(...); it registers the
completion grant first and keeps the proof bound to the buyer's browser.
API
See docs.shoppex.io/developers/embeds/reference for the modal API and docs.shoppex.io/headless/checkout for the headless session contract, CSP, and origin setup.
