How it works
Your website can stay on its own domain and hosting provider. Your backend creates a checkout session. Open its SpecialX link, or mount Checkout Elements inside a page on your domain. Both use your SpecialX store’s existing payment providers. A signed webhook tells your backend the result even if the visitor closes the browser.
Your website → your backend → SpecialX checkout → your return page.
A redirect integration needs no browser SDK or DNS changes. The optional Elements script is hosted by you and mounts the payment form from your canonical SpecialX store origin. Both integrations use your SpecialX app/store’s checkout design, product catalog, and configured payment providers.
Before you start
You need an active SpecialX app/store with products and a configured payment provider. Use its store ID and product SKUs, not product names or arbitrary amounts. Open Checkout Elements in SpecialX Admin and select your store for setup, installation instructions, and the matching catalog. Create a store-scoped sk_ key in API Keys, or use an owner sx_ key from SpecialX Developers. Both keys stay on your backend; a store key can access only its own store.
The SpecialX checkout app must support hosted sessions. New specialx create store and specialx create quiz projects using store kit 2026-10-18.1 or later include the vendored Elements package and initialize the store connector before rendering. Existing deployed app bundles must be rebuilt with the current connector and shared checkout components to receive Elements messaging and wallet handling; changing the parent page alone cannot update an older app bundle. See “Custom checkout apps” below.
Keep your API key on your backend or in a serverless function’s secret store. Never place it in frontend JavaScript, HTML, or a checkout URL.
Create a checkout session
Make this request from your backend. Replace the store, SKU, return URLs, and order reference with your own values. All return URLs must use HTTPS.
curl https://checkout.specialx.ai/api/checkout/sessions \
-H "Authorization: Bearer $SPECIALX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"store_id": "your-store",
"items": [{ "sku": "your-product-sku", "quantity": 1 }],
"client_reference_id": "your-order-123",
"success_url": "https://your-site.com/payment/success",
"error_url": "https://your-site.com/payment/error",
"cancel_url": "https://your-site.com/cart"
}'
A successful request returns HTTP 201:
{
"id": "cs_example",
"url": "https://checkout.specialx.ai/sessions/opaque-token",
"status": "open",
"expires_at": "2026-10-01T12:30:00.000Z"
}
Save the session ID and returned URL against your own order. Then redirect the visitor to url. The opaque token in that URL grants access to that checkout; share it only with the intended buyer.
Use a store-scoped key for the selected store or an owner key for a store you own. Missing, invalid, or revoked credentials return 401; a different store or a store outside the owner key’s ownership returns 403. Invalid input or products return 400. Store lookup can return 404. Handle non-201 responses before attempting to redirect.
Node.js / Express example
This small example sells one fixed catalog product. For a cart-based site, load your authenticated customer’s cart on the server and build items from that cart. Keep store selection and return URLs under your server’s control.
import express from 'express';
import { randomUUID } from 'node:crypto';
const app = express();
const siteOrigin = 'https://your-site.com';
app.post('/start-checkout', async (_req, res) => {
const orderReference = randomUUID();
try {
const response = await fetch(
'https://checkout.specialx.ai/api/checkout/sessions',
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SPECIALX_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
store_id: process.env.SPECIALX_STORE_ID,
items: [{ sku: 'your-product-sku', quantity: 1 }],
client_reference_id: orderReference,
success_url: `${siteOrigin}/payment/success`,
error_url: `${siteOrigin}/payment/error`,
cancel_url: `${siteOrigin}/cart`,
}),
signal: AbortSignal.timeout(15000),
},
);
if (response.status !== 201) {
return res.status(502).send('Unable to open checkout. Please try again.');
}
const session = await response.json();
// Production: persist orderReference, session.id and session.url in your
// order database here, before sending the buyer to the payment page.
res.redirect(303, session.url);
} catch {
res.status(502).send('Unable to open checkout. Please try again.');
}
});
app.listen(process.env.PORT || 3000);
Your website’s buy button can be a normal HTML form:
<form action="/start-checkout" method="post">
<button type="submit">Continue to secure checkout</button>
</form>
If the backend is on another host, use its absolute HTTPS URL as the form’s action. A normal form navigation does not require cross-origin JavaScript or shared browser storage. Apply your application’s usual authentication, CSRF protection, and rate limits to the backend route.
If you use a JavaScript frontend instead, have your own backend return { "url": "..." }, then call window.location.assign(url). The frontend still never receives the API key.
Checkout elements on your domain
Host your checkout page and the specialx-checkout-elements.js script on your own domain. The library mounts your SpecialX app’s payment form in a direct iframe from https://your-store.specialx.ai; payment processing stays with your existing SpecialX providers. This follows the mounting pattern of Whop Elements, with no Whop dependency or change of payment provider.
Create an embeddable session on your backend
Add embed_origin to the session creation request above:
{
"store_id": "your-store",
"items": [{ "sku": "your-product-sku", "quantity": 1 }],
"embed_origin": "https://your-site.com",
"client_reference_id": "your-order-123",
"success_url": "https://your-site.com/payment/success",
"error_url": "https://your-site.com/payment/error",
"cancel_url": "https://your-site.com/cart"
}
embed_origin is one exact HTTPS origin: scheme, hostname and optional port, without a path, trailing slash, credentials or wildcard. https://your-site.com and https://your-site.com:8443 are different origins. Keep this value under server control rather than accepting any browser-supplied origin. The session is embeddable only from that saved origin.
For this integration, expose a backend route such as POST /checkout-session that creates or reuses your order’s session, then returns res.json({ url: session.url }) instead of redirecting. Save the session against your order before returning it. The SpecialX creation response also includes embed_origin and embed_url; the browser library retrieves and validates these through the token-scoped API. Never send either API key to the browser.
Install in a custom app or existing store
New store and quiz projects created from store kit 2026-10-18.1 or later already have @specialx/checkout-elements as file:vendor/checkout-elements. Existing projects and other custom apps can install the versioned package without npm registry credentials:
pnpm add https://checkout.specialx.ai/packages/specialx-checkout-elements-0.1.1.tgz
Commit the dependency and lockfile and rebuild your app. The archive contains ESM, TypeScript declarations, the browser script, and an optional React adapter. Its SHA-256 checksum is published beside it. Pin a reviewed version and update it deliberately; existing vendored projects do not update automatically.
import { Checkout } from '@specialx/checkout-elements/react';
// Obtain or reuse sessionUrl through your backend before rendering.
export function PaymentPage({ sessionUrl }: { sessionUrl: string }) {
return <Checkout sessionUrl={sessionUrl} />;
}
The framework-neutral import is createCheckout from @specialx/checkout-elements. Installing the Elements library does not update an older SpecialX checkout app’s payment runtime; rebuild that app with the current connector and shared checkout components too.
Host the browser script
Download specialx-checkout-elements.js from SpecialX and save it in your website’s public assets, for example /vendor/specialx-checkout-elements.js. Deploy that copy with your normal website release process. Your checkout page then loads the library from your own domain.
You can also build the same browser script from the SpecialX repository. The source package is @specialx/checkout-elements; after installing workspace dependencies, run:
pnpm --filter @specialx/checkout-elements build
Copy packages/checkout-elements/dist/specialx-checkout-elements.js to your website’s public assets. These downloads do not require an npm registry release.
Mount the checkout
The following page uses your backend route to obtain the saved session URL:
<div id="checkout"></div>
<p id="checkout-message" role="status"></p>
<button id="checkout-fallback" type="button" hidden>
Open full-window checkout
</button>
<script src="/vendor/specialx-checkout-elements.js"></script>
<script>
async function showCheckout() {
const response = await fetch('/checkout-session', { method: 'POST' });
if (!response.ok) throw new Error('Unable to create checkout.');
const { url } = await response.json();
const checkout = SpecialXCheckoutElements.createCheckout({
sessionUrl: url,
onError(error) {
document.querySelector('#checkout-message').textContent = error.message;
},
});
const fallback = document.querySelector('#checkout-fallback');
fallback.hidden = false;
fallback.onclick = () => checkout.open();
await checkout.mount('#checkout');
}
showCheckout().catch(() => {
document.querySelector('#checkout-message').textContent =
'Unable to load checkout. Please try again.';
});
</script>
Apply your normal authentication, CSRF protection and rate limits to /checkout-session, and reuse the saved order session when the page reloads. Each mount adds a full-window fallback link. checkout.open() navigates the current page to that checkout; unmount() removes the frame and listeners, and destroy() permanently disposes of the instance. The source package also provides a React Checkout component from @specialx/checkout-elements/react for projects that consume the built package.
onReady(session) fires after the checkout runtime initializes. onStatusChange(session) and onComplete(session) use status retrieved from the SpecialX session API; messages from the iframe are used only for readiness and sizing. These callbacks are for display, not fulfillment authorization. Use signed webhooks to fulfill orders. By default, the merchant page navigates to the verified return URL on every terminal state, including failure or cancellation. Set redirectOnComplete: false to keep the result on the page.
Browser policies and wallets
If your website restricts content security policy, allow https://your-store.specialx.ai in frame-src, https://checkout.specialx.ai in connect-src, and your hosted script in script-src. Integrate the example initialization code with your existing script nonce or external script policy. If you restrict Permissions Policy, include payment=(self "https://your-store.specialx.ai"); the Elements iframe delegates the payment permission too. Keep all checkout pages on HTTPS.
PayPal approval uses a full-window flow. Clicking PayPal in the embedded checkout opens the same session in the full window before creating the provider order. The buyer selects PayPal there to continue. The session token stays in the checkout and provider-return URLs; the iframe’s browser storage is not assumed to be shared with the full-window page.
Apple Pay appears only when the store enables it, the browser and device are eligible, and both the canonical iframe hostname (your-store.specialx.ai) and your merchant page hostname are registered and approved in the store’s payment settings. The parent hostname comes from the saved embed_origin, not a query parameter. Cross-origin Apple Pay requires a Safari version that supports payment in delegated cross-origin iframes, and provider eligibility still applies. Use the full-window fallback when embedded Apple Pay is unavailable; support is not guaranteed in every browser. Wallet sessions currently support one catalog product with quantity one.
Return pages and payment status
Create the success, error, and cancellation pages on your own site. SpecialX preserves their existing query parameters and adds:
checkout_session_id: the session ID returned when you created the link.checkout_status:completed,failed,expired, orcanceled.transaction_id: included when a transaction exists.client_reference_id: your order reference, if supplied.
For example:
https://your-site.com/payment/success
?checkout_session_id=cs_example
&checkout_status=completed
&transaction_id=transaction-example
&client_reference_id=your-order-123
This is one URL; line breaks above are only for readability. Cancellation uses cancel_url, or falls back to error_url if you omitted it. Expired and failed sessions use error_url.
Do not fulfill an order from query parameters. A visitor can edit them. Show the state from your backend and fulfill using a verified webhook. You can also retrieve the session from your backend using the opaque token saved from its URL:
GET https://checkout.specialx.ai/api/checkout/sessions/<opaque-token>
No API key is required for that token-scoped read. It returns the status and expiry, plus redirect_url for a terminal session. The endpoint accepts the opaque token, not the cs_... session ID.
States are open, processing, completed, failed, expired, and canceled. A definite failed payment ends the session; create a new session for another payment attempt. A payment still processing must be reconciled before starting another charge. A payment already started can settle after the link expires.
Session creation has no idempotency-key contract. Save and reuse the link for an order instead of creating a session on every page render or double-click. Do not blindly create a replacement for a payment with an unknown outcome.
Receive signed webhooks
Register your public HTTPS webhook endpoint once per store, using the same store-scoped or owner API key:
curl https://checkout.specialx.ai/api/checkout/webhooks \
-H "Authorization: Bearer $SPECIALX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"store_id": "your-store",
"url": "https://your-site.com/webhooks/specialx",
"events": [
"checkout.session.completed",
"checkout.session.failed",
"checkout.session.expired",
"checkout.session.canceled"
]
}'
The response includes the webhook subscription’s id and signing secret. Store that secret securely as SPECIALX_WEBHOOK_SECRET. Each matching subscription receives events for the whole store, so do not register one for every order.
Deliveries carry these headers:
x-webhook-id: unique-delivery-id
x-webhook-event: checkout.session.completed
x-webhook-signature: sha256=<hex-hmac>
The signature is HMAC-SHA256 over the exact raw request body, using your webhook secret. Verify it before parsing or acting on the event. For Node.js:
import { createHmac, timingSafeEqual } from 'node:crypto';
function validSignature(rawBody, signature, secret) {
if (!secret || typeof signature !== 'string') return false;
const expected = Buffer.from(
'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex'),
);
const received = Buffer.from(signature);
return received.length === expected.length &&
timingSafeEqual(received, expected);
}
With Express, register this route before any app.use(express.json()), so the raw bytes remain available:
app.post('/webhooks/specialx', express.raw({ type: 'application/json' }),
async (req, res) => {
if (!validSignature(req.body, req.get('x-webhook-signature'),
process.env.SPECIALX_WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString('utf8'));
// Implement in your application: durably record event.id once, confirm
// event.store_id and data.client_reference_id match your saved order,
// and enqueue fulfillment for checkout.session.completed. Use a unique
// database constraint and transactional outbox so retries cannot fulfill
// the same order twice. Respond 2xx only after durable acceptance.
await acceptEventOnce(event);
res.sendStatus(204);
});
acceptEventOnce is your application’s database/fulfillment function, not a SpecialX SDK method. The signed event body looks like:
{
"id": "unique-delivery-id",
"event": "checkout.session.completed",
"store_id": "your-store",
"created_at": "2026-10-01T12:00:00.000Z",
"data": {
"checkout_session_id": "cs_example",
"client_reference_id": "your-order-123",
"transaction_id": "transaction-example",
"status": "completed"
}
}
Deduplicate using the signed event.id (also sent as x-webhook-id). Return 2xx for events already accepted. Failed deliveries retry with exponential backoff, up to eight attempts. Session reconciliation continues when the buyer closes the browser. Existing webhook dashboard controls manage subscriptions and delivery retries.
CLI and hosted page design
With the CLI authenticated as the store owner, inspect setup and payment settings:
specialx call run_store_action name=get_checkout_elements_setup input='{"store_id":"your-store"}'
specialx call run_store_action name=get_store_payment_settings input='{"store_id":"your-store"}'
Setup returns the script and package URLs, catalog SKUs, canonical frame origin, and payment configuration summary. It does not verify that an older deployed checkout bundle has Elements runtime support. Store-scoped sk_ keys are for the checkout HTTP API; use the CLI’s saved owner authentication for these admin actions.
update_store_payment_settings accepts { "store_id": "your-store", "settings": { ... } } and replaces all non-secret payment settings. Read with get_store_payment_settings first. Save its complete {store_id, settings} result to payment-settings-update.json, edit only the intended values, and preserve every other field, including any nextCommerce settings. With --json, the result is under data.structured. After reviewing the complete file, submit it:
specialx call run_store_action name=update_store_payment_settings input="$(cat payment-settings-update.json)"
Apple Pay settings include enabled, approvedDomains, countryCode, and merchantName. approvedDomains contains hostnames already registered and approved by the payment provider; saving them here does not register domains or complete provider verification. Configure credentials in Payment Providers, and manage these non-secret settings there. Never send payment credentials through the settings action.
You can also create a session through the CLI:
specialx call run_store_action name=create_checkout_session input='{"store_id":"your-store","items":[{"sku":"your-product-sku","quantity":1}],"success_url":"https://your-site.com/thanks","error_url":"https://your-site.com/error","cancel_url":"https://your-site.com/cart","appearance":{"title":"Your store checkout","layout":"contained","max_width":960,"background_color":"#f3f4f6","accent_color":"#3730a3"}}'
The optional appearance object configures the hosted page around your app:
title (up to 120 characters), logo_url (HTTPS), background_color and
accent_color (six-digit hex colors), layout (full or contained), and
max_width (320–1600 pixels, used for contained layout). The payment form itself
is your app's design. These options are saved per session; existing sessions
keep their original settings. Arbitrary HTML, CSS and scripts are not accepted.
Options and limits
- Required:
store_id,items,success_url,error_url. - Cart: 1–20 catalog items, each with a
skuand integerquantityfrom 1–99. Prices and currency come from the live SpecialX catalog at payment time; this is not an arbitrary-amount payment API. client_reference_id: your order/reference string, up to 256 characters.cancel_url: optional HTTPS cancellation destination.embed_origin: optional exact HTTPS merchant origin for Checkout Elements. Includes a non-default port when used; no path, credentials or wildcard. Without it, use the hosted link integration.expires_in: seconds; default 1800, minimum 300, maximum 86400.checkout_path: the checkout route in your SpecialX app; default/checkout. Must be a local path without a query string or another origin. It does not select a page on your external website.locale: defaults toen; use a locale supported by your store and the platform, for exampleen,de,fr, orlt.discount_code: optional; fixed for the session. Custom checkout UI must show the same discount and fixed cart.- Wallet payments: currently one product with quantity one. Multi-item sessions use card checkout. Available methods depend on the store and browser.
Simple link without an API
For a store’s standard checkout, you can link directly:
<a href="https://checkout.specialx.ai/your-store/checkout">
Open checkout
</a>
For example, open SymptomScore checkout. Its usual https://symptomscore.com/checkout remains available too.
To transfer a cart from another domain, add a URL-encoded cart containing only SKUs and quantities:
const link = new URL('https://checkout.specialx.ai/your-store/checkout');
link.searchParams.set('cart', JSON.stringify([
{ sku: 'your-product-sku', quantity: 1 },
]));
window.location.assign(link.toString());
The store’s checkout component must support the supplied products. This simple link does not create a session or configure your success/error URLs and order reference. Use the session API for those features and the checkout.session.* webhook events. A plain link without a cart uses the store’s default checkout behavior; an external site’s localStorage/cart is not shared automatically.
Custom checkout apps
This step is for the app running on SpecialX, not your external website. Rebuild older apps with the current store connector and initialize it before rendering:
import { prepareHostedCheckout } from '@specialx/store-connector/runtime/hosted-checkout';
await prepareHostedCheckout('your-store');
// Render your app after initialization succeeds.
The helper loads the session cart and routes the existing window.fetch calls to /api/checkout/{charge,initiate,capture,confirm} through the session API. Your component must render the session’s fixed cart and use the platform payment APIs. A hardcoded SKU, an independent payment SDK, or a direct provider call will not automatically implement the session contract.
The shared checkout embeds the store’s own origin to retain its custom design. Elements loads that app directly without a second hosted-shell iframe. The SpecialX routing layer grants the saved merchant origin frame access only for the matching token, store and checkout path; other app pages retain their normal frame restrictions. Existing worker-app policies must also permit the hosted shell at https://checkout.specialx.ai for the ordinary hosted-link flow. Rebuild and publish the app with the current connector and shared wallet hook before relying on Elements readiness, resizing or PayPal handoff. A full-window checkout link remains available when the browser or payment method cannot work in a frame.
Test your integration
Create a session, follow it in a browser, and check the saved order, return page, and signature-verified webhook. Also check cancellation, a declined payment, duplicate deliveries, and your normal /checkout route. For Elements, test the exact merchant origin, an unauthorized origin, PayPal’s full-window handoff, and Apple Pay on an eligible Safari device with both hostnames approved.
Creating a hosted session does not enable sandbox payments. It uses the store’s configured provider mode. Stripe test cards are rejected by a live-mode provider. Use an appropriately configured sandbox store/provider for processor testing. A fully covering gift card can verify a one-time order without charging a card; subscription carts do not accept gift cards.