Developers
Add group payments to your checkout
Built to integrate with booking platforms, travel websites and checkout systems. Your server makes one API call, your customer lands on a hosted group checkout, and a signed webhook tells you when the booking is fully paid.
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.
POST /api/v1/group-payments
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"
}'201 Created
{
"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:
server.js
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 add the embed script and a button:
HTML
<script src="https://splitpay.pro/embed.js"></script>
<!-- a) your server rendered the checkout_url into the page -->
<a data-splitpay data-checkout-url="https://splitpay.pro/checkout/…">Pay with SplitPay</a>
<!-- b) the button calls YOUR backend route, which returns { checkout_url } -->
<button data-splitpay data-endpoint="/checkout/splitpay" data-reference="BK-1042">Pay with SplitPay</button>JavaScript
SplitPay.createCheckout({ endpoint: "/checkout/splitpay", body: { bookingId: "BK-1042" } });
SplitPay.createCheckout({ checkoutUrl: "https://splitpay.pro/checkout/…" });
// errors are dispatched on the button:
document.addEventListener("splitpay:error", (e) => console.warn(e.detail.message));Without a backend, a publishable key can create the checkout from the browser: <script src="https://splitpay.pro/embed.js" data-key="pk_test_…"> and <button data-splitpay data-amount="120000" data-name="Chalet" data-reference="BK-1042" data-count="4">. Restrict the key to your domain and a maximum amount.
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:
POST /api/v1/group-payments/{id}/participants
{
"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. |
POST https://shop.example.com/webhooks/splitpay
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:
verify.js (Node.js, Express)
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 accountQuestions about an integration? Contact us.