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.

Zero backend Split data is encoded in the URL hash fragment. No server, no Redis, no env vars.
🔗
One shareable link Generate a single link. Share it via WhatsApp, Telegram, or any messenger.
🔒
Private by design UPI IDs live in the hash fragment — never sent to any server, CDN, or analytics tool.
📱
UPI app buttons Recipients see GPay, PhonePe, and Paytm buttons. One tap to pay.

Installation

$ npm install splitpay-sdk

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

Your checkout component
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

/pay route
import { SplitPayRecipientPage } from 'splitpay-sdk'
import 'splitpay-sdk/css'

// Route: /pay — reads the URL hash automatically
<SplitPayRecipientPage />
That's it. No environment variables, no backend deployment, no API keys.

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).

PropTypeDefaultDescription
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.

PropTypeDefaultDescription
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 from core
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',
  }}
  ...
/>
TokenDefaultDescription
primaryColor#358858Buttons, active states
primaryDeep#2a6e47Hover state
onPrimaryColor#ffffffText on primary
fontFamilyInter, sans-serifAll SDK text
borderRadius6pxButtons, inputs
modalWidth480pxModal max-width
triggerTextSplit this paymentButton label


UPI App Routing

On mobile, the recipient page shows per-app pay buttons using app-specific deep link schemes:

AppScheme
Google Paytez://upi/pay?...
PhonePephonepe://pay?...
Paytmpaytmmp://pay?...
Otherupi://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:

$ cd server && npx vercel

This gives you SplitStatusPanel, useSplit, and useSplitStatus — the same components from v1, running on your own infrastructure.

Your data, your responsibility. The server stores UPI IDs and payment confirmations. The core SDK intentionally avoids storing any user data.

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.

One component Drop <SplitPayTrigger /> on your confirmation page. That's the full integration.
🔗
Shareable links Generates a unique payment link per person, or a single shared link for the group.
📱
Mobile-first UPI Deep links open GPay / PhonePe directly. Desktop shows a scannable QR code.
🗄️
Cross-device state Split data lives in a serverless KV store — works when initiator and recipient are on different devices.
INR and UPI only in V1. If 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:

bash
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.

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

SSR safe. All 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:

ConfirmationPage.jsx
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.

React Router example
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.

jsx
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.

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

jsx
<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.

Before you start — copy .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

# From the repo root
$ cd server
$ npm install
$ npm run dev
 
SplitPay API dev server → http://localhost:3001

Terminal 2 — SDK demo (Vite)

# From the repo root
$ npm install
$ npm run dev
 
VITE ready → http://localhost:5173/demo/index.html

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:

$ curl -s -X POST http://localhost:3001/v1/splits \
-H "Content-Type: application/json" \
-d '{"orderId":"TEST-1","totalAmount":1200,"description":"Test","initiatorUpiId":"you@upi","members":[{"name":"Priya"}]}'
 
{"splitId":"sp_Hn11Jnom","orderId":"TEST-1", ...}

Deploying the API

The backend is a standalone Vercel serverless project in the server/ directory. It targets api.splitpay.io as its production domain.

1

Create an Upstash Redis database

Go to console.upstash.com, create a database, and copy the REST URL and token.

2

Install Vercel CLI and deploy

$ npm i -g vercel
$ cd server && vercel deploy --prod
3

Add environment variables in Vercel

In your Vercel project → Settings → Environment Variables:

UPSTASH_REDIS_REST_URL=https://your-db.upstash.io
UPSTASH_REDIS_REST_TOKEN=your-token

Redeploy after adding the vars.

4

Add a custom domain

In Vercel → Settings → Domains, add api.splitpay.io. Vercel provides a CNAME record to add to your DNS provider.

5

Update the SDK

In your merchant app's environment config, set:

VITE_SPLITPAY_API_URL=https://api.splitpay.io/v1

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.
Never commit secrets. Both .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)."