JavaScript SDK
The SDK creates a session via the v1 API and opens checkout in an iframe on plurelpay.com. Card entry happens there (hosted checkout). Your page stays visible behind a modal overlay. All browser paths share the same credentials: publishable key + signing secret — not the secret API key.
Copy this page as a setup prompt for your coding assistant.
Sandbox demo store
Walk through group checkout end to end at the demo store. The store runs in sandbox mode with test cards — you can split an order, see how settlement looks, and confirm webhook delivery. No real charges.
Need a reference storefront for your engineers? Source is public on GitHub: plurel-company/split-shop.
Choose an integration path
| Path | Best for | Install | Credentials |
|---|---|---|---|
| CDN script tag | WordPress, static HTML, no bundler | <script src="https://plurelpay.com/sdk/v1/1.2.1/plurel.js"> | Publishable key + signing route |
| npm — vanilla | Vite, webpack, bundled JS without React | npm install @plurel/sdk | Same as CDN |
| npm — React | Next.js, Remix, React SPAs | npm install @plurel/react-sdk | Same as CDN (react-sdk wraps core) |
| REST API only | Custom checkout UI, no modal embed | curl or @plurel/sdk PlurelApiClient | Secret API key (+ headless approval) |
Prerequisite: cart signing
Every browser path above uses a publishable key. You must deploy POST /api/cart/sign on your domain and set PLUREL_SIGNING_SECRET. Full spec: Cart signing. Credential cheat sheet: Credentials.
Use @plurel/sdk and @plurel/react-sdk at version 1.2.1 or later for the three-figure checkout button and byte-compatible HMAC signing with the Plurel Pay API.
Why two npm packages?
@plurel/sdk is the core library: hosted modal, session client, TypeScript types, cart validation, and the server-only @plurel/sdk/signing subpath. @plurel/react-sdk is a thin React layer on top — PlurelProvider, PlurelButton, and polling hooks. It depends on @plurel/sdk and adds no payment logic of its own.
| Package | Install when | Main exports |
|---|---|---|
| @plurel/sdk | Any integration (required for signing on the server too) | Plurel, PlurelApiClient, PlurelSessionClient, types; signing via @plurel/sdk/signing |
| @plurel/react-sdk | React / Next.js apps only | PlurelProvider, PlurelButton, usePlurelSession, useGroupStatus |
Prefer the version-pinned CDN URL (https://plurelpay.com/sdk/v1/1.2.1/plurel.js) so browser caches pick up CTA updates. Floating /sdk/v1/plurel.js still works but may lag behind if an older response was cached as immutable. Load plurel.js, which sets window.Plurel.
1. CDN script tag (vanilla HTML)
No build step. Loads window.Plurel globally. Best for quick pilots and legacy stacks.
<script src="https://plurelpay.com/sdk/v1/1.2.1/plurel.js" async></script>
<script>
const cart = {
total: 12800,
currency: "usd",
items: [{ id: "sku_1", name: "Ticket", quantity: 2, unit_price: 5000 }],
tax: 1000,
shipping: 1000,
fees: [
{ id: "cleaning", label: "Cleaning fee", amount: 500 },
{ id: "processing", label: "Ticket processing", amount: 300 },
],
metadata: { order_ref: "ORD-1042" },
};
// From your logged-in shopper when available:
const customer = { name: "Alex", email: "alex@example.com" };
Plurel.init({
merchantId: "plurel_merch_YOUR_ID",
publishableKey: "plurel_pk_test_YOUR_KEY",
getSignature: async (c) => {
const res = await fetch("/api/cart/sign", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ cart: c }),
});
if (!res.ok) throw new Error("Cart signing failed");
return (await res.json()).signature;
},
});
Plurel.createButton({
cart,
group: { minSize: 2, maxSize: 6 },
customer,
success_url: "https://your-store.com/checkout/success",
cancel_url: "https://your-store.com/checkout",
callbacks: {
onGroupFunded: (id, ref) => console.log("Funded", id, ref),
},
}).mount("#plurel-button");
</script>2. npm — bundled JavaScript (no React)
Import Plurel from @plurel/sdk when you have a bundler but are not using React. API is identical to the CDN global.
import { Plurel } from "@plurel/sdk";
Plurel.init({
merchantId: import.meta.env.VITE_PLUREL_MERCHANT_ID,
publishableKey: import.meta.env.VITE_PLUREL_PUBLISHABLE_KEY,
getSignature: async (cart) => {
const res = await fetch("/api/cart/sign", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ cart }),
});
if (!res.ok) throw new Error("Cart signing failed");
return (await res.json()).signature;
},
});
Plurel.createButton({ cart, group: { minSize: 2, maxSize: 6 } }).mount("#plurel-button");Server-side signing uses a separate import — never bundle this in client code:
import { createCartSignature } from "@plurel/sdk/signing";
export async function POST(req: Request) {
const { cart } = await req.json();
const signature = createCartSignature(cart, process.env.PLUREL_SIGNING_SECRET!);
return Response.json({ signature });
}3. npm — React
Install @plurel/react-sdk (pulls in @plurel/sdk automatically). Wrap checkout in PlurelProvider; mount PlurelButton where the shopper pays.
"use client";
import { PlurelProvider, PlurelButton } from "@plurel/react-sdk";
const cart = {
total: 12000,
currency: "usd",
items: [{ id: "sku_1", name: "Ticket", quantity: 2, unit_price: 5000 }],
metadata: { order_ref: "ORD-1042" },
};
export default function Checkout() {
const customer = { name: "Alex", email: "alex@example.com" };
return (
<PlurelProvider
merchantId={process.env.NEXT_PUBLIC_PLUREL_MERCHANT_ID!}
publishableKey={process.env.NEXT_PUBLIC_PLUREL_PUBLISHABLE_KEY!}
environment="sandbox"
getSignature={async (c) => {
const res = await fetch("/api/cart/sign", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ cart: c }),
});
if (!res.ok) throw new Error("Cart signing failed");
return (await res.json()).signature;
}}
>
<PlurelButton
cart={cart}
group={{ minSize: 2, maxSize: 6 }}
customer={customer}
success_url="https://your-store.com/checkout/success"
cancel_url="https://your-store.com/checkout"
appearance={{ size: "md", variant: "primary" }}
/>
</PlurelProvider>
);
}Hooks for status outside the modal: usePlurelSession(sessionId), useGroupStatus(sessionId) (polls GET /api/v1/sessions/:id every 3s).
Prefer a working storefront: the sandbox demo store (sandbox checkout) · source on GitHub.
4. REST API only (no SDK button)
Skip the modal embed entirely. Your server creates sessions with a secret API key (plurel_sk_*). Requires headless_api_enabled on your merchant to omit cart signatures. See REST API and Credentials.
import { PlurelApiClient } from "@plurel/sdk";
const client = new PlurelApiClient({
apiKey: process.env.PLUREL_SECRET_KEY!,
merchantId: "plurel_merch_YOUR_ID",
});
const session = await client.createSession({
cart,
group_config: { min_size: 2, max_size: 6 },
});Plurel.init(options)
Call once per page (CDN / npm vanilla) or via PlurelProvider (React) before opening checkout.
| Option | Required | Notes |
|---|---|---|
| merchantId | Yes | plurel_merch_* from dashboard |
| publishableKey | Yes | plurel_pk_test_* or plurel_pk_live_* |
| getSignature | Yes* | async (cart) => hex string from your /api/cart/sign |
| environment | No | sandbox | production — keys control sandbox vs live mode |
| theme | No | auto | light | dark |
| apiBaseUrl | No | Default https://plurelpay.com/api/v1 |
| payBaseUrl | No | Default https://plurelpay.com (iframe origin) |
*Omit if every button passes signature directly.
Button appearance
Layout follows the Apple Pay button pattern: flat fill, rounded rectangle, 44px default height, three-figure mark + wordmark centered. The label is always Split with Plurel. Merchants may adjust layout and variant only — do not recolor or recompose the lockup (see terms).
| appearance field | Values | Notes |
|---|---|---|
| fullWidth | boolean (default true) | Span container width |
| size | sm | md | lg | Height and typography scale |
| variant | primary | outline | dark | primary = terra (default) · dark = black, pairs with Apple Pay · outline = white + hairline |
Static SVG pack and dynamic endpoint for theme editors: /brand/buttons/ and /api/button?variant=primary&size=md&logo=1 — also in the merchant integration wizard.
Plurel.createButton(options)
Recommended session fields
Pass these on every createSession or Plurel.createButton call when your storefront already knows the values. The API accepts sessions without them, but production integrations are smoother when you send organizer context up front.
| Field | Pass when | Why |
|---|---|---|
customer.name | You know the shopper starting the group | Pre-fills the organizer in group setup; avoids blank “Person 1” labels. |
customer.email | You have a logged-in or checkout email | Support and correlation; required for POST /sessions/:id/remind to reach pending members. |
customer.phone | You already collect it | Optional support contact — only send if you use it elsewhere. |
success_url | Hosted modal / SDK embed | Redirects the organizer back to your store after the group is fully funded. |
cancel_url | Hosted modal / SDK embed | Redirects the organizer after they close the modal or the group is cancelled (refunds run automatically). |
cart.metadata.order_ref | Always (your order or cart id) | Echoes as order_ref on webhooks and GET /sessions — tie fulfillment to your system. |
Guest payers (share links)
Do not collect payer email or address in your storefront before Plurel Pay opens. Friends who open /s/{shareId} pay through hosted checkout; Plurel Pay stores payer email from checkout when provided (receipt / billing details). Your integration only needs to pass organizer customer at session create.
Privacy
If you pass customer.email or customer.phone, disclose that in your privacy policy and only send fields you already collect for checkout. See Go live and your obligations under Plurel Pay's privacy policy for participant data.
| Option | Notes |
|---|---|
| cart | total, currency, items; optional tax, shipping, fees[], metadata (use metadata.order_ref) |
| group | minSize, maxSize, defaultMode, allowedModes, expiryHours |
| signature | Pre-computed HMAC; skips getSignature for this button |
| getSignature | Overrides init-level signer |
| customer | Organizer only — name, email, phone when your storefront knows them |
| success_url | HTTPS URL — redirect organizer after group funded (recommended in embed) |
| cancel_url | HTTPS URL — redirect organizer after cancel/abandon (recommended in embed) |
| callbacks | onGroupCreated, onGroupFunded, onGroupExpired, onGroupCancelled, onError |
| appearance | fullWidth, size (sm | md | lg), variant (primary | outline | dark) — label is always Split with Plurel |
Instance methods: mount(selector | element), unmount(), open(), destroy().
Split modes
| Mode | Meaning |
|---|---|
| equal | Even split of cart total |
| itemized | Each member takes line items |
| custom | Organizer sets amounts |
| percentage | Percentage split |
| host-pays | Organizer absorbs remainder |
Modal URL and postMessage
After session create, the SDK opens {payBaseUrl}/session/{id}?access={token}&embed=1&parent_origin={your origin}. Callbacks fire from postMessage; the SDK also polls GET /api/v1/sessions/:id every 3 seconds as a fallback. Fulfill orders on group.funded webhooks, not callbacks alone — see Webhooks.
Fees on create
POST /sessions returns merchant_fee_rate (default 0.02) and consumer_fee_rate (0.02 per share, plus a flat $0.30 per payment). The modal shows these to payers.