Developers
Add group payments to your checkout
Let customers split one booking across multiple payments.
Built for booking platforms, travel websites and checkout systems.
Add SplitPay to your checkout
Option 1 · Copy & paste
<script src="https://splitpay.pro/sdk.js"></script> <button data-splitpay data-amount="120000" data-currency="EUR" data-reference="BOOKING-12345" data-endpoint="/api/splitpay/checkout"> Pay with SplitPay </button>
Your server creates the SplitPay group payment securely. When the customer clicks, the button posts the reference to your own route /api/splitpay/checkout. That route calls SplitPay with your secret key and returns the checkout_url; the SDK then opens the SplitPay checkout.
app.post("/api/splitpay/checkout", async (req, res) => {
const booking = await bookings.find(req.body.reference); // use YOUR price, not the browser's
const r = await fetch("https://splitpay.pro/api/v1/group-payments", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SPLITPAY_SECRET_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
reference: booking.id,
amount: booking.totalCents, // 120000 = EUR 1,200.00
currency: "EUR",
description: booking.title,
participants: booking.guests,
success_url: `https://merchant.com/booking/${booking.id}`,
cancel_url: "https://merchant.com/checkout",
webhook_url: "https://merchant.com/api/splitpay",
}),
});
const { checkout_url } = await r.json();
res.json({ checkout_url });
});checkout_url; it refuses secret keys.Complete example shop in one file (Node.js, no dependencies) →
3 steps
1. Create a group payment
Server-side API call with your secret key.
const response = await fetch("https://splitpay.pro/api/v1/group-payments", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SPLITPAY_SECRET_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
reference: "BOOKING-12345",
amount: 120000,
currency: "EUR",
description: "Val Thorens Ski Trip",
participants: 4,
success_url: "https://merchant.com/success",
cancel_url: "https://merchant.com/cancel",
webhook_url: "https://merchant.com/api/splitpay"
})
});
const checkout = await response.json();
// checkout.checkout_url -> "https://splitpay.pro/checkout/…"2. Add the SplitPay button
Put the checkout_url on the button:
<script src="https://splitpay.pro/sdk.js"></script>
<button data-splitpay
data-checkout-url="CHECKOUT_URL">
Pay with SplitPay
</button>Or pass it with JavaScript:
<script src="https://splitpay.pro/sdk.js"></script>
<button id="splitpay-button" data-splitpay>
Pay with SplitPay
</button>
<script>
SplitPay.mount({
element: "#splitpay-button",
checkoutUrl: checkoutUrl
});
// or open it directly: SplitPay.checkout({ checkoutUrl })
</script>The button opens the SplitPay checkout as a full-page step (reliable on iOS Safari and Android). The SDK only opens URLs on splitpay.pro under /checkout/. Errors are dispatched as a splitpay:error event on the button.
3. Listen for payment completion
group_payment.paid
Sent to your webhook_url when all participants have paid. Verify the signature, then confirm the booking. Webhook details
Keep keys secret
Test
sp_test_…
Test data and Mollie test payments. No real money.
Live
sp_live_…
Real payments on your connected Mollie account.
Never expose secret keys in browser code.
❌ In the browser
const key = "sp_live_...";
✓ On your server
process.env.SPLITPAY_SECRET_KEY
The browser only needs sdk.js and a checkout_url. No backend yet? A publishable key (pk_test_…) can create checkouts from the browser, limited to your domains and a maximum amount: SplitPay.init({ publicKey: "pk_test_…" }) plus <button data-splitpay data-amount="120000" data-reference="BOOKING-12345">. Always check the amount in the webhook before you confirm.
How it works
- 1Your server creates a group payment with a secret key: reference, amount, description, where to send the customer back and where to send webhooks.
- 2SplitPay answers with a checkout_url. You redirect the customer there (or open it from a “Pay with SplitPay” button).
- 3The customer chooses how many people pay and enters their names. SplitPay splits the amount equally and creates one payment link per person.
- 4Everyone pays only their own share through Mollie (Bancontact, iDEAL, cards, Apple Pay…). Nobody pays for someone else.
- 5Your server receives participant.paid events and finally group_payment.paid, verifies the signature and confirms the booking.
SplitPay never holds the money: every share is paid into the merchant's own connected Mollie account.
1.Authentication
Create keys in your dashboard under Developers. Send the secret key as a Bearer token. Secret keys are shown once at creation and stored only as a hash; revoke a key at any time.
Authorization: Bearer sp_test_… # test mode: test data only, no real money Authorization: Bearer sp_live_… # live mode: real payments on your connected Mollie account
sp_…) must never appear in a web page, mobile app or public repository. Call the API from your backend. For a button on a site without a backend, use a publishable key (pk_…), which can only create checkouts limited to your domains and a maximum amount.2.Create a group payment
Amounts are integers in minor units: 120000 is EUR 1,200.00. Leave participants as a number (the suggested group size) or omit it: the customer chooses on the checkout.
curl https://splitpay.pro/api/v1/group-payments \
-H "Authorization: Bearer sp_test_…" \
-H "Content-Type: application/json" \
-d '{
"reference": "BK-1042",
"amount": 120000,
"currency": "EUR",
"description": "Chalet Val Thorens, 7 nights",
"participants": 4,
"success_url": "https://shop.example.com/booking/BK-1042",
"cancel_url": "https://shop.example.com/checkout",
"webhook_url": "https://shop.example.com/webhooks/splitpay"
}'{
"id": "5d0c2a7e-…",
"object": "group_payment",
"livemode": false,
"reference": "BK-1042",
"description": "Chalet Val Thorens, 7 nights",
"amount": 120000,
"currency": "EUR",
"status": "OPEN",
"checkout_url": "https://splitpay.pro/checkout/…",
"awaiting_participants": true,
"paid_amount": 0,
"remaining_amount": 120000,
"participant_count": 0,
"deadline": "2026-10-06T10:00:00.000Z",
"participants": []
}Optional fields:
deadlineISO 8601; default: your account setting (7 days).min_participants,max_participantslimits on the checkout (default 2 to 10).participantsas an array[{"name": "Anna", "email": "…"}]skips the checkout step and returns apay_urlper person.customer{"name", "email"}prefills participant 1;localeen | nl | fr | de;metadatais returned in every webhook.
Node.js, on your server:
app.post("/checkout/splitpay", async (req, res) => {
const booking = await bookings.create(req.body); // your own logic
const r = await fetch("https://splitpay.pro/api/v1/group-payments", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SPLITPAY_SECRET_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
reference: booking.id,
amount: booking.totalCents,
currency: "EUR",
description: booking.title,
participants: booking.guests,
success_url: `https://shop.example.com/booking/${booking.id}`,
cancel_url: "https://shop.example.com/checkout",
webhook_url: "https://shop.example.com/webhooks/splitpay",
}),
});
const groupPayment = await r.json();
await bookings.update(booking.id, { splitpayId: groupPayment.id, status: "awaiting_payment" });
res.json({ checkout_url: groupPayment.checkout_url });
});3.Open the checkout
Redirect to checkout_url from your server, or use the SDK button (see 3 steps). SDK reference:
SplitPay.checkout({ checkoutUrl }) // open a server-created checkout
SplitPay.checkout({ endpoint: "/api/splitpay/checkout", body: { reference } })
SplitPay.mount({ element: "#splitpay-button", checkoutUrl }) // returns { update, unmount }
SplitPay.init({ publicKey: "pk_test_…" }) // optional, publishable keys only
// data attributes on [data-splitpay]:
// data-checkout-url | data-endpoint | data-amount data-currency data-reference data-participants
document.addEventListener("splitpay:error", (e) => console.warn(e.detail.message));Using a bundler? The same SDK ships as an ES module in @splitpay/sdk/browser (npm publication is planned; until then load https://splitpay.pro/sdk.js). The older /embed.js URL serves the same file.
4.Participant flow
- The customer opens the checkout and chooses the number of payers (within your min/max).
- They enter names (email optional). The amount is split equally; leftover cents go to the first shares, so the shares always add up exactly.
- The overview shows every personal link with copy, WhatsApp and email buttons, and a “Pay your share” button for the customer. Everyone pays only their own share.
- Each person pays through Mollie. SplitPay confirms every payment by re-fetching it from Mollie, never from a browser redirect.
- When all shares are paid the checkout shows Payment complete with a button back to your
success_url(withgroup_payment_idandreferenceappended).
The success_url is a convenience redirect, not a confirmation. Confirm bookings from the webhook, or with GET /api/v1/group-payments/{id}.
Collecting names yourself? Set them through the API instead of the hosted checkout:
{
"participants": [{ "name": "Anna", "email": "anna@example.com" }, { "name": "Ben" }, { "name": "Chloé" }, { "name": "Daan" }],
"first_is_organizer": true
}
→ 201, participants[].pay_url for each person5.Webhooks
Pass webhook_url per group payment, or register endpoints under Developers (or POST /api/v1/webhook-endpoints). Events:
| group_payment.created | The group payment was created. |
| participant.paid | One person paid their share. data.participant and data.group_payment. |
| group_payment.partially_paid | Once per new paid count (1/4, 2/4, 3/4). |
| group_payment.paid | Every share is paid. Confirm the booking. |
| group_payment.expired | Deadline passed before everyone paid. |
| group_payment.cancelled | Cancelled by you (dashboard or API). |
| participant.refunded / group_payment.refunded | Refunds confirmed by Mollie. |
SplitPay-Event: group_payment.paid
SplitPay-Event-Id: evt_3f1c9a…
SplitPay-Delivery: 8b2e…
SplitPay-Signature: t=1759312800,v1=5b6c…
{
"id": "evt_3f1c9a…",
"type": "group_payment.paid",
"created_at": "2026-10-01T10:00:00.000Z",
"livemode": false,
"group_payment_id": "5d0c2a7e-…",
"reference": "BK-1042",
"amount": 120000,
"currency": "EUR",
"status": "PAID",
"data": { "paid_amount": 120000, "paid_count": 4, "participant_count": 4, "participants": [ … ], "metadata": { … } }
}Verify every request. Compute HMAC-SHA256 over `${t}.${rawBody}` with your signing secret, compare in constant time and reject old timestamps:
import crypto from "node:crypto";
app.post("/webhooks/splitpay", express.raw({ type: "application/json" }), async (req, res) => {
const raw = req.body.toString("utf8");
const parts = Object.fromEntries(req.get("SplitPay-Signature").split(",").map((p) => p.split("=")));
const expected = crypto.createHmac("sha256", process.env.SPLITPAY_WEBHOOK_SECRET).update(`${parts.t}.${raw}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
const valid = fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ""));
if (!valid) return res.sendStatus(400);
const event = JSON.parse(raw);
if (await processedEvents.has(event.id)) return res.sendStatus(200); // idempotent
if (event.type === "group_payment.paid") await bookings.confirm(event.reference);
await processedEvents.add(event.id);
res.sendStatus(200);
});- Endpoints registered in the dashboard have their own signing secret. A per-payment
webhook_urlis signed with your account signing secret (Developers › Webhooks). - Respond with a 2xx within 10 seconds. Anything else is retried with exponential backoff, 8 attempts over roughly a day.
- Event ids are stable: a retry or manual resend carries the same
id. Store processed ids and ignore duplicates. - Every delivery, its HTTP status and response are visible in Developers › Webhook log, with a resend button.
6.Test mode
sp_test_keys create test group payments (livemode: false) and can only read test data.- Payments run through Mollie test mode. On the Mollie test page choose “Paid”, “Failed” or “Expired” to simulate the outcome.
- Test webhooks go to your test endpoints and may use http:// URLs.
- Test mode works before you connect Mollie, so you can build the whole integration first.
7.Live mode
- Connect your Mollie account in Settings and switch it to live. Only then can you create
sp_live_keys and live endpoints. - Live keys only create and see live group payments. A test key can never touch live data, and the other way round.
- Live webhook and redirect URLs must use https on a public host.
- Pricing: 1% + €0.25 per paid share, free until 1 January 2027. Mollie's own transaction fees apply.
8.API reference
POST https://splitpay.pro/api/v1/group-payments create (checkout or direct)
GET https://splitpay.pro/api/v1/group-payments?limit=&offset= list (key's mode only)
GET https://splitpay.pro/api/v1/group-payments/{id} retrieve, incl. participants and checkout_url
GET https://splitpay.pro/api/v1/group-payments/{id}/participants participants with pay_url
POST https://splitpay.pro/api/v1/group-payments/{id}/participants set participants once (409 if already set)
POST https://splitpay.pro/api/v1/group-payments/{id}/complete one payment for all unpaid shares → payment_url
POST https://splitpay.pro/api/v1/group-payments/{id}/cancel {"refund_paid": true}
POST https://splitpay.pro/api/v1/group-payments/{id}/extend {"deadline": "…"} also re-opens an expired group
POST https://splitpay.pro/api/v1/group-payments/{id}/remind email/WhatsApp every pending participant with contact details
GET https://splitpay.pro/api/v1/group-payments/{id}/export GDPR data export
POST https://splitpay.pro/api/v1/group-payments/{id}/anonymize GDPR erasure
GET https://splitpay.pro/api/v1/webhook-endpoints endpoints of the key's mode
POST https://splitpay.pro/api/v1/webhook-endpoints {"url", "description"} → signing secret (shown once)
DELETE https://splitpay.pro/api/v1/webhook-endpoints/{id} disable
POST https://splitpay.pro/api/v1/webhook-endpoints/{id}/test send a signed pingThe v1 field names from earlier integrations (name, total_amount, merchant_reference, participant_count, organizer) are still accepted and still returned.
9.Errors
{ "error": { "code": "VALIDATION", "message": "amount: required (integer, minor units: 120000 = EUR 1,200.00)" } }
401 UNAUTHORIZED missing, invalid or revoked key
403 LIVEMODE_UNAVAILABLE live key or live request without a live Mollie connection
404 NOT_FOUND unknown id, or an id from the other mode
409 ALREADY_SET participants were already set
409 NOT_ALLOWED nothing left to pay, or the group is closed
422 VALIDATION invalid body
429 RATE_LIMITED 300 requests per minute per accountPlatforms
Available today: any website or booking system through the API and the SDK. Plugins are planned, not available yet:
Custom website
AvailableAPI + sdk.js
Booking and travel platforms
AvailableAPI, webhooks, per-merchant keys
- Planned
Plugin not available yet
- Planned
App not available yet
Questions about an integration? Contact us.