SplitPay SDK — v2.0.0
Split payments via UPI. No backend required.
An open-source SDK that lets your users split a completed payment with friends via UPI. All split data lives in the URL — no server, no database, no API keys.
Installation
Peer dependency: React 16.8+. The core export works without React.
Quick Start
Two components. That's the full integration.
1. Trigger — your checkout page
import { SplitPayTrigger } from 'splitpay-sdk'
import 'splitpay-sdk/css'
<SplitPayTrigger
amount={1497}
description="Swiggy Order #4821"
merchantName="Swiggy"
baseUrl="https://yourapp.com/pay"
/>
2. Recipient page — one route in your app
import { SplitPayRecipientPage } from 'splitpay-sdk'
import 'splitpay-sdk/css'
// Route: /pay — reads the URL hash automatically
<SplitPayRecipientPage />
SplitPayTrigger
The main entry point. Renders a "Split this payment" button and a self-contained modal with a 2-step flow: setup (UPI ID + headcount) and share (link + WhatsApp/native share).
| Prop | Type | Default | Description |
|---|---|---|---|
| amount | number | Required | Total bill amount |
| description | string | — | Order description shown in the modal and on the recipient page |
| merchantName | string | '' | Your brand name, shown on the recipient page |
| merchantLogo | string | — | URL to your logo, shown on the recipient page |
| baseUrl | string | origin + '/pay' | Base URL for the recipient page |
| currency | string | 'INR' | Only INR supported |
| amountUnit | string | 'rupees' | 'rupees' or 'paise' |
| orderId | string | — | Your order ID, passed through to onSplitCreated callback |
| theme | object | {} | Colors, fonts, border radius — see Theming |
| onSplitCreated | function | — | Called with { link, orderId, amount, count, recipientAmount } |
| onModalOpen | function | — | Called when modal opens |
| onModalClose | function | — | Called when modal closes |
SplitPayRecipientPage
The page recipients land on when they open the payment link. Reads split data from window.location.hash, shows the merchant context, amount breakdown, and per-app UPI pay buttons.
| Prop | Type | Default | Description |
|---|---|---|---|
| hash | string | window.location.hash | Override the hash for testing |
This component takes zero required props in production. Mount it on a /pay route and it handles everything.
Core Functions
For custom UIs, server-side link generation, or non-React apps. Zero dependencies.
import { encodeSplitUrl, decodeSplitUrl, splitAmount, generateUpiLink } from 'splitpay-sdk/core'
encodeSplitUrl(options)
Generates a payment link with all split data encoded in the hash fragment.
const link = encodeSplitUrl({
baseUrl: 'https://yourapp.com/pay',
upiId: 'xyz@upi',
amount: 1497,
count: 3,
description: 'Dinner',
initiatorName: 'Nikhil',
merchantName: 'Swiggy', // optional
merchantLogo: 'https://...', // optional
})
decodeSplitUrl(urlOrHash)
Decodes a payment link back into a split object.
const data = decodeSplitUrl(link)
// { upiId, perPersonAmount, totalAmount, count, description, initiatorName, merchantName, merchantLogo }
splitAmount(total, count)
Splits a total among N people. Remainder goes to the initiator.
splitAmount(1000, 3)
// { initiatorAmount: 334, recipientAmount: 333 }
generateUpiLink(options)
Generates a upi://pay?... deep link string.
generateUpiLink({ upiId: 'xyz@upi', amount: 499, description: 'Dinner' })
// "upi://pay?pa=xyz%40upi&am=499&tn=SplitPay+Dinner&cu=INR"
Theming
Pass a theme object to SplitPayTrigger. All values are optional.
<SplitPayTrigger
theme={{
primaryColor: '#e53935',
primaryDeep: '#c62828',
onPrimaryColor: '#ffffff',
fontFamily: 'Your Font, sans-serif',
borderRadius: '12px',
modalWidth: '520px',
triggerText: 'Split the bill',
}}
...
/>
| Token | Default | Description |
|---|---|---|
| primaryColor | #358858 | Buttons, active states |
| primaryDeep | #2a6e47 | Hover state |
| onPrimaryColor | #ffffff | Text on primary |
| fontFamily | Inter, sans-serif | All SDK text |
| borderRadius | 6px | Buttons, inputs |
| modalWidth | 480px | Modal max-width |
| triggerText | Split this payment | Button label |
Link Format
The payment link encodes all split data in the URL hash fragment:
https://yourapp.com/pay#eyJ0IjoieHl6QHVwaSIsImEiOjQ5OSwiZCI6IkRpbm5lciIsImYiOiJOaWtoaWwi...}
The hash decodes to a compact JSON payload:
{
"t": "xyz@upi", // UPI ID (to)
"a": 499, // per-person amount
"d": "Swiggy Order", // description
"f": "Nikhil", // from (initiator name)
"T": 1497, // total amount
"n": 3, // split count
"m": "Swiggy", // merchant name (optional)
"l": "https://..." // merchant logo (optional)
}
UPI App Routing
On mobile, the recipient page shows per-app pay buttons using app-specific deep link schemes:
| App | Scheme |
|---|---|
| Google Pay | tez://upi/pay?... |
| PhonePe | phonepe://pay?... |
| Paytm | paytmmp://pay?... |
| Other | upi://pay?... (generic fallback) |
On desktop, a QR code is shown instead. The qrcode library is lazy-loaded via dynamic import.
Privacy
Everything after # in a URL is never sent over HTTP. This means:
- Your server never sees the UPI ID (even if you host the recipient page)
- CDN and proxy access logs don't contain it
- Analytics tools (Google Analytics, Mixpanel) don't capture it by default
- URL shorteners preserve hash fragments but don't log them server-side
The UPI ID only exists in two browsers: the initiator's (which generated the link) and the recipient's (which decoded it).
Status Tracking (Optional)
The default SDK is fully stateless — there's no server to tell you who paid. The initiator gets a UPI push notification when money arrives.
If you want an in-app "who paid?" panel, deploy the server template from server/ to your own Vercel + Upstash Redis:
This gives you SplitStatusPanel, useSplit, and useSplitStatus — the same components from v1, running on your own infrastructure.
SplitPay SDK — v1.1.0
Post-payment bill splitting for Indian merchants
A white-label React widget that lets your customers split a completed payment with friends via UPI — without you touching money, registering with a payment gateway, or building anything beyond a single component.
<SplitPayTrigger /> on your confirmation page. That's the full
integration.
currency is anything other than "INR",
the SDK logs a console warning and disables the trigger. V2 will add multi-currency support via a payment
gateway.
Installation
Install from npm:
npm install splitpay-sdk
Import the CSS once at your app's entry point. The SDK's styles are scoped with a .sp- prefix
and won't conflict with your existing styles.
import 'splitpay-sdk/dist/splitpay.css'
Peer dependencies: React ≥ 16.8 and ReactDOM ≥ 16.8 must already be installed in your project. The SDK will not bundle its own copy of React.
window and localStorage access is wrapped in
typeof window !== 'undefined' checks. Safe to use in Next.js App Router, Remix, and Gatsby.
Quick Start
Add one component to your payment confirmation page:
import { SplitPayTrigger } from 'splitpay-sdk'
import 'splitpay-sdk/dist/splitpay.css'
export function ConfirmationPage() {
return (
<div>
<h1>Payment successful</h1>
<SplitPayTrigger
amount={12400}
currency="INR"
orderId="ORD-20489"
description="Mumbai → Goa flight · IndiGo · 14 Jul"
merchantName="SkyBook"
onSplitCreated={(split) => console.log(split)}
/>
</div>
)
}
That is the complete merchant integration. The component handles the modal, the split link generation, and the Upstash KV persistence automatically.
Adding the recipient route
Mount SplitPayRecipientPage at /pay/:splitId and
/pay/:splitId/:personSlug. The component is router-agnostic — pass IDs as props.
import { SplitPayRecipientPage } from 'splitpay-sdk'
import { useParams } from 'react-router-dom'
function PayRoute() {
const { splitId, personSlug } = useParams()
return <SplitPayRecipientPage splitId={splitId} personSlug={personSlug} />
}
SplitPayTrigger
The primary integration point. Renders a CTA card on your confirmation page. When clicked, opens a 3-step modal that guides the user through setting up the split.
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| amount | number | Required | — | Total amount paid. Unit controlled by amountUnit. |
| orderId | string | Required | — | Your order ID. Used as the key in localStorage and Upstash. |
| description | string | Required | — | Human-readable description shown to recipients. e.g. "Mumbai → Goa flight" |
| currency | string | Optional | "INR" | ISO currency code. Must be "INR" in V1. |
| amountUnit | "rupees" | "paise" | Optional | "rupees" | Whether amount is in rupees or paise. |
| merchantName | string | Optional | "" | Shown in the modal header alongside "Split this payment". |
| merchantLogo | string | Optional | — | URL to your logo. Shown in the modal header and on the recipient page. |
| variant | "unique" | "single" | Optional | "unique" | "unique" generates one link per person. "single" generates one shared
link. |
| showVariantToggle | boolean | Optional | false | Shows an A/B toggle in the modal. Enable for demos and testing. |
| baseUrl | string | Optional | window.location.origin | Base URL for generated recipient links. |
| theme | object | Optional | {} | Theme overrides. See Theming. |
| onSplitCreated | (split) => void | Optional | — | Fires after split is created and persisted. Receives the full split object. |
| onModalOpen | () => void | Optional | — | Fires when the modal opens. Use for analytics. |
| onModalClose | () => void | Optional | — | Fires when the modal closes. |
SplitStatusPanel
Optional component for your order detail page. Polls the KV store every 5 seconds and shows who has confirmed payment.
import { SplitStatusPanel } from 'splitpay-sdk'
function OrderDetail({ orderId }) {
return <SplitStatusPanel
orderId={orderId}
onResend={(memberId) => console.log('resend to', memberId)}
/>
}
| Prop | Type | Required | Description |
|---|---|---|---|
| orderId | string | Required | The order ID used when creating the split. Resolved via localStorage → KV. |
| onResend | (memberId: string) => void | Optional | Called when the user clicks "Resend link" for a pending member. |
SplitPayRecipientPage
The full recipient-facing page. Router-agnostic — mount it at /pay/:splitId/:personSlug (Variant
B) and /pay/:splitId (Variant A) and pass the IDs as props.
Handles mobile and desktop automatically: on mobile it fires a upi:// deep link; on desktop it
lazy-loads a QR code via import('qrcode') so the QR library is never sent to mobile clients.
| Prop | Type | Required | Description |
|---|---|---|---|
| splitId | string | Required | The split ID from the URL. Fetched from KV on mount. |
| personSlug | string | Optional | Present for Variant B (unique links). Omit for Variant A (shared link), where the page will first ask the recipient their name. |
Utility Functions
Imported from splitpay-sdk/utils. These wrap the KV API and can be used independently of the
React components.
createSplit(config)
Creates a new split via POST /v1/splits. Stores the returned splitId in
localStorage against orderId for SplitStatusPanel to find later.
const split = await createSplit({
orderId: 'ORD-20489',
totalAmount: 12400,
currency: 'INR',
description: 'Mumbai → Goa flight',
initiatorUpiId: 'rahul@okicici',
initiatorName: 'Rahul',
members: [{ name: 'Priya Sharma' }, { name: 'Sneha Kapoor' }],
variant: 'unique',
})
getSplit(splitId)
Fetches a split from the KV store by ID. Used internally by SplitPayRecipientPage.
const split = await getSplit('sp_abc123')
getSplitByOrderId(orderId)
Resolves orderId → splitId via localStorage, then fetches from KV. Used by
SplitStatusPanel.
const split = await getSplitByOrderId('ORD-20489')
confirmPayment(splitId, memberId?)
Self-confirms a payment via PATCH /v1/splits/:splitId/confirm. The call is idempotent —
confirming twice returns the current state without error. For Variant A (shared link) splits, omit
memberId — the server claims the first unpaid slot automatically.
// Variant B — unique link, memberId required
await confirmPayment('sp_abc123', 'm_001')
// Variant A — shared link, memberId omitted
await confirmPayment('sp_abc123')
generateUpiLink(config)
Builds a upi://pay deep link string. Transaction note is capped at 50 chars per UPI spec.
generateUpiLink({
upiId: 'rahul@okicici',
amount: 4133,
description: 'Goa flight',
currency: 'INR',
})
// → 'upi://pay?pa=rahul%40okicici&am=4133&tn=SplitPay+Goa+flight&cu=INR'
formatAmount(amount, currency)
formatAmount(4133, 'INR') // → '₹4,133'
Hooks
useSplit(splitId)
Fetches a split on mount and returns loading/error state.
const { split, loading, error, refetch } = useSplit(splitId)
useSplitStatus(orderId, options?)
Polls for split status on a configurable interval. Default interval is 5 seconds.
const { split, loading, error } = useSplitStatus(orderId, {
pollInterval: 5000, // ms, default
})
Theming
Pass a theme object to SplitPayTrigger. The SDK injects CSS custom properties on
:root so every component inherits the overrides.
<SplitPayTrigger
theme={{
primaryColor: '#0052B4', // IndiGo blue
onPrimaryColor: '#ffffff', // white text on dark button
borderRadius: '4px',
brandName: 'IndiGo',
logo: '/indigo-logo.png',
}}
// ... other props
/>
| Token | Default | Description |
|---|---|---|
| primaryColor | #358858 | Button fill, focus rings, active states. |
| primaryDeep | #2a6e47 | Hover state of primary button. |
| onPrimaryColor | #ffffff | Text color on primary buttons. Set to a dark color if using a light primaryColor. |
| textPrimary | #171717 | Default text colour. |
| textSecondary | #707070 | Helper text, descriptions. |
| borderColor | #dfdfdf | Card and input borders. |
| borderRadius | 6px | Radius for buttons and inputs. |
| fontFamily | Inter, … | Font stack for all SDK text. |
| modalWidth | 480px | Max-width of the split modal. |
| logo | null | URL to your logo. Shown in the trigger CTA and modal header in place of the SplitPay logo. |
| brandName | 'SplitPay' | Brand name shown in the modal header alongside the logo. |
| triggerText | 'Split this payment' | Label text on the trigger CTA button. |
Local Development
Running Locally
The project has two servers: the Vite dev server for the SDK demo, and the Express API server that proxies requests to Upstash Redis. You need both running simultaneously.
.env.example to .env.local in the root and
set VITE_SPLITPAY_API_URL=/v1. Copy server/.env.example to
server/.env.local and add your Upstash credentials.
Terminal 1 — API backend
Terminal 2 — SDK demo (Vite)
Vite proxies all /v1/ requests to localhost:3001, so the browser never crosses
origins. No CORS issues.
Smoke-testing the API
You can test the backend directly with curl:
Deploying the API
The backend is a standalone Vercel serverless project in the server/ directory. It targets
api.splitpay.io as its production domain.
Create an Upstash Redis database
Go to console.upstash.com, create a database, and copy the REST URL and token.
Install Vercel CLI and deploy
Add environment variables in Vercel
In your Vercel project → Settings → Environment Variables:
Redeploy after adding the vars.
Add a custom domain
In Vercel → Settings → Domains, add api.splitpay.io. Vercel provides a CNAME record to add
to your DNS provider.
Update the SDK
In your merchant app's environment config, set:
Environment Variables
SDK (root .env.local)
| Variable | Description |
|---|---|
| VITE_SPLITPAY_API_URL | Base URL for all API calls. Set to /v1 for local dev (proxied by Vite), or
https://api.splitpay.io/v1 in production. |
API backend (server/.env.local)
| Variable | Description |
|---|---|
| UPSTASH_REDIS_REST_URL | Your Upstash Redis REST endpoint. Found in the Upstash console under "REST API". |
| UPSTASH_REDIS_REST_TOKEN | Upstash read-write token. Keep this secret — never commit it or expose it to the browser. |
.env.local files are gitignored. Only commit the
.env.example files which contain placeholder values.
Amount Splitting Logic
Remainders from integer division stay with the initiator — the person who already paid. This ensures the split always sums exactly to the total with no floating-point leakage.
// ₹12,400 ÷ 3 people
splitAmount(12400, 3)
// → { initiatorAmount: 4134, recipientAmount: 4133 }
// 4134 + 4133 + 4133 = 12400 ✓
The modal shows this live as the user adjusts the count: "₹12,400 ÷ 3 people = ₹4,133 each. You already paid your share (₹4,134)."
UPI Deep Link Format
| Parameter | Value | Notes |
|---|---|---|
| pa | UPI ID | Payee address, URL-encoded |
| am | Amount in rupees | Integer, no decimals for whole amounts |
| tn | Transaction note | Max 50 chars per UPI spec; prefixed with "SplitPay " |
| cu | INR | Currency code |
upi://pay?pa=rahul%40okicici&am=4133&tn=SplitPay+Goa+flight&cu=INR
On desktop, upi:// links don't open an app. The SDK detects non-mobile user agents and
lazy-loads qrcode via import('qrcode'), rendering a 180×180px scannable QR. The QR
library is never sent to mobile clients.