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

checkout.html
<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.

server.js (Node.js)
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 });
});
Your API key must never be in frontend JavaScript. The SDK only ever receives a 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.

server.js
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:

HTML
<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:

HTML + 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

webhook event
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

checkout.js
const key = "sp_live_...";

✓ On your server

server.js
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

  1. 1Your server creates a group payment with a secret key: reference, amount, description, where to send the customer back and where to send webhooks.
  2. 2SplitPay answers with a checkout_url. You redirect the customer there (or open it from a “Pay with SplitPay” button).
  3. 3The customer chooses how many people pay and enters their names. SplitPay splits the amount equally and creates one payment link per person.
  4. 4Everyone pays only their own share through Mollie (Bancontact, iDEAL, cards, Apple Pay…). Nobody pays for someone else.
  5. 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
Server-side only. Secret keys (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:

  • deadline ISO 8601; default: your account setting (7 days).
  • min_participants, max_participants limits on the checkout (default 2 to 10).
  • participants as an array [{"name": "Anna", "email": "…"}] skips the checkout step and returns a pay_url per person.
  • customer {"name", "email"} prefills participant 1; locale en | nl | fr | de; metadata is 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 use the SDK button (see 3 steps). SDK reference:

sdk.js
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

  1. The customer opens the checkout and chooses the number of payers (within your min/max).
  2. They enter names (email optional). The amount is split equally; leftover cents go to the first shares, so the shares always add up exactly.
  3. 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.
  4. Each person pays through Mollie. SplitPay confirms every payment by re-fetching it from Mollie, never from a browser redirect.
  5. When all shares are paid the checkout shows Payment complete with a button back to your success_url (with group_payment_id and reference appended).

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 person

5.Webhooks

Pass webhook_url per group payment, or register endpoints under Developers (or POST /api/v1/webhook-endpoints). Events:

group_payment.createdThe group payment was created.
participant.paidOne person paid their share. data.participant and data.group_payment.
group_payment.partially_paidOnce per new paid count (1/4, 2/4, 3/4).
group_payment.paidEvery share is paid. Confirm the booking.
group_payment.expiredDeadline passed before everyone paid.
group_payment.cancelledCancelled by you (dashboard or API).
participant.refunded / group_payment.refundedRefunds 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_url is 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 ping

The 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 account

Platforms

Available today: any website or booking system through the API and the SDK. Plugins are planned, not available yet:

  • Custom website

    Available

    API + sdk.js

  • Booking and travel platforms

    Available

    API, webhooks, per-merchant keys

  • Plugin not available yet

  • Shopify

    Planned

    App not available yet

Questions about an integration? Contact us.