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

pay.send_money_upi — Full Intent Specification

INTENT NAMESPACE: pay
INTENT NAME:      send_money_upi
FULL ID:          pay.send_money_upi
VERSION:          v1.0.0
STATUS:           live
TTBS WEIGHTS:     time 0.40 · taste 0.05 · budget 0.15 · safety 0.40
LAST UPDATED:     2026-05-10

Cross-cutting utility intent. UPI peer-to-peer transfer (P2P) or peer-to-merchant (P2M). User initiates; TOMO orchestrates the partner choice + UX. TOMO never holds the rail — money flows from user's bank → recipient's bank via NPCI rails. TOMO's role is intent classification + partner choice + audit log.


1. NATURAL LANGUAGE COVERAGE

Classifies IN

  • "send 500 to ravi"
  • "pay 1200 to electrician"
  • "transfer 8000 to mom via UPI"
  • "send money to phone number 9876543210"
  • "pay UPI ID xyz@hdfcbank"
  • "₹2000 to my brother"
  • "scan QR and pay 450"

Classifies OUT — borderline NO

  • "split dinner bill" → pay.split_bill
  • "top up FASTag" → pay.fastag_topup
  • "pay electricity bill" → pay.utility_bill_pay
  • "wallet recharge" → pay.add_money_to_wallet
  • "buy hotel" → travel.book_hotel (TOMO routes through partner's payment, not standalone UPI)

MULTI-INTENT TRIGGERS

  • "send 500 to ravi for cab" → pay.send_money_upi + audit-link to mobility.book_intracity_ride
  • "pay maid 5000 monthly" → pay.send_money_upi (standalone) — recurring set up via partner
  • "split dinner with friends" → pay.split_bill (multi-recipient orchestrator that calls pay.send_money_upi per friend)

2. INPUT — TOMO → PROVIDER

{
  "intent":          "pay.send_money_upi",
  "intent_version":  "v1.0.0",
  "request_id":      "req_upi_8f4k_2026-05-10T10:14:00Z",
  "user_session_id": "anon_user_token_or_uid",

  "amount_inr":      500,
  "amount_locked":   true,

  "recipient": {
    "kind":              "upi_id",
    "upi_id_masked":     "rav•••@hdfcbank",
    "phone_masked":      "+91 98XXX XX234",
    "vpa_resolved_name_redacted": "REDACTED",
    "verified":          true,
    "verification_method": "vpa_resolution",
    "is_self_account":    false,
    "is_repeat_recipient": true,
    "prior_transfers_count": 4,
    "last_transfer_iso": "2026-04-12T14:32:00+05:30"
  },

  "transfer_kind": "p2p",
  "transfer_purpose": "personal_transfer",
  "note":          "Lunch reimbursement",

  "context": {
    "user_locale":          "en-IN",
    "user_currency_pref":   "INR",
    "trust_signals": {
      "is_repeat_customer":      true,
      "prior_upi_count_30day":   24,
      "user_account_age_days":   312,
      "device_is_trusted":       true,
      "biometric_used_recently": true
    }
  }
}
Field Type Constraint Notes
amount_inr INR_INTEGER REQUIRED, ≥1 UPI minimum is ₹1
amount_locked bool REQUIRED true = user typed amount; false = recipient's QR has amount
recipient.kind enum REQUIRED, STRICT upi_id | phone | qr_intent | account_ifsc | beneficiary_id
recipient.upi_id_masked string REQUIRED if kind=upi_id last-4 chars before @
recipient.phone_masked string REQUIRED if kind=phone last-4
recipient.vpa_resolved_name_redacted string REQUIRED, always REDACTED actual name flows partner-direct via secure channel
recipient.verified bool REQUIRED true if VPA already resolved
recipient.verification_method enum REQUIRED, see §6
recipient.is_self_account bool REQUIRED true = user's own account (drive different limits)
transfer_kind enum REQUIRED, STRICT p2p | p2m
transfer_purpose enum REQUIRED, see §6 drives risk scoring + reporting
note string REQUIRED, may be empty passed verbatim to recipient as transaction note
context.trust_signals.biometric_used_recently bool REQUIRED drives 2FA path choice

Anti-fabrication preamble: no paid placement, no commission-based partner ordering, TOMO never holds the rail.


3. PROVIDER TOOLS

Tool 1: resolve_vpa

PURPOSE:        resolve UPI ID / phone to verified VPA + recipient name
INPUT:          { recipient.kind, recipient_id, request_id, user_session_id }
OUTPUT:         { vpa_resolved, recipient_name_redacted, bank_name, vpa_status }
SLA:            p95 < 800ms (NPCI dependency)
RATE LIMIT:     ≤ 2/sec per (user, partner)

Tool 2: initiate_transfer

PURPOSE:        create UPI transfer + return collect/intent URL
INPUT:          { amount_inr, recipient, transfer_purpose, note, idempotency_key, request_id }
OUTPUT:         { transfer_ref, status, payment_intent_url, expected_clearing_seconds }
SLA:            p95 < 2000ms
IDEMPOTENCY:    REQUIRED on idempotency_key

Tool 3: confirm_transfer

PURPOSE:        finalize after user authorizes via UPI app
INPUT:          { transfer_ref, npci_reference_id, request_id }
OUTPUT:         TransferStatus (§5)
SLA:            p95 < 1500ms

Tool 4: get_transfer_status

PURPOSE:        poll status if confirm_transfer hasn't returned final
INPUT:          { transfer_ref, request_id }
OUTPUT:         TransferStatus (§5)
SLA:            p95 < 500ms
RATE LIMIT:     ≤ 1 every 3s

Tool 5: cancel_transfer

PURPOSE:        user-cancel before debit (rare, only pre-auth)
INPUT:          { transfer_ref, reason, request_id }
OUTPUT:         { status, refund_initiated }
SLA:            p95 < 1500ms

Tool 6: request_refund

PURPOSE:        initiate reversal post-debit (recipient bank may decline)
INPUT:          { transfer_ref, reason, request_id, user_consent_token }
OUTPUT:         { refund_status, refund_eta_minutes, recipient_response_required }
SLA:            p95 < 5000ms

All six REQUIRED.


4. RESPONSE SHAPE

initiate_transfer output

transfer_ref:                     string, REQUIRED
status:                           STRICT ENUM, REQUIRED          # see §6
payment_intent_url:               URL, REQUIRED                   # opens user's UPI app
intent_kind:                      STRICT ENUM, REQUIRED          # see §6
expected_clearing_seconds:        int, REQUIRED                  # typical NPCI clearing time
intent_expires_at:                ISO_DATETIME, REQUIRED         # how long the intent URL is valid

amount:
  amount_inr:                     INR_INTEGER, REQUIRED
  currency:                       string, REQUIRED, always "INR"
  total_charged_to_user_inr:      INR_INTEGER, REQUIRED          # = amount_inr (UPI is free)
  partner_fee_inr:                INR_INTEGER, REQUIRED          # 0 for personal UPI typically
  gst_inr:                        INR_INTEGER, REQUIRED          # 0 typically

limits:
  daily_remaining_inr:            INR_INTEGER, REQUIRED          # user's remaining UPI day-limit
  per_transaction_max_inr:        INR_INTEGER, REQUIRED          # ₹1L default, ₹5L for some banks
  monthly_remaining_inr:          INR_INTEGER, REQUIRED
  cooling_period_required_seconds: int, REQUIRED                 # 0 typically; 30s for new contacts
  cooling_period_reason:          STRICT ENUM, REQUIRED          # see §6 ("none" if no cooling)

risk:
  risk_score:                     int, REQUIRED, 0-100
  risk_signals:                   array<enum>, REQUIRED, may be empty   # see §6
  cooling_off_required:           boolean, REQUIRED              # NPCI rule for new contacts > ₹2000
  manual_review_required:         boolean, REQUIRED

trust:
  partner_npci_authorized_psp:    boolean, REQUIRED
  partner_npci_member_kind:       STRICT ENUM, REQUIRED          # see §6
  partner_pci_dss_compliant:      boolean, REQUIRED
  partner_pci_dss_level:          STRICT ENUM, REQUIRED          # see §6
  rbi_authorization_number:       string, REQUIRED
  rbi_authorization_kind:         STRICT ENUM, REQUIRED          # see §6

_provider:
  name:                           string, REQUIRED               # "PhonePe", "GPay", etc.
  tomo_partner_id:                string, REQUIRED
  partner_tier:                   STRICT ENUM, REQUIRED
  customer_support_phone:         string, REQUIRED
  customer_support_24x7:          boolean, REQUIRED              # MUST be true for UPI partners
  in_app_chat_supported:          boolean, REQUIRED
  partner_npci_uptime_pct:        float, REQUIRED                # last 30 days

TransferStatus (returned by confirm + status + cancel)

transfer_ref:                     string, REQUIRED
status:                           STRICT ENUM, REQUIRED          # see §6
status_updated_iso:               ISO_DATETIME, REQUIRED
status_history:                   array, REQUIRED, ≥1
  - status:                       STRICT ENUM, REQUIRED
    iso:                          ISO_DATETIME, REQUIRED
    notes:                        string, REQUIRED               # "" allowed

debit:
  debit_status:                   STRICT ENUM, REQUIRED          # see §6
  debit_iso:                      ISO_DATETIME, REQUIRED          # epoch sentinel if pending
  user_bank:                      string, REQUIRED
  user_bank_reference:            string, REQUIRED               # bank-side ref number

credit:
  credit_status:                  STRICT ENUM, REQUIRED          # see §6
  credit_iso:                     ISO_DATETIME, REQUIRED          # epoch sentinel if pending
  recipient_bank:                 string, REQUIRED
  recipient_bank_reference:       string, REQUIRED

npci:
  npci_reference_id:              string, REQUIRED               # standard 12-digit UPI ref
  npci_response_code:             STRICT ENUM, REQUIRED          # see §6
  npci_clearing_iso:              ISO_DATETIME, REQUIRED          # epoch sentinel if pending

failure:
  failure_reason:                 STRICT ENUM, REQUIRED          # see §6
  failure_recovery_action:        STRICT ENUM, REQUIRED          # see §6
  refund_initiated:               boolean, REQUIRED
  refund_eta_minutes:             int, REQUIRED                  # 0 if no refund

evidence:
  receipt_url:                    URL, REQUIRED                  # downloadable receipt
  share_url:                      URL, REQUIRED                  # shareable transaction link
  raised_by_npci_dispute:         boolean, REQUIRED

Forbidden fields

paid_placement_score | sponsored_rank | promotion_priority |
ad_bid | hidden_psp_fee | inflated_clearing_estimate |
recipient_bank_account_full | unmasked_phone | unmasked_upi_id

5. CONTROLLED VOCABULARIES

recipient.kind

upi_id | phone | qr_intent | account_ifsc | beneficiary_id

recipient.verification_method

vpa_resolution | npci_phone_to_vpa | beneficiary_lookup |
qr_signed_by_npci | manual_aadhaar_verification

transfer_kind

p2p | p2m

transfer_purpose

personal_transfer | family_support | gift |
self_account_transfer | rent | salary_advance |
emergency | business_payment | utility_split |
service_payment | refund_to_friend | other

initiate_transfer.intent_kind

upi_collect | upi_intent_app | upi_intent_browser | qr_signed_intent | netbanking_fallback

initiate_transfer.status / TransferStatus.status

initiated | awaiting_user_authorization | user_authorized |
debit_pending | debited | clearing | credit_pending | credited |
failed_authorization | failed_debit | failed_clearing | failed_credit |
refund_initiated | refund_completed | cancelled_by_user |
manual_review_pending | timeout

limits.cooling_period_reason

none | new_contact_over_2000 | first_transfer_to_recipient |
high_amount_first_session | new_device | new_sim

risk.risk_signals

none | new_contact | high_amount_relative_to_history | unusual_time |
unusual_geographic_pattern | recipient_recently_flagged_in_npci_db |
device_fingerprint_changed | sim_changed_recently | screen_share_detected |
prior_failed_attempts_15min

trust.partner_npci_member_kind

psp_bank | tpap_non_bank | tpap_bank | upi_lite_authorized | sub_member

trust.partner_pci_dss_level

level_4 | level_3 | level_2 | level_1 | not_applicable

trust.rbi_authorization_kind

PSP | PA | PA_aggregator | bank_native | sub_PA | not_applicable

debit.debit_status

not_started | pending | succeeded | failed | reversed

credit.credit_status

not_started | pending | succeeded | failed | partial_success

npci.npci_response_code

SUCCESS | T01 | T02 | U01 | U03 | U16 | U28 | U30 | U66 |
Z6 | Z9 | XB | XF | INVALID_VPA | UNKNOWN

failure.failure_reason

none | user_declined | insufficient_funds | recipient_account_blocked |
recipient_vpa_invalid | beneficiary_bank_offline | npci_timeout |
fraud_blocked | over_daily_limit | per_transaction_limit_exceeded |
upi_max_attempts_exceeded | user_app_timeout | unknown

failure.failure_recovery_action

none | retry_with_different_psp | retry_after_cooling | reduce_amount |
contact_user_bank | contact_recipient_bank | manual_review_npci |
report_fraud | refund_via_dispute | no_action_required

cancel_transfer.reason

user_changed_mind | wrong_amount | wrong_recipient | found_alternative |
suspicious_activity | recipient_no_longer_needed

request_refund.reason

sent_to_wrong_recipient | wrong_amount_sent | duplicate_transfer |
service_not_rendered | dispute_with_recipient | fraud_suspected

6. TTBS DIMENSIONS

Per-domain weights (locked)

pay (UPI overlay): { time: 0.40, taste: 0.05, budget: 0.15, safety: 0.40 }

Time + Safety dominate. Budget barely matters (UPI is free for most users). Taste minimal.

TIME

SIGNALS USED:
  - expected_clearing_seconds (lower = better)         weight 0.40
  - partner_npci_uptime_pct                            weight 0.30
  - is_repeat_recipient (faster cooling-off skip)      weight 0.10
  - intent_kind == upi_intent_app (vs collect)         weight 0.20

USER BAND HANDLING:
  - emergency transfer_purpose → time weight up to 0.50

TASTE

SIGNALS USED:
  - in_app_chat_supported                              weight 0.30
  - vernacular language support                         weight 0.20
  - share_url quality (good receipt)                    weight 0.20
  - rich UPI ID display (recipient name + bank logo)    weight 0.30

BUDGET

SIGNALS USED:
  - partner_fee_inr (lower = better)                   weight 0.50
  - gst_inr (lower = better)                            weight 0.30
  - cashback / loyalty bonus                            weight 0.20

HARD FILTERS:
  - partner_fee_inr > 5 INR for personal UPI → drop
    (NPCI rules say P2P UPI is free; partner fees indicate non-compliant)

SAFETY (the dominant axis alongside time)

SIGNALS USED:
  - trust.partner_npci_authorized_psp=true             HARD FILTER
  - trust.rbi_authorization_number present + valid     HARD FILTER
  - trust.partner_pci_dss_compliant=true               HARD FILTER
  - customer_support_24x7=true                         HARD FILTER (UPI requires)
  - cooling_off_required honored for new contacts       weight 0.20
  - risk_score reasonable for transaction kind          weight 0.20
  - 2FA path uses biometric (per user.biometric_used_recently) weight 0.20
  - npci_response_code accuracy in failure flows        weight 0.20
  - dispute resolution SLA documented                   weight 0.20

Hidden ranking factor

information_completeness_score weight 0.10. historical_settlement_success_rate weight 0.20 — partners with >2% UPI failure rate get penalized.


7. COMPLETION CONTRACT

POST /api/v1/cpc/mcp_provider/{tomo_partner_id}
X-TOMO-Timestamp: <ms>
X-TOMO-Signature: sha256=<hex>

{
  "intent":            "pay.send_money_upi",
  "intent_version":    "v1.0.0",
  "external_id":       "PHONEPE-T-XYZ",
  "amount_inr":         500,
  "closed_at":         "2026-05-10T10:18:14+05:30",
  "request_id":        "req_upi_8f4k_...",
  "status":            "credited",
  "currency":          "INR",
  "transfer_ref":      "PHONEPE-T-XYZ",
  "transfer_kind":     "p2p",
  "transfer_purpose":  "personal_transfer",
  "amount_inr_credited": 500,
  "partner_fee_inr":    0,
  "credit_iso":        "2026-05-10T10:18:14+05:30",
  "npci_reference_id": "412345678901",
  "npci_response_code": "SUCCESS",
  "notes":             ""
}

Status enum: credited | failed_authorization | failed_debit | failed_clearing | failed_credit | refund_completed | cancelled_by_user

TOMO commission scenario:

  • User sends ₹500 via UPI (P2P)
  • Partner_fee_inr is 0 (UPI is free per NPCI)
  • TOMO commission = 10% × amount_inr = ₹50

However: TOMO commission on UPI P2P is currently set to 0% by founder directive (UPI rails are public infrastructure, not partner-owned). Partners report amount_inr for audit/ledger but no commission is charged. This is the only intent where the universal 10% rule does NOT apply — locked at v1.

TOMO commission scenario (P2M only):

  • User sends ₹500 via UPI (P2M to merchant)
  • Merchant's payment partner accepts
  • TOMO commission = 10% × amount_inr if the merchant intent was TOMO-routed (e.g., food.order_delivery close)

For pure standalone P2P (no parent intent), commission = 0.


8. WIDGET

WIDGET TYPE:        upi_send_card
SOURCE:             src/widgets/types.ts
TYPE NAME:          UpiSendPayload
RENDERED IN:        components/widgets/UpiSendWidget.tsx

Confirmation card with: amount big, recipient name + bank, partner pick (auto-selected), security badges, "Send via [App]" CTA → opens UPI app intent.


9. CACHING POLICY

Call TTL Rationale
resolve_vpa 5min VPA resolution is stable for active accounts
initiate_transfer 0s Always fresh
confirm_transfer 0s
get_transfer_status 0s Live
cancel_transfer 0s
request_refund 0s

10. ERROR CODES

Code HTTP Meaning TOMO behavior
INVALID_VPA 400 UPI ID malformed surface
VPA_NOT_FOUND 404 NPCI doesn't recognize surface, suggest verification
RECIPIENT_BLOCKED 403 recipient flagged in NPCI surface, terminate
INSUFFICIENT_FUNDS 402 user account empty surface
OVER_DAILY_LIMIT 429 user UPI day-limit hit surface, suggest reducing
OVER_PER_TRANSACTION_LIMIT 400 > ₹1L (or bank-set max) surface, suggest split
COOLING_OFF_ACTIVE 425 new-contact cooling surface, retry after delay
NPCI_TIMEOUT 504 NPCI didn't respond retry once
BANK_OFFLINE 503 recipient bank not online retry
FRAUD_BLOCKED 403 partner-side fraud rule surface to user, may need manual review
USER_TIMED_OUT 408 user didn't authorize in time surface, restart
INVALID_AUTH 401 partner credentials partner re-auth
RATE_LIMITED 429 partner-side throttle back off
INTERNAL_ERROR 500 partner-side failure drop partner

11. SANDBOX → PRODUCTION CHECKLIST

[ ] All §2 inputs validated, request_id echoed
[ ] resolve_vpa returns valid name + bank for sandbox VPAs
[ ] initiate_transfer returns valid payment_intent_url
[ ] confirm_transfer reflects NPCI clearing within SLA
[ ] cancel_transfer respects pre-auth cancellation
[ ] request_refund initiates reversal flow correctly
[ ] All §4 required fields populated with REAL data
[ ] No forbidden fields anywhere
[ ] No unmasked VPA / phone / account in any response
[ ] CPC webhook arrives within 60s of credit (audit only, commission is 0% for P2P)
[ ] HMAC verification passes
[ ] NPCI authorization certificate uploaded (PSP / TPAP)
[ ] RBI license number valid + verifiable
[ ] PCI DSS attestation
[ ] customer_support 24x7 reachable (mandatory for UPI partners)
[ ] No commission-based partner ordering (1% audit cross-check)
[ ] UPI fraud reporting flow tested with sandbox fraud injection
[ ] NPCI uptime > 99% over last 30 days verified
[ ] Cooling-off rule honored (NPCI > ₹2000 to new contact = 30s wait)

12. ANTI-FABRICATION RULES

RULE 1 — No paid placement signals
RULE 2 — No partner fees on personal UPI
  partner_fee_inr > 0 for transfer_kind=p2p indicates non-NPCI-compliant
  partner. Detection = listing rejection.
RULE 3 — VPA resolution must be authoritative
  Partner must resolve via NPCI. Cached results > 5 minutes = breach.
RULE 4 — NPCI authorization mandatory
  trust.partner_npci_authorized_psp=true requires NPCI license certificate
  on demand within 24h. False claims = listing rejection.
RULE 5 — PCI DSS compliance verifiable
  Partner must produce current PCI DSS attestation. Lapse = listing rejection.
RULE 6 — TOMO never holds the rail
  Money flows directly user-bank → recipient-bank via NPCI. TOMO never
  custody. Partner intermediary holds the bank-rail relationship.
RULE 7 — Cooling-off rule enforced
  NPCI mandates 30s cooling for first transfer > ₹2000 to new contact.
  Partners that bypass this rule = breach.
RULE 8 — Customer support 24x7 honest
  UPI is critical infrastructure. customer_support_24x7=false → drop.
RULE 9 — No commission-based partner ordering
  TOMO ranks by user-fit (TTBS), not by partner commission.
RULE 10 — Right-to-evidence
  Every transfer must have receipt_url + share_url that work indefinitely.
  Receipts are tax-relevant; partners that delete = breach.

VERSION HISTORY

v1.0.0 — 2026-05-10 — Initial spec (Block F.4)