T
TOMO
Developer Docs
BETA These docs are under partner review. Some features described are roadmap items, not yet shipped. Verify against your sandbox before relying on any contract.
● LIVEv1.0.0pay.split_bill

pay.split_bill — Full Intent Specification

INTENT NAMESPACE: pay
INTENT NAME:      split_bill
FULL ID:          pay.split_bill
VERSION:          v1.0.0
STATUS:           live
TTBS WEIGHTS:     time 0.30 · taste 0.15 · budget 0.35 · safety 0.20
LAST UPDATED:     2026-05-14

Split a known amount across multiple known recipients, generate UPI collect requests (or shareable UPI links / QR), and reconcile as each leg settles. Distinct from pay.send_money_upi because: (a) one logical payee on the user's side maps to N actual UPI VPAs; (b) each leg has its own state machine (pending / paid / expired / declined); (c) the surface must reconcile partial settlement (3 of 5 paid) and let the user nudge the remaining; (d) TOMO never holds the money — every leg flows directly between the payer's UPI app and the user's UPI VPA, mediated by an RBI-licensed PSP partner; (e) splits can be equal, item-wise, share-weighted, or custom-per-person.


1. NATURAL LANGUAGE COVERAGE

Classifies IN

  • "split ₹2400 dinner bill 4 ways"
  • "share trip cost ₹18000 among 6 friends"
  • "split this Swiggy order with Rohit and Anu"
  • "I paid the cab, collect ₹160 each from 3 people"
  • "split rent ₹45000 — me 20000, roommate 25000"
  • "send collect request ₹500 to mom for groceries"
  • "split per dish, I had only paneer"
  • "everyone owes me ₹350 for movie tickets"
  • "make a UPI link, ₹250 each, 4 people"
  • "split airport cab — Rohit 40%, me 30%, Anu 30%"

Classifies OUT — borderline NO

  • "send ₹500 to Rohit" → pay.send_money_upi (single-recipient one-shot)
  • "recharge my phone" → pay.utility_bill_pay
  • "pay restaurant bill, I'm at the table" → could be food.book_dine_in payment flow if intent context is dining
  • "loan money to Rohit, he'll pay later" → not in v1 (lending requires interest/promise legal framing)
  • "settle group expenses for our 3-day trip" → group ledger, multi-bill — out of v1 scope (single bill only)
  • "split with stranger via QR" → out of scope (stranger requires identity binding)

MULTI-INTENT TRIGGERS

  • "split dinner bill and book cab back" → pay.split_bill + mobility.book_intracity_ride
  • "split airport cab and add fuel-up next time I drive" → pay.split_bill + future fuel intent
  • "split this Swiggy order with Rohit" → triggered automatically from food.order_delivery post-completion

2. INPUT — TOMO → PROVIDER

{
  "intent":          "pay.split_bill",
  "intent_version":  "v1.0.0",
  "request_id":      "req_sp_4r9q_2026-05-14T20:15:00Z",
  "user_session_id": "anon_user_token_or_uid",

  "bill": {
    "total_inr":  2400,
    "label":      "Dinner at Cafe Jubilee Hills",
    "occurred_at_iso": "2026-05-14T20:00:00+05:30",
    "merchant_name_optional": "Cafe Jubilee Hills",
    "source_intent_id_optional": "food.book_dine_in:req_din_…"
  },

  "user_collect_vpa": "keerthi@hdfcbank",

  "split_method": "equal",
  "split_method_allowed": ["equal", "custom_amount", "share_weighted", "per_item"],

  "participants": [
    {"vpa": "rohit.k@okaxis", "display_name": "Rohit", "amount_inr": 600},
    {"vpa": "anu@upi",         "display_name": "Anu",   "amount_inr": 600},
    {"vpa": "vivek@paytm",     "display_name": "Vivek", "amount_inr": 600},
    {"vpa": "self",            "display_name": "Me",    "amount_inr": 600}
  ],

  "expiry_minutes": 60,

  "reminder_policy": {
    "nudge_after_minutes": 30,
    "max_nudges": 2
  },

  "user_constants": {
    "user_vpa_resolved": "keerthi@hdfcbank",
    "psp_partner": "psp.npci_authorized_aggregator_001"
  }
}

Field rules

  • total_inr MUST equal Σ participants[].amount_inr. Provider validates with 1-paise tolerance — else ERR_AMOUNT_MISMATCH.
  • Each participants[].vpa MUST resolve via UPI directory; unresolvable → drop participant + flag to user.
  • self is a literal sentinel for the user's own share — no collect leg generated.
  • expiry_minutes ≤ 60 enforced (NPCI collect request TTL).
  • split_method ENUM strict.

3. PROVIDER TOOLS

The pay-split MCP server (pay-split-mcp) exposes exactly these tools. The PSP partner is RBI/NPCI-licensed; TOMO is a routing surface, never a fund holder.

split.resolve_participants

Resolves each VPA against the UPI directory. Returns verified_name for confirmation (NPCI rule: must be shown before any collect).

{
  "tool": "split.resolve_participants",
  "input": {"vpas": ["rohit.k@okaxis", "anu@upi", "vivek@paytm"]},
  "output": [
    {"vpa": "rohit.k@okaxis", "verified_name": "ROHIT KUMAR",   "status": "active"},
    {"vpa": "anu@upi",         "verified_name": "ANUSHA RAO",     "status": "active"},
    {"vpa": "vivek@paytm",     "verified_name": "VIVEK SHARMA",  "status": "active"}
  ]
}

split.create

Creates the bill and N collect-request legs. Returns one split_id + N leg_ids.

split.send_collect_leg

Triggers actual UPI collect to a participant's UPI app. Each leg is independent.

split.send_share_link

Generates a UPI deep-link / QR for the participant if they prefer to pay-self vs respond-to-collect.

split.nudge_pending

Sends a polite reminder via SMS/WhatsApp/in-app to unpaid participants. Respects max_nudges.

split.cancel_leg

Cancels a pending leg. No-op if already paid.

split.status

Read-only snapshot of all legs.

split.refund_leg

Refund a paid leg if user initiates (rare — e.g. payer pays wrong amount). Through PSP's UPI refund rail.


4. RESPONSE SHAPE — PROVIDER → TOMO

{
  "intent": "pay.split_bill",
  "request_id": "req_sp_4r9q_2026-05-14T20:15:00Z",
  "split_id":   "spl_8K2pq7",
  "total_inr":  2400,
  "user_share_inr": 600,
  "collect_target_inr": 1800,
  "expiry_at_iso": "2026-05-14T21:15:00+05:30",
  "legs": [
    {
      "leg_id":         "leg_8K2pq7_a",
      "participant":    {"vpa": "rohit.k@okaxis", "verified_name": "ROHIT KUMAR", "display_name": "Rohit"},
      "amount_inr":     600,
      "state":          "pending",
      "collect_sent_at_iso": "2026-05-14T20:15:02+05:30",
      "share_link":     "upi://pay?pa=keerthi@hdfcbank&pn=Keerthi&am=600&tn=Dinner_Cafe_JH&cu=INR",
      "tier_signals":   {"vpa_age_days": 1240, "psp_trust_band": "high"}
    },
    {
      "leg_id":         "leg_8K2pq7_b",
      "participant":    {"vpa": "anu@upi", "verified_name": "ANUSHA RAO", "display_name": "Anu"},
      "amount_inr":     600,
      "state":          "pending",
      "collect_sent_at_iso": "2026-05-14T20:15:02+05:30",
      "share_link":     "upi://pay?pa=keerthi@hdfcbank&pn=Keerthi&am=600&tn=Dinner_Cafe_JH&cu=INR",
      "tier_signals":   {"vpa_age_days": 720, "psp_trust_band": "high"}
    },
    {
      "leg_id":         "leg_8K2pq7_c",
      "participant":    {"vpa": "vivek@paytm", "verified_name": "VIVEK SHARMA", "display_name": "Vivek"},
      "amount_inr":     600,
      "state":          "pending",
      "collect_sent_at_iso": "2026-05-14T20:15:02+05:30",
      "share_link":     "upi://pay?pa=keerthi@hdfcbank&pn=Keerthi&am=600&tn=Dinner_Cafe_JH&cu=INR",
      "tier_signals":   {"vpa_age_days": 2100, "psp_trust_band": "high"}
    }
  ],
  "summary_strip": "3 collect requests sent · ₹1,800 incoming · expires in 60 min",
  "tier_choices": {
    "OK":    "split equally · 4 ways · ₹600 each",
    "GOOD":  "split equally and nudge after 30 min (default)",
    "GREAT": "split equally + nudge + auto-issue share link to each participant if collect declines"
  }
}

Field rules

  • verified_name MUST appear in UI for every leg before user taps "Send".
  • state ∈ {pending, paid, expired, declined, cancelled, refunded}.
  • share_link is the UPI URI fallback — works on every UPI app, no NPCI handshake required.
  • tier_signals.psp_trust_band ∈ {high, medium, low} based on PSP fraud heuristics; low warns user in UI.

5. CONTROLLED VOCABULARIES

split_method

equal · custom_amount · share_weighted · per_item

leg.state

pending · paid · expired · declined · cancelled · refunded

psp_trust_band

high · medium · low

All STRICT ENUM. Anything else → ERR_VOCAB.


6. TTBS DIMENSIONS

TIME (weight 0.30)

  • Time-to-collect-sent (must be sub-second after user confirms)
  • Reminder cadence honored
  • Expiry handled cleanly (auto-issue share-link upgrade on expiry)
  • TIME score = 1 − collect_send_latency_seconds / 5

TASTE (weight 0.15)

  • UI clarity: verified_name + amount + state per leg
  • Nudge tone honesty ("Rohit, ₹600 for dinner") — never aggressive
  • One-tap to re-send link if collect declined
  • TASTE score = readable_state_table_flag × nudge_tone_band

BUDGET (weight 0.35)

  • ZERO fees on UPI rail to user — TOMO surfaces this fact
  • Custom-split rounding handled to last paise without "hidden adjustment"
  • BUDGET score = 1.0 if no fees; deducted if PSP charges hidden MDR

SAFETY (weight 0.20)

  • VPA verified-name match before send
  • PSP NPCI-licensed
  • Per-leg state visible to user — no batch obfuscation
  • TOMO never holds funds — explicit footer disclosure
  • Anti-impersonation: warning if verified_name mismatches display_name user typed
  • SAFETY score = vpa_directory_verified × psp_license_valid × name_mismatch_alert_flag

HARD FILTERS (apply before send)

  1. PSP partner must be NPCI-listed — else reject.
  2. Each VPA must resolve to a verified_name — else drop leg.
  3. Σ legs = total_inr (1 paise tolerance) — else reject entire split.
  4. expiry_minutes ≤ 60 — else reject.
  5. User's own verified_name must match user's KYC profile.

7. COMPLETION CONTRACT

Per-leg success

  • UPI collect request delivered to participant's UPI app within 5s of split.create.
  • Participant responds (accept → paid; decline → declined; ignore → expired after TTL).
  • On paid, PSP fires UPI settlement webhook to TOMO.
  • TOMO updates leg.state and re-renders the split widget.

Split-level success

  • All legs reach a terminal state (paid/expired/declined/cancelled/refunded).
  • User can mark a split as "settled" once Σ paid_legs ≥ collect_target_inr.

CPC webhook (HMAC-SHA256, 5-min replay window)

Note: split_bill is a free service — TOMO charges nothing on UPI P2P. The CPC payload exists only for analytics + audit, with tomo_commission_inr: 0.

{
  "event": "pay.split_bill.completed",
  "intent_id": "pay.split_bill",
  "request_id": "req_sp_4r9q_2026-05-14T20:15:00Z",
  "split_id":   "spl_8K2pq7",
  "total_inr":  2400,
  "legs_total": 3,
  "legs_paid":  3,
  "legs_expired": 0,
  "legs_declined": 0,
  "tomo_commission_inr": 0,
  "psp_partner": "psp.npci_authorized_aggregator_001",
  "completed_at_iso": "2026-05-14T20:42:00+05:30",
  "signature_hmac_sha256": "…"
}

Failure cases

  • psp_offline → user warned, can retry; never silently fail.
  • vpa_invalid → drop leg, notify user inline with exact reason.
  • leg_expired → auto-suggest sending share-link as fallback.
  • name_mismatch_severe → block send; ask user to confirm or correct.

8. WIDGET — UOE → UI

{
  "widget": "SplitBillWidget",
  "header": {
    "title": "Dinner at Cafe Jubilee Hills",
    "total_strip": "Total ₹2,400 · my share ₹600 · collecting ₹1,800",
    "expiry_strip": "Expires in 59:48"
  },
  "regions": {
    "region_1_intelligence": ["3 of 3 VPAs verified", "all on high-trust PSPs", "no name mismatches"],
    "region_2_summary":      "Equal split, 4 ways, ₹600 each",
    "region_3_visual":       null,
    "region_4_now_pin":      "Send 3 collect requests now",
    "region_5_tomo_choices": [
      {"tier": "OK",    "label": "send collect only",                "reason": "fast"},
      {"tier": "GOOD",  "label": "send collect + nudge in 30 min",    "reason": "balanced"},
      {"tier": "GREAT", "label": "send collect + share-link fallback if declined", "reason": "highest completion"}
    ]
  },
  "legs_table": [
    {"name": "Rohit",  "verified": "ROHIT KUMAR",   "amount": "₹600", "state": "pending", "action": "Resend"},
    {"name": "Anu",    "verified": "ANUSHA RAO",    "amount": "₹600", "state": "pending", "action": "Resend"},
    {"name": "Vivek",  "verified": "VIVEK SHARMA",  "amount": "₹600", "state": "pending", "action": "Resend"}
  ],
  "footer_disclosures": [
    "TOMO never holds your money. Each leg goes directly from your friend's UPI app to your bank.",
    "UPI is free for you — no fees deducted by TOMO or the PSP.",
    "We always show the name the bank has on file before you confirm — protects against typos."
  ]
}

UI invariants:

  • Verified name always visible per leg.
  • Per-leg state pill updates live.
  • "Resend" / "Cancel" available per leg.
  • Footer disclosure about TOMO-not-holding-money is mandatory.

9. CACHING POLICY

  • VPA → verified_name: cache 24h on-device. Refresh before any collect send (UPI directory can change).
  • PSP partner status: cache 1h.
  • Split state: live websocket subscription on PSP webhook. Optimistic UI update permitted with rollback on webhook contradiction.
  • Past splits (read-only ledger): cache 30 days on-device, encrypted.
  • No PAN, no bank account number, no balance cached.

10. ERROR CODES

Code Meaning UI surface
ERR_AMOUNT_MISMATCH Σ legs ≠ total_inr "Numbers don't add up — please re-check shares"
ERR_VPA_UNRESOLVED Directory lookup failed "We couldn't find [vpa] — typo?" + retry input
ERR_NAME_MISMATCH_SEVERE Verified name very different from display name Modal asking user to confirm or correct
ERR_PSP_OFFLINE NPCI/PSP rail unavailable Retry banner; never silent fail
ERR_EXPIRY_OUT_OF_BOUNDS expiry_minutes > 60 "Max 60 min — adjust?"
ERR_LEG_ALREADY_PAID Resend/cancel attempted on paid leg "This one's already settled"
ERR_KYC_BLOCK User's own UPI VPA in KYC freeze Plain redirect to user's bank app
ERR_SELF_SHARE_MISSING No self sentinel in participants "Did you forget to include yourself?"
ERR_DUPLICATE_VPA Same VPA twice in participants "[vpa] appears twice — merge?"
ERR_TRUST_BAND_LOW PSP fraud heuristic flagged participant Warn but allow user to proceed with explicit confirm

11. SANDBOX → PRODUCTION CHECKLIST

  • Sandbox split.resolve_participants returns mock verified_names within 200ms.
  • Sandbox split.create enforces Σ legs = total (test ±1 paise both sides).
  • Sandbox expiry_minutes rejected at 61.
  • Sandbox each leg's state machine cycles: pending → paid (webhook), pending → expired (TTL), pending → declined (user simulated), pending → cancelled (user-initiated).
  • Sandbox split.nudge_pending respects max_nudges cap.
  • Sandbox split.refund_leg reverses a paid leg correctly.
  • Sandbox CPC webhook fires with tomo_commission_inr: 0 always.
  • Production PSP partner NPCI license verified (live RBI directory cross-check, weekly).
  • Production name-mismatch threshold tuned (Levenshtein distance band) — false-positive < 1%.
  • Production legal disclosure visible in widget footer — every render.
  • Production rate-limiting: max 20 collect requests per user per hour to prevent abuse.
  • Production share-link fallback tested across PhonePe, GPay, Paytm, BHIM, Cred.

12. ANTI-FABRICATION RULES

  • NO synthesized "verified_name" — only directory-returned name shown.
  • NO paid_placement / partner-bidding on which PSP routes a user — PSP is the user's bank-side UPI rail, not a marketplace.
  • NO "you saved ₹X by splitting" — UPI is free; no fee narrative.
  • NO showing leg as paid until PSP settlement webhook fires (no optimistic-only success).
  • NO hiding declined or expired legs — surface every terminal state.
  • NO auto-nudge beyond max_nudges — respects participant boundary.
  • NO bundling split with promo/cashback notifications — keeps the payment surface clean.
  • NO "settle later" credit/loan suggestion — out of scope, regulatory grey.
  • NO commission line item — TOMO charges nothing on P2P UPI splits.
  • NO marketing language ("smart split", "intelligent split") — boring plain UI is the doctrine.

13. REGULATORY FRAMING

  • NPCI UPI 2.0 spec — collect requests, deep-link URI scheme, 60-min TTL ceiling, mandatory name-display before send.
  • RBI PSS Act 2007 — only NPCI-licensed PSPs route UPI traffic; TOMO uses partner, never operates rail itself.
  • PMLA Rules 2005 — KYC is at the bank/PSP, never at TOMO.
  • DPDPA 2023 — VPA and verified_name are personal data; cached on-device only; cleared on session-end if user opts out.
  • Consumer Protection Act 2019 — refund handling on partial-paid splits must be transparent; TOMO surfaces refund button per leg.
  • TOMO does NOT issue a Payment Aggregator license — partner does. TOMO is an "outsourced UPI initiation surface" per partner agreement, no fund-holding role.