From 5feeea909dd7bd7e137790d62e8b081ca787aecc Mon Sep 17 00:00:00 2001 From: Anindya Pandey Date: Mon, 13 Jul 2026 22:45:25 +0530 Subject: [PATCH 01/27] docs(UPIIS-47): UPI Issuance onboarding developer docs + OpenAPI reference Client-facing developer documentation for the UPI Issuance onboarding surface, published to the Setu docs site. - Guide pages: overview, quickstart, API envelope, device binding, OTP verification, VPA management, programs, payee blocklist, QA-env testing. - OpenAPI spec (api-references/payments/upi-issuance.json) driving the interactive API reference: human-readable operation summaries, doc-ordered sections, internal endpoints excluded, per-operation error-code enums + examples, and consistent deviceId / mobile / idempotencyKey / cursor / token / id conventions across docs and refs. - Nav registration in endpoints.json, menuItems.json, redirects.json. Spec/code counterparts land under UPIIS-33/34/35. Closes UPIIS-47 --- api-references/payments/upi-issuance.json | 4427 +++++++++++++++++ content/endpoints.json | 6 + .../payments/upi-issuance/api-envelope.mdx | 89 + .../payments/upi-issuance/api-reference.mdx | 24 + content/payments/upi-issuance/onboarding.mdx | 49 + .../onboarding/api-integration.mdx | 46 + .../api-integration/device-binding.mdx | 156 + .../onboarding/api-integration/programs.mdx | 144 + .../onboarding/api-integration/user-otp.mdx | 184 + .../api-integration/vpa-management.mdx | 359 ++ .../onboarding/onboarding-states.mdx | 59 + content/payments/upi-issuance/overview.mdx | 38 + .../payments/upi-issuance/payee-blocklist.mdx | 187 + content/payments/upi-issuance/qa-testing.mdx | 91 + content/payments/upi-issuance/quickstart.mdx | 65 + 15 files changed, 5924 insertions(+) create mode 100644 api-references/payments/upi-issuance.json create mode 100644 content/payments/upi-issuance/api-envelope.mdx create mode 100644 content/payments/upi-issuance/api-reference.mdx create mode 100644 content/payments/upi-issuance/onboarding.mdx create mode 100644 content/payments/upi-issuance/onboarding/api-integration.mdx create mode 100644 content/payments/upi-issuance/onboarding/api-integration/device-binding.mdx create mode 100644 content/payments/upi-issuance/onboarding/api-integration/programs.mdx create mode 100644 content/payments/upi-issuance/onboarding/api-integration/user-otp.mdx create mode 100644 content/payments/upi-issuance/onboarding/api-integration/vpa-management.mdx create mode 100644 content/payments/upi-issuance/onboarding/onboarding-states.mdx create mode 100644 content/payments/upi-issuance/overview.mdx create mode 100644 content/payments/upi-issuance/payee-blocklist.mdx create mode 100644 content/payments/upi-issuance/qa-testing.mdx create mode 100644 content/payments/upi-issuance/quickstart.mdx diff --git a/api-references/payments/upi-issuance.json b/api-references/payments/upi-issuance.json new file mode 100644 index 00000000..c2349072 --- /dev/null +++ b/api-references/payments/upi-issuance.json @@ -0,0 +1,4427 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Setu UPI Issuance — TPAP API", + "description": "TPAP-facing APIs for the Setu UPI Issuance switch (closed-stack PPI issuer). Phase 1: onboarding / device binding.", + "version": "1.0" + }, + "servers": [ + { + "url": "https://upi-issuance-qa.setu.co" + } + ], + "paths": { + "/api/v1/onboarding/binding-token": { + "post": { + "tags": [ + "Device binding" + ], + "summary": "Generate a binding token", + "description": "Generate a device-binding token: returns the VMN and the ready-to-send silent-SMS body, and starts onboarding for the user's mobile number.", + "operationId": "onboarding#requestBindingToken", + "parameters": [ + { + "name": "idempotencyKey", + "in": "header", + "description": "Optional client-generated idempotency key for safe retries.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "description": "Optional client-generated idempotency key for safe retries.", + "example": "req-8f3a2b1c" + }, + "example": "req-8f3a2b1c" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestBindingTokenRequestBody" + }, + "example": { + "deviceId": "device-abc-123", + "mobile": "919999999999", + "os": "android", + "otpRequired": true + } + } + } + }, + "responses": { + "200": { + "description": "OK response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BindingTokenResponse" + }, + "example": { + "smsBody": "VERIFY SETqxf8aemkxxdrhbifd4n4zldq7guvxb2k", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "vmn": "919900000001" + } + } + } + }, + "400": { + "description": "Bad request: Bad Request response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "invalid-request" + ], + "example": "invalid-request" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "invalid-request", + "message": "The request could not be parsed.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "429": { + "description": "Too many requests: Too Many Requests response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "device-binding-cap-exceeded", + "mobile-binding-cap-exceeded" + ], + "example": "device-binding-cap-exceeded" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "device-binding-cap-exceeded", + "message": "Too many binding attempts for this device.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "500": { + "description": "Internal server error: Internal Server Error response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "internal-error" + ], + "example": "internal-error" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "internal-error", + "message": "Something went wrong on Setu's side.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + } + } + } + }, + "/api/v1/onboarding/binding-status": { + "post": { + "tags": [ + "Device binding" + ], + "summary": "Poll binding status", + "description": "Poll a user's device-binding status. Returns the user object in its current state.", + "operationId": "onboarding#pollBindingStatus", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PollBindingStatusRequestBody" + }, + "example": { + "deviceId": "device-abc-123", + "mobile": "919999999999" + } + } + } + }, + "responses": { + "200": { + "description": "OK response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BindingStatusResponse" + }, + "example": { + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "user": { + "deviceId": "device-abc-123", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "mobile": "919999999999", + "status": "active" + } + } + } + } + }, + "400": { + "description": "Bad request: Bad Request response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "invalid-request" + ], + "example": "invalid-request" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "invalid-request", + "message": "The request could not be parsed.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "404": { + "description": "Not found: Not Found response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "binding-not-found" + ], + "example": "binding-not-found" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "binding-not-found", + "message": "No binding for this device.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "500": { + "description": "Internal server error: Internal Server Error response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "internal-error" + ], + "example": "internal-error" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "internal-error", + "message": "Something went wrong on Setu's side.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + } + } + } + }, + "/api/v1/onboarding/otp/request": { + "post": { + "tags": [ + "OTP verification" + ], + "summary": "Request an OTP", + "description": "Request the user-presence OTP SMS — include the vpa to verify a new VPA (new onboarding / new VPA); omit the vpa for a device change (SIM re-binding). Asynchronous: returns 202; the OTP SMS is sent shortly after.", + "operationId": "onboarding#requestOTP", + "parameters": [ + { + "name": "idempotencyKey", + "in": "header", + "description": "Optional client-generated idempotency key for safe retries.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "description": "Optional client-generated idempotency key for safe retries.", + "example": "req-8f3a2b1c" + }, + "example": "req-8f3a2b1c" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestOTPRequestBody" + }, + "example": { + "deviceId": "device-abc-123", + "mobile": "919999999999", + "vpa": "919999999999-alice@setu" + } + } + } + }, + "responses": { + "202": { + "description": "Accepted response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OTPRequestResponse" + }, + "example": { + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "400": { + "description": "Bad request: Bad Request response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "invalid-request" + ], + "example": "invalid-request" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "invalid-request", + "message": "The request could not be parsed.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "401": { + "description": "Unauthorized: Unauthorized response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "device-not-linked" + ], + "example": "device-not-linked" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "device-not-linked", + "message": "This device is not bound for this user.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "404": { + "description": "Not found: Not Found response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "user-not-found", + "vpa-not-found" + ], + "example": "user-not-found" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "user-not-found", + "message": "No user for this mobile number.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "409": { + "description": "Conflict: Conflict response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "invalid-user-state" + ], + "example": "invalid-user-state" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "invalid-user-state", + "message": "The user is not in a valid state for this operation.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "429": { + "description": "Too many requests: Too Many Requests response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "otp-cap-exceeded" + ], + "example": "otp-cap-exceeded" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "otp-cap-exceeded", + "message": "Too many OTP requests.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "500": { + "description": "Internal server error: Internal Server Error response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "internal-error" + ], + "example": "internal-error" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "internal-error", + "message": "Something went wrong on Setu's side.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + } + } + } + }, + "/api/v1/onboarding/otp/verify": { + "post": { + "tags": [ + "OTP verification" + ], + "summary": "Verify an OTP", + "description": "Verify the OTP the user entered. Activates what it was requested for — a new VPA (new onboarding / new VPA, vpa in the body) or the user (a device change, no vpa).", + "operationId": "onboarding#verifyOTP", + "parameters": [ + { + "name": "idempotencyKey", + "in": "header", + "description": "Optional client-generated idempotency key for safe retries.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "description": "Optional client-generated idempotency key for safe retries.", + "example": "req-8f3a2b1c" + }, + "example": "req-8f3a2b1c" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyOTPRequestBody" + }, + "example": { + "deviceId": "device-abc-123", + "mobile": "919999999999", + "otp": "123456", + "vpa": "919999999999-alice@setu" + } + } + } + }, + "responses": { + "200": { + "description": "OK response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OTPVerifyResponse" + }, + "example": { + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "user": { + "deviceId": "device-abc-123", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "mobile": "919999999999", + "status": "active" + }, + "vpa": { + "accountName": "Alice Doe", + "accountNo": "99887766554433", + "accountProviderId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "accountType": "PPIWALLET", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "ifsc": "SETU0000001", + "mobile": "919999999999", + "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "status": "active", + "vpa": "919999999999-alice@setu" + } + } + } + } + }, + "400": { + "description": "Bad request: Bad Request response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "invalid-request", + "missing-parameter" + ], + "example": "invalid-request" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "invalid-request", + "message": "The request could not be parsed.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "401": { + "description": "Unauthorized: Unauthorized response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "device-not-linked", + "invalid-otp" + ], + "example": "device-not-linked" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "device-not-linked", + "message": "This device is not bound for this user.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "404": { + "description": "Not found: Not Found response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "user-not-found" + ], + "example": "user-not-found" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "user-not-found", + "message": "No user for this mobile number.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "409": { + "description": "Conflict: Conflict response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "invalid-user-state" + ], + "example": "invalid-user-state" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "invalid-user-state", + "message": "The user is not in a valid state for this operation.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "410": { + "description": "Gone: Gone response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "otp-expired-or-exhausted" + ], + "example": "otp-expired-or-exhausted" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "otp-expired-or-exhausted", + "message": "No live OTP to verify.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "500": { + "description": "Internal server error: Internal Server Error response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "internal-error" + ], + "example": "internal-error" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "internal-error", + "message": "Something went wrong on Setu's side.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + } + } + } + }, + "/api/v1/onboarding/vpa/check": { + "post": { + "tags": [ + "VPA management" + ], + "summary": "Check VPA availability", + "description": "Check whether a chosen VPA prefix is available. A taken prefix is not an error.", + "operationId": "onboarding#checkVPA", + "parameters": [ + { + "name": "idempotencyKey", + "in": "header", + "description": "Optional client-generated idempotency key for safe retries.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "description": "Optional client-generated idempotency key for safe retries.", + "example": "req-8f3a2b1c" + }, + "example": "req-8f3a2b1c" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CheckVPARequestBody" + }, + "example": { + "deviceId": "device-abc-123", + "mobile": "919999999999", + "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "vpa": "919999999999-alice@setu" + } + } + } + }, + "responses": { + "200": { + "description": "OK response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpaCheckResponse" + }, + "example": { + "available": true, + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "vpa": "919999999999-alice@setu" + } + } + } + }, + "400": { + "description": "Bad request: Bad Request response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "invalid-request", + "invalid-program", + "missing-parameter", + "invalid-vpa", + "invalid-handle" + ], + "example": "invalid-request" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "invalid-request", + "message": "The request could not be parsed.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "401": { + "description": "Unauthorized: Unauthorized response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "device-not-linked" + ], + "example": "device-not-linked" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "device-not-linked", + "message": "This device is not bound for this user.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "404": { + "description": "Not found: Not Found response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "user-not-found" + ], + "example": "user-not-found" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "user-not-found", + "message": "No user for this mobile number.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "409": { + "description": "Conflict: Conflict response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "invalid-user-state" + ], + "example": "invalid-user-state" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "invalid-user-state", + "message": "The user is not in a valid state for this operation.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "500": { + "description": "Internal server error: Internal Server Error response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "internal-error" + ], + "example": "internal-error" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "internal-error", + "message": "Something went wrong on Setu's side.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + } + } + } + }, + "/api/v1/onboarding/create-vpa": { + "post": { + "tags": [ + "VPA management" + ], + "summary": "Create a VPA", + "description": "Create a VPA over the user's chosen account. Requires an active user. With otpRequired, the VPA is created pending-verification until an OTP activates it.", + "operationId": "onboarding#createVPA", + "parameters": [ + { + "name": "idempotencyKey", + "in": "header", + "description": "Optional client-generated idempotency key for safe retries.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "description": "Optional client-generated idempotency key for safe retries.", + "example": "req-8f3a2b1c" + }, + "example": "req-8f3a2b1c" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateVPARequestBody" + }, + "example": { + "accountName": "Alice Doe", + "accountNo": "99887766554433", + "accountProviderId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "defaultCredit": true, + "defaultDebit": true, + "deviceId": "device-abc-123", + "ifsc": "SETU0000001", + "mobile": "919999999999", + "otpRequired": false, + "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "vpa": "919999999999-alice@setu" + } + } + } + }, + "responses": { + "200": { + "description": "OK response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateVPAResponse" + }, + "example": { + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "vpa": { + "accountName": "Alice Doe", + "accountNo": "99887766554433", + "accountProviderId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "accountType": "PPIWALLET", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "ifsc": "SETU0000001", + "mobile": "919999999999", + "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "status": "active", + "vpa": "919999999999-alice@setu" + } + } + } + } + }, + "400": { + "description": "Bad request: Bad Request response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "invalid-request", + "invalid-program", + "missing-parameter", + "invalid-vpa", + "invalid-handle" + ], + "example": "invalid-request" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "invalid-request", + "message": "The request could not be parsed.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "401": { + "description": "Unauthorized: Unauthorized response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "device-not-linked" + ], + "example": "device-not-linked" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "device-not-linked", + "message": "This device is not bound for this user.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "404": { + "description": "Not found: Not Found response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "user-not-found" + ], + "example": "user-not-found" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "user-not-found", + "message": "No user for this mobile number.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "409": { + "description": "Conflict: Conflict response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "invalid-user-state", + "vpa-taken" + ], + "example": "invalid-user-state" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "invalid-user-state", + "message": "The user is not in a valid state for this operation.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "500": { + "description": "Internal server error: Internal Server Error response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "internal-error" + ], + "example": "internal-error" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "internal-error", + "message": "Something went wrong on Setu's side.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + } + } + } + }, + "/api/v1/onboarding/get-vpa": { + "post": { + "tags": [ + "VPA management" + ], + "summary": "Get a VPA", + "description": "Fetch one of the user's VPAs by its VPA string.", + "operationId": "onboarding#getVPA", + "parameters": [ + { + "name": "idempotencyKey", + "in": "header", + "description": "Optional client-generated idempotency key for safe retries.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "description": "Optional client-generated idempotency key for safe retries.", + "example": "req-8f3a2b1c" + }, + "example": "req-8f3a2b1c" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetVPARequestBody" + }, + "example": { + "deviceId": "device-abc-123", + "mobile": "919999999999", + "vpa": "919999999999-alice@setu" + } + } + } + }, + "responses": { + "200": { + "description": "OK response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateVPAResponse" + }, + "example": { + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "vpa": { + "accountName": "Alice Doe", + "accountNo": "99887766554433", + "accountProviderId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "accountType": "PPIWALLET", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "ifsc": "SETU0000001", + "mobile": "919999999999", + "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "status": "active", + "vpa": "919999999999-alice@setu" + } + } + } + } + }, + "400": { + "description": "Bad request: Bad Request response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "invalid-request", + "missing-parameter", + "invalid-vpa", + "invalid-handle" + ], + "example": "invalid-request" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "invalid-request", + "message": "The request could not be parsed.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "401": { + "description": "Unauthorized: Unauthorized response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "device-not-linked" + ], + "example": "device-not-linked" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "device-not-linked", + "message": "This device is not bound for this user.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "404": { + "description": "Not found: Not Found response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "user-not-found", + "vpa-not-found" + ], + "example": "user-not-found" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "user-not-found", + "message": "No user for this mobile number.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "409": { + "description": "Conflict: Conflict response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "invalid-user-state" + ], + "example": "invalid-user-state" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "invalid-user-state", + "message": "The user is not in a valid state for this operation.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "500": { + "description": "Internal server error: Internal Server Error response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "internal-error" + ], + "example": "internal-error" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "internal-error", + "message": "Something went wrong on Setu's side.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + } + } + } + }, + "/api/v1/onboarding/list-vpas": { + "post": { + "tags": [ + "VPA management" + ], + "summary": "List VPAs", + "description": "List the user's live VPAs (active and pending-verification), newest-added first, keyset-paginated.", + "operationId": "onboarding#listVPAs", + "parameters": [ + { + "name": "idempotencyKey", + "in": "header", + "description": "Optional client-generated idempotency key for safe retries.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "description": "Optional client-generated idempotency key for safe retries.", + "example": "req-8f3a2b1c" + }, + "example": "req-8f3a2b1c" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListVPAsRequestBody" + }, + "example": { + "cursor": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "deviceId": "device-abc-123", + "limit": 20, + "mobile": "919999999999" + } + } + } + }, + "responses": { + "200": { + "description": "OK response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpaListResponse" + }, + "example": { + "nextCursor": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "vpas": [ + { + "accountName": "Alice Doe", + "accountNo": "99887766554433", + "accountProviderId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "accountType": "PPIWALLET", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "ifsc": "SETU0000001", + "mobile": "919999999999", + "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "status": "active", + "vpa": "919999999999-alice@setu" + }, + { + "accountName": "Alice Doe", + "accountNo": "99887766554433", + "accountProviderId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "accountType": "PPIWALLET", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "ifsc": "SETU0000001", + "mobile": "919999999999", + "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "status": "active", + "vpa": "919999999999-alice@setu" + } + ] + } + } + } + }, + "400": { + "description": "Bad request: Bad Request response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "invalid-request" + ], + "example": "invalid-request" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "invalid-request", + "message": "The request could not be parsed.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "401": { + "description": "Unauthorized: Unauthorized response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "device-not-linked" + ], + "example": "device-not-linked" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "device-not-linked", + "message": "This device is not bound for this user.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "404": { + "description": "Not found: Not Found response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "user-not-found" + ], + "example": "user-not-found" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "user-not-found", + "message": "No user for this mobile number.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "409": { + "description": "Conflict: Conflict response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "invalid-user-state" + ], + "example": "invalid-user-state" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "invalid-user-state", + "message": "The user is not in a valid state for this operation.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "500": { + "description": "Internal server error: Internal Server Error response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "internal-error" + ], + "example": "internal-error" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "internal-error", + "message": "Something went wrong on Setu's side.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + } + } + } + }, + "/api/v1/onboarding/vpa/deregister": { + "post": { + "tags": [ + "VPA management" + ], + "summary": "Deregister a VPA", + "description": "Deregister one of the user's VPAs (soft delete; the entry is preserved for audit and re-registration). Idempotent for an already-deregistered VPA.", + "operationId": "onboarding#deregisterVPA", + "parameters": [ + { + "name": "idempotencyKey", + "in": "header", + "description": "Optional client-generated idempotency key for safe retries.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "description": "Optional client-generated idempotency key for safe retries.", + "example": "req-8f3a2b1c" + }, + "example": "req-8f3a2b1c" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetVPARequestBody" + }, + "example": { + "deviceId": "device-abc-123", + "mobile": "919999999999", + "vpa": "919999999999-alice@setu" + } + } + } + }, + "responses": { + "200": { + "description": "OK response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateVPAResponse" + }, + "example": { + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "vpa": { + "accountName": "Alice Doe", + "accountNo": "99887766554433", + "accountProviderId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "accountType": "PPIWALLET", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "ifsc": "SETU0000001", + "mobile": "919999999999", + "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "status": "active", + "vpa": "919999999999-alice@setu" + } + } + } + } + }, + "400": { + "description": "Bad request: Bad Request response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "invalid-request", + "missing-parameter", + "invalid-vpa", + "invalid-handle" + ], + "example": "invalid-request" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "invalid-request", + "message": "The request could not be parsed.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "401": { + "description": "Unauthorized: Unauthorized response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "device-not-linked" + ], + "example": "device-not-linked" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "device-not-linked", + "message": "This device is not bound for this user.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "404": { + "description": "Not found: Not Found response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "user-not-found", + "vpa-not-found" + ], + "example": "user-not-found" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "user-not-found", + "message": "No user for this mobile number.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "409": { + "description": "Conflict: Conflict response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "invalid-user-state" + ], + "example": "invalid-user-state" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "invalid-user-state", + "message": "The user is not in a valid state for this operation.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "500": { + "description": "Internal server error: Internal Server Error response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "internal-error" + ], + "example": "internal-error" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "internal-error", + "message": "Something went wrong on Setu's side.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + } + } + } + }, + "/api/v1/programs": { + "post": { + "tags": [ + "Programs" + ], + "summary": "Create a program", + "description": "Create a program — an engagement channel that VPAs are created under — and return its programId.", + "operationId": "onboarding#createProgram", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateProgramRequestBody" + }, + "example": { + "amountLimit": 500000, + "code": "ACME", + "isCreditAllowed": true, + "isDebitAllowed": true, + "name": "Acme Wallet" + } + } + } + }, + "responses": { + "201": { + "description": "Created response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProgramResponse" + }, + "example": { + "program": { + "amountLimit": 500000, + "code": "ACME", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "isCreditAllowed": true, + "isDebitAllowed": true, + "name": "Acme Wallet", + "status": "active" + }, + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "400": { + "description": "Bad request: Bad Request response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "invalid-request", + "missing-parameter" + ], + "example": "invalid-request" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "invalid-request", + "message": "The request could not be parsed.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "500": { + "description": "Internal server error: Internal Server Error response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "internal-error" + ], + "example": "internal-error" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "internal-error", + "message": "Something went wrong on Setu's side.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + } + } + } + }, + "/api/v1/programs/{programId}": { + "patch": { + "tags": [ + "Programs" + ], + "summary": "Update a program", + "description": "Update a program's configuration. Only the fields supplied in the body are changed.", + "operationId": "onboarding#updateProgram", + "parameters": [ + { + "name": "programId", + "in": "path", + "description": "The program to update (ULID).", + "required": true, + "schema": { + "type": "string", + "description": "The program to update (ULID).", + "example": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + }, + "example": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateProgramRequestBody" + }, + "example": { + "amountLimit": 500000, + "code": "ACME", + "isCreditAllowed": true, + "isDebitAllowed": false, + "name": "Acme Wallet", + "status": "inactive" + } + } + } + }, + "responses": { + "200": { + "description": "OK response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProgramResponse" + }, + "example": { + "program": { + "amountLimit": 500000, + "code": "ACME", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "isCreditAllowed": true, + "isDebitAllowed": true, + "name": "Acme Wallet", + "status": "active" + }, + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "400": { + "description": "Bad request: Bad Request response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "invalid-request", + "missing-parameter" + ], + "example": "invalid-request" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "invalid-request", + "message": "The request could not be parsed.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "404": { + "description": "Not found: Not Found response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "program-not-found" + ], + "example": "program-not-found" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "program-not-found", + "message": "No program with that id.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "500": { + "description": "Internal server error: Internal Server Error response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "internal-error" + ], + "example": "internal-error" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "internal-error", + "message": "Something went wrong on Setu's side.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + } + } + } + }, + "/api/v1/onboarding/block-vpa": { + "post": { + "tags": [ + "Payee blocklist" + ], + "summary": "Block a payee VPA", + "description": "Block an external payee VPA for the user. Idempotent; requires an active user.", + "operationId": "onboarding#blockPayeeVPA", + "parameters": [ + { + "name": "idempotencyKey", + "in": "header", + "description": "Optional client-generated idempotency key for safe retries.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "description": "Optional client-generated idempotency key for safe retries.", + "example": "req-8f3a2b1c" + }, + "example": "req-8f3a2b1c" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlockPayeeVPARequestBody" + }, + "example": { + "deviceId": "device-abc-123", + "mobile": "919999999999", + "payeeVpa": "merchant@otherbank" + } + } + } + }, + "responses": { + "200": { + "description": "OK response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayeeBlockResponse" + }, + "example": { + "blockedPayee": { + "blockedAt": "2026-07-09T12:00:00Z", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "payeeVpa": "merchant@otherbank", + "status": "blocked", + "unblockedAt": "2026-07-09T12:30:00Z" + }, + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "400": { + "description": "Bad request: Bad Request response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "invalid-request", + "missing-parameter", + "invalid-vpa" + ], + "example": "invalid-request" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "invalid-request", + "message": "The request could not be parsed.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "401": { + "description": "Unauthorized: Unauthorized response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "device-not-linked" + ], + "example": "device-not-linked" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "device-not-linked", + "message": "This device is not bound for this user.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "404": { + "description": "Not found: Not Found response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "user-not-found" + ], + "example": "user-not-found" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "user-not-found", + "message": "No user for this mobile number.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "409": { + "description": "Conflict: Conflict response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "invalid-user-state" + ], + "example": "invalid-user-state" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "invalid-user-state", + "message": "The user is not in a valid state for this operation.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "500": { + "description": "Internal server error: Internal Server Error response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "internal-error" + ], + "example": "internal-error" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "internal-error", + "message": "Something went wrong on Setu's side.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + } + } + } + }, + "/api/v1/onboarding/unblock-vpa": { + "post": { + "tags": [ + "Payee blocklist" + ], + "summary": "Unblock a payee VPA", + "description": "Unblock a previously-blocked payee VPA (soft flip; the entry is preserved). Idempotent for an already-unblocked payee; requires an active user.", + "operationId": "onboarding#unblockPayeeVPA", + "parameters": [ + { + "name": "idempotencyKey", + "in": "header", + "description": "Optional client-generated idempotency key for safe retries.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "description": "Optional client-generated idempotency key for safe retries.", + "example": "req-8f3a2b1c" + }, + "example": "req-8f3a2b1c" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlockPayeeVPARequestBody" + }, + "example": { + "deviceId": "device-abc-123", + "mobile": "919999999999", + "payeeVpa": "merchant@otherbank" + } + } + } + }, + "responses": { + "200": { + "description": "OK response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayeeBlockResponse" + }, + "example": { + "blockedPayee": { + "blockedAt": "2026-07-09T12:00:00Z", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "payeeVpa": "merchant@otherbank", + "status": "blocked", + "unblockedAt": "2026-07-09T12:30:00Z" + }, + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "400": { + "description": "Bad request: Bad Request response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "invalid-request", + "missing-parameter", + "invalid-vpa" + ], + "example": "invalid-request" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "invalid-request", + "message": "The request could not be parsed.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "401": { + "description": "Unauthorized: Unauthorized response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "device-not-linked" + ], + "example": "device-not-linked" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "device-not-linked", + "message": "This device is not bound for this user.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "404": { + "description": "Not found: Not Found response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "user-not-found", + "payee-not-blocked" + ], + "example": "user-not-found" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "user-not-found", + "message": "No user for this mobile number.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "409": { + "description": "Conflict: Conflict response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "invalid-user-state" + ], + "example": "invalid-user-state" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "invalid-user-state", + "message": "The user is not in a valid state for this operation.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "500": { + "description": "Internal server error: Internal Server Error response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "internal-error" + ], + "example": "internal-error" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "internal-error", + "message": "Something went wrong on Setu's side.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + } + } + } + }, + "/api/v1/onboarding/list-blocked-vpas": { + "post": { + "tags": [ + "Payee blocklist" + ], + "summary": "List blocked payee VPAs", + "description": "List the user's currently-blocked payee VPAs, newest first, keyset-paginated.", + "operationId": "onboarding#listBlockedPayeeVPAs", + "parameters": [ + { + "name": "idempotencyKey", + "in": "header", + "description": "Optional client-generated idempotency key for safe retries.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "description": "Optional client-generated idempotency key for safe retries.", + "example": "req-8f3a2b1c" + }, + "example": "req-8f3a2b1c" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListVPAsRequestBody" + }, + "example": { + "cursor": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "deviceId": "device-abc-123", + "limit": 20, + "mobile": "919999999999" + } + } + } + }, + "responses": { + "200": { + "description": "OK response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BlockedPayeeListResponse" + }, + "example": { + "blockedPayees": [ + { + "blockedAt": "2026-07-09T12:00:00Z", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "payeeVpa": "merchant@otherbank", + "status": "blocked", + "unblockedAt": "2026-07-09T12:30:00Z" + }, + { + "blockedAt": "2026-07-09T12:00:00Z", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "payeeVpa": "merchant@otherbank", + "status": "blocked", + "unblockedAt": "2026-07-09T12:30:00Z" + }, + { + "blockedAt": "2026-07-09T12:00:00Z", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "payeeVpa": "merchant@otherbank", + "status": "blocked", + "unblockedAt": "2026-07-09T12:30:00Z" + } + ], + "nextCursor": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "400": { + "description": "Bad request: Bad Request response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "invalid-request" + ], + "example": "invalid-request" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "invalid-request", + "message": "The request could not be parsed.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "401": { + "description": "Unauthorized: Unauthorized response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "device-not-linked" + ], + "example": "device-not-linked" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "device-not-linked", + "message": "This device is not bound for this user.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "404": { + "description": "Not found: Not Found response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "user-not-found" + ], + "example": "user-not-found" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "user-not-found", + "message": "No user for this mobile number.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "409": { + "description": "Conflict: Conflict response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "invalid-user-state" + ], + "example": "invalid-user-state" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "invalid-user-state", + "message": "The user is not in a valid state for this operation.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "500": { + "description": "Internal server error: Internal Server Error response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "internal-error" + ], + "example": "internal-error" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "internal-error", + "message": "Something went wrong on Setu's side.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "BadRequestError": { + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code from the published dictionary.", + "example": "invalid-request", + "enum": [ + "invalid-request", + "missing-parameter", + "invalid-program", + "invalid-vpa", + "invalid-handle" + ] + }, + "message": { + "type": "string", + "description": "Human-readable description of the error.", + "example": "A required parameter is missing or invalid." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID).", + "example": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + }, + "example": { + "code": "invalid-request", + "message": "A required parameter is missing or invalid.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + }, + "required": [ + "traceId", + "code", + "message" + ] + }, + "BindingStatusResponse": { + "type": "object", + "properties": { + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID).", + "example": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + }, + "user": { + "$ref": "#/components/schemas/UserView" + } + }, + "example": { + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "user": { + "deviceId": "device-abc-123", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "mobile": "919999999999", + "status": "active" + } + }, + "required": [ + "traceId", + "user" + ] + }, + "BindingTokenResponse": { + "type": "object", + "properties": { + "smsBody": { + "type": "string", + "description": "The ready-to-send silent-SMS body. The app sends this verbatim to the VMN; the token is embedded here and is never returned separately.", + "example": "VERIFY SETqxf8aemkxxdrhbifd4n4zldq7guvxb2k" + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID).", + "example": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + }, + "vmn": { + "type": "string", + "description": "The Virtual Mobile Number to send the SMS to.", + "example": "919900000001" + } + }, + "example": { + "smsBody": "VERIFY SETqxf8aemkxxdrhbifd4n4zldq7guvxb2k", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "vmn": "919900000001" + }, + "required": [ + "traceId", + "vmn", + "smsBody" + ] + }, + "BlockPayeeVPARequestBody": { + "type": "object", + "properties": { + "deviceId": { + "type": "string", + "description": "The user's device id.", + "example": "device-abc-123" + }, + "mobile": { + "type": "string", + "description": "The user's mobile number, with country code (e.g. 919999999999).", + "example": "919999999999" + }, + "payeeVpa": { + "type": "string", + "description": "The external payee VPA to block or unblock (prefix@handle; any handle).", + "example": "merchant@otherbank" + } + }, + "example": { + "deviceId": "device-abc-123", + "mobile": "919999999999", + "payeeVpa": "merchant@otherbank" + }, + "required": [ + "deviceId", + "mobile", + "payeeVpa" + ] + }, + "BlockedPayeeListResponse": { + "type": "object", + "properties": { + "blockedPayees": { + "type": "array", + "items": { + "$ref": "#/components/schemas/BlockedPayeeView" + }, + "description": "This page of the user's currently-blocked payee VPAs (newest-added first); empty when none.", + "example": [ + { + "blockedAt": "2026-07-09T12:00:00Z", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "payeeVpa": "merchant@otherbank", + "status": "blocked", + "unblockedAt": "2026-07-09T12:30:00Z" + }, + { + "blockedAt": "2026-07-09T12:00:00Z", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "payeeVpa": "merchant@otherbank", + "status": "blocked", + "unblockedAt": "2026-07-09T12:30:00Z" + }, + { + "blockedAt": "2026-07-09T12:00:00Z", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "payeeVpa": "merchant@otherbank", + "status": "blocked", + "unblockedAt": "2026-07-09T12:30:00Z" + } + ] + }, + "nextCursor": { + "type": "string", + "description": "Cursor for the next page; absent on the last page.", + "example": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID).", + "example": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + }, + "example": { + "blockedPayees": [ + { + "blockedAt": "2026-07-09T12:00:00Z", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "payeeVpa": "merchant@otherbank", + "status": "blocked", + "unblockedAt": "2026-07-09T12:30:00Z" + }, + { + "blockedAt": "2026-07-09T12:00:00Z", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "payeeVpa": "merchant@otherbank", + "status": "blocked", + "unblockedAt": "2026-07-09T12:30:00Z" + }, + { + "blockedAt": "2026-07-09T12:00:00Z", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "payeeVpa": "merchant@otherbank", + "status": "blocked", + "unblockedAt": "2026-07-09T12:30:00Z" + } + ], + "nextCursor": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + }, + "required": [ + "traceId", + "blockedPayees" + ] + }, + "BlockedPayeeView": { + "type": "object", + "properties": { + "blockedAt": { + "type": "string", + "description": "When the payee was most recently blocked (RFC3339).", + "example": "2026-07-09T12:00:00Z" + }, + "id": { + "type": "string", + "description": "The blocklist entry id (ULID).", + "example": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + }, + "payeeVpa": { + "type": "string", + "description": "The blocked payee VPA (prefix@handle).", + "example": "merchant@otherbank" + }, + "status": { + "type": "string", + "description": "blocked or unblocked.", + "example": "blocked" + }, + "unblockedAt": { + "type": "string", + "description": "When the payee was unblocked (RFC3339); absent while blocked.", + "example": "2026-07-09T12:30:00Z" + } + }, + "example": { + "blockedAt": "2026-07-09T12:00:00Z", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "payeeVpa": "merchant@otherbank", + "status": "blocked", + "unblockedAt": "2026-07-09T12:30:00Z" + }, + "required": [ + "id", + "payeeVpa", + "status" + ] + }, + "CheckVPARequestBody": { + "type": "object", + "properties": { + "deviceId": { + "type": "string", + "description": "The user's device id.", + "example": "device-abc-123" + }, + "mobile": { + "type": "string", + "description": "The user's mobile number, with country code (e.g. 919999999999).", + "example": "919999999999" + }, + "programId": { + "type": "string", + "description": "Optional program id (ULID). Validated when supplied.", + "example": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + }, + "vpa": { + "type": "string", + "description": "The full VPA to check (prefix@handle, e.g. 919999999999-alice@setu). Its prefix must start with the user's mobile number.", + "example": "919999999999-alice@setu" + } + }, + "example": { + "deviceId": "device-abc-123", + "mobile": "919999999999", + "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "vpa": "919999999999-alice@setu" + }, + "required": [ + "deviceId", + "mobile", + "vpa" + ] + }, + "CreateProgramRequestBody": { + "type": "object", + "properties": { + "amountLimit": { + "type": "integer", + "description": "Per-transaction cap, in paise. Omit for no cap.", + "example": 500000, + "format": "int64" + }, + "code": { + "type": "string", + "description": "The program's short code — its channel identifier.", + "example": "ACME" + }, + "isCreditAllowed": { + "type": "boolean", + "description": "May wallets on this program receive incoming UPI? Defaults true.", + "example": true + }, + "isDebitAllowed": { + "type": "boolean", + "description": "May wallets on this program pay outgoing UPI? Defaults true.", + "example": true + }, + "name": { + "type": "string", + "description": "The program's display name.", + "example": "Acme Wallet" + } + }, + "example": { + "amountLimit": 500000, + "code": "ACME", + "isCreditAllowed": true, + "isDebitAllowed": true, + "name": "Acme Wallet" + }, + "required": [ + "name", + "code" + ] + }, + "CreateVPARequestBody": { + "type": "object", + "properties": { + "accountName": { + "type": "string", + "description": "The account-holder name.", + "example": "Alice Doe" + }, + "accountNo": { + "type": "string", + "description": "The wallet / pool account number.", + "example": "99887766554433" + }, + "accountProviderId": { + "type": "string", + "description": "The account-provider id. Optional when a TPAP default is configured.", + "example": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + }, + "defaultCredit": { + "type": "boolean", + "description": "Make this VPA the user's default credit account.", + "example": true + }, + "defaultDebit": { + "type": "boolean", + "description": "Make this VPA the user's default debit account.", + "example": true + }, + "deviceId": { + "type": "string", + "description": "The user's device id.", + "example": "device-abc-123" + }, + "ifsc": { + "type": "string", + "description": "The IFSC of the account. Optional when a TPAP default is configured.", + "example": "SETU0000001" + }, + "mobile": { + "type": "string", + "description": "The user's mobile number, with country code (e.g. 919999999999).", + "example": "919999999999" + }, + "otpRequired": { + "type": "boolean", + "description": "Whether OTP verification is needed to activate the new VPA — set true for Android, false for iOS. When true, the VPA is created pending-verification until an OTP activates it; false (default) creates it active.", + "example": false + }, + "programId": { + "type": "string", + "description": "Optional program id (ULID) — the program this VPA belongs to. Validated when supplied.", + "example": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + }, + "vpa": { + "type": "string", + "description": "The full VPA to create (prefix@handle, e.g. 919999999999-alice@setu). Its prefix must start with the user's mobile number.", + "example": "919999999999-alice@setu" + } + }, + "example": { + "accountName": "Alice Doe", + "accountNo": "99887766554433", + "accountProviderId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "defaultCredit": true, + "defaultDebit": true, + "deviceId": "device-abc-123", + "ifsc": "SETU0000001", + "mobile": "919999999999", + "otpRequired": false, + "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "vpa": "919999999999-alice@setu" + }, + "required": [ + "deviceId", + "mobile", + "accountNo", + "vpa", + "accountName" + ] + }, + "CreateVPAResponse": { + "type": "object", + "properties": { + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID).", + "example": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + }, + "vpa": { + "$ref": "#/components/schemas/VpaView" + } + }, + "example": { + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "vpa": { + "accountName": "Alice Doe", + "accountNo": "99887766554433", + "accountProviderId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "accountType": "PPIWALLET", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "ifsc": "SETU0000001", + "mobile": "919999999999", + "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "status": "active", + "vpa": "919999999999-alice@setu" + } + }, + "required": [ + "traceId", + "vpa" + ] + }, + "GetVPARequestBody": { + "type": "object", + "properties": { + "deviceId": { + "type": "string", + "description": "The user's device id.", + "example": "device-abc-123" + }, + "mobile": { + "type": "string", + "description": "The user's mobile number, with country code (e.g. 919999999999).", + "example": "919999999999" + }, + "vpa": { + "type": "string", + "description": "The full VPA to fetch (prefix@handle, e.g. 919999999999-alice@setu). Its prefix must start with the user's mobile number.", + "example": "919999999999-alice@setu" + } + }, + "example": { + "deviceId": "device-abc-123", + "mobile": "919999999999", + "vpa": "919999999999-alice@setu" + }, + "required": [ + "deviceId", + "mobile", + "vpa" + ] + }, + "ListVPAsRequestBody": { + "type": "object", + "properties": { + "cursor": { + "type": "string", + "description": "The nextCursor from the previous page. Omit for the first page.", + "example": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + }, + "deviceId": { + "type": "string", + "description": "The user's device id.", + "example": "device-abc-123" + }, + "limit": { + "type": "integer", + "description": "Max entries to return this page. Omitted or non-positive defaults to 20; clamped to 100.", + "example": 20, + "format": "int64" + }, + "mobile": { + "type": "string", + "description": "The user's mobile number, with country code (e.g. 919999999999).", + "example": "919999999999" + } + }, + "example": { + "cursor": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "deviceId": "device-abc-123", + "limit": 20, + "mobile": "919999999999" + }, + "required": [ + "deviceId", + "mobile" + ] + }, + "OTPRequestResponse": { + "type": "object", + "properties": { + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID).", + "example": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + }, + "example": { + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + }, + "required": [ + "traceId" + ] + }, + "OTPVerifyResponse": { + "type": "object", + "properties": { + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID).", + "example": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + }, + "user": { + "$ref": "#/components/schemas/UserView" + }, + "vpa": { + "$ref": "#/components/schemas/VpaView" + } + }, + "example": { + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "user": { + "deviceId": "device-abc-123", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "mobile": "919999999999", + "status": "active" + }, + "vpa": { + "accountName": "Alice Doe", + "accountNo": "99887766554433", + "accountProviderId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "accountType": "PPIWALLET", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "ifsc": "SETU0000001", + "mobile": "919999999999", + "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "status": "active", + "vpa": "919999999999-alice@setu" + } + }, + "required": [ + "traceId" + ] + }, + "PayeeBlockResponse": { + "type": "object", + "properties": { + "blockedPayee": { + "$ref": "#/components/schemas/BlockedPayeeView" + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID).", + "example": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + }, + "example": { + "blockedPayee": { + "blockedAt": "2026-07-09T12:00:00Z", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "payeeVpa": "merchant@otherbank", + "status": "blocked", + "unblockedAt": "2026-07-09T12:30:00Z" + }, + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + }, + "required": [ + "traceId", + "blockedPayee" + ] + }, + "PollBindingStatusRequestBody": { + "type": "object", + "properties": { + "deviceId": { + "type": "string", + "description": "The user's device id.", + "example": "device-abc-123" + }, + "mobile": { + "type": "string", + "description": "The user's mobile number, with country code (e.g. 919999999999).", + "example": "919999999999" + } + }, + "example": { + "deviceId": "device-abc-123", + "mobile": "919999999999" + }, + "required": [ + "deviceId", + "mobile" + ] + }, + "ProgramResponse": { + "type": "object", + "properties": { + "program": { + "$ref": "#/components/schemas/ProgramView" + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID).", + "example": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + }, + "example": { + "program": { + "amountLimit": 500000, + "code": "ACME", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "isCreditAllowed": true, + "isDebitAllowed": true, + "name": "Acme Wallet", + "status": "active" + }, + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + }, + "required": [ + "traceId", + "program" + ] + }, + "ProgramView": { + "type": "object", + "properties": { + "amountLimit": { + "type": "integer", + "description": "Per-transaction cap, in paise; absent when there is no cap.", + "example": 500000, + "format": "int64" + }, + "code": { + "type": "string", + "description": "The program's short code.", + "example": "ACME" + }, + "id": { + "type": "string", + "description": "The program id (ULID); pass it as programId on onboarding calls.", + "example": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + }, + "isCreditAllowed": { + "type": "boolean", + "description": "May wallets on this program receive incoming UPI?", + "example": true + }, + "isDebitAllowed": { + "type": "boolean", + "description": "May wallets on this program pay outgoing UPI?", + "example": true + }, + "name": { + "type": "string", + "description": "The program's display name.", + "example": "Acme Wallet" + }, + "status": { + "type": "string", + "description": "active or inactive.", + "example": "active" + } + }, + "example": { + "amountLimit": 500000, + "code": "ACME", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "isCreditAllowed": true, + "isDebitAllowed": true, + "name": "Acme Wallet", + "status": "active" + }, + "required": [ + "id", + "name", + "isCreditAllowed", + "isDebitAllowed", + "status" + ] + }, + "RequestBindingTokenRequestBody": { + "type": "object", + "properties": { + "deviceId": { + "type": "string", + "description": "The user's device id.", + "example": "device-abc-123" + }, + "mobile": { + "type": "string", + "description": "The user's mobile number, with country code (e.g. 919999999999).", + "example": "919999999999" + }, + "os": { + "type": "string", + "description": "The device OS: android or ios.", + "example": "android" + }, + "otpRequired": { + "type": "boolean", + "description": "Set true for the device-change case where an OTP is needed after SIM binding (e.g. Android). Set false for iOS, or for the new-onboarding / new-VPA case where the OTP happens after VPA creation (gate it on create-vpa's otpRequired instead).", + "example": true + } + }, + "example": { + "deviceId": "device-abc-123", + "mobile": "919999999999", + "os": "android", + "otpRequired": true + }, + "required": [ + "deviceId", + "mobile", + "os", + "otpRequired" + ] + }, + "RequestOTPRequestBody": { + "type": "object", + "properties": { + "deviceId": { + "type": "string", + "description": "The user's device id.", + "example": "device-abc-123" + }, + "mobile": { + "type": "string", + "description": "The user's mobile number, with country code (e.g. 919999999999).", + "example": "919999999999" + }, + "vpa": { + "type": "string", + "description": "Include the VPA to verify a new VPA (new onboarding / new VPA); omit for a device-change OTP (SIM re-binding).", + "example": "919999999999-alice@setu" + } + }, + "example": { + "deviceId": "device-abc-123", + "mobile": "919999999999", + "vpa": "919999999999-alice@setu" + }, + "required": [ + "deviceId", + "mobile" + ] + }, + "UpdateProgramRequestBody": { + "type": "object", + "properties": { + "amountLimit": { + "type": "integer", + "description": "Per-transaction cap, in paise. Omit for no cap.", + "example": 500000, + "format": "int64" + }, + "code": { + "type": "string", + "description": "The program's short code.", + "example": "ACME" + }, + "isCreditAllowed": { + "type": "boolean", + "description": "May wallets on this program receive incoming UPI?", + "example": true + }, + "isDebitAllowed": { + "type": "boolean", + "description": "May wallets on this program pay outgoing UPI?", + "example": false + }, + "name": { + "type": "string", + "description": "The program's display name.", + "example": "Acme Wallet" + }, + "status": { + "type": "string", + "description": "active or inactive.", + "example": "inactive" + } + }, + "example": { + "amountLimit": 500000, + "code": "ACME", + "isCreditAllowed": true, + "isDebitAllowed": false, + "name": "Acme Wallet", + "status": "inactive" + } + }, + "UserView": { + "type": "object", + "properties": { + "deviceId": { + "type": "string", + "description": "The user's device id.", + "example": "device-abc-123" + }, + "id": { + "type": "string", + "description": "The user id (ULID).", + "example": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + }, + "mobile": { + "type": "string", + "description": "The user's mobile number, with country code.", + "example": "919999999999" + }, + "status": { + "type": "string", + "description": "binding-pending | device-bound | otp-pending | active.", + "example": "active" + } + }, + "example": { + "deviceId": "device-abc-123", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "mobile": "919999999999", + "status": "active" + }, + "required": [ + "id", + "status" + ] + }, + "VerifyOTPRequestBody": { + "type": "object", + "properties": { + "deviceId": { + "type": "string", + "description": "The user's device id.", + "example": "device-abc-123" + }, + "mobile": { + "type": "string", + "description": "The user's mobile number, with country code (e.g. 919999999999).", + "example": "919999999999" + }, + "otp": { + "type": "string", + "description": "The OTP the user entered.", + "example": "123456" + }, + "vpa": { + "type": "string", + "description": "Include the VPA to verify a new VPA (new onboarding / new VPA); omit for a device-change OTP (SIM re-binding).", + "example": "919999999999-alice@setu" + } + }, + "example": { + "deviceId": "device-abc-123", + "mobile": "919999999999", + "otp": "123456", + "vpa": "919999999999-alice@setu" + }, + "required": [ + "deviceId", + "mobile", + "otp" + ] + }, + "VpaCheckResponse": { + "type": "object", + "properties": { + "available": { + "type": "boolean", + "description": "false means the VPA is already taken by another user's active VPA. A taken VPA is not an error.", + "example": true + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID).", + "example": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + }, + "vpa": { + "type": "string", + "description": "The full VPA that was checked (prefix@handle).", + "example": "919999999999-alice@setu" + } + }, + "example": { + "available": true, + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "vpa": "919999999999-alice@setu" + }, + "required": [ + "traceId", + "vpa", + "available" + ] + }, + "VpaListResponse": { + "type": "object", + "properties": { + "nextCursor": { + "type": "string", + "description": "Cursor for the next page; absent on the last page.", + "example": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID).", + "example": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + }, + "vpas": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VpaView" + }, + "description": "This page of the user's live VPAs (newest-added first); empty when none.", + "example": [ + { + "accountName": "Alice Doe", + "accountNo": "99887766554433", + "accountProviderId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "accountType": "PPIWALLET", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "ifsc": "SETU0000001", + "mobile": "919999999999", + "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "status": "active", + "vpa": "919999999999-alice@setu" + }, + { + "accountName": "Alice Doe", + "accountNo": "99887766554433", + "accountProviderId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "accountType": "PPIWALLET", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "ifsc": "SETU0000001", + "mobile": "919999999999", + "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "status": "active", + "vpa": "919999999999-alice@setu" + }, + { + "accountName": "Alice Doe", + "accountNo": "99887766554433", + "accountProviderId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "accountType": "PPIWALLET", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "ifsc": "SETU0000001", + "mobile": "919999999999", + "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "status": "active", + "vpa": "919999999999-alice@setu" + } + ] + } + }, + "example": { + "nextCursor": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "vpas": [ + { + "accountName": "Alice Doe", + "accountNo": "99887766554433", + "accountProviderId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "accountType": "PPIWALLET", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "ifsc": "SETU0000001", + "mobile": "919999999999", + "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "status": "active", + "vpa": "919999999999-alice@setu" + }, + { + "accountName": "Alice Doe", + "accountNo": "99887766554433", + "accountProviderId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "accountType": "PPIWALLET", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "ifsc": "SETU0000001", + "mobile": "919999999999", + "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "status": "active", + "vpa": "919999999999-alice@setu" + }, + { + "accountName": "Alice Doe", + "accountNo": "99887766554433", + "accountProviderId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "accountType": "PPIWALLET", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "ifsc": "SETU0000001", + "mobile": "919999999999", + "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "status": "active", + "vpa": "919999999999-alice@setu" + } + ] + }, + "required": [ + "traceId", + "vpas" + ] + }, + "VpaView": { + "type": "object", + "properties": { + "accountName": { + "type": "string", + "description": "The account-holder name.", + "example": "Alice Doe" + }, + "accountNo": { + "type": "string", + "description": "The wallet / pool account number.", + "example": "99887766554433" + }, + "accountProviderId": { + "type": "string", + "description": "The account-provider id.", + "example": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + }, + "accountType": { + "type": "string", + "description": "The account type (e.g. PPIWALLET).", + "example": "PPIWALLET" + }, + "id": { + "type": "string", + "description": "The VPA id (ULID).", + "example": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + }, + "ifsc": { + "type": "string", + "description": "The IFSC of the account.", + "example": "SETU0000001" + }, + "mobile": { + "type": "string", + "description": "The owner's mobile number, with country code.", + "example": "919999999999" + }, + "programId": { + "type": "string", + "description": "The program the VPA belongs to (ULID).", + "example": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + }, + "status": { + "type": "string", + "description": "active | pending-verification | deregistered.", + "example": "active" + }, + "vpa": { + "type": "string", + "description": "The full VPA, prefix@handle.", + "example": "919999999999-alice@setu" + } + }, + "example": { + "accountName": "Alice Doe", + "accountNo": "99887766554433", + "accountProviderId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "accountType": "PPIWALLET", + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "ifsc": "SETU0000001", + "mobile": "919999999999", + "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "status": "active", + "vpa": "919999999999-alice@setu" + }, + "required": [ + "id", + "vpa" + ] + } + } + }, + "tags": [ + { + "name": "Device binding" + }, + { + "name": "OTP verification" + }, + { + "name": "VPA management" + }, + { + "name": "Programs" + }, + { + "name": "Payee blocklist" + } + ] +} \ No newline at end of file diff --git a/content/endpoints.json b/content/endpoints.json index 2b27d444..7cba1db5 100644 --- a/content/endpoints.json +++ b/content/endpoints.json @@ -38,6 +38,12 @@ "path": "umap", "order": 7, "visible_in_sidebar": true + }, + { + "name": "UPI Issuance", + "path": "upi-issuance", + "order": 8, + "visible_in_sidebar": true } ] }, diff --git a/content/payments/upi-issuance/api-envelope.mdx b/content/payments/upi-issuance/api-envelope.mdx new file mode 100644 index 00000000..cb8ecb6d --- /dev/null +++ b/content/payments/upi-issuance/api-envelope.mdx @@ -0,0 +1,89 @@ +--- +sidebar_title: The API envelope +page_title: UPI Issuance API envelope +order: 2 +visible_in_sidebar: true +--- + +## The API envelope + +Every request and response body is encrypted, keeping PII (`deviceId`, `mobile`, account details) out of plain text on the wire. The scheme is a standard hybrid encryption envelope. + +### What travels where + +- **PII in the body.** `deviceId` and `mobile` are in the encrypted body, never in headers or the URL. +- **`idempotencyKey` in a header.** It stays a plain header, outside the encrypted body. +- **Reads are POSTs.** Even pure reads (`binding-status`, `list-vpas`) are `POST` so their PII body can be encrypted. + +
+ +### Request format + +The TPAP / Issuing App encrypts the request body into an envelope object with three base64 fields: + + + {`{ + "ct": "", + "sk": "", + "iv": "" +}`} + + +For each request, the TPAP / Issuing App generates a random **32-byte AES-256 key** and **16-byte IV**, encrypts the JSON body with **AES-256-CBC** and PKCS#7 padding, and wraps the session key with **RSA-OAEP (SHA-1)** under Setu's public key. The TPAP / Issuing App keeps the session key and IV to decrypt the response. + +### Response format + +The response is encrypted under the same session key and IV the TPAP / Issuing App generated: + + + {`{ + "ct": "", + "oha": "" +}`} + + +The TPAP / Issuing App decrypts `ct` with the session key and IV, then verifies `oha` matches the SHA-256 of the decrypted plaintext. + + + The wire suite is fixed: RSA-OAEP-SHA1 for the key wrap, AES-256-CBC with PKCS#7 + for the body, SHA-256 for the response integrity hash. + + +
+ +### Reference implementation (Python) + + + {`import os, json, base64, hashlib +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives.asymmetric import padding +from cryptography.hazmat.primitives import hashes, serialization +def _pad(b, k=16): n = k - len(b) % k; return b + bytes([n]) * n +def _unpad(b): return b[:-b[-1]] +def encrypt(pub_pem: str, body: dict): + pub = serialization.load_pem_public_key(pub_pem.encode()) + key, iv = os.urandom(32), os.urandom(16) + sk = pub.encrypt(key, padding.OAEP(mgf=padding.MGF1(hashes.SHA1()), + algorithm=hashes.SHA1(), label=None)) + enc = Cipher(algorithms.AES(key), modes.CBC(iv)).encryptor() + ct = enc.update(_pad(json.dumps(body).encode())) + enc.finalize() + env = {"ct": base64.b64encode(ct).decode(), + "sk": base64.b64encode(sk).decode(), + "iv": base64.b64encode(iv).decode()} + return env, key, iv +def decrypt_response(key, iv, enc: dict): + ct = base64.b64decode(enc["ct"]) + dec = Cipher(algorithms.AES(key), modes.CBC(iv)).decryptor() + plain = _unpad(dec.update(ct) + dec.finalize()) + assert enc["oha"] == hashlib.sha256(plain).hexdigest(), "integrity mismatch" + return json.loads(plain)`} + + +`encrypt()` returns the request envelope `env` — the `{ct, sk, iv}` object — along with the session `key` and `iv`. The TPAP / Issuing App sends `env` as the request body with `Content-Type: application/json`, and passes the same `key` and `iv` to `decrypt_response()` to read the response. + +### Next + +- **[User Onboarding](/payments/upi-issuance/onboarding)** — bind a device, create a VPA, and verify OTPs. +- **[Testing on QA env](/payments/upi-issuance/qa-testing)** — reproduce every scenario. + + diff --git a/content/payments/upi-issuance/api-reference.mdx b/content/payments/upi-issuance/api-reference.mdx new file mode 100644 index 00000000..a17eb3f2 --- /dev/null +++ b/content/payments/upi-issuance/api-reference.mdx @@ -0,0 +1,24 @@ +--- +sidebar_title: API reference +page_title: UPI Issuance API reference +order: 6 +visible_in_sidebar: true +--- + +## API reference + +The complete, spec-backed API reference (request and response schemas, every field, every error) is being finalised and will render here from the OpenAPI definition. + +In the meantime, each operation is documented with its request, response, and error table under [API integration](/payments/upi-issuance/onboarding/api-integration): + +- [Device binding](/payments/upi-issuance/onboarding/api-integration/device-binding) — generate binding token, `binding-status` +- [OTP verification](/payments/upi-issuance/onboarding/api-integration/user-otp) — `otp/request`, `otp/verify` +- [VPA management](/payments/upi-issuance/onboarding/api-integration/vpa-management) — `vpa/check`, `create-vpa`, `get-vpa`, `list-vpas`, `vpa/deregister` +- [Payee blocklist](/payments/upi-issuance/payee-blocklist) — `block-vpa`, `unblock-vpa`, `list-blocked-vpas` +- [Programs](/payments/upi-issuance/onboarding/api-integration/programs) — `POST /programs`, `PATCH /programs/{id}` + +### Next + +- **[User Onboarding](/payments/upi-issuance/onboarding)** — the onboarding flow and a step-by-step API walkthrough. + + diff --git a/content/payments/upi-issuance/onboarding.mdx b/content/payments/upi-issuance/onboarding.mdx new file mode 100644 index 00000000..13e9ae57 --- /dev/null +++ b/content/payments/upi-issuance/onboarding.mdx @@ -0,0 +1,49 @@ +--- +sidebar_title: User Onboarding +page_title: UPI Issuance User Onboarding +order: 3 +visible_in_sidebar: true +--- + +## User Onboarding + +Onboarding gets a user ready to transact on UPI. It runs in three steps. + +**1. Device binding.** The TPAP / Issuing App requests a binding token from Setu, which generates one and returns it with a Virtual Mobile Number (VMN). The token is sent as a silent SMS from the user's SIM to the VMN. The telecom then notifies Setu of the VMN, the sending mobile, and the SMS body, and Setu checks all three to bind the device to the user. + +**2. VPA creation.** The TPAP / Issuing App creates a VPA — the user's UPI address — over the user's account. + +**3. OTP verification.** The TPAP / Issuing App requests an OTP from Setu, Setu delivers it to the user's mobile by SMS, and the TPAP / Issuing App submits it back to Setu to activate the VPA. Today this is **required on Android** and **not needed on iOS**. + +New user onboarding and VPA creation: device binding, VPA creation, and the OTP that activates the new VPA (Android) + +

New user onboarding / VPA creation for an existing user

+ +Once onboarded, the user has an active VPA and can transact on UPI. + +On a **device change**, only device binding is repeated — a device change does not require VPA creation. As at onboarding, an OTP is required on Android and not on iOS. + +On **iOS**, a successful SIM binding moves the user status straight to `active`. + +On **Android**, a successful SIM binding moves the user status to `device-bound`. Requesting an OTP then moves the user to `otp-pending`. A successful OTP verification moves the user status to `active`. + +Device change: device binding on the new device, with an OTP on Android moving the user from device-bound to active and iOS activating directly, no VPA creation + +

Device change — OTP verification, no VPA creation

+ + + NPCI is not involved in onboarding for PPI Issuing Apps: the account provider is + already set, and PPI accounts do not need a UPI PIN to be set up. + + +### In this section + +- **[Onboarding states](/payments/upi-issuance/onboarding/onboarding-states)** — the user and VPA state machines, and what moves them. +- **[API integration](/payments/upi-issuance/onboarding/api-integration)** — a step-by-step walkthrough of each onboarding operation. + +### Next + +- **[Payee blocklist](/payments/upi-issuance/payee-blocklist)** — block, unblock, and list blocked payee VPAs. +- **[Testing on QA env](/payments/upi-issuance/qa-testing)** — reproduce every scenario. + + diff --git a/content/payments/upi-issuance/onboarding/api-integration.mdx b/content/payments/upi-issuance/onboarding/api-integration.mdx new file mode 100644 index 00000000..ca2b8fd5 --- /dev/null +++ b/content/payments/upi-issuance/onboarding/api-integration.mdx @@ -0,0 +1,46 @@ +--- +sidebar_title: API integration +page_title: UPI Issuance API integration +order: 1 +visible_in_sidebar: true +--- + +## API integration + +A step-by-step walkthrough of each onboarding operation. All routes are under `/api/v1`, all body-carrying routes use [the envelope](/payments/upi-issuance/api-envelope), and `idempotencyKey` is always a header. + +- **[Device binding](/payments/upi-issuance/onboarding/api-integration/device-binding)** — generate binding token, the silent SMS, and the `binding-status` poll. +- **[OTP verification](/payments/upi-issuance/onboarding/api-integration/user-otp)** — `otp/request` and `otp/verify` to activate a VPA on Android. +- **[VPA management](/payments/upi-issuance/onboarding/api-integration/vpa-management)** — check, create, fetch, list, and deregister VPAs. +- **[Programs](/payments/upi-issuance/onboarding/api-integration/programs)** — create and update program configuration. + +
+ + + Testing any of these? Every scenario is reproducible on the QA env — see + Testing on QA env. + + +## Error responses + +Every error, on every endpoint, returns the **same shape** — encrypted in the envelope like any response: + + + {`{ + "traceId": "01J...", + "code": "invalid-user-state", + "message": "A human-readable description of what went wrong." +}`} + + +- The **HTTP status is authoritative** — branch on it first (`4xx`/`5xx` = error). +- `code` is a **stable machine string** to switch on; `message` is descriptive and may change. +- `traceId` is on **every** response, success or error. Log it and quote it to Setu when raising an issue. + +A `500` with `code: internal-error` means something failed on Setu's side; retry with the same `idempotencyKey`. Each operation's page lists the specific `4xx` codes it can return. + +### Next + +- **[Device binding](/payments/upi-issuance/onboarding/api-integration/device-binding)** — generate a binding token and prove the SIM. + + diff --git a/content/payments/upi-issuance/onboarding/api-integration/device-binding.mdx b/content/payments/upi-issuance/onboarding/api-integration/device-binding.mdx new file mode 100644 index 00000000..409256c4 --- /dev/null +++ b/content/payments/upi-issuance/onboarding/api-integration/device-binding.mdx @@ -0,0 +1,156 @@ +--- +sidebar_title: Device binding +page_title: UPI Issuance device binding +order: 0 +visible_in_sidebar: true +--- + +## Device binding + +Device binding proves that the user's SIM, mobile number, and device belong together. It is the entry point on any device — a fresh install, a new phone, or a re-login. + +Device binding sequence: binding token, silent SMS from the SIM to the VMN, and the binding-status poll to active + +### 1. Generate a binding token + +`POST /api/v1/onboarding/binding-token` (enveloped) + +| Field | Type | Required | Notes | +| :--- | :--- | :--- | :--- | +| `deviceId` | string | yes | The user's device id. | +| `mobile` | string | yes | The user's mobile number, with country code (e.g. `919999999999`). | +| `os` | string | yes | `android` or `ios`. | +| `otpRequired` | boolean | yes | Whether a successful SIM binding fully activates the user. `false` — the user goes straight to `active`. `true` — the user stops at `device-bound` until an [OTP](/payments/upi-issuance/onboarding/api-integration/user-otp) is verified. | + +
+ +`idempotencyKey` is an optional request header. + +**When OTP verification happens is controlled by the TPAP / Issuing App, and Setu respects it.** There are two options: + +- To verify OTP **after SIM binding and before VPA creation**, generate the binding token with `otpRequired: true`. The user stays at `device-bound` until an OTP is verified. This is the **device change** case, where no new VPA is created. +- To verify OTP **after VPA creation**, generate the binding token with `otpRequired: false` and set `otpRequired: true` on [`create-vpa`](/payments/upi-issuance/onboarding/api-integration/vpa-management) instead. The new VPA stays `pending-verification` until an OTP is verified. This is the **new onboarding / new VPA** case. + +##### Sample request + + + {`{ + "deviceId": "device-abc-123", + "mobile": "919999999999", + "os": "android", + "otpRequired": false +}`} + + +##### Success response — `200` + +| Field | Type | Notes | +| :--- | :--- | :--- | +| `traceId` | string | Trace handle for this call (ULID). | +| `vmn` | string | The Virtual Mobile Number to send the SMS to. | +| `smsBody` | string | The ready-to-send silent-SMS body. The token is embedded here and is never returned separately. | + + + {`{ + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "vmn": "919900000001", + "smsBody": "VERIFY SETqxf8aemkxxdrhbifd4n4zldq7guvxb2k" +}`} + + +##### Errors + +| Status | Code | When | +| :--- | :--- | :--- | +| 400 | `invalid-request` | A required field is missing (`deviceId`, `mobile`, `os`, `otpRequired`) or the request body is malformed. | +| 429 | `device-binding-cap-exceeded` | Too many binding attempts for this device today. | +| 429 | `mobile-binding-cap-exceeded` | Too many binding attempts for this mobile today. | +| 500 | `internal-error` | Something went wrong on Setu's side. Retry with the same `idempotencyKey`. | + +### 2. Send the silent SMS + +`vmn` is a **Virtual Mobile Number** — a receive-only number that the binding SMS is sent to. The TPAP / Issuing App sends `smsBody` as a silent SMS from the user's SIM to `vmn`. The telecom provider then calls a webhook to Setu, indicating the VMN that received the SMS, the mobile it was sent from, and the SMS body. Setu checks all three — the SMS body must carry a token for a live binding, the VMN must be the one that binding was issued for, and the sending mobile must match the claimed `mobile`. When all three match, the SIM is proven and the binding advances. The TPAP / Issuing App does not call anything for this step. + +The binding token is short-lived — it expires **45 seconds** after it is generated. The silent SMS (and the telecom's webhook back to Setu) must complete within that window; if it lapses, generate a fresh binding token. + + + On the QA env there is no telecom. Drive the outcome with a + sim.bind-* directive in the idempotencyKey of the + generate binding token call — see + Testing on QA env. + + +### 3. Poll the binding status + +`POST /api/v1/onboarding/binding-status` (enveloped) + +| Field | Type | Required | Notes | +| :--- | :--- | :--- | :--- | +| `deviceId` | string | yes | The user's device id. | +| `mobile` | string | yes | The user's mobile number, with country code (e.g. `919999999999`). | + +
+ +`idempotencyKey` is an optional request header. + +##### Sample request + + + {`{ + "deviceId": "device-abc-123", + "mobile": "919999999999" +}`} + + +##### Success response — `200` + +| Field | Type | Notes | +| :--- | :--- | :--- | +| `traceId` | string | Trace handle for this call (ULID). | +| `user` | object | The user in its current state (see [The user object](#the-user-object)). | + + + {`{ + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "user": { + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "status": "active", + "deviceId": "device-abc-123", + "mobile": "919999999999" + } +}`} + + +Poll until the status settles. With `otpRequired: false` a successful SIM binding lands the user on `active`, and the TPAP / Issuing App can [create a VPA](/payments/upi-issuance/onboarding/api-integration/vpa-management). With `otpRequired: true` the user settles at `device-bound` and needs an [OTP](/payments/upi-issuance/onboarding/api-integration/user-otp) to reach `active` — the OTP is valid for **60 seconds** after it is requested. + +##### Errors + +| Status | Code | When | +| :--- | :--- | :--- | +| 400 | `invalid-request` | A required field is missing (`deviceId`, `mobile`) or the request body is malformed. | +| 404 | `binding-not-found` | No binding for this device — polled before generating a binding token, or from a device that is not the bound one. | +| 500 | `internal-error` | Something went wrong on Setu's side. | + +### The user object + +The [binding status](#3-poll-the-binding-status) and a device-change [OTP verification](/payments/upi-issuance/onboarding/api-integration/user-otp) return the user in this shape: + +| Field | Type | Notes | +| :--- | :--- | :--- | +| `id` | string | The user's id (ULID). | +| `status` | string | `binding-pending`, `device-bound`, `otp-pending`, or `active`. | +| `deviceId` | string | The user's device id. | +| `mobile` | string | The user's mobile number, with country code (e.g. `919999999999`). | + +
+ +### On a device change + +When the user moves to a new phone or re-installs, only device binding is repeated — the three steps above, against the new `deviceId`. A device change does not require VPA creation. The OTP is OS-driven, just as at onboarding. On **iOS** (`otpRequired: false`) a successful SIM binding moves the user to `active`. On **Android** (`otpRequired: true`) a successful SIM binding moves the user to `device-bound`, and an [OTP](/payments/upi-issuance/onboarding/api-integration/user-otp) then moves the user to `active` before the account is usable on the new device. + +### Next + +- **[OTP verification](/payments/upi-issuance/onboarding/api-integration/user-otp)** — request and verify OTPs. +- **[VPA management](/payments/upi-issuance/onboarding/api-integration/vpa-management)** — create and manage VPAs. + + diff --git a/content/payments/upi-issuance/onboarding/api-integration/programs.mdx b/content/payments/upi-issuance/onboarding/api-integration/programs.mdx new file mode 100644 index 00000000..da1c7d14 --- /dev/null +++ b/content/payments/upi-issuance/onboarding/api-integration/programs.mdx @@ -0,0 +1,144 @@ +--- +sidebar_title: Programs +page_title: UPI Issuance programs +order: 3 +visible_in_sidebar: true +--- + +## Programs + +A program is an **engagement channel** through which users are onboarded onto the TPAP / Issuing App and issued a wallet — for example a distinct branded app or offering built on the stack. Each program carries its own configuration: whether wallets on it may pay and receive, and an optional per-transaction limit. A `programId` is optional on the onboarding calls that accept it. It matters most on `create-vpa`, which ties each VPA to the program it was created under so the program's rules apply to that wallet. + +Setu can provision a program, or the TPAP / Issuing App can manage its own through these endpoints. Program routes are enveloped like the rest of the onboarding surface. + +### The program object + +Both endpoints return the program under `program` in this shape: + +| Field | Type | Notes | +| :--- | :--- | :--- | +| `id` | string | The program id (ULID); pass it as `programId` on onboarding calls. | +| `name` | string | The program's display name. | +| `code` | string | The program's short code — its channel identifier. | +| `isCreditAllowed` | boolean | May wallets on this program receive incoming UPI? | +| `isDebitAllowed` | boolean | May wallets on this program pay outgoing UPI? | +| `amountLimit` | integer | Per-transaction cap, in paise; absent when there is no cap. | +| `status` | string | `active` or `inactive`. | + +
+ +### Create a program + +`POST /api/v1/programs` (enveloped) + +| Field | Type | Required | Notes | +| :--- | :--- | :--- | :--- | +| `name` | string | yes | The program's display name. | +| `code` | string | yes | The program's short code — its channel identifier. | +| `isCreditAllowed` | boolean | no | May wallets on this program receive incoming UPI? Defaults `true`. | +| `isDebitAllowed` | boolean | no | May wallets on this program pay outgoing UPI? Defaults `true`. | +| `amountLimit` | integer | no | Per-transaction cap, in paise. Omit for no cap. | + +##### Sample request + + + {`{ + "name": "Acme Wallet", + "code": "ACME", + "isCreditAllowed": true, + "isDebitAllowed": true, + "amountLimit": 2000000 +}`} + + +##### Success response — `201` + +| Field | Type | Notes | +| :--- | :--- | :--- | +| `traceId` | string | Trace handle for this call (ULID). | +| `program` | object | The created program (see [The program object](#the-program-object)). Use its `id` (passed as `programId`) in onboarding calls. | + + + {`{ + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "program": { + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "name": "Acme Wallet", + "code": "ACME", + "isCreditAllowed": true, + "isDebitAllowed": true, + "amountLimit": 2000000, + "status": "active" + } +}`} + + +##### Errors + +| Status | Code | When | +| :--- | :--- | :--- | +| 400 | `invalid-request` | The request body is malformed. | +| 400 | `missing-parameter` | `name` or `code` is missing. | +| 500 | `internal-error` | Something went wrong on Setu's side. | + +
+ +### Update a program + +`PATCH /api/v1/programs/{programId}` (enveloped) — only the fields sent change. `programId` is a path parameter. + +| Field | Type | Required | Notes | +| :--- | :--- | :--- | :--- | +| `name` | string | no | The program's display name. | +| `code` | string | no | The program's short code. | +| `isCreditAllowed` | boolean | no | May wallets on this program receive incoming UPI? | +| `isDebitAllowed` | boolean | no | May wallets on this program pay outgoing UPI? | +| `amountLimit` | integer | no | Per-transaction cap, in paise. | +| `status` | string | no | `active` or `inactive`. An `inactive` program is rejected at the onboarding gate (`400 invalid-program`). | + +##### Sample request + +For example, deactivate a program: + + + {`{ + "status": "inactive" +}`} + + +##### Success response — `200` + +| Field | Type | Notes | +| :--- | :--- | :--- | +| `traceId` | string | Trace handle for this call (ULID). | +| `program` | object | The updated program (see [The program object](#the-program-object)). | + + + {`{ + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "program": { + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "name": "Acme Wallet", + "code": "ACME", + "isCreditAllowed": true, + "isDebitAllowed": true, + "amountLimit": 2000000, + "status": "inactive" + } +}`} + + +##### Errors + +| Status | Code | When | +| :--- | :--- | :--- | +| 400 | `invalid-request` | The request body is malformed. | +| 400 | `missing-parameter` | `programId` is missing. | +| 404 | `program-not-found` | No program with that `programId`. | +| 500 | `internal-error` | Something went wrong on Setu's side. | + +### Next + +- **[Device binding](/payments/upi-issuance/onboarding/api-integration/device-binding)** — generate a binding token and start onboarding a user. + + diff --git a/content/payments/upi-issuance/onboarding/api-integration/user-otp.mdx b/content/payments/upi-issuance/onboarding/api-integration/user-otp.mdx new file mode 100644 index 00000000..99948861 --- /dev/null +++ b/content/payments/upi-issuance/onboarding/api-integration/user-otp.mdx @@ -0,0 +1,184 @@ +--- +sidebar_title: OTP verification +page_title: UPI Issuance OTP verification +order: 1 +visible_in_sidebar: true +--- + +## OTP verification + +Onboarding involves two kinds of OTP. Both use the same `otp/request` + `otp/verify` pair, but they unlock different things and are triggered by different calls: + +| Use case | What the OTP unlocks | Triggered by | `vpa` in the request | Verify returns | +| :--- | :--- | :--- | :--- | :--- | +| **New onboarding / new VPA** | transacting on the new VPA (`pending-verification → active`) | `otpRequired: true` on `create-vpa` | yes | the now-`active` VPA | +| **Device change** | transacting from the new device (`device-bound → active`) | `otpRequired: true` on generate binding token | no | the now-`active` user | + +
+ +The OTP for a new VPA never changes the user's status. The OTP for a device change never changes a VPA's status. Whether an `otp/request` or `otp/verify` call applies to a VPA or to the user is decided by whether a `vpa` is present in the body. + +
+ +## New onboarding / new VPA + +A newly created VPA can require an OTP before it can transact. This is **OS-dependent**: on **Android** the OTP is required and the VPA is created `pending-verification`, on **iOS** it is not and the VPA is created `active`. The TPAP / Issuing App signals it by setting `otpRequired` on [`create-vpa`](/payments/upi-issuance/onboarding/api-integration/vpa-management) to match the device OS. + +New VPA activation: on Android create-vpa creates the VPA pending-verification and an OTP promotes it to active, on iOS it is created active directly + +## Device change + +When `otpRequired: true` is sent on the [generate binding token](/payments/upi-issuance/onboarding/api-integration/device-binding) call, a successful SIM binding leaves the user at `device-bound` rather than `active`. Requesting the OTP moves the user to `otp-pending`, and a successful verification moves the user to `active`, letting them transact from the new device. This confirms the user is present on the new device. + +This is **OS-dependent** — `otpRequired` is `true` on **Android** (the user stops at `device-bound`) and `false` on **iOS** (a successful SIM binding activates the user directly). + +Device change: on Android otpRequired true leaves the user device-bound and an OTP promotes them to active, on iOS the SIM binding activates the user directly + +
+ +## Request an OTP + +`POST /api/v1/onboarding/otp/request` (enveloped) — asynchronous, returns `202`. Setu sends the OTP to the user's mobile by SMS. The OTP is valid for **60 seconds** after it is requested; once it lapses, `otp/verify` returns `410 otp-expired-or-exhausted` and a fresh `otp/request` is needed. + +| Field | Type | Required | Notes | +| :--- | :--- | :--- | :--- | +| `deviceId` | string | yes | The user's device id. | +| `mobile` | string | yes | The user's mobile number, with country code (e.g. `919999999999`). | +| `vpa` | string | conditional | Include it for the **new onboarding / new VPA** case (the VPA being activated). Omit it for the **device change** case. | + +
+ +`idempotencyKey` is an optional request header. + +##### Sample request + + + {`{ + "deviceId": "device-abc-123", + "mobile": "919999999999", + "vpa": "919999999999-alice@setu" +}`} + + +Omit `vpa` for the **device change** case. + +##### Success response — `202` + +| Field | Type | Notes | +| :--- | :--- | :--- | +| `traceId` | string | Trace handle for this call (ULID). | + + + {`{ + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" +}`} + + +##### Errors + +| Status | Code | When | +| :--- | :--- | :--- | +| 400 | `invalid-request` | `deviceId` or `mobile` is missing, or the request body is malformed. | +| 401 | `device-not-linked` | This device is not bound for this user. | +| 404 | `user-not-found` | No user for this mobile. | +| 404 | `vpa-not-found` | A `vpa` was sent but the user does not own a VPA with that string. | +| 409 | `invalid-user-state` | Not in a state that owes an OTP. The **new onboarding / new VPA** case needs the user `active` and the VPA `pending-verification`. The **device change** case needs the user at `device-bound` or `otp-pending`. | +| 429 | `otp-cap-exceeded` | Too many OTP requests. | +| 500 | `internal-error` | Something went wrong on Setu's side. | + +
+ +## Verify an OTP + +`POST /api/v1/onboarding/otp/verify` (enveloped) + +| Field | Type | Required | Notes | +| :--- | :--- | :--- | :--- | +| `deviceId` | string | yes | The user's device id. | +| `mobile` | string | yes | The user's mobile number, with country code (e.g. `919999999999`). | +| `otp` | string | yes | The digits the user entered. | +| `vpa` | string | conditional | Include it for the **new onboarding / new VPA** case (the VPA being activated). Omit it for the **device change** case. | + +
+ +`idempotencyKey` is an optional request header. One attempt per OTP — a wrong guess consumes it, so a retry must `otp/request` again. + +##### Sample request + + + {`{ + "deviceId": "device-abc-123", + "mobile": "919999999999", + "otp": "123456", + "vpa": "919999999999-alice@setu" +}`} + + +##### Success response — `200` + +| Field | Type | Notes | +| :--- | :--- | :--- | +| `traceId` | string | Trace handle for this call (ULID). | +| `vpa` | object | Present for the **new onboarding / new VPA** case — the now-`active` VPA (see [The VPA object](/payments/upi-issuance/onboarding/api-integration/vpa-management#the-vpa-object)). | +| `user` | object | Present for the **device change** case — the now-`active` user (see [The user object](/payments/upi-issuance/onboarding/api-integration/device-binding#the-user-object)). | + +Exactly one of `vpa` or `user` is present. + + + {`{ + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "vpa": { + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "vpa": "919999999999-alice@setu", + "status": "active", + "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "accountNo": "99887766554433", + "accountProviderId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "ifsc": "SETU0000001", + "accountType": "PPIWALLET", + "accountName": "Alice Doe", + "mobile": "919999999999" + } +}`} + + +The **device change** case returns the user instead: + + + {`{ + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "user": { + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "status": "active", + "deviceId": "device-abc-123", + "mobile": "919999999999" + } +}`} + + +##### Errors + +| Status | Code | When | +| :--- | :--- | :--- | +| 400 | `invalid-request` | `deviceId` or `mobile` is missing, or the request body is malformed. | +| 400 | `missing-parameter` | `otp` is missing. | +| 401 | `device-not-linked` | This device is not bound for this user. | +| 401 | `invalid-otp` | The OTP is incorrect. The attempt is consumed. | +| 404 | `user-not-found` | No user for this mobile. | +| 409 | `invalid-user-state` | Not in a state that owes an OTP. The **new onboarding / new VPA** case needs the user `active` and the VPA `pending-verification`. The **device change** case needs the user at `device-bound` or `otp-pending`. | +| 410 | `otp-expired-or-exhausted` | No live OTP to verify — never requested, expired, or already consumed. | +| 500 | `internal-error` | Something went wrong on Setu's side. | + +
+ + + On the QA env there is no telecom, so no OTP SMS is sent. Set idempotencyKey: sim.otp-ok + on otp/verify to pass with any code — see + Testing on QA env. + + +### Next + +- **[VPA management](/payments/upi-issuance/onboarding/api-integration/vpa-management)** — create and manage VPAs. + + diff --git a/content/payments/upi-issuance/onboarding/api-integration/vpa-management.mdx b/content/payments/upi-issuance/onboarding/api-integration/vpa-management.mdx new file mode 100644 index 00000000..8aee3003 --- /dev/null +++ b/content/payments/upi-issuance/onboarding/api-integration/vpa-management.mdx @@ -0,0 +1,359 @@ +--- +sidebar_title: VPA management +page_title: UPI Issuance VPA management +order: 2 +visible_in_sidebar: true +--- + +## VPA management + +Once a user is `active`, the TPAP / Issuing App creates and manages their VPAs. All routes are enveloped and under `/api/v1`. `idempotencyKey` is an optional request header on every call. + +### The VPA object + +`create-vpa`, `get-vpa`, `list-vpas`, and `vpa/deregister` return the VPA in this shape: + +| Field | Type | Notes | +| :--- | :--- | :--- | +| `id` | string | The VPA id (ULID). | +| `vpa` | string | The full VPA, `prefix@handle`. | +| `status` | string | `active`, `pending-verification`, or `deregistered`. | +| `programId` | string | The program the VPA belongs to (ULID). | +| `accountNo` | string | The wallet / pool account number. | +| `accountProviderId` | string | The account-provider id. | +| `ifsc` | string | The IFSC of the account. | +| `accountType` | string | The account type (e.g. `PPIWALLET`). | +| `accountName` | string | The account-holder name. | +| `mobile` | string | The owner's mobile number, with country code (e.g. `919999999999`). | + +
+ +### Check availability + +`POST /api/v1/onboarding/vpa/check` (enveloped) — is a chosen VPA free? + +| Field | Type | Required | Notes | +| :--- | :--- | :--- | :--- | +| `deviceId` | string | yes | The user's device id. | +| `mobile` | string | yes | The user's mobile number, with country code (e.g. `919999999999`). | +| `vpa` | string | yes | The VPA to check — the full VPA `prefix@handle` (e.g. `919999999999-alice@setu`). Its VPA prefix must start with the subscriber's mobile. | +| `programId` | string | no | ULID. Validated for existence only. | + +##### Sample request + + + {`{ + "deviceId": "device-abc-123", + "mobile": "919999999999", + "vpa": "919999999999-alice@setu" +}`} + + +##### Success response — `200` + +| Field | Type | Notes | +| :--- | :--- | :--- | +| `traceId` | string | Trace handle for this call (ULID). | +| `vpa` | string | The full VPA that was checked (`prefix@handle`). | +| `available` | boolean | `false` means the VPA is already taken by another user's active VPA. A taken VPA is not an error. | + + + {`{ + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "vpa": "919999999999-alice@setu", + "available": true +}`} + + +##### Errors + +| Status | Code | When | +| :--- | :--- | :--- | +| 400 | `invalid-request` | `deviceId` or `mobile` is missing, or the request body is malformed. | +| 400 | `missing-parameter` | `vpa` is missing. | +| 400 | `invalid-program` | Unknown or inactive `programId`. | +| 400 | `invalid-vpa` | The VPA prefix does not start with the subscriber's mobile number. | +| 400 | `invalid-handle` | The VPA carries a handle that is not the one configured for this TPAP. | +| 401 | `device-not-linked` | This device is not bound for this user. | +| 404 | `user-not-found` | No user for this mobile. | +| 409 | `invalid-user-state` | The user is not `active`. | +| 500 | `internal-error` | Something went wrong on Setu's side. | + +
+ +### Create a VPA + +`POST /api/v1/onboarding/create-vpa` (enveloped) + +| Field | Type | Required | Notes | +| :--- | :--- | :--- | :--- | +| `deviceId` | string | yes | The user's device id. | +| `mobile` | string | yes | The user's mobile number, with country code (e.g. `919999999999`). | +| `vpa` | string | yes | The VPA to create — the full VPA `prefix@handle` (e.g. `919999999999-alice@setu`). Its VPA prefix must start with the subscriber's mobile. | +| `accountNo` | string | yes | The wallet / pool account number. | +| `accountName` | string | yes | The account-holder name. | +| `otpRequired` | boolean | no | `true` (Android) — the VPA is created `pending-verification` and an [OTP](/payments/upi-issuance/onboarding/api-integration/user-otp) activates it. `false` (iOS, default) — the VPA is created `active`. | +| `ifsc` | string | no | The IFSC of the account. Optional when a TPAP default is configured. | +| `accountProviderId` | string | no | The account-provider id. Optional when a TPAP default is configured. | +| `programId` | string | no | ULID. Validated for existence when supplied. | +| `defaultDebit` | boolean | no | Make this the user's default debit account. | +| `defaultCredit` | boolean | no | Make this the user's default credit account. | + +The account details are taken from the request. There is no bank round-trip at onboarding. + +##### Sample request + + + {`{ + "deviceId": "device-abc-123", + "mobile": "919999999999", + "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "vpa": "919999999999-alice@setu", + "accountNo": "99887766554433", + "accountName": "Alice Doe", + "otpRequired": false +}`} + + +##### Success response — `200` + +Returns the created VPA; its `status` is `active` or `pending-verification`. + +| Field | Type | Notes | +| :--- | :--- | :--- | +| `traceId` | string | Trace handle for this call (ULID). | +| `vpa` | object | The created VPA (see [The VPA object](#the-vpa-object)). | + + + {`{ + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "vpa": { + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "vpa": "919999999999-alice@setu", + "status": "active", + "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "accountNo": "99887766554433", + "accountProviderId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "ifsc": "SETU0000001", + "accountType": "PPIWALLET", + "accountName": "Alice Doe", + "mobile": "919999999999" + } +}`} + + +##### Errors + +| Status | Code | When | +| :--- | :--- | :--- | +| 400 | `invalid-request` | `deviceId` or `mobile` is missing, or the request body is malformed. | +| 400 | `missing-parameter` | `vpa`, `accountNo`, or `accountName` is empty, or `ifsc` / `accountProviderId` is required (no TPAP default configured) and was not supplied. | +| 400 | `invalid-program` | Unknown or inactive `programId`. | +| 400 | `invalid-vpa` | The VPA prefix does not start with the subscriber's mobile number. | +| 400 | `invalid-handle` | The VPA carries a handle that is not the one configured for this TPAP. | +| 401 | `device-not-linked` | This device is not bound for this user. | +| 404 | `user-not-found` | No user for this mobile. | +| 409 | `invalid-user-state` | The user is not `active`. | +| 409 | `vpa-taken` | That VPA is already in use. | +| 500 | `internal-error` | Something went wrong on Setu's side. | + +
+ + + A VPA prefix is scoped to the owner's mobile, so a caller cannot collide + with another user's VPA (that request is rejected as invalid-vpa), + and re-creating the caller's own VPA is idempotent (returns 200). + + +
+ +### Get a VPA + +`POST /api/v1/onboarding/get-vpa` (enveloped) — fetch one of the user's VPAs by its VPA string. + +| Field | Type | Required | Notes | +| :--- | :--- | :--- | :--- | +| `deviceId` | string | yes | The user's device id. | +| `mobile` | string | yes | The user's mobile number, with country code (e.g. `919999999999`). | +| `vpa` | string | yes | The VPA to fetch — the full VPA `prefix@handle` (e.g. `919999999999-alice@setu`). Its VPA prefix must start with the subscriber's mobile. | + +##### Sample request + + + {`{ + "deviceId": "device-abc-123", + "mobile": "919999999999", + "vpa": "919999999999-alice@setu" +}`} + + +##### Success response — `200` + +Returns the VPA under `vpa`. + +| Field | Type | Notes | +| :--- | :--- | :--- | +| `traceId` | string | Trace handle for this call (ULID). | +| `vpa` | object | The requested VPA (see [The VPA object](#the-vpa-object)). | + + + {`{ + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "vpa": { + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "vpa": "919999999999-alice@setu", + "status": "active", + "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "accountNo": "99887766554433", + "accountProviderId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "ifsc": "SETU0000001", + "accountType": "PPIWALLET", + "accountName": "Alice Doe", + "mobile": "919999999999" + } +}`} + + +##### Errors + +| Status | Code | When | +| :--- | :--- | :--- | +| 400 | `invalid-request` | `deviceId`, `mobile`, or `vpa` is missing, or the request body is malformed. | +| 400 | `invalid-vpa` | The VPA prefix does not start with the subscriber's mobile number. | +| 400 | `invalid-handle` | The VPA carries a handle that is not the one configured for this TPAP. | +| 401 | `device-not-linked` | This device is not bound for this user. | +| 404 | `user-not-found` | No user for this mobile. | +| 404 | `vpa-not-found` | The user does not own a VPA with that string. | +| 409 | `invalid-user-state` | The user is not `active`. | +| 500 | `internal-error` | Something went wrong on Setu's side. | + +
+ +### List VPAs + +`POST /api/v1/onboarding/list-vpas` (enveloped) — the user's live VPAs (`active` + `pending-verification`), newest-added first, keyset-paginated. + +| Field | Type | Required | Notes | +| :--- | :--- | :--- | :--- | +| `deviceId` | string | yes | The user's device id. | +| `mobile` | string | yes | The user's mobile number, with country code (e.g. `919999999999`). | +| `limit` | integer | no | Max entries this page. Omitted or non-positive defaults to `20`, clamped to `100`. | +| `cursor` | string | no | The `nextCursor` from the previous page. Omit for the first page. | + +##### Sample request + + + {`{ + "deviceId": "device-abc-123", + "mobile": "919999999999", + "limit": 20 +}`} + + +##### Success response — `200` + +| Field | Type | Notes | +| :--- | :--- | :--- | +| `traceId` | string | Trace handle for this call (ULID). | +| `vpas` | array | This page of the user's live VPAs (see [The VPA object](#the-vpa-object)); empty when none. | +| `nextCursor` | string | Cursor for the next page; absent on the last page. | + + + {`{ + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "vpas": [ + { + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "vpa": "919999999999-alice@setu", + "status": "active", + "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "accountNo": "99887766554433", + "accountProviderId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "ifsc": "SETU0000001", + "accountType": "PPIWALLET", + "accountName": "Alice Doe", + "mobile": "919999999999" + } + ], + "nextCursor": "01ARZ3NDEKTSV4RRFFQ69G5FAV" +}`} + + +##### Errors + +| Status | Code | When | +| :--- | :--- | :--- | +| 400 | `invalid-request` | `deviceId` or `mobile` is missing, the request body is malformed, or `cursor` is malformed. | +| 401 | `device-not-linked` | This device is not bound for this user. | +| 404 | `user-not-found` | No user for this mobile. | +| 409 | `invalid-user-state` | The user is not `active`. | +| 500 | `internal-error` | Something went wrong on Setu's side. | + +
+ +### Deregister a VPA + +`POST /api/v1/onboarding/vpa/deregister` (enveloped) — soft-delete a VPA. The row is preserved for audit and re-registration, and the prefix becomes reusable. Idempotent for an already-deregistered VPA. + +| Field | Type | Required | Notes | +| :--- | :--- | :--- | :--- | +| `deviceId` | string | yes | The user's device id. | +| `mobile` | string | yes | The user's mobile number, with country code (e.g. `919999999999`). | +| `vpa` | string | yes | The VPA to deregister — the full VPA `prefix@handle` (e.g. `919999999999-alice@setu`). Its VPA prefix must start with the subscriber's mobile. | + +##### Sample request + + + {`{ + "deviceId": "device-abc-123", + "mobile": "919999999999", + "vpa": "919999999999-alice@setu" +}`} + + +##### Success response — `200` + +Returns the deregistered VPA; its `status` is `deregistered`. + +| Field | Type | Notes | +| :--- | :--- | :--- | +| `traceId` | string | Trace handle for this call (ULID). | +| `vpa` | object | The deregistered VPA (see [The VPA object](#the-vpa-object)). | + + + {`{ + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "vpa": { + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "vpa": "919999999999-alice@setu", + "status": "deregistered", + "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "accountNo": "99887766554433", + "accountProviderId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "ifsc": "SETU0000001", + "accountType": "PPIWALLET", + "accountName": "Alice Doe", + "mobile": "919999999999" + } +}`} + + +##### Errors + +| Status | Code | When | +| :--- | :--- | :--- | +| 400 | `invalid-request` | `deviceId` or `mobile` is missing, or the request body is malformed. | +| 400 | `missing-parameter` | `vpa` is missing. | +| 400 | `invalid-vpa` | The VPA prefix does not start with the subscriber's mobile number. | +| 400 | `invalid-handle` | The VPA carries a handle that is not the one configured for this TPAP. | +| 401 | `device-not-linked` | This device is not bound for this user. | +| 404 | `user-not-found` | No user for this mobile. | +| 404 | `vpa-not-found` | The user does not own a VPA with that string. | +| 409 | `invalid-user-state` | The user is not `active`. | +| 500 | `internal-error` | Something went wrong on Setu's side. | + +### Next + +- **[Programs](/payments/upi-issuance/onboarding/api-integration/programs)** — provision and manage engagement channels that VPAs are created under. + + diff --git a/content/payments/upi-issuance/onboarding/onboarding-states.mdx b/content/payments/upi-issuance/onboarding/onboarding-states.mdx new file mode 100644 index 00000000..35fd998b --- /dev/null +++ b/content/payments/upi-issuance/onboarding/onboarding-states.mdx @@ -0,0 +1,59 @@ +--- +sidebar_title: Onboarding states +page_title: UPI Issuance onboarding states +order: 0 +visible_in_sidebar: true +--- + +## Onboarding states + +Onboarding tracks two things, both driven by the TPAP / Issuing App's API calls: + +- the **user** — is the device active / is OTP verification pending / is the user active? +- the onboarded **VPA** — is it usable? + +### User states + +| Status | Meaning | +| :--- | :--- | +| `binding-pending` | The user exists (created at token generation) but the SIM is not yet proven. | +| `device-bound` | The SIM binding succeeded, but OTP verification is still owed before the user is active. Only reached when `otpRequired: true` was sent during token generation. | +| `otp-pending` | The OTP has been sent and is awaiting verification. | +| `active` | The user is fully onboarded. Required before any VPA can be created or used. | + +### VPA states + +| Status | Meaning | +| :--- | :--- | +| `pending-verification` | The VPA is created but OTP verification is still owed before it can transact. | +| `active` | The VPA is verified and usable. | +| `deregistered` | The VPA was removed. Its prefix becomes reusable. | + +### When OTP verification happens + +OTP verification is **required for Android** and not needed on iOS. **When** it happens is controlled by the TPAP / Issuing App through the `otpRequired` flag, and Setu respects it: + +- To verify OTP **after SIM binding and before VPA creation**, set `otpRequired: true` on the [generate binding token](/payments/upi-issuance/onboarding/api-integration/device-binding) call. The user stays at `device-bound` until an OTP is verified. Use case: a **device change** (no new VPA is created). +- To verify OTP **after VPA creation**, set `otpRequired: true` on [`create-vpa`](/payments/upi-issuance/onboarding/api-integration/vpa-management). The new VPA stays `pending-verification` until an OTP is verified. Use case: a **new onboarding / new VPA**. + +
+ +### The two scenarios in detail + +#### New onboarding + +OTP verification is performed after VPA creation and blocks VPA activation, preventing any transactions on the new VPA until OTP verification is done. + +New onboarding: on Android create-vpa creates the VPA pending-verification and an OTP promotes it to active, on iOS the VPA is created active directly + +#### Device change + +OTP verification is performed at the user level, right after device binding on the new device. A successful SIM binding leaves the user at `device-bound`, and OTP verification moves the user to `active`. No new VPA is created. + +Device change: on Android binding on the new device leaves the user device-bound and an OTP promotes them to active, on iOS the SIM binding activates the user directly + +### Next + +- **[API integration](/payments/upi-issuance/onboarding/api-integration)** — a step-by-step walkthrough of each operation. + + diff --git a/content/payments/upi-issuance/overview.mdx b/content/payments/upi-issuance/overview.mdx new file mode 100644 index 00000000..7255fd18 --- /dev/null +++ b/content/payments/upi-issuance/overview.mdx @@ -0,0 +1,38 @@ +--- +sidebar_title: Overview +page_title: UPI Issuance Overview +order: 0 +visible_in_sidebar: true +--- + +## UPI Issuance + +Setu UPI Issuance is a **PSP stack** that enables TPAPs and Issuing Apps to allow their users to make and receive UPI payments. Setu provides the PSP roles and the UPI-network integration, exposed as a set of APIs. + +The stack spans the full UPI journey, starting with onboarding. These docs cover the onboarding APIs today, and grow with the rest of the stack. + +### How to read these docs + + + + +

START HERE

+ Quickstart — the first call -> +
+
+ + +

TRY EVERY SCENARIO

+ Testing on QA env -> +
+
+
+ +- **[Quickstart](/payments/upi-issuance/quickstart)** — the base URL, the envelope public key, and how requests are authenticated. +- **[The API envelope](/payments/upi-issuance/api-envelope)** — how every request and response is encrypted. +- **[User Onboarding](/payments/upi-issuance/onboarding)** — the onboarding flow, its state machines, and a step-by-step API walkthrough. +- **[Payee blocklist](/payments/upi-issuance/payee-blocklist)** — block, unblock, and list blocked payee VPAs. +- **[Testing on QA env](/payments/upi-issuance/qa-testing)** — reproduce every success and error scenario on the QA env. +- **[API reference](/payments/upi-issuance/api-reference)** — the full endpoint reference. + + diff --git a/content/payments/upi-issuance/payee-blocklist.mdx b/content/payments/upi-issuance/payee-blocklist.mdx new file mode 100644 index 00000000..61ede0d9 --- /dev/null +++ b/content/payments/upi-issuance/payee-blocklist.mdx @@ -0,0 +1,187 @@ +--- +sidebar_title: Payee blocklist +page_title: UPI Issuance payee blocklist +order: 4 +visible_in_sidebar: true +--- + +## Payee blocklist + +Let a user block payee VPAs they never want to transact with. All routes are enveloped, under `/api/v1`, and require an `active` user. `idempotencyKey` is an optional request header on every call. + +### The blocklist entry object + +`block-vpa` and `unblock-vpa` return the entry (under `blockedPayee`) in this shape: + +| Field | Type | Notes | +| :--- | :--- | :--- | +| `id` | string | The blocklist entry id (ULID). | +| `payeeVpa` | string | The payee VPA, `prefix@handle`. | +| `status` | string | `blocked` or `unblocked`. | +| `blockedAt` | string | When the payee was most recently blocked (RFC3339). | +| `unblockedAt` | string | When the payee was unblocked (RFC3339); absent while blocked. | + +
+ +### Block a payee + +`POST /api/v1/onboarding/block-vpa` (enveloped) — idempotent; blocking an already-blocked payee is a no-op. + +| Field | Type | Required | Notes | +| :--- | :--- | :--- | :--- | +| `deviceId` | string | yes | The user's device id. | +| `mobile` | string | yes | The user's mobile number, with country code (e.g. `919999999999`). | +| `payeeVpa` | string | yes | The external payee VPA to block (`prefix@handle`; any handle). | + +##### Sample request + + + {`{ + "deviceId": "device-abc-123", + "mobile": "919999999999", + "payeeVpa": "merchant@otherbank" +}`} + + +##### Success response — `200` + +| Field | Type | Notes | +| :--- | :--- | :--- | +| `traceId` | string | Trace handle for this call (ULID). | +| `blockedPayee` | object | The blocklist entry, now `blocked` (see [the entry object](#the-blocklist-entry-object)). | + + + {`{ + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "blockedPayee": { + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "payeeVpa": "merchant@otherbank", + "status": "blocked", + "blockedAt": "2026-07-09T12:00:00Z" + } +}`} + + +##### Errors + +| Status | Code | When | +| :--- | :--- | :--- | +| 400 | `invalid-request` | `deviceId` or `mobile` is missing, or the request body is malformed. | +| 400 | `missing-parameter` | `payeeVpa` is missing. | +| 400 | `invalid-vpa` | `payeeVpa` is not a valid VPA of the form `prefix@handle`. | +| 401 | `device-not-linked` | This device is not bound for this user. | +| 404 | `user-not-found` | No user for this mobile. | +| 409 | `invalid-user-state` | The user is not `active`. | +| 500 | `internal-error` | Something went wrong on Setu's side. | + +
+ +### Unblock a payee + +`POST /api/v1/onboarding/unblock-vpa` (enveloped) — a soft flip to `unblocked`; the entry is preserved for audit. Idempotent for an already-unblocked payee. The request shape matches [Block a payee](#block-a-payee). + +##### Sample request + + + {`{ + "deviceId": "device-abc-123", + "mobile": "919999999999", + "payeeVpa": "merchant@otherbank" +}`} + + +##### Success response — `200` + +| Field | Type | Notes | +| :--- | :--- | :--- | +| `traceId` | string | Trace handle for this call (ULID). | +| `blockedPayee` | object | The blocklist entry, now `unblocked` (see [the entry object](#the-blocklist-entry-object)). | + + + {`{ + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "blockedPayee": { + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "payeeVpa": "merchant@otherbank", + "status": "unblocked", + "blockedAt": "2026-07-09T12:00:00Z", + "unblockedAt": "2026-07-09T12:30:00Z" + } +}`} + + +##### Errors + +| Status | Code | When | +| :--- | :--- | :--- | +| 400 | `invalid-request` | `deviceId` or `mobile` is missing, or the request body is malformed. | +| 400 | `missing-parameter` | `payeeVpa` is missing. | +| 400 | `invalid-vpa` | `payeeVpa` is not a valid VPA of the form `prefix@handle`. | +| 401 | `device-not-linked` | This device is not bound for this user. | +| 404 | `user-not-found` | No user for this mobile. | +| 404 | `payee-not-blocked` | That payee VPA is not on the user's blocklist. | +| 409 | `invalid-user-state` | The user is not `active`. | +| 500 | `internal-error` | Something went wrong on Setu's side. | + +
+ +### List blocked payees + +`POST /api/v1/onboarding/list-blocked-vpas` (enveloped) — the user's currently-blocked payees, newest-first, keyset-paginated. + +| Field | Type | Required | Notes | +| :--- | :--- | :--- | :--- | +| `deviceId` | string | yes | The user's device id. | +| `mobile` | string | yes | The user's mobile number, with country code (e.g. `919999999999`). | +| `limit` | integer | no | Max entries this page. Omitted or non-positive defaults to `20`, clamped to `100`. | +| `cursor` | string | no | The `nextCursor` from the previous page. Omit for the first page. | + +##### Sample request + + + {`{ + "deviceId": "device-abc-123", + "mobile": "919999999999", + "limit": 20 +}`} + + +##### Success response — `200` + +| Field | Type | Notes | +| :--- | :--- | :--- | +| `traceId` | string | Trace handle for this call (ULID). | +| `blockedPayees` | array | This page of currently-blocked payees (see [the entry object](#the-blocklist-entry-object)); empty when none. | +| `nextCursor` | string | Cursor for the next page; absent on the last page. | + + + {`{ + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "blockedPayees": [ + { + "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "payeeVpa": "merchant@otherbank", + "status": "blocked", + "blockedAt": "2026-07-09T12:00:00Z" + } + ], + "nextCursor": "01ARZ3NDEKTSV4RRFFQ69G5FAV" +}`} + + +##### Errors + +| Status | Code | When | +| :--- | :--- | :--- | +| 400 | `invalid-request` | `deviceId` or `mobile` is missing, the request body is malformed, or `cursor` is malformed. | +| 401 | `device-not-linked` | This device is not bound for this user. | +| 404 | `user-not-found` | No user for this mobile. | +| 409 | `invalid-user-state` | The user is not `active`. | +| 500 | `internal-error` | Something went wrong on Setu's side. | + +### Next + +- **[Testing on QA env](/payments/upi-issuance/qa-testing)** — reproduce every scenario. +- **[API reference](/payments/upi-issuance/api-reference)** — the full endpoint reference. + + diff --git a/content/payments/upi-issuance/qa-testing.mdx b/content/payments/upi-issuance/qa-testing.mdx new file mode 100644 index 00000000..b72d453a --- /dev/null +++ b/content/payments/upi-issuance/qa-testing.mdx @@ -0,0 +1,91 @@ +--- +sidebar_title: Testing on QA env +page_title: UPI Issuance QA env testing +order: 5 +visible_in_sidebar: true +--- + +## Testing on QA env + +The TPAP / Issuing App can reproduce every onboarding scenario — success, and every error and precondition — against the **real QA env endpoints**. + +A few outcomes depend on an external event the TPAP / Issuing App cannot trigger on the QA env (the SIM-proof callback from the telecom, and the OTP SMS). For those, put a **directive in the `idempotencyKey`** of the initiating call. + +
+ + + Simulation directives work on the QA env only. In production a + sim.* value is treated as an ordinary idempotency key and changes + nothing. + + +
+ +### Simulated scenarios + +Only two things need a directive, because the TPAP / Issuing App cannot otherwise produce them on the QA env. + +#### Device binding + +On production, binding completes when the telecom delivers the silent SMS. On the QA env, set the `idempotencyKey` on `POST /onboarding/binding-token` to drive the outcome — no webhook, no real SMS: + +| `idempotencyKey` | Result | +| :--- | :--- | +| `sim.bind-ok` | The SIM proof succeeds and the user advances to `active`. | +| `sim.bind-fail-mobile` | The proven SIM does not match the claimed mobile. Binding does not complete. | +| `sim.bind-fail-vmn` | The callback carries a token that does not match a live binding. Binding does not complete. | + +After the call, poll `binding-status` as usual to observe the result. To test **reclaim** (the same mobile on a new device), just call `sim.bind-ok` again with a different `deviceId` — no special directive needed. + +#### OTP verification + +On the QA env there is no telecom, so no OTP SMS is sent. To pass verification, set the `idempotencyKey` on `POST /onboarding/otp/verify`: + +| `idempotencyKey` | Result | +| :--- | :--- | +| `sim.otp-ok` | The OTP verifies regardless of the code submitted, activating what it was requested for — the user, or a VPA. | + + + Use sim.otp-ok for the success path. To see a failure, + request the OTP and submit a wrong code (no directive) while it is live — that + returns 401 invalid-otp. + + +
+ +### Natural scenarios + +Everything else is reproduced with ordinary requests. A sampling: + +| To see this | Do this | +| :--- | :--- | +| `device-binding-cap-exceeded` (429) | Generate binding tokens past the per-device daily cap. | +| `otp-cap-exceeded` (429) | Call `otp/request` past the cap. | +| `invalid-otp` (401) | `otp/request`, then `otp/verify` with a wrong code (no directive) while the OTP is live. | +| `otp-expired-or-exhausted` (410) | `otp/verify` with no live OTP (never requested, or the window passed). | +| `device-not-linked` (401) | Call a VPA operation from a device not bound to the user. | +| `user-not-found` (404) | Use a mobile that was never onboarded. | +| `invalid-user-state` (409) | Call `create-vpa` before the user is `active`. | +| `invalid-vpa` (400) | Use a prefix not scoped to the caller's mobile (incl. another user's VPA). | +| `binding-not-found` (404) | Poll `binding-status` before generating a binding token. | +| `invalid-request` (400) | Send a malformed or incomplete body. | +| An abandoned binding | Request a token and never complete it — it expires on its own. | + +
+ +### A full QA env run + +| # | Call | Key inputs | Result | +| :--- | :--- | :--- | :--- | +| 1 | `binding-token` | `idempotencyKey: sim.bind-ok`, `otpRequired: false` | User → `active` | +| 2 | `binding-status` | — | `active` | +| 3 | `vpa/check` | — | Available | +| 4 | `create-vpa` | `otpRequired: true` (the Android path) | VPA → `pending-verification` | +| 5 | `otp/request` | With the VPA | `202` accepted | +| 6 | `otp/verify` | `idempotencyKey: sim.otp-ok`, with the VPA | VPA → `active` | + +### Next + +- **[API reference](/payments/upi-issuance/api-reference)** — the full endpoint reference. + + diff --git a/content/payments/upi-issuance/quickstart.mdx b/content/payments/upi-issuance/quickstart.mdx new file mode 100644 index 00000000..a8f87d2b --- /dev/null +++ b/content/payments/upi-issuance/quickstart.mdx @@ -0,0 +1,65 @@ +--- +sidebar_title: Quickstart +page_title: UPI Issuance Quickstart +order: 1 +visible_in_sidebar: true +--- + +## Quickstart + +This guide covers the one-time setup the TPAP / Issuing App needs before it can call the APIs. Once these are in place, head to [User Onboarding](/payments/upi-issuance/onboarding) for the full API journey. + +
+ +### Base URL + +- QA env — `https://upi-issuance-qa.setu.co/api/v1` + +
+ +### The envelope public key + +Every request and response body is encrypted with the **API envelope**. + +The TPAP / Issuing App can reach out to Setu to provide the **envelope public key**. With this key, the TPAP / Issuing App is supposed to encrypt each request and decrypt each response. + +The [API envelope](/payments/upi-issuance/api-envelope) page has the full wire format and a copy-paste reference implementation. + +For example, a generate binding token request and its response look like this on the wire — the encrypted body (`ct`), the request's one-time key encrypted under Setu's public key (`sk`), and the IV (`iv`): + +##### Sample request + + + {`{ + "ct": "HBAHaU47cY2pu9+9AkzlAYKr8s4CEDhz…Bn1uIfe9ufHgu1", + "sk": "hLGbHiL+TbnjYJhCgWtZM8m/yoWb0bHO…DOENEuzVbdFrX4a70A==", + "iv": "OTLfPjWZQhMRCIoAsEBAtg==" +}`} + + +The response comes back encrypted under the same key — the encrypted body (`ct`) and an integrity hash (`oha`): + +##### Sample response + + + {`{ + "ct": "VbtoxGJjpmPaj40Fo4rhGCxCvN246N7Y…7UZvM24sM0+Q=", + "oha": "7c2eae6a112e43f2b99f16ee9b5a2cd910b88c5463dca683b5a5ad053dbe7c30" +}`} + + +
+ +### Authentication + +The TPAP / Issuing App can share its outbound IP address with Setu, and Setu would whitelist that IP. This is how the TPAP / Issuing App is authenticated, so requests must be server-to-server. + +
+ +### Next + +- **[The API envelope](/payments/upi-issuance/api-envelope)** — encrypt requests and decrypt responses. +- **[User Onboarding](/payments/upi-issuance/onboarding)** — bind a device, create a VPA, and verify OTPs. +- **[Testing on QA env](/payments/upi-issuance/qa-testing)** — reproduce every scenario on the QA env. + + From db0c987e370f1c428d06fce8666ca769620edc23 Mon Sep 17 00:00:00 2001 From: Anindya Pandey Date: Fri, 17 Jul 2026 16:44:31 +0530 Subject: [PATCH 02/27] docs(UPIIS-47): CDN image refs + tighten API field validation docs - swap 7 local img srcs to docs-assets CDN - add field constraints/regex to user-otp & device-binding tables Co-Authored-By: Claude Opus 4.8 --- content/payments/upi-issuance/onboarding.mdx | 4 +-- .../api-integration/device-binding.mdx | 16 ++++++------ .../onboarding/api-integration/user-otp.mdx | 25 +++++++++---------- .../onboarding/onboarding-states.mdx | 4 +-- 4 files changed, 24 insertions(+), 25 deletions(-) diff --git a/content/payments/upi-issuance/onboarding.mdx b/content/payments/upi-issuance/onboarding.mdx index 13e9ae57..88a31036 100644 --- a/content/payments/upi-issuance/onboarding.mdx +++ b/content/payments/upi-issuance/onboarding.mdx @@ -15,7 +15,7 @@ Onboarding gets a user ready to transact on UPI. It runs in three steps. **3. OTP verification.** The TPAP / Issuing App requests an OTP from Setu, Setu delivers it to the user's mobile by SMS, and the TPAP / Issuing App submits it back to Setu to activate the VPA. Today this is **required on Android** and **not needed on iOS**. -New user onboarding and VPA creation: device binding, VPA creation, and the OTP that activates the new VPA (Android) +New user onboarding and VPA creation: device binding, VPA creation, and the OTP that activates the new VPA (Android)

New user onboarding / VPA creation for an existing user

@@ -27,7 +27,7 @@ On **iOS**, a successful SIM binding moves the user status straight to `active`. On **Android**, a successful SIM binding moves the user status to `device-bound`. Requesting an OTP then moves the user to `otp-pending`. A successful OTP verification moves the user status to `active`. -Device change: device binding on the new device, with an OTP on Android moving the user from device-bound to active and iOS activating directly, no VPA creation +Device change: device binding on the new device, with an OTP on Android moving the user from device-bound to active and iOS activating directly, no VPA creation

Device change — OTP verification, no VPA creation

diff --git a/content/payments/upi-issuance/onboarding/api-integration/device-binding.mdx b/content/payments/upi-issuance/onboarding/api-integration/device-binding.mdx index 409256c4..4893a482 100644 --- a/content/payments/upi-issuance/onboarding/api-integration/device-binding.mdx +++ b/content/payments/upi-issuance/onboarding/api-integration/device-binding.mdx @@ -9,7 +9,7 @@ visible_in_sidebar: true Device binding proves that the user's SIM, mobile number, and device belong together. It is the entry point on any device — a fresh install, a new phone, or a re-login. -Device binding sequence: binding token, silent SMS from the SIM to the VMN, and the binding-status poll to active +Device binding sequence: binding token, silent SMS from the SIM to the VMN, and the binding-status poll to active ### 1. Generate a binding token @@ -17,8 +17,8 @@ Device binding proves that the user's SIM, mobile number, and device belong toge | Field | Type | Required | Notes | | :--- | :--- | :--- | :--- | -| `deviceId` | string | yes | The user's device id. | -| `mobile` | string | yes | The user's mobile number, with country code (e.g. `919999999999`). | +| `deviceId` | string | yes | The user's device id. Non-empty, up to 128 characters. | +| `mobile` | string | yes | The user's mobile number, with country code — 12 digits, `^[0-9]{12}$` (e.g. `919999999999`). | | `os` | string | yes | `android` or `ios`. | | `otpRequired` | boolean | yes | Whether a successful SIM binding fully activates the user. `false` — the user goes straight to `active`. `true` — the user stops at `device-bound` until an [OTP](/payments/upi-issuance/onboarding/api-integration/user-otp) is verified. | @@ -62,7 +62,7 @@ Device binding proves that the user's SIM, mobile number, and device belong toge | Status | Code | When | | :--- | :--- | :--- | -| 400 | `invalid-request` | A required field is missing (`deviceId`, `mobile`, `os`, `otpRequired`) or the request body is malformed. | +| 400 | `invalid-request` | A required field is missing (`deviceId`, `mobile`, `os`, `otpRequired`), a field fails format validation (e.g. `mobile` is not 12 digits, `os` is not `android`/`ios`), or the request body is malformed. | | 429 | `device-binding-cap-exceeded` | Too many binding attempts for this device today. | | 429 | `mobile-binding-cap-exceeded` | Too many binding attempts for this mobile today. | | 500 | `internal-error` | Something went wrong on Setu's side. Retry with the same `idempotencyKey`. | @@ -86,12 +86,12 @@ The binding token is short-lived — it expires **45 seconds** after it is gener | Field | Type | Required | Notes | | :--- | :--- | :--- | :--- | -| `deviceId` | string | yes | The user's device id. | -| `mobile` | string | yes | The user's mobile number, with country code (e.g. `919999999999`). | +| `deviceId` | string | yes | The user's device id. Non-empty, up to 128 characters. | +| `mobile` | string | yes | The user's mobile number, with country code — 12 digits, `^[0-9]{12}$` (e.g. `919999999999`). |
-`idempotencyKey` is an optional request header. +`idempotencyKey` is an optional request header (opaque, up to 128 characters). ##### Sample request @@ -127,7 +127,7 @@ Poll until the status settles. With `otpRequired: false` a successful SIM bindin | Status | Code | When | | :--- | :--- | :--- | -| 400 | `invalid-request` | A required field is missing (`deviceId`, `mobile`) or the request body is malformed. | +| 400 | `invalid-request` | A required field is missing (`deviceId`, `mobile`), a field fails format validation (e.g. `mobile` is not 12 digits), or the request body is malformed. | | 404 | `binding-not-found` | No binding for this device — polled before generating a binding token, or from a device that is not the bound one. | | 500 | `internal-error` | Something went wrong on Setu's side. | diff --git a/content/payments/upi-issuance/onboarding/api-integration/user-otp.mdx b/content/payments/upi-issuance/onboarding/api-integration/user-otp.mdx index 99948861..90328ded 100644 --- a/content/payments/upi-issuance/onboarding/api-integration/user-otp.mdx +++ b/content/payments/upi-issuance/onboarding/api-integration/user-otp.mdx @@ -24,7 +24,7 @@ The OTP for a new VPA never changes the user's status. The OTP for a device chan A newly created VPA can require an OTP before it can transact. This is **OS-dependent**: on **Android** the OTP is required and the VPA is created `pending-verification`, on **iOS** it is not and the VPA is created `active`. The TPAP / Issuing App signals it by setting `otpRequired` on [`create-vpa`](/payments/upi-issuance/onboarding/api-integration/vpa-management) to match the device OS. -New VPA activation: on Android create-vpa creates the VPA pending-verification and an OTP promotes it to active, on iOS it is created active directly +New VPA activation: on Android create-vpa creates the VPA pending-verification and an OTP promotes it to active, on iOS it is created active directly ## Device change @@ -32,7 +32,7 @@ When `otpRequired: true` is sent on the [generate binding token](/payments/upi-i This is **OS-dependent** — `otpRequired` is `true` on **Android** (the user stops at `device-bound`) and `false` on **iOS** (a successful SIM binding activates the user directly). -Device change: on Android otpRequired true leaves the user device-bound and an OTP promotes them to active, on iOS the SIM binding activates the user directly +Device change: on Android otpRequired true leaves the user device-bound and an OTP promotes them to active, on iOS the SIM binding activates the user directly
@@ -42,13 +42,13 @@ This is **OS-dependent** — `otpRequired` is `true` on **Android** (the user st | Field | Type | Required | Notes | | :--- | :--- | :--- | :--- | -| `deviceId` | string | yes | The user's device id. | -| `mobile` | string | yes | The user's mobile number, with country code (e.g. `919999999999`). | -| `vpa` | string | conditional | Include it for the **new onboarding / new VPA** case (the VPA being activated). Omit it for the **device change** case. | +| `deviceId` | string | yes | The user's device id. Non-empty, up to 128 characters. | +| `mobile` | string | yes | The user's mobile number, with country code — 12 digits, `^[0-9]{12}$` (e.g. `919999999999`). | +| `vpa` | string | conditional | Include it for the **new onboarding / new VPA** case (the VPA being activated). Omit it for the **device change** case. When present: `prefix@handle`, max 255 characters — `^([A-Za-z0-9.-]+)@([A-Za-z0-9.-]+)$`. |
-`idempotencyKey` is an optional request header. +`idempotencyKey` is an optional request header (opaque, up to 128 characters). ##### Sample request @@ -78,7 +78,7 @@ Omit `vpa` for the **device change** case. | Status | Code | When | | :--- | :--- | :--- | -| 400 | `invalid-request` | `deviceId` or `mobile` is missing, or the request body is malformed. | +| 400 | `invalid-request` | `deviceId` or `mobile` is missing or fails format validation (e.g. `mobile` not 12 digits, a malformed `vpa`), or the request body is malformed. | | 401 | `device-not-linked` | This device is not bound for this user. | | 404 | `user-not-found` | No user for this mobile. | | 404 | `vpa-not-found` | A `vpa` was sent but the user does not own a VPA with that string. | @@ -94,10 +94,10 @@ Omit `vpa` for the **device change** case. | Field | Type | Required | Notes | | :--- | :--- | :--- | :--- | -| `deviceId` | string | yes | The user's device id. | -| `mobile` | string | yes | The user's mobile number, with country code (e.g. `919999999999`). | -| `otp` | string | yes | The digits the user entered. | -| `vpa` | string | conditional | Include it for the **new onboarding / new VPA** case (the VPA being activated). Omit it for the **device change** case. | +| `deviceId` | string | yes | The user's device id. Non-empty, up to 128 characters. | +| `mobile` | string | yes | The user's mobile number, with country code — 12 digits, `^[0-9]{12}$` (e.g. `919999999999`). | +| `otp` | string | yes | The 6-digit code the user entered — `^[0-9]{6}$`. | +| `vpa` | string | conditional | Include it for the **new onboarding / new VPA** case (the VPA being activated). Omit it for the **device change** case. When present: `prefix@handle`, max 255 characters — `^([A-Za-z0-9.-]+)@([A-Za-z0-9.-]+)$`. |
@@ -160,8 +160,7 @@ The **device change** case returns the user instead: | Status | Code | When | | :--- | :--- | :--- | -| 400 | `invalid-request` | `deviceId` or `mobile` is missing, or the request body is malformed. | -| 400 | `missing-parameter` | `otp` is missing. | +| 400 | `invalid-request` | `deviceId`, `mobile`, or `otp` is missing or fails format validation (`otp` must be 6 digits, `mobile` 12 digits), or the request body is malformed. | | 401 | `device-not-linked` | This device is not bound for this user. | | 401 | `invalid-otp` | The OTP is incorrect. The attempt is consumed. | | 404 | `user-not-found` | No user for this mobile. | diff --git a/content/payments/upi-issuance/onboarding/onboarding-states.mdx b/content/payments/upi-issuance/onboarding/onboarding-states.mdx index 35fd998b..a33e82c1 100644 --- a/content/payments/upi-issuance/onboarding/onboarding-states.mdx +++ b/content/payments/upi-issuance/onboarding/onboarding-states.mdx @@ -44,13 +44,13 @@ OTP verification is **required for Android** and not needed on iOS. **When** it OTP verification is performed after VPA creation and blocks VPA activation, preventing any transactions on the new VPA until OTP verification is done. -New onboarding: on Android create-vpa creates the VPA pending-verification and an OTP promotes it to active, on iOS the VPA is created active directly +New onboarding: on Android create-vpa creates the VPA pending-verification and an OTP promotes it to active, on iOS the VPA is created active directly #### Device change OTP verification is performed at the user level, right after device binding on the new device. A successful SIM binding leaves the user at `device-bound`, and OTP verification moves the user to `active`. No new VPA is created. -Device change: on Android binding on the new device leaves the user device-bound and an OTP promotes them to active, on iOS the SIM binding activates the user directly +Device change: on Android binding on the new device leaves the user device-bound and an OTP promotes them to active, on iOS the SIM binding activates the user directly ### Next From ff646b595a2c76ba29361c5bb05c6048f2f3b519 Mon Sep 17 00:00:00 2001 From: Anindya Pandey Date: Sat, 18 Jul 2026 23:08:36 +0530 Subject: [PATCH 03/27] docs(UPIIS-47): document inline validation rules + vpa-account-mismatch Add the create-VPA `vpa-account-mismatch` (409) error and a note that re-linking a VPA to a different account requires an explicit deregister first (Setu never silently re-points an active VPA at a different account). Finish documenting the inline field-validation rules (formats, lengths, patterns, enums) across the onboarding, program and payee-blocklist API references, mirrored in the OpenAPI spec. --- api-references/payments/upi-issuance.json | 99 ++++++++++++++++++- .../onboarding/api-integration/programs.mdx | 20 ++-- .../api-integration/vpa-management.mdx | 61 ++++++------ .../payments/upi-issuance/payee-blocklist.mdx | 22 ++--- 4 files changed, 143 insertions(+), 59 deletions(-) diff --git a/api-references/payments/upi-issuance.json b/api-references/payments/upi-issuance.json index c2349072..7017f460 100644 --- a/api-references/payments/upi-issuance.json +++ b/api-references/payments/upi-issuance.json @@ -27,6 +27,7 @@ "allowEmptyValue": true, "schema": { "type": "string", + "maxLength": 128, "description": "Optional client-generated idempotency key for safe retries.", "example": "req-8f3a2b1c" }, @@ -358,6 +359,7 @@ "allowEmptyValue": true, "schema": { "type": "string", + "maxLength": 128, "description": "Optional client-generated idempotency key for safe retries.", "example": "req-8f3a2b1c" }, @@ -641,6 +643,7 @@ "allowEmptyValue": true, "schema": { "type": "string", + "maxLength": 128, "description": "Optional client-generated idempotency key for safe retries.", "example": "req-8f3a2b1c" }, @@ -944,6 +947,7 @@ "allowEmptyValue": true, "schema": { "type": "string", + "maxLength": 128, "description": "Optional client-generated idempotency key for safe retries.", "example": "req-8f3a2b1c" }, @@ -1195,6 +1199,7 @@ "allowEmptyValue": true, "schema": { "type": "string", + "maxLength": 128, "description": "Optional client-generated idempotency key for safe retries.", "example": "req-8f3a2b1c" }, @@ -1385,7 +1390,8 @@ "description": "Machine-readable error code.", "enum": [ "invalid-user-state", - "vpa-taken" + "vpa-taken", + "vpa-account-mismatch" ], "example": "invalid-user-state" }, @@ -1464,6 +1470,7 @@ "allowEmptyValue": true, "schema": { "type": "string", + "maxLength": 128, "description": "Optional client-generated idempotency key for safe retries.", "example": "req-8f3a2b1c" }, @@ -1724,6 +1731,7 @@ "allowEmptyValue": true, "schema": { "type": "string", + "maxLength": 128, "description": "Optional client-generated idempotency key for safe retries.", "example": "req-8f3a2b1c" }, @@ -1996,6 +2004,7 @@ "allowEmptyValue": true, "schema": { "type": "string", + "maxLength": 128, "description": "Optional client-generated idempotency key for safe retries.", "example": "req-8f3a2b1c" }, @@ -2384,6 +2393,7 @@ "required": true, "schema": { "type": "string", + "pattern": "^[0-9A-HJKMNP-TV-Z]{26}$", "description": "The program to update (ULID).", "example": "01ARZ3NDEKTSV4RRFFQ69G5FAV" }, @@ -2565,6 +2575,7 @@ "allowEmptyValue": true, "schema": { "type": "string", + "maxLength": 128, "description": "Optional client-generated idempotency key for safe retries.", "example": "req-8f3a2b1c" }, @@ -2818,6 +2829,7 @@ "allowEmptyValue": true, "schema": { "type": "string", + "maxLength": 128, "description": "Optional client-generated idempotency key for safe retries.", "example": "req-8f3a2b1c" }, @@ -3072,6 +3084,7 @@ "allowEmptyValue": true, "schema": { "type": "string", + "maxLength": 128, "description": "Optional client-generated idempotency key for safe retries.", "example": "req-8f3a2b1c" }, @@ -3426,16 +3439,22 @@ "properties": { "deviceId": { "type": "string", + "minLength": 1, + "maxLength": 128, "description": "The user's device id.", "example": "device-abc-123" }, "mobile": { "type": "string", + "pattern": "^[0-9]{12}$", "description": "The user's mobile number, with country code (e.g. 919999999999).", "example": "919999999999" }, "payeeVpa": { "type": "string", + "pattern": "^([A-Za-z0-9.-]+)@([A-Za-z0-9.-]+)$", + "minLength": 1, + "maxLength": 255, "description": "The external payee VPA to block or unblock (prefix@handle; any handle).", "example": "merchant@otherbank" } @@ -3574,21 +3593,28 @@ "properties": { "deviceId": { "type": "string", + "minLength": 1, + "maxLength": 128, "description": "The user's device id.", "example": "device-abc-123" }, "mobile": { "type": "string", + "pattern": "^[0-9]{12}$", "description": "The user's mobile number, with country code (e.g. 919999999999).", "example": "919999999999" }, "programId": { "type": "string", + "pattern": "^[0-9A-HJKMNP-TV-Z]{26}$", "description": "Optional program id (ULID). Validated when supplied.", "example": "01ARZ3NDEKTSV4RRFFQ69G5FAV" }, "vpa": { "type": "string", + "pattern": "^([A-Za-z0-9.-]+)@([A-Za-z0-9.-]+)$", + "minLength": 1, + "maxLength": 255, "description": "The full VPA to check (prefix@handle, e.g. 919999999999-alice@setu). Its prefix must start with the user's mobile number.", "example": "919999999999-alice@setu" } @@ -3610,12 +3636,16 @@ "properties": { "amountLimit": { "type": "integer", + "format": "int64", + "minimum": 0, "description": "Per-transaction cap, in paise. Omit for no cap.", - "example": 500000, - "format": "int64" + "example": 500000 }, "code": { "type": "string", + "pattern": "^[A-Za-z0-9_-]+$", + "minLength": 1, + "maxLength": 32, "description": "The program's short code — its channel identifier.", "example": "ACME" }, @@ -3631,6 +3661,9 @@ }, "name": { "type": "string", + "pattern": "^[A-Za-z0-9 .'&-]+$", + "minLength": 1, + "maxLength": 99, "description": "The program's display name.", "example": "Acme Wallet" } @@ -3652,16 +3685,21 @@ "properties": { "accountName": { "type": "string", + "pattern": "^[A-Za-z0-9 .'&-]+$", + "minLength": 1, + "maxLength": 99, "description": "The account-holder name.", "example": "Alice Doe" }, "accountNo": { "type": "string", + "pattern": "^[A-Z0-9]{6,18}$", "description": "The wallet / pool account number.", "example": "99887766554433" }, "accountProviderId": { "type": "string", + "pattern": "^[0-9A-HJKMNP-TV-Z]{26}$", "description": "The account-provider id. Optional when a TPAP default is configured.", "example": "01ARZ3NDEKTSV4RRFFQ69G5FAV" }, @@ -3677,16 +3715,20 @@ }, "deviceId": { "type": "string", + "minLength": 1, + "maxLength": 128, "description": "The user's device id.", "example": "device-abc-123" }, "ifsc": { "type": "string", + "pattern": "^[A-Z]{4}0[A-Z0-9]{6}$", "description": "The IFSC of the account. Optional when a TPAP default is configured.", "example": "SETU0000001" }, "mobile": { "type": "string", + "pattern": "^[0-9]{12}$", "description": "The user's mobile number, with country code (e.g. 919999999999).", "example": "919999999999" }, @@ -3697,11 +3739,15 @@ }, "programId": { "type": "string", + "pattern": "^[0-9A-HJKMNP-TV-Z]{26}$", "description": "Optional program id (ULID) — the program this VPA belongs to. Validated when supplied.", "example": "01ARZ3NDEKTSV4RRFFQ69G5FAV" }, "vpa": { "type": "string", + "pattern": "^([A-Za-z0-9.-]+)@([A-Za-z0-9.-]+)$", + "minLength": 1, + "maxLength": 255, "description": "The full VPA to create (prefix@handle, e.g. 919999999999-alice@setu). Its prefix must start with the user's mobile number.", "example": "919999999999-alice@setu" } @@ -3764,16 +3810,22 @@ "properties": { "deviceId": { "type": "string", + "minLength": 1, + "maxLength": 128, "description": "The user's device id.", "example": "device-abc-123" }, "mobile": { "type": "string", + "pattern": "^[0-9]{12}$", "description": "The user's mobile number, with country code (e.g. 919999999999).", "example": "919999999999" }, "vpa": { "type": "string", + "pattern": "^([A-Za-z0-9.-]+)@([A-Za-z0-9.-]+)$", + "minLength": 1, + "maxLength": 255, "description": "The full VPA to fetch (prefix@handle, e.g. 919999999999-alice@setu). Its prefix must start with the user's mobile number.", "example": "919999999999-alice@setu" } @@ -3799,6 +3851,8 @@ }, "deviceId": { "type": "string", + "minLength": 1, + "maxLength": 128, "description": "The user's device id.", "example": "device-abc-123" }, @@ -3810,6 +3864,7 @@ }, "mobile": { "type": "string", + "pattern": "^[0-9]{12}$", "description": "The user's mobile number, with country code (e.g. 919999999999).", "example": "919999999999" } @@ -3913,11 +3968,14 @@ "properties": { "deviceId": { "type": "string", + "minLength": 1, + "maxLength": 128, "description": "The user's device id.", "example": "device-abc-123" }, "mobile": { "type": "string", + "pattern": "^[0-9]{12}$", "description": "The user's mobile number, with country code (e.g. 919999999999).", "example": "919999999999" } @@ -4022,16 +4080,23 @@ "properties": { "deviceId": { "type": "string", + "minLength": 1, + "maxLength": 128, "description": "The user's device id.", "example": "device-abc-123" }, "mobile": { "type": "string", + "pattern": "^[0-9]{12}$", "description": "The user's mobile number, with country code (e.g. 919999999999).", "example": "919999999999" }, "os": { "type": "string", + "enum": [ + "android", + "ios" + ], "description": "The device OS: android or ios.", "example": "android" }, @@ -4059,16 +4124,22 @@ "properties": { "deviceId": { "type": "string", + "minLength": 1, + "maxLength": 128, "description": "The user's device id.", "example": "device-abc-123" }, "mobile": { "type": "string", + "pattern": "^[0-9]{12}$", "description": "The user's mobile number, with country code (e.g. 919999999999).", "example": "919999999999" }, "vpa": { "type": "string", + "pattern": "^([A-Za-z0-9.-]+)@([A-Za-z0-9.-]+)$", + "minLength": 1, + "maxLength": 255, "description": "Include the VPA to verify a new VPA (new onboarding / new VPA); omit for a device-change OTP (SIM re-binding).", "example": "919999999999-alice@setu" } @@ -4088,12 +4159,16 @@ "properties": { "amountLimit": { "type": "integer", + "format": "int64", + "minimum": 0, "description": "Per-transaction cap, in paise. Omit for no cap.", - "example": 500000, - "format": "int64" + "example": 500000 }, "code": { "type": "string", + "pattern": "^[A-Za-z0-9_-]+$", + "minLength": 1, + "maxLength": 32, "description": "The program's short code.", "example": "ACME" }, @@ -4109,11 +4184,18 @@ }, "name": { "type": "string", + "pattern": "^[A-Za-z0-9 .'&-]+$", + "minLength": 1, + "maxLength": 99, "description": "The program's display name.", "example": "Acme Wallet" }, "status": { "type": "string", + "enum": [ + "active", + "inactive" + ], "description": "active or inactive.", "example": "inactive" } @@ -4167,21 +4249,28 @@ "properties": { "deviceId": { "type": "string", + "minLength": 1, + "maxLength": 128, "description": "The user's device id.", "example": "device-abc-123" }, "mobile": { "type": "string", + "pattern": "^[0-9]{12}$", "description": "The user's mobile number, with country code (e.g. 919999999999).", "example": "919999999999" }, "otp": { "type": "string", + "pattern": "^[0-9]{6}$", "description": "The OTP the user entered.", "example": "123456" }, "vpa": { "type": "string", + "pattern": "^([A-Za-z0-9.-]+)@([A-Za-z0-9.-]+)$", + "minLength": 1, + "maxLength": 255, "description": "Include the VPA to verify a new VPA (new onboarding / new VPA); omit for a device-change OTP (SIM re-binding).", "example": "919999999999-alice@setu" } diff --git a/content/payments/upi-issuance/onboarding/api-integration/programs.mdx b/content/payments/upi-issuance/onboarding/api-integration/programs.mdx index da1c7d14..152d782e 100644 --- a/content/payments/upi-issuance/onboarding/api-integration/programs.mdx +++ b/content/payments/upi-issuance/onboarding/api-integration/programs.mdx @@ -33,11 +33,11 @@ Both endpoints return the program under `program` in this shape: | Field | Type | Required | Notes | | :--- | :--- | :--- | :--- | -| `name` | string | yes | The program's display name. | -| `code` | string | yes | The program's short code — its channel identifier. | +| `name` | string | yes | The program's display name. 1–99 characters; letters, digits, spaces and `. ' & -`. | +| `code` | string | yes | The program's short code — its channel identifier. 1–32 characters; letters, digits, `_` and `-`. | | `isCreditAllowed` | boolean | no | May wallets on this program receive incoming UPI? Defaults `true`. | | `isDebitAllowed` | boolean | no | May wallets on this program pay outgoing UPI? Defaults `true`. | -| `amountLimit` | integer | no | Per-transaction cap, in paise. Omit for no cap. | +| `amountLimit` | integer | no | Per-transaction cap, in paise. Omit for no cap. Integer `≥ 0`. | ##### Sample request @@ -77,8 +77,7 @@ Both endpoints return the program under `program` in this shape: | Status | Code | When | | :--- | :--- | :--- | -| 400 | `invalid-request` | The request body is malformed. | -| 400 | `missing-parameter` | `name` or `code` is missing. | +| 400 | `invalid-request` | `name` or `code` is missing or fails format validation, or the request body is malformed. | | 500 | `internal-error` | Something went wrong on Setu's side. |
@@ -89,11 +88,11 @@ Both endpoints return the program under `program` in this shape: | Field | Type | Required | Notes | | :--- | :--- | :--- | :--- | -| `name` | string | no | The program's display name. | -| `code` | string | no | The program's short code. | +| `name` | string | no | The program's display name. 1–99 characters; letters, digits, spaces and `. ' & -`. | +| `code` | string | no | The program's short code. 1–32 characters; letters, digits, `_` and `-`. | | `isCreditAllowed` | boolean | no | May wallets on this program receive incoming UPI? | | `isDebitAllowed` | boolean | no | May wallets on this program pay outgoing UPI? | -| `amountLimit` | integer | no | Per-transaction cap, in paise. | +| `amountLimit` | integer | no | Per-transaction cap, in paise. Integer `≥ 0`. | | `status` | string | no | `active` or `inactive`. An `inactive` program is rejected at the onboarding gate (`400 invalid-program`). | ##### Sample request @@ -132,9 +131,8 @@ For example, deactivate a program: | Status | Code | When | | :--- | :--- | :--- | -| 400 | `invalid-request` | The request body is malformed. | -| 400 | `missing-parameter` | `programId` is missing. | -| 404 | `program-not-found` | No program with that `programId`. | +| 400 | `invalid-request` | `programId` is not a valid ULID, a body field fails format validation, or the request body is malformed. | +| 404 | `program-not-found` | No program with that (well-formed) `programId`. | | 500 | `internal-error` | Something went wrong on Setu's side. | ### Next diff --git a/content/payments/upi-issuance/onboarding/api-integration/vpa-management.mdx b/content/payments/upi-issuance/onboarding/api-integration/vpa-management.mdx index 8aee3003..c547a704 100644 --- a/content/payments/upi-issuance/onboarding/api-integration/vpa-management.mdx +++ b/content/payments/upi-issuance/onboarding/api-integration/vpa-management.mdx @@ -34,10 +34,10 @@ Once a user is `active`, the TPAP / Issuing App creates and manages their VPAs. | Field | Type | Required | Notes | | :--- | :--- | :--- | :--- | -| `deviceId` | string | yes | The user's device id. | -| `mobile` | string | yes | The user's mobile number, with country code (e.g. `919999999999`). | -| `vpa` | string | yes | The VPA to check — the full VPA `prefix@handle` (e.g. `919999999999-alice@setu`). Its VPA prefix must start with the subscriber's mobile. | -| `programId` | string | no | ULID. Validated for existence only. | +| `deviceId` | string | yes | The user's device id. Non-empty, up to 128 characters. | +| `mobile` | string | yes | The user's mobile number, with country code — 12 digits, `^[0-9]{12}$` (e.g. `919999999999`). | +| `vpa` | string | yes | The VPA to check — the full VPA `prefix@handle` (e.g. `919999999999-alice@setu`), max 255 characters, `^([A-Za-z0-9.-]+)@([A-Za-z0-9.-]+)$`. Its VPA prefix must start with the subscriber's mobile. | +| `programId` | string | no | A 26-character ULID — `^[0-9A-HJKMNP-TV-Z]{26}$`. Validated for existence when supplied. | ##### Sample request @@ -69,9 +69,8 @@ Once a user is `active`, the TPAP / Issuing App creates and manages their VPAs. | Status | Code | When | | :--- | :--- | :--- | -| 400 | `invalid-request` | `deviceId` or `mobile` is missing, or the request body is malformed. | -| 400 | `missing-parameter` | `vpa` is missing. | -| 400 | `invalid-program` | Unknown or inactive `programId`. | +| 400 | `invalid-request` | `deviceId`, `mobile`, or `vpa` is missing or fails format validation, or the request body is malformed. | +| 400 | `invalid-program` | A well-formed `programId` that is unknown or inactive. | | 400 | `invalid-vpa` | The VPA prefix does not start with the subscriber's mobile number. | | 400 | `invalid-handle` | The VPA carries a handle that is not the one configured for this TPAP. | | 401 | `device-not-linked` | This device is not bound for this user. | @@ -87,20 +86,22 @@ Once a user is `active`, the TPAP / Issuing App creates and manages their VPAs. | Field | Type | Required | Notes | | :--- | :--- | :--- | :--- | -| `deviceId` | string | yes | The user's device id. | -| `mobile` | string | yes | The user's mobile number, with country code (e.g. `919999999999`). | -| `vpa` | string | yes | The VPA to create — the full VPA `prefix@handle` (e.g. `919999999999-alice@setu`). Its VPA prefix must start with the subscriber's mobile. | -| `accountNo` | string | yes | The wallet / pool account number. | -| `accountName` | string | yes | The account-holder name. | +| `deviceId` | string | yes | The user's device id. Non-empty, up to 128 characters. | +| `mobile` | string | yes | The user's mobile number, with country code — 12 digits, `^[0-9]{12}$` (e.g. `919999999999`). | +| `vpa` | string | yes | The VPA to create — the full VPA `prefix@handle` (e.g. `919999999999-alice@setu`), max 255 characters, `^([A-Za-z0-9.-]+)@([A-Za-z0-9.-]+)$`. Its VPA prefix must start with the subscriber's mobile. | +| `accountNo` | string | yes | The wallet / pool account number. 6–18 characters, uppercase letters and digits — `^[A-Z0-9]{6,18}$`. | +| `accountName` | string | yes | The account-holder name. 1–99 characters; letters, digits, spaces and `. ' & -`. | | `otpRequired` | boolean | no | `true` (Android) — the VPA is created `pending-verification` and an [OTP](/payments/upi-issuance/onboarding/api-integration/user-otp) activates it. `false` (iOS, default) — the VPA is created `active`. | -| `ifsc` | string | no | The IFSC of the account. Optional when a TPAP default is configured. | -| `accountProviderId` | string | no | The account-provider id. Optional when a TPAP default is configured. | -| `programId` | string | no | ULID. Validated for existence when supplied. | +| `ifsc` | string | no | The IFSC of the account — `^[A-Z]{4}0[A-Z0-9]{6}$` (e.g. `SETU0000001`). Optional when a TPAP default is configured. | +| `accountProviderId` | string | no | The account-provider id — a 26-character ULID (`^[0-9A-HJKMNP-TV-Z]{26}$`). Optional when a TPAP default is configured. | +| `programId` | string | no | A 26-character ULID — `^[0-9A-HJKMNP-TV-Z]{26}$`. Validated for existence when supplied. | | `defaultDebit` | boolean | no | Make this the user's default debit account. | | `defaultCredit` | boolean | no | Make this the user's default credit account. | The account details are taken from the request. There is no bank round-trip at onboarding. +**Re-linking an existing VPA to a different account.** Re-sending `create-vpa` for a VPA you already hold is idempotent **only when the account details match**. If you re-create an existing active VPA with a **different account**, the call is rejected with `vpa-account-mismatch` (`409`) — deregister the VPA first, then create it against the new account. Setu never silently re-points an active VPA at a different account. This matters when a mobile number is reassigned to a new user: it is the app's / CBS's responsibility to clean up the prior user's VPAs, and this guard ensures an existing VPA cannot be quietly moved to a different account. + ##### Sample request @@ -146,8 +147,8 @@ Returns the created VPA; its `status` is `active` or `pending-verification`. | Status | Code | When | | :--- | :--- | :--- | -| 400 | `invalid-request` | `deviceId` or `mobile` is missing, or the request body is malformed. | -| 400 | `missing-parameter` | `vpa`, `accountNo`, or `accountName` is empty, or `ifsc` / `accountProviderId` is required (no TPAP default configured) and was not supplied. | +| 400 | `invalid-request` | `deviceId`, `mobile`, `vpa`, `accountNo`, or `accountName` is missing or fails format validation, or the request body is malformed. | +| 400 | `missing-parameter` | `ifsc` or `accountProviderId` is required (no TPAP default configured) and was not supplied. | | 400 | `invalid-program` | Unknown or inactive `programId`. | | 400 | `invalid-vpa` | The VPA prefix does not start with the subscriber's mobile number. | | 400 | `invalid-handle` | The VPA carries a handle that is not the one configured for this TPAP. | @@ -155,6 +156,7 @@ Returns the created VPA; its `status` is `active` or `pending-verification`. | 404 | `user-not-found` | No user for this mobile. | | 409 | `invalid-user-state` | The user is not `active`. | | 409 | `vpa-taken` | That VPA is already in use. | +| 409 | `vpa-account-mismatch` | This VPA is already active on a **different account**. Deregister it first to re-link it to another account. | | 500 | `internal-error` | Something went wrong on Setu's side. |
@@ -173,9 +175,9 @@ Returns the created VPA; its `status` is `active` or `pending-verification`. | Field | Type | Required | Notes | | :--- | :--- | :--- | :--- | -| `deviceId` | string | yes | The user's device id. | -| `mobile` | string | yes | The user's mobile number, with country code (e.g. `919999999999`). | -| `vpa` | string | yes | The VPA to fetch — the full VPA `prefix@handle` (e.g. `919999999999-alice@setu`). Its VPA prefix must start with the subscriber's mobile. | +| `deviceId` | string | yes | The user's device id. Non-empty, up to 128 characters. | +| `mobile` | string | yes | The user's mobile number, with country code — 12 digits, `^[0-9]{12}$` (e.g. `919999999999`). | +| `vpa` | string | yes | The VPA to fetch — the full VPA `prefix@handle` (e.g. `919999999999-alice@setu`), max 255 characters, `^([A-Za-z0-9.-]+)@([A-Za-z0-9.-]+)$`. Its VPA prefix must start with the subscriber's mobile. | ##### Sample request @@ -218,7 +220,7 @@ Returns the VPA under `vpa`. | Status | Code | When | | :--- | :--- | :--- | -| 400 | `invalid-request` | `deviceId`, `mobile`, or `vpa` is missing, or the request body is malformed. | +| 400 | `invalid-request` | `deviceId`, `mobile`, or `vpa` is missing or fails format validation, or the request body is malformed. | | 400 | `invalid-vpa` | The VPA prefix does not start with the subscriber's mobile number. | | 400 | `invalid-handle` | The VPA carries a handle that is not the one configured for this TPAP. | | 401 | `device-not-linked` | This device is not bound for this user. | @@ -235,10 +237,10 @@ Returns the VPA under `vpa`. | Field | Type | Required | Notes | | :--- | :--- | :--- | :--- | -| `deviceId` | string | yes | The user's device id. | -| `mobile` | string | yes | The user's mobile number, with country code (e.g. `919999999999`). | +| `deviceId` | string | yes | The user's device id. Non-empty, up to 128 characters. | +| `mobile` | string | yes | The user's mobile number, with country code — 12 digits, `^[0-9]{12}$` (e.g. `919999999999`). | | `limit` | integer | no | Max entries this page. Omitted or non-positive defaults to `20`, clamped to `100`. | -| `cursor` | string | no | The `nextCursor` from the previous page. Omit for the first page. | +| `cursor` | string | no | The `nextCursor` from the previous page (opaque). Omit for the first page. | ##### Sample request @@ -283,7 +285,7 @@ Returns the VPA under `vpa`. | Status | Code | When | | :--- | :--- | :--- | -| 400 | `invalid-request` | `deviceId` or `mobile` is missing, the request body is malformed, or `cursor` is malformed. | +| 400 | `invalid-request` | `deviceId` or `mobile` is missing or fails format validation, the request body is malformed, or `cursor` is malformed. | | 401 | `device-not-linked` | This device is not bound for this user. | | 404 | `user-not-found` | No user for this mobile. | | 409 | `invalid-user-state` | The user is not `active`. | @@ -297,9 +299,9 @@ Returns the VPA under `vpa`. | Field | Type | Required | Notes | | :--- | :--- | :--- | :--- | -| `deviceId` | string | yes | The user's device id. | -| `mobile` | string | yes | The user's mobile number, with country code (e.g. `919999999999`). | -| `vpa` | string | yes | The VPA to deregister — the full VPA `prefix@handle` (e.g. `919999999999-alice@setu`). Its VPA prefix must start with the subscriber's mobile. | +| `deviceId` | string | yes | The user's device id. Non-empty, up to 128 characters. | +| `mobile` | string | yes | The user's mobile number, with country code — 12 digits, `^[0-9]{12}$` (e.g. `919999999999`). | +| `vpa` | string | yes | The VPA to deregister — the full VPA `prefix@handle` (e.g. `919999999999-alice@setu`), max 255 characters, `^([A-Za-z0-9.-]+)@([A-Za-z0-9.-]+)$`. Its VPA prefix must start with the subscriber's mobile. | ##### Sample request @@ -342,8 +344,7 @@ Returns the deregistered VPA; its `status` is `deregistered`. | Status | Code | When | | :--- | :--- | :--- | -| 400 | `invalid-request` | `deviceId` or `mobile` is missing, or the request body is malformed. | -| 400 | `missing-parameter` | `vpa` is missing. | +| 400 | `invalid-request` | `deviceId`, `mobile`, or `vpa` is missing or fails format validation, or the request body is malformed. | | 400 | `invalid-vpa` | The VPA prefix does not start with the subscriber's mobile number. | | 400 | `invalid-handle` | The VPA carries a handle that is not the one configured for this TPAP. | | 401 | `device-not-linked` | This device is not bound for this user. | diff --git a/content/payments/upi-issuance/payee-blocklist.mdx b/content/payments/upi-issuance/payee-blocklist.mdx index 61ede0d9..6f5890ac 100644 --- a/content/payments/upi-issuance/payee-blocklist.mdx +++ b/content/payments/upi-issuance/payee-blocklist.mdx @@ -29,9 +29,9 @@ Let a user block payee VPAs they never want to transact with. All routes are env | Field | Type | Required | Notes | | :--- | :--- | :--- | :--- | -| `deviceId` | string | yes | The user's device id. | -| `mobile` | string | yes | The user's mobile number, with country code (e.g. `919999999999`). | -| `payeeVpa` | string | yes | The external payee VPA to block (`prefix@handle`; any handle). | +| `deviceId` | string | yes | The user's device id. Non-empty, up to 128 characters. | +| `mobile` | string | yes | The user's mobile number, with country code — 12 digits, `^[0-9]{12}$` (e.g. `919999999999`). | +| `payeeVpa` | string | yes | The external payee VPA to block (`prefix@handle`; any handle). Max 255 characters — `^([A-Za-z0-9.-]+)@([A-Za-z0-9.-]+)$`. | ##### Sample request @@ -66,9 +66,7 @@ Let a user block payee VPAs they never want to transact with. All routes are env | Status | Code | When | | :--- | :--- | :--- | -| 400 | `invalid-request` | `deviceId` or `mobile` is missing, or the request body is malformed. | -| 400 | `missing-parameter` | `payeeVpa` is missing. | -| 400 | `invalid-vpa` | `payeeVpa` is not a valid VPA of the form `prefix@handle`. | +| 400 | `invalid-request` | `deviceId`, `mobile`, or `payeeVpa` is missing or fails format validation (`payeeVpa` must be `prefix@handle`, max 255 characters), or the request body is malformed. | | 401 | `device-not-linked` | This device is not bound for this user. | | 404 | `user-not-found` | No user for this mobile. | | 409 | `invalid-user-state` | The user is not `active`. | @@ -114,9 +112,7 @@ Let a user block payee VPAs they never want to transact with. All routes are env | Status | Code | When | | :--- | :--- | :--- | -| 400 | `invalid-request` | `deviceId` or `mobile` is missing, or the request body is malformed. | -| 400 | `missing-parameter` | `payeeVpa` is missing. | -| 400 | `invalid-vpa` | `payeeVpa` is not a valid VPA of the form `prefix@handle`. | +| 400 | `invalid-request` | `deviceId`, `mobile`, or `payeeVpa` is missing or fails format validation (`payeeVpa` must be `prefix@handle`, max 255 characters), or the request body is malformed. | | 401 | `device-not-linked` | This device is not bound for this user. | | 404 | `user-not-found` | No user for this mobile. | | 404 | `payee-not-blocked` | That payee VPA is not on the user's blocklist. | @@ -131,10 +127,10 @@ Let a user block payee VPAs they never want to transact with. All routes are env | Field | Type | Required | Notes | | :--- | :--- | :--- | :--- | -| `deviceId` | string | yes | The user's device id. | -| `mobile` | string | yes | The user's mobile number, with country code (e.g. `919999999999`). | +| `deviceId` | string | yes | The user's device id. Non-empty, up to 128 characters. | +| `mobile` | string | yes | The user's mobile number, with country code — 12 digits, `^[0-9]{12}$` (e.g. `919999999999`). | | `limit` | integer | no | Max entries this page. Omitted or non-positive defaults to `20`, clamped to `100`. | -| `cursor` | string | no | The `nextCursor` from the previous page. Omit for the first page. | +| `cursor` | string | no | The `nextCursor` from the previous page (opaque). Omit for the first page. | ##### Sample request @@ -173,7 +169,7 @@ Let a user block payee VPAs they never want to transact with. All routes are env | Status | Code | When | | :--- | :--- | :--- | -| 400 | `invalid-request` | `deviceId` or `mobile` is missing, the request body is malformed, or `cursor` is malformed. | +| 400 | `invalid-request` | `deviceId` or `mobile` is missing or fails format validation, the request body is malformed, or `cursor` is malformed. | | 401 | `device-not-linked` | This device is not bound for this user. | | 404 | `user-not-found` | No user for this mobile. | | 409 | `invalid-user-state` | The user is not `active`. | From 1a619f4f75e460c9340d41e931f027f5deed7c48 Mon Sep 17 00:00:00 2001 From: Anindya Pandey Date: Sun, 19 Jul 2026 23:44:03 +0530 Subject: [PATCH 04/27] docs(UPIIS-53): document the client-secret request signature The API envelope now carries clientId and sig on every request. Document the signing steps (sig = base64(HMAC-SHA256(clientSecret, plaintext body))), add the fields to the request format and the Python reference implementation, and update the quickstart credentials + authentication sections. A missing or invalid signature is rejected 401 invalid-signature. Closes UPIIS-53 --- .../payments/upi-issuance/api-envelope.mdx | 36 +++++++++++++++---- content/payments/upi-issuance/quickstart.mdx | 19 ++++++---- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/content/payments/upi-issuance/api-envelope.mdx b/content/payments/upi-issuance/api-envelope.mdx index cb8ecb6d..9f16d093 100644 --- a/content/payments/upi-issuance/api-envelope.mdx +++ b/content/payments/upi-issuance/api-envelope.mdx @@ -14,23 +14,41 @@ Every request and response body is encrypted, keeping PII (`deviceId`, `mobile`, - **PII in the body.** `deviceId` and `mobile` are in the encrypted body, never in headers or the URL. - **`idempotencyKey` in a header.** It stays a plain header, outside the encrypted body. - **Reads are POSTs.** Even pure reads (`binding-status`, `list-vpas`) are `POST` so their PII body can be encrypted. +- **Your credentials sign the request.** Setu issues you a `clientId` and a `clientSecret`. Every request carries your `clientId` and a `sig` (an HMAC of the request payload keyed by your secret) so we can authenticate that the call is really from you — see [Signing the request](#signing-the-request).
### Request format -The TPAP / Issuing App encrypts the request body into an envelope object with three base64 fields: +The TPAP / Issuing App encrypts the request body into an envelope object. Alongside the three base64 encryption fields, it carries your `clientId` and the request signature `sig`: {`{ "ct": "", "sk": "", - "iv": "" + "iv": "", + "clientId": "", + "sig": "" }`} For each request, the TPAP / Issuing App generates a random **32-byte AES-256 key** and **16-byte IV**, encrypts the JSON body with **AES-256-CBC** and PKCS#7 padding, and wraps the session key with **RSA-OAEP (SHA-1)** under Setu's public key. The TPAP / Issuing App keeps the session key and IV to decrypt the response. +
+ +### Signing the request + +Setu issues you two credentials: a **`clientId`** and a **`clientSecret`**. On every request you: + +1. Set `clientId` to the value Setu gave you. +2. Compute `sig` = **base64( HMAC-SHA256( clientSecret, plaintext JSON body ) )** — sign the *same* JSON string you encrypt, **before** encryption, using your `clientSecret` as the HMAC key. + +Setu verifies the signature after decrypting your request. A missing signature, a signature that does not verify, or a `clientId` that isn't yours is rejected with **`401`** and the error code **`invalid-signature`**. + + + Keep your `clientSecret` secret: it lives only on your backend, never in a mobile app or browser. Sign on the server, then send the envelope. If you rotate the secret with Setu, the previous secret keeps working through the overlap window so you can cut over without downtime. + + ### Response format The response is encrypted under the same session key and IV the TPAP / Issuing App generated: @@ -54,22 +72,26 @@ The TPAP / Issuing App decrypts `ct` with the session key and IV, then verifies ### Reference implementation (Python) - {`import os, json, base64, hashlib + {`import os, json, base64, hashlib, hmac from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives import hashes, serialization def _pad(b, k=16): n = k - len(b) % k; return b + bytes([n]) * n def _unpad(b): return b[:-b[-1]] -def encrypt(pub_pem: str, body: dict): +def encrypt(pub_pem: str, client_id: str, client_secret: str, body: dict): pub = serialization.load_pem_public_key(pub_pem.encode()) key, iv = os.urandom(32), os.urandom(16) + plaintext = json.dumps(body).encode() sk = pub.encrypt(key, padding.OAEP(mgf=padding.MGF1(hashes.SHA1()), algorithm=hashes.SHA1(), label=None)) enc = Cipher(algorithms.AES(key), modes.CBC(iv)).encryptor() - ct = enc.update(_pad(json.dumps(body).encode())) + enc.finalize() + ct = enc.update(_pad(plaintext)) + enc.finalize() + sig = hmac.new(client_secret.encode(), plaintext, hashlib.sha256).digest() env = {"ct": base64.b64encode(ct).decode(), "sk": base64.b64encode(sk).decode(), - "iv": base64.b64encode(iv).decode()} + "iv": base64.b64encode(iv).decode(), + "clientId": client_id, + "sig": base64.b64encode(sig).decode()} return env, key, iv def decrypt_response(key, iv, enc: dict): ct = base64.b64decode(enc["ct"]) @@ -79,7 +101,7 @@ def decrypt_response(key, iv, enc: dict): return json.loads(plain)`} -`encrypt()` returns the request envelope `env` — the `{ct, sk, iv}` object — along with the session `key` and `iv`. The TPAP / Issuing App sends `env` as the request body with `Content-Type: application/json`, and passes the same `key` and `iv` to `decrypt_response()` to read the response. +`encrypt()` signs the plaintext body with your `clientSecret`, then returns the request envelope `env` — the `{ct, sk, iv, clientId, sig}` object — along with the session `key` and `iv`. The TPAP / Issuing App sends `env` as the request body with `Content-Type: application/json`, and passes the same `key` and `iv` to `decrypt_response()` to read the response. The signature is computed over the exact `json.dumps(body)` bytes that are encrypted, so encrypt and sign use the *same* serialization. ### Next diff --git a/content/payments/upi-issuance/quickstart.mdx b/content/payments/upi-issuance/quickstart.mdx index a8f87d2b..32ec4945 100644 --- a/content/payments/upi-issuance/quickstart.mdx +++ b/content/payments/upi-issuance/quickstart.mdx @@ -17,15 +17,15 @@ This guide covers the one-time setup the TPAP / Issuing App needs before it can
-### The envelope public key +### Your keys and credentials -Every request and response body is encrypted with the **API envelope**. +Every request and response body is encrypted with the **API envelope**, and every request is signed with your client secret. -The TPAP / Issuing App can reach out to Setu to provide the **envelope public key**. With this key, the TPAP / Issuing App is supposed to encrypt each request and decrypt each response. +The TPAP / Issuing App reaches out to Setu, which provides three things: the **envelope public key** (to encrypt requests and decrypt responses), a **`clientId`**, and a **`clientSecret`** (to sign each request). -The [API envelope](/payments/upi-issuance/api-envelope) page has the full wire format and a copy-paste reference implementation. +The [API envelope](/payments/upi-issuance/api-envelope) page has the full wire format, the [signing steps](/payments/upi-issuance/api-envelope#signing-the-request), and a copy-paste reference implementation. -For example, a generate binding token request and its response look like this on the wire — the encrypted body (`ct`), the request's one-time key encrypted under Setu's public key (`sk`), and the IV (`iv`): +For example, a generate binding token request and its response look like this on the wire — the encrypted body (`ct`), the request's one-time key encrypted under Setu's public key (`sk`), the IV (`iv`), your `clientId`, and the request signature (`sig`): ##### Sample request @@ -33,7 +33,9 @@ For example, a generate binding token request and its response look like this on {`{ "ct": "HBAHaU47cY2pu9+9AkzlAYKr8s4CEDhz…Bn1uIfe9ufHgu1", "sk": "hLGbHiL+TbnjYJhCgWtZM8m/yoWb0bHO…DOENEuzVbdFrX4a70A==", - "iv": "OTLfPjWZQhMRCIoAsEBAtg==" + "iv": "OTLfPjWZQhMRCIoAsEBAtg==", + "clientId": "01KTVREBB5BM15AFA8SQBJGRS5", + "sig": "k7mS0mHiSx8s0cJ2c3Yb1xkZ0oJc9m8s5eJ8p8b2w4A=" }`}
@@ -52,7 +54,10 @@ The response comes back encrypted under the same key — the encrypted body (`ct ### Authentication -The TPAP / Issuing App can share its outbound IP address with Setu, and Setu would whitelist that IP. This is how the TPAP / Issuing App is authenticated, so requests must be server-to-server. +Requests are authenticated two ways, together: + +1. **IP whitelisting.** The TPAP / Issuing App shares its outbound IP address with Setu, and Setu whitelists it. Requests must be server-to-server. +2. **Client-secret signature.** Each request carries your `clientId` and a `sig` — an HMAC of the request payload keyed by your `clientSecret` — so Setu can verify the call is genuinely from you. See [Signing the request](/payments/upi-issuance/api-envelope#signing-the-request). Keep the `clientSecret` on your backend only.
From a3689acf18c40fea842461f102a92aafd55bca9c Mon Sep 17 00:00:00 2001 From: Anindya Pandey Date: Mon, 20 Jul 2026 03:41:37 +0530 Subject: [PATCH 05/27] docs(UPIIS-47): document alias, param defaults, otpRequired semantics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Align the UPI Issuance docs and OpenAPI reference with the service's actual behaviour, so clients don't hit surprises the docs never described. - alias: add to the user object and the UserView schema (plus the four inline response examples). It ships in binding-status and otp/verify responses today but was documented nowhere. The set-alias endpoint stays undocumented for now. - Defaults: state them for the optional params that omitted them — otpRequired/defaultDebit/defaultCredit default to false, an omitted programId creates the VPA without a program, and update-program keeps omitted fields unchanged. - otpRequired: the docs read as if Setu enforced the OS pairing. It isn't validated against os — the flow follows whatever is sent. Reframed as the value Setu expects the TPAP / Issuing App to send, per current understanding, with a new "Choosing the otpRequired value" section. - VPA prefix: corrected from "must start with the subscriber's mobile" to the user's alias, which defaults to the mobile — the old wording is wrong for any user with a custom alias. - Voice: second person -> TPAP / Issuing App across all pages, matching the convention used elsewhere in these docs. - Samples: VPA suffix alice -> acme, since the suffix is a program code ("Acme Wallet" / ACME is the program example already used). Co-Authored-By: Claude Opus 4.8 --- api-references/payments/upi-issuance.json | 86 +++++++++++-------- .../payments/upi-issuance/api-envelope.mdx | 18 ++-- .../api-integration/device-binding.mdx | 21 ++++- .../onboarding/api-integration/programs.mdx | 4 +- .../onboarding/api-integration/user-otp.mdx | 15 ++-- .../api-integration/vpa-management.mdx | 46 +++++----- .../onboarding/onboarding-states.mdx | 2 +- content/payments/upi-issuance/quickstart.mdx | 8 +- 8 files changed, 116 insertions(+), 84 deletions(-) diff --git a/api-references/payments/upi-issuance.json b/api-references/payments/upi-issuance.json index 7017f460..ab766fc3 100644 --- a/api-references/payments/upi-issuance.json +++ b/api-references/payments/upi-issuance.json @@ -217,6 +217,7 @@ "example": { "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "user": { + "alias": "919999999999", "deviceId": "device-abc-123", "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "mobile": "919999999999", @@ -376,7 +377,7 @@ "example": { "deviceId": "device-abc-123", "mobile": "919999999999", - "vpa": "919999999999-alice@setu" + "vpa": "919999999999-acme@setu" } } } @@ -661,7 +662,7 @@ "deviceId": "device-abc-123", "mobile": "919999999999", "otp": "123456", - "vpa": "919999999999-alice@setu" + "vpa": "919999999999-acme@setu" } } } @@ -677,6 +678,7 @@ "example": { "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "user": { + "alias": "919999999999", "deviceId": "device-abc-123", "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "mobile": "919999999999", @@ -692,7 +694,7 @@ "mobile": "919999999999", "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "status": "active", - "vpa": "919999999999-alice@setu" + "vpa": "919999999999-acme@setu" } } } @@ -965,7 +967,7 @@ "deviceId": "device-abc-123", "mobile": "919999999999", "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", - "vpa": "919999999999-alice@setu" + "vpa": "919999999999-acme@setu" } } } @@ -981,7 +983,7 @@ "example": { "available": true, "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", - "vpa": "919999999999-alice@setu" + "vpa": "919999999999-acme@setu" } } } @@ -1224,7 +1226,7 @@ "mobile": "919999999999", "otpRequired": false, "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", - "vpa": "919999999999-alice@setu" + "vpa": "919999999999-acme@setu" } } } @@ -1249,7 +1251,7 @@ "mobile": "919999999999", "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "status": "active", - "vpa": "919999999999-alice@setu" + "vpa": "919999999999-acme@setu" } } } @@ -1487,7 +1489,7 @@ "example": { "deviceId": "device-abc-123", "mobile": "919999999999", - "vpa": "919999999999-alice@setu" + "vpa": "919999999999-acme@setu" } } } @@ -1512,7 +1514,7 @@ "mobile": "919999999999", "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "status": "active", - "vpa": "919999999999-alice@setu" + "vpa": "919999999999-acme@setu" } } } @@ -1776,7 +1778,7 @@ "mobile": "919999999999", "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "status": "active", - "vpa": "919999999999-alice@setu" + "vpa": "919999999999-acme@setu" }, { "accountName": "Alice Doe", @@ -1788,7 +1790,7 @@ "mobile": "919999999999", "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "status": "active", - "vpa": "919999999999-alice@setu" + "vpa": "919999999999-acme@setu" } ] } @@ -2021,7 +2023,7 @@ "example": { "deviceId": "device-abc-123", "mobile": "919999999999", - "vpa": "919999999999-alice@setu" + "vpa": "919999999999-acme@setu" } } } @@ -2046,7 +2048,7 @@ "mobile": "919999999999", "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "status": "active", - "vpa": "919999999999-alice@setu" + "vpa": "919999999999-acme@setu" } } } @@ -3393,6 +3395,7 @@ "example": { "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "user": { + "alias": "919999999999", "deviceId": "device-abc-123", "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "mobile": "919999999999", @@ -3615,15 +3618,15 @@ "pattern": "^([A-Za-z0-9.-]+)@([A-Za-z0-9.-]+)$", "minLength": 1, "maxLength": 255, - "description": "The full VPA to check (prefix@handle, e.g. 919999999999-alice@setu). Its prefix must start with the user's mobile number.", - "example": "919999999999-alice@setu" + "description": "The full VPA to check (prefix@handle, e.g. 919999999999-acme@setu). Its prefix must start with the user's mobile number.", + "example": "919999999999-acme@setu" } }, "example": { "deviceId": "device-abc-123", "mobile": "919999999999", "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", - "vpa": "919999999999-alice@setu" + "vpa": "919999999999-acme@setu" }, "required": [ "deviceId", @@ -3748,8 +3751,8 @@ "pattern": "^([A-Za-z0-9.-]+)@([A-Za-z0-9.-]+)$", "minLength": 1, "maxLength": 255, - "description": "The full VPA to create (prefix@handle, e.g. 919999999999-alice@setu). Its prefix must start with the user's mobile number.", - "example": "919999999999-alice@setu" + "description": "The full VPA to create (prefix@handle, e.g. 919999999999-acme@setu). Its prefix must start with the user's mobile number.", + "example": "919999999999-acme@setu" } }, "example": { @@ -3763,7 +3766,7 @@ "mobile": "919999999999", "otpRequired": false, "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", - "vpa": "919999999999-alice@setu" + "vpa": "919999999999-acme@setu" }, "required": [ "deviceId", @@ -3797,7 +3800,7 @@ "mobile": "919999999999", "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "status": "active", - "vpa": "919999999999-alice@setu" + "vpa": "919999999999-acme@setu" } }, "required": [ @@ -3826,14 +3829,14 @@ "pattern": "^([A-Za-z0-9.-]+)@([A-Za-z0-9.-]+)$", "minLength": 1, "maxLength": 255, - "description": "The full VPA to fetch (prefix@handle, e.g. 919999999999-alice@setu). Its prefix must start with the user's mobile number.", - "example": "919999999999-alice@setu" + "description": "The full VPA to fetch (prefix@handle, e.g. 919999999999-acme@setu). Its prefix must start with the user's mobile number.", + "example": "919999999999-acme@setu" } }, "example": { "deviceId": "device-abc-123", "mobile": "919999999999", - "vpa": "919999999999-alice@setu" + "vpa": "919999999999-acme@setu" }, "required": [ "deviceId", @@ -3914,6 +3917,7 @@ "example": { "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "user": { + "alias": "919999999999", "deviceId": "device-abc-123", "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "mobile": "919999999999", @@ -3929,7 +3933,7 @@ "mobile": "919999999999", "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "status": "active", - "vpa": "919999999999-alice@setu" + "vpa": "919999999999-acme@setu" } }, "required": [ @@ -4141,13 +4145,13 @@ "minLength": 1, "maxLength": 255, "description": "Include the VPA to verify a new VPA (new onboarding / new VPA); omit for a device-change OTP (SIM re-binding).", - "example": "919999999999-alice@setu" + "example": "919999999999-acme@setu" } }, "example": { "deviceId": "device-abc-123", "mobile": "919999999999", - "vpa": "919999999999-alice@setu" + "vpa": "919999999999-acme@setu" }, "required": [ "deviceId", @@ -4212,6 +4216,11 @@ "UserView": { "type": "object", "properties": { + "alias": { + "type": "string", + "description": "The user's alias: the prefix of their VPAs. Defaults to the mobile.", + "example": "919999999999" + }, "deviceId": { "type": "string", "description": "The user's device id.", @@ -4234,6 +4243,7 @@ } }, "example": { + "alias": "919999999999", "deviceId": "device-abc-123", "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "mobile": "919999999999", @@ -4272,14 +4282,14 @@ "minLength": 1, "maxLength": 255, "description": "Include the VPA to verify a new VPA (new onboarding / new VPA); omit for a device-change OTP (SIM re-binding).", - "example": "919999999999-alice@setu" + "example": "919999999999-acme@setu" } }, "example": { "deviceId": "device-abc-123", "mobile": "919999999999", "otp": "123456", - "vpa": "919999999999-alice@setu" + "vpa": "919999999999-acme@setu" }, "required": [ "deviceId", @@ -4303,13 +4313,13 @@ "vpa": { "type": "string", "description": "The full VPA that was checked (prefix@handle).", - "example": "919999999999-alice@setu" + "example": "919999999999-acme@setu" } }, "example": { "available": true, "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", - "vpa": "919999999999-alice@setu" + "vpa": "919999999999-acme@setu" }, "required": [ "traceId", @@ -4347,7 +4357,7 @@ "mobile": "919999999999", "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "status": "active", - "vpa": "919999999999-alice@setu" + "vpa": "919999999999-acme@setu" }, { "accountName": "Alice Doe", @@ -4359,7 +4369,7 @@ "mobile": "919999999999", "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "status": "active", - "vpa": "919999999999-alice@setu" + "vpa": "919999999999-acme@setu" }, { "accountName": "Alice Doe", @@ -4371,7 +4381,7 @@ "mobile": "919999999999", "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "status": "active", - "vpa": "919999999999-alice@setu" + "vpa": "919999999999-acme@setu" } ] } @@ -4390,7 +4400,7 @@ "mobile": "919999999999", "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "status": "active", - "vpa": "919999999999-alice@setu" + "vpa": "919999999999-acme@setu" }, { "accountName": "Alice Doe", @@ -4402,7 +4412,7 @@ "mobile": "919999999999", "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "status": "active", - "vpa": "919999999999-alice@setu" + "vpa": "919999999999-acme@setu" }, { "accountName": "Alice Doe", @@ -4414,7 +4424,7 @@ "mobile": "919999999999", "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "status": "active", - "vpa": "919999999999-alice@setu" + "vpa": "919999999999-acme@setu" } ] }, @@ -4474,7 +4484,7 @@ "vpa": { "type": "string", "description": "The full VPA, prefix@handle.", - "example": "919999999999-alice@setu" + "example": "919999999999-acme@setu" } }, "example": { @@ -4487,7 +4497,7 @@ "mobile": "919999999999", "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "status": "active", - "vpa": "919999999999-alice@setu" + "vpa": "919999999999-acme@setu" }, "required": [ "id", diff --git a/content/payments/upi-issuance/api-envelope.mdx b/content/payments/upi-issuance/api-envelope.mdx index 9f16d093..f2089ca5 100644 --- a/content/payments/upi-issuance/api-envelope.mdx +++ b/content/payments/upi-issuance/api-envelope.mdx @@ -14,20 +14,20 @@ Every request and response body is encrypted, keeping PII (`deviceId`, `mobile`, - **PII in the body.** `deviceId` and `mobile` are in the encrypted body, never in headers or the URL. - **`idempotencyKey` in a header.** It stays a plain header, outside the encrypted body. - **Reads are POSTs.** Even pure reads (`binding-status`, `list-vpas`) are `POST` so their PII body can be encrypted. -- **Your credentials sign the request.** Setu issues you a `clientId` and a `clientSecret`. Every request carries your `clientId` and a `sig` (an HMAC of the request payload keyed by your secret) so we can authenticate that the call is really from you — see [Signing the request](#signing-the-request). +- **The TPAP / Issuing App's credentials sign the request.** Setu issues the TPAP / Issuing App a `clientId` and a `clientSecret`. Every request carries that `clientId` and a `sig` (an HMAC of the request payload keyed by the client secret) so Setu can authenticate that the call is really from the TPAP / Issuing App — see [Signing the request](#signing-the-request).
### Request format -The TPAP / Issuing App encrypts the request body into an envelope object. Alongside the three base64 encryption fields, it carries your `clientId` and the request signature `sig`: +The TPAP / Issuing App encrypts the request body into an envelope object. Alongside the three base64 encryption fields, it carries the TPAP / Issuing App's `clientId` and the request signature `sig`: {`{ "ct": "", "sk": "", "iv": "", - "clientId": "", + "clientId": "", "sig": "" }`} @@ -38,15 +38,15 @@ For each request, the TPAP / Issuing App generates a random **32-byte AES-256 ke ### Signing the request -Setu issues you two credentials: a **`clientId`** and a **`clientSecret`**. On every request you: +Setu issues the TPAP / Issuing App two credentials: a **`clientId`** and a **`clientSecret`**. On every request, the TPAP / Issuing App: -1. Set `clientId` to the value Setu gave you. -2. Compute `sig` = **base64( HMAC-SHA256( clientSecret, plaintext JSON body ) )** — sign the *same* JSON string you encrypt, **before** encryption, using your `clientSecret` as the HMAC key. +1. Sets `clientId` to the value Setu issued. +2. Computes `sig` = **base64( HMAC-SHA256( clientSecret, plaintext JSON body ) )** — signing the *same* JSON string that is encrypted, **before** encryption, using the `clientSecret` as the HMAC key. -Setu verifies the signature after decrypting your request. A missing signature, a signature that does not verify, or a `clientId` that isn't yours is rejected with **`401`** and the error code **`invalid-signature`**. +Setu verifies the signature after decrypting the request. A missing signature, a signature that does not verify, or a `clientId` that does not belong to the TPAP / Issuing App is rejected with **`401`** and the error code **`invalid-signature`**. - Keep your `clientSecret` secret: it lives only on your backend, never in a mobile app or browser. Sign on the server, then send the envelope. If you rotate the secret with Setu, the previous secret keeps working through the overlap window so you can cut over without downtime. + Keep the `clientSecret` secret: it lives only on the TPAP / Issuing App's backend, never in a mobile app or browser. Sign on the server, then send the envelope. If the TPAP / Issuing App rotates the secret with Setu, the previous secret keeps working through the overlap window, so the cut-over happens without downtime. ### Response format @@ -101,7 +101,7 @@ def decrypt_response(key, iv, enc: dict): return json.loads(plain)`} -`encrypt()` signs the plaintext body with your `clientSecret`, then returns the request envelope `env` — the `{ct, sk, iv, clientId, sig}` object — along with the session `key` and `iv`. The TPAP / Issuing App sends `env` as the request body with `Content-Type: application/json`, and passes the same `key` and `iv` to `decrypt_response()` to read the response. The signature is computed over the exact `json.dumps(body)` bytes that are encrypted, so encrypt and sign use the *same* serialization. +`encrypt()` signs the plaintext body with the `clientSecret`, then returns the request envelope `env` — the `{ct, sk, iv, clientId, sig}` object — along with the session `key` and `iv`. The TPAP / Issuing App sends `env` as the request body with `Content-Type: application/json`, and passes the same `key` and `iv` to `decrypt_response()` to read the response. The signature is computed over the exact `json.dumps(body)` bytes that are encrypted, so encrypt and sign use the *same* serialization. ### Next diff --git a/content/payments/upi-issuance/onboarding/api-integration/device-binding.mdx b/content/payments/upi-issuance/onboarding/api-integration/device-binding.mdx index 4893a482..4ab4e588 100644 --- a/content/payments/upi-issuance/onboarding/api-integration/device-binding.mdx +++ b/content/payments/upi-issuance/onboarding/api-integration/device-binding.mdx @@ -20,7 +20,7 @@ Device binding proves that the user's SIM, mobile number, and device belong toge | `deviceId` | string | yes | The user's device id. Non-empty, up to 128 characters. | | `mobile` | string | yes | The user's mobile number, with country code — 12 digits, `^[0-9]{12}$` (e.g. `919999999999`). | | `os` | string | yes | `android` or `ios`. | -| `otpRequired` | boolean | yes | Whether a successful SIM binding fully activates the user. `false` — the user goes straight to `active`. `true` — the user stops at `device-bound` until an [OTP](/payments/upi-issuance/onboarding/api-integration/user-otp) is verified. | +| `otpRequired` | boolean | yes | Whether a successful SIM binding fully activates the user. `false` — the user goes straight to `active`. `true` — the user stops at `device-bound` until an [OTP](/payments/upi-issuance/onboarding/api-integration/user-otp) is verified. Setu does not validate this against `os` — the flow follows whatever the TPAP / Issuing App sends. See [Choosing the value](#choosing-the-otprequired-value). |
@@ -116,7 +116,8 @@ The binding token is short-lived — it expires **45 seconds** after it is gener "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "status": "active", "deviceId": "device-abc-123", - "mobile": "919999999999" + "mobile": "919999999999", + "alias": "919999999999" } }`} @@ -141,6 +142,22 @@ The [binding status](#3-poll-the-binding-status) and a device-change [OTP verifi | `status` | string | `binding-pending`, `device-bound`, `otp-pending`, or `active`. | | `deviceId` | string | The user's device id. | | `mobile` | string | The user's mobile number, with country code (e.g. `919999999999`). | +| `alias` | string | The prefix of the user's VPAs — they are minted as `[-suffix]@handle`. Defaults to the user's `mobile`. | + +
+ +### Choosing the otpRequired value + +`otpRequired` is a flag the TPAP / Issuing App sets, not a property Setu derives. **Setu does not validate it against `os`** — the onboarding flow follows whatever value is sent, for either OS. + +These are the values Setu expects the TPAP / Issuing App to send, per current understanding of the OS behaviour: + +| `os` | Expected `otpRequired` | Why | +| :--- | :--- | :--- | +| `android` | `true` | The OTP is needed to confirm the user is present on the device. | +| `ios` | `false` | A successful SIM binding is sufficient. | + +Sending a value that differs from the table is accepted and changes the flow accordingly — so the TPAP / Issuing App should send the value that matches the device's OS.
diff --git a/content/payments/upi-issuance/onboarding/api-integration/programs.mdx b/content/payments/upi-issuance/onboarding/api-integration/programs.mdx index 152d782e..b41ec474 100644 --- a/content/payments/upi-issuance/onboarding/api-integration/programs.mdx +++ b/content/payments/upi-issuance/onboarding/api-integration/programs.mdx @@ -84,7 +84,9 @@ Both endpoints return the program under `program` in this shape: ### Update a program -`PATCH /api/v1/programs/{programId}` (enveloped) — only the fields sent change. `programId` is a path parameter. +`PATCH /api/v1/programs/{programId}` (enveloped) — `programId` is a path parameter. + +Every field below is optional and **any field that is omitted keeps its current value** — there are no defaults applied on update. Send only the fields that should change. | Field | Type | Required | Notes | | :--- | :--- | :--- | :--- | diff --git a/content/payments/upi-issuance/onboarding/api-integration/user-otp.mdx b/content/payments/upi-issuance/onboarding/api-integration/user-otp.mdx index 90328ded..b4010b7f 100644 --- a/content/payments/upi-issuance/onboarding/api-integration/user-otp.mdx +++ b/content/payments/upi-issuance/onboarding/api-integration/user-otp.mdx @@ -22,7 +22,9 @@ The OTP for a new VPA never changes the user's status. The OTP for a device chan ## New onboarding / new VPA -A newly created VPA can require an OTP before it can transact. This is **OS-dependent**: on **Android** the OTP is required and the VPA is created `pending-verification`, on **iOS** it is not and the VPA is created `active`. The TPAP / Issuing App signals it by setting `otpRequired` on [`create-vpa`](/payments/upi-issuance/onboarding/api-integration/vpa-management) to match the device OS. +A newly created VPA can require an OTP before it can transact. The TPAP / Issuing App controls this by setting `otpRequired` on [`create-vpa`](/payments/upi-issuance/onboarding/api-integration/vpa-management): `true` creates the VPA `pending-verification`, `false` creates it `active`. + +The expected value is **OS-driven** — `true` on **Android**, `false` on **iOS** — but Setu does not validate `otpRequired` against `os`. The TPAP / Issuing App should send the value that matches the device's OS; the flow follows whatever is sent. See [Choosing the value](/payments/upi-issuance/onboarding/api-integration/device-binding#choosing-the-otprequired-value). New VPA activation: on Android create-vpa creates the VPA pending-verification and an OTP promotes it to active, on iOS it is created active directly @@ -30,7 +32,7 @@ A newly created VPA can require an OTP before it can transact. This is **OS-depe When `otpRequired: true` is sent on the [generate binding token](/payments/upi-issuance/onboarding/api-integration/device-binding) call, a successful SIM binding leaves the user at `device-bound` rather than `active`. Requesting the OTP moves the user to `otp-pending`, and a successful verification moves the user to `active`, letting them transact from the new device. This confirms the user is present on the new device. -This is **OS-dependent** — `otpRequired` is `true` on **Android** (the user stops at `device-bound`) and `false` on **iOS** (a successful SIM binding activates the user directly). +The expected value is **OS-driven** — `true` on **Android** (the user stops at `device-bound`) and `false` on **iOS** (a successful SIM binding activates the user directly) — but Setu does not validate `otpRequired` against `os`. Device change: on Android otpRequired true leaves the user device-bound and an OTP promotes them to active, on iOS the SIM binding activates the user directly @@ -56,7 +58,7 @@ This is **OS-dependent** — `otpRequired` is `true` on **Android** (the user st {`{ "deviceId": "device-abc-123", "mobile": "919999999999", - "vpa": "919999999999-alice@setu" + "vpa": "919999999999-acme@setu" }`} @@ -110,7 +112,7 @@ Omit `vpa` for the **device change** case. "deviceId": "device-abc-123", "mobile": "919999999999", "otp": "123456", - "vpa": "919999999999-alice@setu" + "vpa": "919999999999-acme@setu" }`} @@ -129,7 +131,7 @@ Exactly one of `vpa` or `user` is present. "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "vpa": { "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", - "vpa": "919999999999-alice@setu", + "vpa": "919999999999-acme@setu", "status": "active", "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "accountNo": "99887766554433", @@ -151,7 +153,8 @@ The **device change** case returns the user instead: "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "status": "active", "deviceId": "device-abc-123", - "mobile": "919999999999" + "mobile": "919999999999", + "alias": "919999999999" } }`} diff --git a/content/payments/upi-issuance/onboarding/api-integration/vpa-management.mdx b/content/payments/upi-issuance/onboarding/api-integration/vpa-management.mdx index c547a704..fb785e4a 100644 --- a/content/payments/upi-issuance/onboarding/api-integration/vpa-management.mdx +++ b/content/payments/upi-issuance/onboarding/api-integration/vpa-management.mdx @@ -36,8 +36,8 @@ Once a user is `active`, the TPAP / Issuing App creates and manages their VPAs. | :--- | :--- | :--- | :--- | | `deviceId` | string | yes | The user's device id. Non-empty, up to 128 characters. | | `mobile` | string | yes | The user's mobile number, with country code — 12 digits, `^[0-9]{12}$` (e.g. `919999999999`). | -| `vpa` | string | yes | The VPA to check — the full VPA `prefix@handle` (e.g. `919999999999-alice@setu`), max 255 characters, `^([A-Za-z0-9.-]+)@([A-Za-z0-9.-]+)$`. Its VPA prefix must start with the subscriber's mobile. | -| `programId` | string | no | A 26-character ULID — `^[0-9A-HJKMNP-TV-Z]{26}$`. Validated for existence when supplied. | +| `vpa` | string | yes | The VPA to check — the full VPA `prefix@handle` (e.g. `919999999999-acme@setu`), max 255 characters, `^([A-Za-z0-9.-]+)@([A-Za-z0-9.-]+)$`. Its VPA prefix must start with the user's `alias` — by default, the user's mobile number. | +| `programId` | string | no | A 26-character ULID — `^[0-9A-HJKMNP-TV-Z]{26}$`. Validated for existence when supplied. **Omit** to check without a program. | ##### Sample request @@ -45,7 +45,7 @@ Once a user is `active`, the TPAP / Issuing App creates and manages their VPAs. {`{ "deviceId": "device-abc-123", "mobile": "919999999999", - "vpa": "919999999999-alice@setu" + "vpa": "919999999999-acme@setu" }`} @@ -60,7 +60,7 @@ Once a user is `active`, the TPAP / Issuing App creates and manages their VPAs. {`{ "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", - "vpa": "919999999999-alice@setu", + "vpa": "919999999999-acme@setu", "available": true }`} @@ -71,7 +71,7 @@ Once a user is `active`, the TPAP / Issuing App creates and manages their VPAs. | :--- | :--- | :--- | | 400 | `invalid-request` | `deviceId`, `mobile`, or `vpa` is missing or fails format validation, or the request body is malformed. | | 400 | `invalid-program` | A well-formed `programId` that is unknown or inactive. | -| 400 | `invalid-vpa` | The VPA prefix does not start with the subscriber's mobile number. | +| 400 | `invalid-vpa` | The VPA prefix does not start with the user's alias (by default, their mobile number). | | 400 | `invalid-handle` | The VPA carries a handle that is not the one configured for this TPAP. | | 401 | `device-not-linked` | This device is not bound for this user. | | 404 | `user-not-found` | No user for this mobile. | @@ -88,19 +88,19 @@ Once a user is `active`, the TPAP / Issuing App creates and manages their VPAs. | :--- | :--- | :--- | :--- | | `deviceId` | string | yes | The user's device id. Non-empty, up to 128 characters. | | `mobile` | string | yes | The user's mobile number, with country code — 12 digits, `^[0-9]{12}$` (e.g. `919999999999`). | -| `vpa` | string | yes | The VPA to create — the full VPA `prefix@handle` (e.g. `919999999999-alice@setu`), max 255 characters, `^([A-Za-z0-9.-]+)@([A-Za-z0-9.-]+)$`. Its VPA prefix must start with the subscriber's mobile. | +| `vpa` | string | yes | The VPA to create — the full VPA `prefix@handle` (e.g. `919999999999-acme@setu`), max 255 characters, `^([A-Za-z0-9.-]+)@([A-Za-z0-9.-]+)$`. Its VPA prefix must start with the user's `alias` — by default, the user's mobile number. | | `accountNo` | string | yes | The wallet / pool account number. 6–18 characters, uppercase letters and digits — `^[A-Z0-9]{6,18}$`. | | `accountName` | string | yes | The account-holder name. 1–99 characters; letters, digits, spaces and `. ' & -`. | -| `otpRequired` | boolean | no | `true` (Android) — the VPA is created `pending-verification` and an [OTP](/payments/upi-issuance/onboarding/api-integration/user-otp) activates it. `false` (iOS, default) — the VPA is created `active`. | +| `otpRequired` | boolean | no | `true` (expected for Android) — the VPA is created `pending-verification` and an [OTP](/payments/upi-issuance/onboarding/api-integration/user-otp) activates it. `false` (expected for iOS) — the VPA is created `active`. Not validated against the device OS. **Defaults to `false`** when omitted. | | `ifsc` | string | no | The IFSC of the account — `^[A-Z]{4}0[A-Z0-9]{6}$` (e.g. `SETU0000001`). Optional when a TPAP default is configured. | | `accountProviderId` | string | no | The account-provider id — a 26-character ULID (`^[0-9A-HJKMNP-TV-Z]{26}$`). Optional when a TPAP default is configured. | -| `programId` | string | no | A 26-character ULID — `^[0-9A-HJKMNP-TV-Z]{26}$`. Validated for existence when supplied. | -| `defaultDebit` | boolean | no | Make this the user's default debit account. | -| `defaultCredit` | boolean | no | Make this the user's default credit account. | +| `programId` | string | no | A 26-character ULID — `^[0-9A-HJKMNP-TV-Z]{26}$`. Validated for existence when supplied. **Omit** and the VPA is created without a program. | +| `defaultDebit` | boolean | no | Make this the user's default debit account. **Defaults to `false`** when omitted. | +| `defaultCredit` | boolean | no | Make this the user's default credit account. **Defaults to `false`** when omitted. | The account details are taken from the request. There is no bank round-trip at onboarding. -**Re-linking an existing VPA to a different account.** Re-sending `create-vpa` for a VPA you already hold is idempotent **only when the account details match**. If you re-create an existing active VPA with a **different account**, the call is rejected with `vpa-account-mismatch` (`409`) — deregister the VPA first, then create it against the new account. Setu never silently re-points an active VPA at a different account. This matters when a mobile number is reassigned to a new user: it is the app's / CBS's responsibility to clean up the prior user's VPAs, and this guard ensures an existing VPA cannot be quietly moved to a different account. +**Re-linking an existing VPA to a different account.** Re-sending `create-vpa` for a VPA the user already holds is idempotent **only when the account details match**. If an existing active VPA is re-created with a **different account**, the call is rejected with `vpa-account-mismatch` (`409`) — deregister the VPA first, then create it against the new account. Setu never silently re-points an active VPA at a different account. This matters when a mobile number is reassigned to a new user: it is the app's / CBS's responsibility to clean up the prior user's VPAs, and this guard ensures an existing VPA cannot be quietly moved to a different account. ##### Sample request @@ -109,7 +109,7 @@ The account details are taken from the request. There is no bank round-trip at o "deviceId": "device-abc-123", "mobile": "919999999999", "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", - "vpa": "919999999999-alice@setu", + "vpa": "919999999999-acme@setu", "accountNo": "99887766554433", "accountName": "Alice Doe", "otpRequired": false @@ -130,7 +130,7 @@ Returns the created VPA; its `status` is `active` or `pending-verification`. "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "vpa": { "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", - "vpa": "919999999999-alice@setu", + "vpa": "919999999999-acme@setu", "status": "active", "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "accountNo": "99887766554433", @@ -150,7 +150,7 @@ Returns the created VPA; its `status` is `active` or `pending-verification`. | 400 | `invalid-request` | `deviceId`, `mobile`, `vpa`, `accountNo`, or `accountName` is missing or fails format validation, or the request body is malformed. | | 400 | `missing-parameter` | `ifsc` or `accountProviderId` is required (no TPAP default configured) and was not supplied. | | 400 | `invalid-program` | Unknown or inactive `programId`. | -| 400 | `invalid-vpa` | The VPA prefix does not start with the subscriber's mobile number. | +| 400 | `invalid-vpa` | The VPA prefix does not start with the user's alias (by default, their mobile number). | | 400 | `invalid-handle` | The VPA carries a handle that is not the one configured for this TPAP. | | 401 | `device-not-linked` | This device is not bound for this user. | | 404 | `user-not-found` | No user for this mobile. | @@ -177,7 +177,7 @@ Returns the created VPA; its `status` is `active` or `pending-verification`. | :--- | :--- | :--- | :--- | | `deviceId` | string | yes | The user's device id. Non-empty, up to 128 characters. | | `mobile` | string | yes | The user's mobile number, with country code — 12 digits, `^[0-9]{12}$` (e.g. `919999999999`). | -| `vpa` | string | yes | The VPA to fetch — the full VPA `prefix@handle` (e.g. `919999999999-alice@setu`), max 255 characters, `^([A-Za-z0-9.-]+)@([A-Za-z0-9.-]+)$`. Its VPA prefix must start with the subscriber's mobile. | +| `vpa` | string | yes | The VPA to fetch — the full VPA `prefix@handle` (e.g. `919999999999-acme@setu`), max 255 characters, `^([A-Za-z0-9.-]+)@([A-Za-z0-9.-]+)$`. Its VPA prefix must start with the user's `alias` — by default, the user's mobile number. | ##### Sample request @@ -185,7 +185,7 @@ Returns the created VPA; its `status` is `active` or `pending-verification`. {`{ "deviceId": "device-abc-123", "mobile": "919999999999", - "vpa": "919999999999-alice@setu" + "vpa": "919999999999-acme@setu" }`} @@ -203,7 +203,7 @@ Returns the VPA under `vpa`. "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "vpa": { "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", - "vpa": "919999999999-alice@setu", + "vpa": "919999999999-acme@setu", "status": "active", "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "accountNo": "99887766554433", @@ -221,7 +221,7 @@ Returns the VPA under `vpa`. | Status | Code | When | | :--- | :--- | :--- | | 400 | `invalid-request` | `deviceId`, `mobile`, or `vpa` is missing or fails format validation, or the request body is malformed. | -| 400 | `invalid-vpa` | The VPA prefix does not start with the subscriber's mobile number. | +| 400 | `invalid-vpa` | The VPA prefix does not start with the user's alias (by default, their mobile number). | | 400 | `invalid-handle` | The VPA carries a handle that is not the one configured for this TPAP. | | 401 | `device-not-linked` | This device is not bound for this user. | | 404 | `user-not-found` | No user for this mobile. | @@ -266,7 +266,7 @@ Returns the VPA under `vpa`. "vpas": [ { "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", - "vpa": "919999999999-alice@setu", + "vpa": "919999999999-acme@setu", "status": "active", "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "accountNo": "99887766554433", @@ -301,7 +301,7 @@ Returns the VPA under `vpa`. | :--- | :--- | :--- | :--- | | `deviceId` | string | yes | The user's device id. Non-empty, up to 128 characters. | | `mobile` | string | yes | The user's mobile number, with country code — 12 digits, `^[0-9]{12}$` (e.g. `919999999999`). | -| `vpa` | string | yes | The VPA to deregister — the full VPA `prefix@handle` (e.g. `919999999999-alice@setu`), max 255 characters, `^([A-Za-z0-9.-]+)@([A-Za-z0-9.-]+)$`. Its VPA prefix must start with the subscriber's mobile. | +| `vpa` | string | yes | The VPA to deregister — the full VPA `prefix@handle` (e.g. `919999999999-acme@setu`), max 255 characters, `^([A-Za-z0-9.-]+)@([A-Za-z0-9.-]+)$`. Its VPA prefix must start with the user's `alias` — by default, the user's mobile number. | ##### Sample request @@ -309,7 +309,7 @@ Returns the VPA under `vpa`. {`{ "deviceId": "device-abc-123", "mobile": "919999999999", - "vpa": "919999999999-alice@setu" + "vpa": "919999999999-acme@setu" }`} @@ -327,7 +327,7 @@ Returns the deregistered VPA; its `status` is `deregistered`. "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "vpa": { "id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", - "vpa": "919999999999-alice@setu", + "vpa": "919999999999-acme@setu", "status": "deregistered", "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "accountNo": "99887766554433", @@ -345,7 +345,7 @@ Returns the deregistered VPA; its `status` is `deregistered`. | Status | Code | When | | :--- | :--- | :--- | | 400 | `invalid-request` | `deviceId`, `mobile`, or `vpa` is missing or fails format validation, or the request body is malformed. | -| 400 | `invalid-vpa` | The VPA prefix does not start with the subscriber's mobile number. | +| 400 | `invalid-vpa` | The VPA prefix does not start with the user's alias (by default, their mobile number). | | 400 | `invalid-handle` | The VPA carries a handle that is not the one configured for this TPAP. | | 401 | `device-not-linked` | This device is not bound for this user. | | 404 | `user-not-found` | No user for this mobile. | diff --git a/content/payments/upi-issuance/onboarding/onboarding-states.mdx b/content/payments/upi-issuance/onboarding/onboarding-states.mdx index a33e82c1..2df25b60 100644 --- a/content/payments/upi-issuance/onboarding/onboarding-states.mdx +++ b/content/payments/upi-issuance/onboarding/onboarding-states.mdx @@ -31,7 +31,7 @@ Onboarding tracks two things, both driven by the TPAP / Issuing App's API calls: ### When OTP verification happens -OTP verification is **required for Android** and not needed on iOS. **When** it happens is controlled by the TPAP / Issuing App through the `otpRequired` flag, and Setu respects it: +OTP verification is **expected for Android** and not needed on iOS. Both **whether** and **when** it happens are controlled by the TPAP / Issuing App through the `otpRequired` flag, and Setu respects it — the flag is not validated against `os`, so the flow follows whatever the TPAP / Issuing App sends: - To verify OTP **after SIM binding and before VPA creation**, set `otpRequired: true` on the [generate binding token](/payments/upi-issuance/onboarding/api-integration/device-binding) call. The user stays at `device-bound` until an OTP is verified. Use case: a **device change** (no new VPA is created). - To verify OTP **after VPA creation**, set `otpRequired: true` on [`create-vpa`](/payments/upi-issuance/onboarding/api-integration/vpa-management). The new VPA stays `pending-verification` until an OTP is verified. Use case: a **new onboarding / new VPA**. diff --git a/content/payments/upi-issuance/quickstart.mdx b/content/payments/upi-issuance/quickstart.mdx index 32ec4945..ee996251 100644 --- a/content/payments/upi-issuance/quickstart.mdx +++ b/content/payments/upi-issuance/quickstart.mdx @@ -17,15 +17,15 @@ This guide covers the one-time setup the TPAP / Issuing App needs before it can
-### Your keys and credentials +### Keys and credentials -Every request and response body is encrypted with the **API envelope**, and every request is signed with your client secret. +Every request and response body is encrypted with the **API envelope**, and every request is signed with the TPAP / Issuing App's client secret. The TPAP / Issuing App reaches out to Setu, which provides three things: the **envelope public key** (to encrypt requests and decrypt responses), a **`clientId`**, and a **`clientSecret`** (to sign each request). The [API envelope](/payments/upi-issuance/api-envelope) page has the full wire format, the [signing steps](/payments/upi-issuance/api-envelope#signing-the-request), and a copy-paste reference implementation. -For example, a generate binding token request and its response look like this on the wire — the encrypted body (`ct`), the request's one-time key encrypted under Setu's public key (`sk`), the IV (`iv`), your `clientId`, and the request signature (`sig`): +For example, a generate binding token request and its response look like this on the wire — the encrypted body (`ct`), the request's one-time key encrypted under Setu's public key (`sk`), the IV (`iv`), the `clientId`, and the request signature (`sig`): ##### Sample request @@ -57,7 +57,7 @@ The response comes back encrypted under the same key — the encrypted body (`ct Requests are authenticated two ways, together: 1. **IP whitelisting.** The TPAP / Issuing App shares its outbound IP address with Setu, and Setu whitelists it. Requests must be server-to-server. -2. **Client-secret signature.** Each request carries your `clientId` and a `sig` — an HMAC of the request payload keyed by your `clientSecret` — so Setu can verify the call is genuinely from you. See [Signing the request](/payments/upi-issuance/api-envelope#signing-the-request). Keep the `clientSecret` on your backend only. +2. **Client-secret signature.** Each request carries the `clientId` and a `sig` — an HMAC of the request payload keyed by the `clientSecret` — so Setu can verify the call is genuinely from the TPAP / Issuing App. See [Signing the request](/payments/upi-issuance/api-envelope#signing-the-request). Keep the `clientSecret` on the TPAP / Issuing App's backend only.
From fde7bb9c8d390e4254b11033aca9ad9e12f6c8b3 Mon Sep 17 00:00:00 2001 From: Anindya Pandey Date: Mon, 20 Jul 2026 18:31:00 +0530 Subject: [PATCH 06/27] docs(UPIIS-57): register UPI Issuance in the API playground Add the two feeder inputs the playground consumes from this repo, so /payments/upi-issuance resolves there instead of 404ing behind the "Test in API playground" button the docs already render. - products.json: add UPI Issuance under Payments. - json/payments/upi-issuance/: 14 mock payloads, one per operation, named after the operationId per this folder's README. Bodies are the spec's existing examples. Each carries sim.plaintext in idempotencyKey, which exchanges the call outside the crypto envelope; an envelope body cannot be authored statically. requestBindingToken and verifyOTP add a scenario directive (sim.bind-ok, sim.otp-ok) for the outcomes that depend on an async event the playground cannot fire. Directives are honoured on sandbox deployments only. No credentials are committed: the sandbox plaintext path needs none. - upi-issuance.json: operationIds drop the "onboarding#" prefix. '#' is the URL fragment delimiter, so a payload file named after the old form truncates at the '#' and 404s when fetched. Also align three vpa request-body descriptions with the prose pages, which already say the prefix must start with the user's alias rather than their mobile number. set-alias stays undocumented, so it has no entry here. Closes UPIIS-57 --- .../payments/upi-issuance/blockPayeeVPA.json | 14 ++++++++ .../json/payments/upi-issuance/checkVPA.json | 15 ++++++++ .../payments/upi-issuance/createProgram.json | 16 +++++++++ .../json/payments/upi-issuance/createVPA.json | 22 ++++++++++++ .../payments/upi-issuance/deregisterVPA.json | 14 ++++++++ .../json/payments/upi-issuance/getVPA.json | 14 ++++++++ .../upi-issuance/listBlockedPayeeVPAs.json | 15 ++++++++ .../json/payments/upi-issuance/listVPAs.json | 15 ++++++++ .../upi-issuance/pollBindingStatus.json | 13 +++++++ .../upi-issuance/requestBindingToken.json | 15 ++++++++ .../payments/upi-issuance/requestOTP.json | 14 ++++++++ .../upi-issuance/unblockPayeeVPA.json | 14 ++++++++ .../payments/upi-issuance/updateProgram.json | 22 ++++++++++++ .../json/payments/upi-issuance/verifyOTP.json | 15 ++++++++ api-playground/products.json | 4 +++ api-references/payments/upi-issuance.json | 34 +++++++++---------- 16 files changed, 239 insertions(+), 17 deletions(-) create mode 100644 api-playground/json/payments/upi-issuance/blockPayeeVPA.json create mode 100644 api-playground/json/payments/upi-issuance/checkVPA.json create mode 100644 api-playground/json/payments/upi-issuance/createProgram.json create mode 100644 api-playground/json/payments/upi-issuance/createVPA.json create mode 100644 api-playground/json/payments/upi-issuance/deregisterVPA.json create mode 100644 api-playground/json/payments/upi-issuance/getVPA.json create mode 100644 api-playground/json/payments/upi-issuance/listBlockedPayeeVPAs.json create mode 100644 api-playground/json/payments/upi-issuance/listVPAs.json create mode 100644 api-playground/json/payments/upi-issuance/pollBindingStatus.json create mode 100644 api-playground/json/payments/upi-issuance/requestBindingToken.json create mode 100644 api-playground/json/payments/upi-issuance/requestOTP.json create mode 100644 api-playground/json/payments/upi-issuance/unblockPayeeVPA.json create mode 100644 api-playground/json/payments/upi-issuance/updateProgram.json create mode 100644 api-playground/json/payments/upi-issuance/verifyOTP.json diff --git a/api-playground/json/payments/upi-issuance/blockPayeeVPA.json b/api-playground/json/payments/upi-issuance/blockPayeeVPA.json new file mode 100644 index 00000000..107f3c45 --- /dev/null +++ b/api-playground/json/payments/upi-issuance/blockPayeeVPA.json @@ -0,0 +1,14 @@ +{ + "parameters": { + "header": [ + { + "idempotencyKey": "sim.plaintext" + } + ] + }, + "body": { + "deviceId": "device-abc-123", + "mobile": "919999999999", + "payeeVpa": "merchant@otherbank" + } +} diff --git a/api-playground/json/payments/upi-issuance/checkVPA.json b/api-playground/json/payments/upi-issuance/checkVPA.json new file mode 100644 index 00000000..250b8122 --- /dev/null +++ b/api-playground/json/payments/upi-issuance/checkVPA.json @@ -0,0 +1,15 @@ +{ + "parameters": { + "header": [ + { + "idempotencyKey": "sim.plaintext" + } + ] + }, + "body": { + "deviceId": "device-abc-123", + "mobile": "919999999999", + "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "vpa": "919999999999-acme@setu" + } +} diff --git a/api-playground/json/payments/upi-issuance/createProgram.json b/api-playground/json/payments/upi-issuance/createProgram.json new file mode 100644 index 00000000..19b5bb0b --- /dev/null +++ b/api-playground/json/payments/upi-issuance/createProgram.json @@ -0,0 +1,16 @@ +{ + "parameters": { + "header": [ + { + "idempotencyKey": "sim.plaintext" + } + ] + }, + "body": { + "amountLimit": 500000, + "code": "ACME", + "isCreditAllowed": true, + "isDebitAllowed": true, + "name": "Acme Wallet" + } +} diff --git a/api-playground/json/payments/upi-issuance/createVPA.json b/api-playground/json/payments/upi-issuance/createVPA.json new file mode 100644 index 00000000..5ae00f12 --- /dev/null +++ b/api-playground/json/payments/upi-issuance/createVPA.json @@ -0,0 +1,22 @@ +{ + "parameters": { + "header": [ + { + "idempotencyKey": "sim.plaintext" + } + ] + }, + "body": { + "accountName": "Alice Doe", + "accountNo": "99887766554433", + "accountProviderId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "defaultCredit": true, + "defaultDebit": true, + "deviceId": "device-abc-123", + "ifsc": "SETU0000001", + "mobile": "919999999999", + "otpRequired": false, + "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "vpa": "919999999999-acme@setu" + } +} diff --git a/api-playground/json/payments/upi-issuance/deregisterVPA.json b/api-playground/json/payments/upi-issuance/deregisterVPA.json new file mode 100644 index 00000000..17b0f0b0 --- /dev/null +++ b/api-playground/json/payments/upi-issuance/deregisterVPA.json @@ -0,0 +1,14 @@ +{ + "parameters": { + "header": [ + { + "idempotencyKey": "sim.plaintext" + } + ] + }, + "body": { + "deviceId": "device-abc-123", + "mobile": "919999999999", + "vpa": "919999999999-acme@setu" + } +} diff --git a/api-playground/json/payments/upi-issuance/getVPA.json b/api-playground/json/payments/upi-issuance/getVPA.json new file mode 100644 index 00000000..17b0f0b0 --- /dev/null +++ b/api-playground/json/payments/upi-issuance/getVPA.json @@ -0,0 +1,14 @@ +{ + "parameters": { + "header": [ + { + "idempotencyKey": "sim.plaintext" + } + ] + }, + "body": { + "deviceId": "device-abc-123", + "mobile": "919999999999", + "vpa": "919999999999-acme@setu" + } +} diff --git a/api-playground/json/payments/upi-issuance/listBlockedPayeeVPAs.json b/api-playground/json/payments/upi-issuance/listBlockedPayeeVPAs.json new file mode 100644 index 00000000..4f660332 --- /dev/null +++ b/api-playground/json/payments/upi-issuance/listBlockedPayeeVPAs.json @@ -0,0 +1,15 @@ +{ + "parameters": { + "header": [ + { + "idempotencyKey": "sim.plaintext" + } + ] + }, + "body": { + "cursor": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "deviceId": "device-abc-123", + "limit": 20, + "mobile": "919999999999" + } +} diff --git a/api-playground/json/payments/upi-issuance/listVPAs.json b/api-playground/json/payments/upi-issuance/listVPAs.json new file mode 100644 index 00000000..4f660332 --- /dev/null +++ b/api-playground/json/payments/upi-issuance/listVPAs.json @@ -0,0 +1,15 @@ +{ + "parameters": { + "header": [ + { + "idempotencyKey": "sim.plaintext" + } + ] + }, + "body": { + "cursor": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "deviceId": "device-abc-123", + "limit": 20, + "mobile": "919999999999" + } +} diff --git a/api-playground/json/payments/upi-issuance/pollBindingStatus.json b/api-playground/json/payments/upi-issuance/pollBindingStatus.json new file mode 100644 index 00000000..7d7a99cd --- /dev/null +++ b/api-playground/json/payments/upi-issuance/pollBindingStatus.json @@ -0,0 +1,13 @@ +{ + "parameters": { + "header": [ + { + "idempotencyKey": "sim.plaintext" + } + ] + }, + "body": { + "deviceId": "device-abc-123", + "mobile": "919999999999" + } +} diff --git a/api-playground/json/payments/upi-issuance/requestBindingToken.json b/api-playground/json/payments/upi-issuance/requestBindingToken.json new file mode 100644 index 00000000..5b0530d3 --- /dev/null +++ b/api-playground/json/payments/upi-issuance/requestBindingToken.json @@ -0,0 +1,15 @@ +{ + "parameters": { + "header": [ + { + "idempotencyKey": "sim.plaintext,sim.bind-ok" + } + ] + }, + "body": { + "deviceId": "device-abc-123", + "mobile": "919999999999", + "os": "android", + "otpRequired": true + } +} diff --git a/api-playground/json/payments/upi-issuance/requestOTP.json b/api-playground/json/payments/upi-issuance/requestOTP.json new file mode 100644 index 00000000..17b0f0b0 --- /dev/null +++ b/api-playground/json/payments/upi-issuance/requestOTP.json @@ -0,0 +1,14 @@ +{ + "parameters": { + "header": [ + { + "idempotencyKey": "sim.plaintext" + } + ] + }, + "body": { + "deviceId": "device-abc-123", + "mobile": "919999999999", + "vpa": "919999999999-acme@setu" + } +} diff --git a/api-playground/json/payments/upi-issuance/unblockPayeeVPA.json b/api-playground/json/payments/upi-issuance/unblockPayeeVPA.json new file mode 100644 index 00000000..107f3c45 --- /dev/null +++ b/api-playground/json/payments/upi-issuance/unblockPayeeVPA.json @@ -0,0 +1,14 @@ +{ + "parameters": { + "header": [ + { + "idempotencyKey": "sim.plaintext" + } + ] + }, + "body": { + "deviceId": "device-abc-123", + "mobile": "919999999999", + "payeeVpa": "merchant@otherbank" + } +} diff --git a/api-playground/json/payments/upi-issuance/updateProgram.json b/api-playground/json/payments/upi-issuance/updateProgram.json new file mode 100644 index 00000000..d7553443 --- /dev/null +++ b/api-playground/json/payments/upi-issuance/updateProgram.json @@ -0,0 +1,22 @@ +{ + "parameters": { + "header": [ + { + "idempotencyKey": "sim.plaintext" + } + ], + "path": [ + { + "programId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + ] + }, + "body": { + "amountLimit": 500000, + "code": "ACME", + "isCreditAllowed": true, + "isDebitAllowed": false, + "name": "Acme Wallet", + "status": "inactive" + } +} diff --git a/api-playground/json/payments/upi-issuance/verifyOTP.json b/api-playground/json/payments/upi-issuance/verifyOTP.json new file mode 100644 index 00000000..125f3fed --- /dev/null +++ b/api-playground/json/payments/upi-issuance/verifyOTP.json @@ -0,0 +1,15 @@ +{ + "parameters": { + "header": [ + { + "idempotencyKey": "sim.plaintext,sim.otp-ok" + } + ] + }, + "body": { + "deviceId": "device-abc-123", + "mobile": "919999999999", + "otp": "123456", + "vpa": "919999999999-acme@setu" + } +} diff --git a/api-playground/products.json b/api-playground/products.json index 2e5e5186..823eb330 100644 --- a/api-playground/products.json +++ b/api-playground/products.json @@ -19,6 +19,10 @@ { "name": "BBPS BillCollect", "path": "payments/bbps" + }, + { + "name": "UPI Issuance", + "path": "payments/upi-issuance" } ] }, diff --git a/api-references/payments/upi-issuance.json b/api-references/payments/upi-issuance.json index ab766fc3..42990bc2 100644 --- a/api-references/payments/upi-issuance.json +++ b/api-references/payments/upi-issuance.json @@ -18,7 +18,7 @@ ], "summary": "Generate a binding token", "description": "Generate a device-binding token: returns the VMN and the ready-to-send silent-SMS body, and starts onboarding for the user's mobile number.", - "operationId": "onboarding#requestBindingToken", + "operationId": "requestBindingToken", "parameters": [ { "name": "idempotencyKey", @@ -191,7 +191,7 @@ ], "summary": "Poll binding status", "description": "Poll a user's device-binding status. Returns the user object in its current state.", - "operationId": "onboarding#pollBindingStatus", + "operationId": "pollBindingStatus", "requestBody": { "required": true, "content": { @@ -351,7 +351,7 @@ ], "summary": "Request an OTP", "description": "Request the user-presence OTP SMS — include the vpa to verify a new VPA (new onboarding / new VPA); omit the vpa for a device change (SIM re-binding). Asynchronous: returns 202; the OTP SMS is sent shortly after.", - "operationId": "onboarding#requestOTP", + "operationId": "requestOTP", "parameters": [ { "name": "idempotencyKey", @@ -635,7 +635,7 @@ ], "summary": "Verify an OTP", "description": "Verify the OTP the user entered. Activates what it was requested for — a new VPA (new onboarding / new VPA, vpa in the body) or the user (a device change, no vpa).", - "operationId": "onboarding#verifyOTP", + "operationId": "verifyOTP", "parameters": [ { "name": "idempotencyKey", @@ -940,7 +940,7 @@ ], "summary": "Check VPA availability", "description": "Check whether a chosen VPA prefix is available. A taken prefix is not an error.", - "operationId": "onboarding#checkVPA", + "operationId": "checkVPA", "parameters": [ { "name": "idempotencyKey", @@ -1192,7 +1192,7 @@ ], "summary": "Create a VPA", "description": "Create a VPA over the user's chosen account. Requires an active user. With otpRequired, the VPA is created pending-verification until an OTP activates it.", - "operationId": "onboarding#createVPA", + "operationId": "createVPA", "parameters": [ { "name": "idempotencyKey", @@ -1463,7 +1463,7 @@ ], "summary": "Get a VPA", "description": "Fetch one of the user's VPAs by its VPA string.", - "operationId": "onboarding#getVPA", + "operationId": "getVPA", "parameters": [ { "name": "idempotencyKey", @@ -1724,7 +1724,7 @@ ], "summary": "List VPAs", "description": "List the user's live VPAs (active and pending-verification), newest-added first, keyset-paginated.", - "operationId": "onboarding#listVPAs", + "operationId": "listVPAs", "parameters": [ { "name": "idempotencyKey", @@ -1997,7 +1997,7 @@ ], "summary": "Deregister a VPA", "description": "Deregister one of the user's VPAs (soft delete; the entry is preserved for audit and re-registration). Idempotent for an already-deregistered VPA.", - "operationId": "onboarding#deregisterVPA", + "operationId": "deregisterVPA", "parameters": [ { "name": "idempotencyKey", @@ -2258,7 +2258,7 @@ ], "summary": "Create a program", "description": "Create a program — an engagement channel that VPAs are created under — and return its programId.", - "operationId": "onboarding#createProgram", + "operationId": "createProgram", "requestBody": { "required": true, "content": { @@ -2386,7 +2386,7 @@ ], "summary": "Update a program", "description": "Update a program's configuration. Only the fields supplied in the body are changed.", - "operationId": "onboarding#updateProgram", + "operationId": "updateProgram", "parameters": [ { "name": "programId", @@ -2568,7 +2568,7 @@ ], "summary": "Block a payee VPA", "description": "Block an external payee VPA for the user. Idempotent; requires an active user.", - "operationId": "onboarding#blockPayeeVPA", + "operationId": "blockPayeeVPA", "parameters": [ { "name": "idempotencyKey", @@ -2822,7 +2822,7 @@ ], "summary": "Unblock a payee VPA", "description": "Unblock a previously-blocked payee VPA (soft flip; the entry is preserved). Idempotent for an already-unblocked payee; requires an active user.", - "operationId": "onboarding#unblockPayeeVPA", + "operationId": "unblockPayeeVPA", "parameters": [ { "name": "idempotencyKey", @@ -3077,7 +3077,7 @@ ], "summary": "List blocked payee VPAs", "description": "List the user's currently-blocked payee VPAs, newest first, keyset-paginated.", - "operationId": "onboarding#listBlockedPayeeVPAs", + "operationId": "listBlockedPayeeVPAs", "parameters": [ { "name": "idempotencyKey", @@ -3618,7 +3618,7 @@ "pattern": "^([A-Za-z0-9.-]+)@([A-Za-z0-9.-]+)$", "minLength": 1, "maxLength": 255, - "description": "The full VPA to check (prefix@handle, e.g. 919999999999-acme@setu). Its prefix must start with the user's mobile number.", + "description": "The full VPA to check (prefix@handle, e.g. 919999999999-acme@setu). Its prefix must start with the user's alias (by default, their mobile number).", "example": "919999999999-acme@setu" } }, @@ -3751,7 +3751,7 @@ "pattern": "^([A-Za-z0-9.-]+)@([A-Za-z0-9.-]+)$", "minLength": 1, "maxLength": 255, - "description": "The full VPA to create (prefix@handle, e.g. 919999999999-acme@setu). Its prefix must start with the user's mobile number.", + "description": "The full VPA to create (prefix@handle, e.g. 919999999999-acme@setu). Its prefix must start with the user's alias (by default, their mobile number).", "example": "919999999999-acme@setu" } }, @@ -3829,7 +3829,7 @@ "pattern": "^([A-Za-z0-9.-]+)@([A-Za-z0-9.-]+)$", "minLength": 1, "maxLength": 255, - "description": "The full VPA to fetch (prefix@handle, e.g. 919999999999-acme@setu). Its prefix must start with the user's mobile number.", + "description": "The full VPA to fetch (prefix@handle, e.g. 919999999999-acme@setu). Its prefix must start with the user's alias (by default, their mobile number).", "example": "919999999999-acme@setu" } }, From dcb73b7fbe063f80c306e3835cf98b9bc20862f9 Mon Sep 17 00:00:00 2001 From: Anindya Pandey Date: Sun, 26 Jul 2026 00:44:50 +0530 Subject: [PATCH 07/27] docs(UPIIS-69): VPA resolution guide + reference + QA X-Sim-Resolution - New VPA resolution guide page (content + api-playground resolveVPA.json) and links from overview + api-reference; order numbers rebalanced. - resolveVPA operation + ResolveVPARequestBody / VpaResolveResponse schemas in the OpenAPI reference. - QA-testing: document the X-Sim-Resolution header for pinning resolved-payee values (person/merchant, verified, failure) on the QA env; honoured on QA only. --- .../payments/upi-issuance/resolveVPA.json | 15 + api-references/payments/upi-issuance.json | 430 ++++++++++++++++++ .../payments/upi-issuance/api-reference.mdx | 3 +- content/payments/upi-issuance/overview.mdx | 1 + content/payments/upi-issuance/qa-testing.mdx | 38 +- .../payments/upi-issuance/vpa-resolution.mdx | 156 +++++++ 6 files changed, 641 insertions(+), 2 deletions(-) create mode 100644 api-playground/json/payments/upi-issuance/resolveVPA.json create mode 100644 content/payments/upi-issuance/vpa-resolution.mdx diff --git a/api-playground/json/payments/upi-issuance/resolveVPA.json b/api-playground/json/payments/upi-issuance/resolveVPA.json new file mode 100644 index 00000000..e5a81b0a --- /dev/null +++ b/api-playground/json/payments/upi-issuance/resolveVPA.json @@ -0,0 +1,15 @@ +{ + "parameters": { + "header": [ + { + "idempotencyKey": "sim.plaintext" + } + ] + }, + "body": { + "deviceId": "device-abc-123", + "mobile": "919999999999", + "vpa": "someone@ybl", + "geocode": "12.9716,77.5946" + } +} diff --git a/api-references/payments/upi-issuance.json b/api-references/payments/upi-issuance.json index 42990bc2..655c2e89 100644 --- a/api-references/payments/upi-issuance.json +++ b/api-references/payments/upi-issuance.json @@ -1185,6 +1185,264 @@ } } }, + "/api/v1/onboarding/vpa/resolve": { + "post": { + "tags": [ + "VPA management" + ], + "summary": "Resolve a payee VPA", + "description": "Resolve a payee VPA on the payer side. A VPA under our handle is answered immediately from local records; a foreign-handle VPA triggers an async NPCI ReqValAdd and returns PENDING until the result arrives. Poll by repeating this same POST. PENDING is not an error.", + "operationId": "resolveVPA", + "parameters": [ + { + "name": "idempotencyKey", + "in": "header", + "description": "Optional client-generated idempotency key for safe retries.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "maxLength": 128, + "description": "Optional client-generated idempotency key for safe retries.", + "example": "req-8f3a2b1c" + }, + "example": "req-8f3a2b1c" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResolveVPARequestBody" + }, + "example": { + "deviceId": "device-abc-123", + "geocode": "12.9716,77.5946", + "mobile": "919999999999", + "vpa": "someone@ybl" + } + } + } + }, + "responses": { + "200": { + "description": "OK response.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VpaResolveResponse" + }, + "example": { + "accountType": "SAVINGS", + "entityType": "PERSON", + "ifsc": "ICIC0000052", + "name": "ANI********DEY", + "status": "RESOLVED", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "verified": true, + "verifiedLogo": "https://cdn.example/brandx.png", + "verifiedName": "Brand-X", + "verifiedUrl": "https://brandx.example", + "vpa": "someone@ybl" + } + } + } + }, + "400": { + "description": "Bad request: Bad Request response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "invalid-request", + "missing-parameter", + "invalid-vpa" + ], + "example": "invalid-request" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "invalid-request", + "message": "The request could not be parsed.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "401": { + "description": "Unauthorized: Unauthorized response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "device-not-linked" + ], + "example": "device-not-linked" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "device-not-linked", + "message": "This device is not bound for this user.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "404": { + "description": "Not found: Not Found response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "user-not-found" + ], + "example": "user-not-found" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "user-not-found", + "message": "No user for this mobile number.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "409": { + "description": "Conflict: Conflict response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "invalid-user-state" + ], + "example": "invalid-user-state" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "invalid-user-state", + "message": "The user has no active VPA to originate a resolution.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + }, + "500": { + "description": "Internal server error: Internal Server Error response.", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "traceId", + "code", + "message" + ], + "properties": { + "code": { + "type": "string", + "description": "Machine-readable error code.", + "enum": [ + "internal-error" + ], + "example": "internal-error" + }, + "message": { + "type": "string", + "description": "Human-readable description of the error." + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID)." + } + } + }, + "example": { + "code": "internal-error", + "message": "Something went wrong on Setu's side.", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + } + } + } + } + } + } + }, "/api/v1/onboarding/create-vpa": { "post": { "tags": [ @@ -3634,6 +3892,50 @@ "vpa" ] }, + "ResolveVPARequestBody": { + "type": "object", + "properties": { + "deviceId": { + "type": "string", + "minLength": 1, + "maxLength": 128, + "description": "The user's device id.", + "example": "device-abc-123" + }, + "geocode": { + "type": "string", + "maxLength": 64, + "pattern": "^-?[0-9]{1,3}(\\.[0-9]+)?,-?[0-9]{1,3}(\\.[0-9]+)?$", + "description": "Current device geocode as \"lat,long\"; forwarded as the payer device GEOCODE tag.", + "example": "12.9716,77.5946" + }, + "mobile": { + "type": "string", + "pattern": "^[0-9]{12}$", + "description": "The user's mobile number, with country code (e.g. 919999999999).", + "example": "919999999999" + }, + "vpa": { + "type": "string", + "pattern": "^([A-Za-z0-9.-]+)@([A-Za-z0-9.-]+)$", + "minLength": 1, + "maxLength": 255, + "description": "The payee VPA to resolve (prefix@handle). Resolved locally when the handle is ours, otherwise via an NPCI ReqValAdd.", + "example": "someone@ybl" + } + }, + "example": { + "deviceId": "device-abc-123", + "geocode": "12.9716,77.5946", + "mobile": "919999999999", + "vpa": "someone@ybl" + }, + "required": [ + "deviceId", + "mobile", + "vpa" + ] + }, "CreateProgramRequestBody": { "type": "object", "properties": { @@ -4327,6 +4629,134 @@ "available" ] }, + "VpaResolveResponse": { + "type": "object", + "properties": { + "accountType": { + "type": "string", + "description": "The payee's account type. Present on RESOLVED.", + "example": "SAVINGS" + }, + "brandName": { + "type": "string", + "description": "Merchant brand name. Present for a resolved merchant.", + "example": "Brand-X" + }, + "entityType": { + "type": "string", + "description": "The payee entity type: PERSON or ENTITY. Present on RESOLVED.", + "example": "PERSON" + }, + "errorCode": { + "type": "string", + "description": "NPCI error code. Present on FAILED (e.g. ZH for an invalid address).", + "example": "ZH" + }, + "errorMessage": { + "type": "string", + "description": "Human-readable failure reason. Present on FAILED.", + "example": "invalid virtual address" + }, + "franchiseName": { + "type": "string", + "description": "Merchant franchise name. Present for a resolved merchant.", + "example": "Brand-X Franchise" + }, + "genre": { + "type": "string", + "description": "Merchant genre: ONLINE or OFFLINE. Present for a resolved merchant.", + "example": "ONLINE" + }, + "ifsc": { + "type": "string", + "description": "The payee's IFSC. Present on RESOLVED.", + "example": "ICIC0000052" + }, + "legalName": { + "type": "string", + "description": "Merchant legal name. Present for a resolved merchant.", + "example": "Acme Pvt Ltd" + }, + "mcc": { + "type": "string", + "description": "Merchant category code. Present for a resolved merchant.", + "example": "5732" + }, + "merchantType": { + "type": "string", + "description": "Merchant type: SMALL or LARGE. Present for a resolved merchant.", + "example": "LARGE" + }, + "name": { + "type": "string", + "description": "The payee's masked account-holder name. Present on RESOLVED.", + "example": "ANI********DEY" + }, + "ownership": { + "type": "string", + "description": "Merchant ownership type: PROPRIETARY, PARTNERSHIP, PRIVATE, PUBLIC or OTHERS. Present for a resolved merchant.", + "example": "PUBLIC" + }, + "status": { + "type": "string", + "enum": [ + "RESOLVED", + "PENDING", + "FAILED" + ], + "description": "Resolution outcome: RESOLVED (details present), PENDING (poll again), or FAILED (error present).", + "example": "RESOLVED" + }, + "traceId": { + "type": "string", + "description": "Trace handle for this call (ULID).", + "example": "01ARZ3NDEKTSV4RRFFQ69G5FAV" + }, + "verified": { + "type": "boolean", + "description": "Whether the resolved payee VPA is in the verified/whitelisted registry. Present on RESOLVED.", + "example": true + }, + "verifiedLogo": { + "type": "string", + "description": "The registered logo url/ref from the verified registry. Present when verified.", + "example": "https://cdn.example/brandx.png" + }, + "verifiedName": { + "type": "string", + "description": "The registered display name from the verified registry. Present when verified.", + "example": "Brand-X" + }, + "verifiedUrl": { + "type": "string", + "description": "The registered brand/callback url from the verified registry. Present when verified.", + "example": "https://brandx.example" + }, + "vpa": { + "type": "string", + "description": "The payee VPA that was resolved (echo of the request).", + "example": "someone@ybl" + } + }, + "example": { + "accountType": "SAVINGS", + "entityType": "PERSON", + "ifsc": "ICIC0000052", + "name": "ANI********DEY", + "status": "RESOLVED", + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "verified": true, + "verifiedLogo": "https://cdn.example/brandx.png", + "verifiedName": "Brand-X", + "verifiedUrl": "https://brandx.example", + "vpa": "someone@ybl" + }, + "required": [ + "traceId", + "status", + "vpa" + ] + }, "VpaListResponse": { "type": "object", "properties": { diff --git a/content/payments/upi-issuance/api-reference.mdx b/content/payments/upi-issuance/api-reference.mdx index a17eb3f2..bf960bf7 100644 --- a/content/payments/upi-issuance/api-reference.mdx +++ b/content/payments/upi-issuance/api-reference.mdx @@ -1,7 +1,7 @@ --- sidebar_title: API reference page_title: UPI Issuance API reference -order: 6 +order: 7 visible_in_sidebar: true --- @@ -14,6 +14,7 @@ In the meantime, each operation is documented with its request, response, and er - [Device binding](/payments/upi-issuance/onboarding/api-integration/device-binding) — generate binding token, `binding-status` - [OTP verification](/payments/upi-issuance/onboarding/api-integration/user-otp) — `otp/request`, `otp/verify` - [VPA management](/payments/upi-issuance/onboarding/api-integration/vpa-management) — `vpa/check`, `create-vpa`, `get-vpa`, `list-vpas`, `vpa/deregister` +- [VPA resolution](/payments/upi-issuance/vpa-resolution) — `vpa/resolve` - [Payee blocklist](/payments/upi-issuance/payee-blocklist) — `block-vpa`, `unblock-vpa`, `list-blocked-vpas` - [Programs](/payments/upi-issuance/onboarding/api-integration/programs) — `POST /programs`, `PATCH /programs/{id}` diff --git a/content/payments/upi-issuance/overview.mdx b/content/payments/upi-issuance/overview.mdx index 7255fd18..597a081b 100644 --- a/content/payments/upi-issuance/overview.mdx +++ b/content/payments/upi-issuance/overview.mdx @@ -31,6 +31,7 @@ The stack spans the full UPI journey, starting with onboarding. These docs cover - **[Quickstart](/payments/upi-issuance/quickstart)** — the base URL, the envelope public key, and how requests are authenticated. - **[The API envelope](/payments/upi-issuance/api-envelope)** — how every request and response is encrypted. - **[User Onboarding](/payments/upi-issuance/onboarding)** — the onboarding flow, its state machines, and a step-by-step API walkthrough. +- **[VPA resolution](/payments/upi-issuance/vpa-resolution)** — resolve a payee VPA before paying it. - **[Payee blocklist](/payments/upi-issuance/payee-blocklist)** — block, unblock, and list blocked payee VPAs. - **[Testing on QA env](/payments/upi-issuance/qa-testing)** — reproduce every success and error scenario on the QA env. - **[API reference](/payments/upi-issuance/api-reference)** — the full endpoint reference. diff --git a/content/payments/upi-issuance/qa-testing.mdx b/content/payments/upi-issuance/qa-testing.mdx index b72d453a..5f7718b0 100644 --- a/content/payments/upi-issuance/qa-testing.mdx +++ b/content/payments/upi-issuance/qa-testing.mdx @@ -1,7 +1,7 @@ --- sidebar_title: Testing on QA env page_title: UPI Issuance QA env testing -order: 5 +order: 6 visible_in_sidebar: true --- @@ -51,6 +51,42 @@ On the QA env there is no telecom, so no OTP SMS is sent. To pass verification, returns 401 invalid-otp. +#### VPA resolution + +On production, resolving a foreign-handle payee returns whatever the payee PSP reports. On the QA env you can pin the resolved values so you can reproduce every shape — a person, a merchant, verified or not, and failures — deterministically. Send an optional `X-Sim-Resolution` header on `POST /onboarding/vpa/resolve` whose value is a JSON object of the values you expect back. Send it on every poll (it is read on the poll that returns `RESOLVED`). + +Every field is optional; anything you leave out falls back to a default, and with no header at all the resolution uses default values. Failures carry no payee detail, so `entityType`, the merchant fields and `verified` apply only to a success. + +| field | meaning | default | +| :--- | :--- | :--- | +| `outcome` | `success` or `failure` | `success` | +| `errorCode` | error code returned for a `failure` | `ZH` | +| `entityType` | `PERSON` or `ENTITY` (a `merchant` block also implies `ENTITY`) | `PERSON` | +| `name` | masked account-holder name | a default | +| `accountType` | e.g. `SAVINGS` / `CURRENT` | a default | +| `ifsc` | payee IFSC | a default | +| `merchant.mcc` / `.brandName` / `.legalName` / `.franchiseName` / `.merchantType` / `.genre` / `.ownership` | merchant detail for an `ENTITY` payee | defaults | +| `verified.name` / `.url` / `.logo` | verified/whitelisted enrichment; its presence makes the payee resolve `verified: true` | not verified | + +A verified merchant with pinned values: + +```bash +curl https:///api/v1/onboarding/vpa/resolve \ + -H 'X-Sim-Resolution: {"entityType":"ENTITY","name":"ACME****PVT","accountType":"CURRENT","ifsc":"HDFC0000123","merchant":{"mcc":"5411","brandName":"MyBrand","legalName":"Acme Pvt Ltd","merchantType":"SMALL"},"verified":{"name":"MyBrand","url":"https://mybrand.example"}}' \ + -H 'Content-Type: application/json' -d '' +``` + +A forced failure: + +``` +X-Sim-Resolution: {"outcome":"failure","errorCode":"ZH"} +``` + + + X-Sim-Resolution is honoured only on the QA env. On production the + header is ignored entirely and the payee resolves for real. + +
### Natural scenarios diff --git a/content/payments/upi-issuance/vpa-resolution.mdx b/content/payments/upi-issuance/vpa-resolution.mdx new file mode 100644 index 00000000..6a8a36f3 --- /dev/null +++ b/content/payments/upi-issuance/vpa-resolution.mdx @@ -0,0 +1,156 @@ +--- +sidebar_title: VPA resolution +page_title: UPI Issuance VPA resolution +order: 5 +visible_in_sidebar: true +--- + +## VPA resolution + +Before an `active` user pays a payee, the TPAP / Issuing App resolves the payee's VPA to confirm it is valid and to show who the money is going to. The route is enveloped, under `/api/v1`, and requires an `active` user. `idempotencyKey` is an optional request header. + +### How resolution works + +Resolution is a **single API, client-polled** call. A payee VPA on Setu's own handle resolves immediately; a **foreign-handle** VPA (any other PSP, e.g. `someone@ybl`) is resolved with the UPI network, which is asynchronous — so the first call returns `PENDING` and the app **polls by sending the same request again** until it settles. + +The `status` field carries the outcome; the HTTP status stays `200` for all three: + +| `status` | Meaning | +| :--- | :--- | +| `RESOLVED` | The payee is valid; the payee details are included. | +| `PENDING` | Resolution is in progress — send the same request again to poll. | +| `FAILED` | The payee VPA could not be resolved; `errorCode` + `errorMessage` explain why. | + +
+ +### Resolve a payee VPA + +`POST /api/v1/onboarding/vpa/resolve` (enveloped) — requires an `active` user. + +| Field | Type | Required | Notes | +| :--- | :--- | :--- | :--- | +| `deviceId` | string | yes | The user's device id. Non-empty, up to 128 characters. | +| `mobile` | string | yes | The user's mobile number, with country code — 12 digits, `^[0-9]{12}$` (e.g. `919999999999`). | +| `vpa` | string | yes | The payee VPA to resolve (`prefix@handle`; any handle). Max 255 characters — `^([A-Za-z0-9.-]+)@([A-Za-z0-9.-]+)$`. | +| `geocode` | string | no | The device geocode as `lat,long` (e.g. `12.9716,77.5946`); forwarded with the network request. | + +##### Sample request + + + {`{ + "deviceId": "device-abc-123", + "mobile": "919999999999", + "vpa": "someone@ybl", + "geocode": "12.9716,77.5946" +}`} + + +##### Success response — `200` + +`status` is always present; the other fields depend on the outcome. + +| Field | Type | Notes | +| :--- | :--- | :--- | +| `traceId` | string | Trace handle for this call (ULID). | +| `status` | string | `RESOLVED`, `PENDING`, or `FAILED`. | +| `vpa` | string | The payee VPA that was resolved (echo of the request). | +| `name` | string | The payee's account-holder name. Present on `RESOLVED`. | +| `accountType` | string | The payee's account type (e.g. `SAVINGS`). Present on `RESOLVED`. | +| `ifsc` | string | The payee's IFSC. Present on `RESOLVED`. | +| `entityType` | string | `PERSON` or `ENTITY` (merchant). Present on `RESOLVED`. | +| `verified` | boolean | `true` when the payee is a verified / whitelisted address. Present (with the fields below) only when verified. | +| `verifiedName` | string | The registered display name from the verified registry. | +| `verifiedLogo` | string | The registered logo url. | +| `verifiedUrl` | string | The registered brand / callback url. | +| `errorCode` | string | Network error code (e.g. `ZH`). Present on `FAILED`. | +| `errorMessage` | string | Human-readable failure reason. Present on `FAILED`. | + +A **merchant** payee (`entityType: ENTITY`) additionally carries a merchant block on `RESOLVED`: + +| Field | Type | Notes | +| :--- | :--- | :--- | +| `genre` | string | Merchant genre: `ONLINE` or `OFFLINE`. | +| `mcc` | string | Merchant category code. | +| `merchantType` | string | `SMALL` or `LARGE`. | +| `brandName` | string | Merchant brand name. | +| `franchiseName` | string | Merchant franchise name. | +| `legalName` | string | Merchant legal name. | +| `ownership` | string | `PROPRIETARY`, `PARTNERSHIP`, `PRIVATE`, `PUBLIC`, or `OTHERS`. | + +##### Resolved (person) + + + {`{ + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "status": "RESOLVED", + "vpa": "someone@ybl", + "name": "ANI********DEY", + "accountType": "SAVINGS", + "ifsc": "ICIC0000052", + "entityType": "PERSON" +}`} + + +##### Resolved (verified merchant) + + + {`{ + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "status": "RESOLVED", + "vpa": "brandx@ybl", + "name": "BRAND****X", + "accountType": "CURRENT", + "ifsc": "ICIC0000052", + "entityType": "ENTITY", + "genre": "ONLINE", + "mcc": "5732", + "merchantType": "LARGE", + "brandName": "Brand-X", + "legalName": "Acme Pvt Ltd", + "ownership": "PUBLIC", + "verified": true, + "verifiedName": "Brand-X", + "verifiedUrl": "https://brandx.example" +}`} + + +##### Pending — poll again + + + {`{ + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "status": "PENDING", + "vpa": "someone@ybl" +}`} + + +##### Failed + + + {`{ + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "status": "FAILED", + "vpa": "someone@ybl", + "errorCode": "ZH", + "errorMessage": "invalid virtual address" +}`} + + +##### Errors + +`PENDING` and `FAILED` are **outcomes**, not HTTP errors — they come back as `200`. The HTTP error statuses are: + +| Status | Code | When | +| :--- | :--- | :--- | +| 400 | `invalid-request` | `deviceId`, `mobile`, or `vpa` is missing or fails format validation (`vpa` must be `prefix@handle`, max 255 characters), or the request body is malformed. | +| 401 | `device-not-linked` | This device is not bound for this user. | +| 404 | `user-not-found` | No user for this mobile. | +| 409 | `invalid-user-state` | The user is not `active`, or has no active VPA to resolve from. | +| 500 | `internal-error` | Something went wrong on Setu's side. | + +### Next + +- **[Payee blocklist](/payments/upi-issuance/payee-blocklist)** — block payees a user never wants to transact with. +- **[Testing on QA env](/payments/upi-issuance/qa-testing)** — reproduce every scenario. + + From e8fbb976fde043daf77a5ef5167c4d2bd9b022fc Mon Sep 17 00:00:00 2001 From: Anindya Pandey Date: Sun, 26 Jul 2026 12:28:33 +0530 Subject: [PATCH 08/27] docs(UPIIS-69): serve VPA resolution at /api/v1/vpa/resolve Move the new resolution endpoint off /onboarding/ to /api/v1/vpa/resolve (VPA/payments operation, not onboarding) across the OpenAPI reference, the guide, and the QA-testing X-Sim-Resolution examples. Existing /onboarding/ endpoints are unchanged. --- api-references/payments/upi-issuance.json | 2 +- content/payments/upi-issuance/qa-testing.mdx | 4 ++-- content/payments/upi-issuance/vpa-resolution.mdx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api-references/payments/upi-issuance.json b/api-references/payments/upi-issuance.json index 655c2e89..f9d926f6 100644 --- a/api-references/payments/upi-issuance.json +++ b/api-references/payments/upi-issuance.json @@ -1185,7 +1185,7 @@ } } }, - "/api/v1/onboarding/vpa/resolve": { + "/api/v1/vpa/resolve": { "post": { "tags": [ "VPA management" diff --git a/content/payments/upi-issuance/qa-testing.mdx b/content/payments/upi-issuance/qa-testing.mdx index 5f7718b0..61e1cbb2 100644 --- a/content/payments/upi-issuance/qa-testing.mdx +++ b/content/payments/upi-issuance/qa-testing.mdx @@ -53,7 +53,7 @@ On the QA env there is no telecom, so no OTP SMS is sent. To pass verification, #### VPA resolution -On production, resolving a foreign-handle payee returns whatever the payee PSP reports. On the QA env you can pin the resolved values so you can reproduce every shape — a person, a merchant, verified or not, and failures — deterministically. Send an optional `X-Sim-Resolution` header on `POST /onboarding/vpa/resolve` whose value is a JSON object of the values you expect back. Send it on every poll (it is read on the poll that returns `RESOLVED`). +On production, resolving a foreign-handle payee returns whatever the payee PSP reports. On the QA env you can pin the resolved values so you can reproduce every shape — a person, a merchant, verified or not, and failures — deterministically. Send an optional `X-Sim-Resolution` header on `POST /vpa/resolve` whose value is a JSON object of the values you expect back. Send it on every poll (it is read on the poll that returns `RESOLVED`). Every field is optional; anything you leave out falls back to a default, and with no header at all the resolution uses default values. Failures carry no payee detail, so `entityType`, the merchant fields and `verified` apply only to a success. @@ -71,7 +71,7 @@ Every field is optional; anything you leave out falls back to a default, and wit A verified merchant with pinned values: ```bash -curl https:///api/v1/onboarding/vpa/resolve \ +curl https:///api/v1/vpa/resolve \ -H 'X-Sim-Resolution: {"entityType":"ENTITY","name":"ACME****PVT","accountType":"CURRENT","ifsc":"HDFC0000123","merchant":{"mcc":"5411","brandName":"MyBrand","legalName":"Acme Pvt Ltd","merchantType":"SMALL"},"verified":{"name":"MyBrand","url":"https://mybrand.example"}}' \ -H 'Content-Type: application/json' -d '' ``` diff --git a/content/payments/upi-issuance/vpa-resolution.mdx b/content/payments/upi-issuance/vpa-resolution.mdx index 6a8a36f3..ef454150 100644 --- a/content/payments/upi-issuance/vpa-resolution.mdx +++ b/content/payments/upi-issuance/vpa-resolution.mdx @@ -25,7 +25,7 @@ The `status` field carries the outcome; the HTTP status stays `200` for all thre ### Resolve a payee VPA -`POST /api/v1/onboarding/vpa/resolve` (enveloped) — requires an `active` user. +`POST /api/v1/vpa/resolve` (enveloped) — requires an `active` user. | Field | Type | Required | Notes | | :--- | :--- | :--- | :--- | From 0aedad93841ead5adc152a4ffe8c93e0e2775fe0 Mon Sep 17 00:00:00 2001 From: Anindya Pandey Date: Mon, 27 Jul 2026 01:38:56 +0530 Subject: [PATCH 09/27] docs(UPIIS-69): add X-Sim-Resolution QA header to VPA resolve reference Co-Authored-By: Claude Opus 5 (1M context) --- api-references/payments/upi-issuance.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/api-references/payments/upi-issuance.json b/api-references/payments/upi-issuance.json index f9d926f6..5d1f53be 100644 --- a/api-references/payments/upi-issuance.json +++ b/api-references/payments/upi-issuance.json @@ -1206,6 +1206,18 @@ "example": "req-8f3a2b1c" }, "example": "req-8f3a2b1c" + }, + { + "name": "X-Sim-Resolution", + "in": "header", + "description": "**QA / sandbox only.** Pins the values this resolution returns, so a person, a merchant, a verified payee, or a failure can be reproduced deterministically instead of depending on what a real payee PSP reports. The value is a JSON object of the fields you expect back, e.g. {\"name\":\"Test Merchant\",\"entityType\":\"ENTITY\",\"verified\":true} or {\"status\":\"FAILED\",\"errorCode\":\"ZH\"}. Send it on every poll \u2014 it is read on the poll that returns RESOLVED. Ignored on production.", + "allowEmptyValue": true, + "schema": { + "type": "string", + "description": "**QA / sandbox only.** Pins the values this resolution returns, so a person, a merchant, a verified payee, or a failure can be reproduced deterministically instead of depending on what a real payee PSP reports. The value is a JSON object of the fields you expect back, e.g. {\"name\":\"Test Merchant\",\"entityType\":\"ENTITY\",\"verified\":true} or {\"status\":\"FAILED\",\"errorCode\":\"ZH\"}. Send it on every poll \u2014 it is read on the poll that returns RESOLVED. Ignored on production.", + "example": "{\"name\":\"Test Merchant\",\"entityType\":\"ENTITY\",\"verified\":true}" + }, + "example": "{\"name\":\"Test Merchant\",\"entityType\":\"ENTITY\",\"verified\":true}" } ], "requestBody": { From cb538e756d24027ae9b442e16b460e0f8504426c Mon Sep 17 00:00:00 2001 From: Anindya Pandey Date: Mon, 27 Jul 2026 02:03:58 +0530 Subject: [PATCH 10/27] docs(UPIIS-69): add VPA resolution to the sidebar 44e7782 added vpa-resolution.mdx and renumbered the surrounding pages' frontmatter, but never regenerated menuItems.json. The app derives both routes and sidebar from that file, so the page 404'd. Syncs menuItems.json to the MDX frontmatter: VPA resolution at 5 (after Payee blocklist), Testing on QA env 5 -> 6, API reference 6 -> 7. Co-Authored-By: Claude Opus 5 (1M context) --- content/menuItems.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/menuItems.json b/content/menuItems.json index 37baad02..0806ad73 100644 --- a/content/menuItems.json +++ b/content/menuItems.json @@ -1 +1 @@ -{"home":[{"name":"Payments","path":"payments","order":0,"visible_in_sidebar":true,"api_reference":true,"children":[{"name":"BBPS BillCollect","path":"bbps","order":0,"visible_in_sidebar":true,"children":[{"name":"API reference","visible_in_sidebar":true,"page_title":"BBPS API reference","path":"api-reference","order":9},{"name":"Axis BBPS","visible_in_sidebar":false,"page_title":"Axis BBPS API Approach Document","path":"axis","order":10},{"name":"Bill Structure","visible_in_sidebar":true,"page_title":"BBPS - Bill Structure","path":"bill-structure","order":5,"children":[{"name":"Special cases","visible_in_sidebar":true,"page_title":"BBPS - Bill Structure special cases","path":"special-cases","order":1}]},{"name":"Go live","visible_in_sidebar":true,"page_title":"BBPS - Go live","path":"go-live","order":3},{"name":"Notifications","visible_in_sidebar":true,"page_title":"BBPS - Notifications","path":"notifications","order":4},{"name":"Overview","visible_in_sidebar":true,"page_title":"BBPS - Overview","path":"overview","order":1},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"BBPS - Quickstart","path":"quickstart","order":2,"children":[{"name":"API integration","visible_in_sidebar":true,"page_title":"BBPS - API integration","path":"api-integration","order":2},{"name":"No-code CSV","visible_in_sidebar":true,"page_title":"BBPS - No-code CSV","path":"no-code-integration","order":1},{"name":"Share bills","visible_in_sidebar":false,"page_title":"BBPS - Share bills","path":"share-biils","order":1}]},{"name":"Reports API","visible_in_sidebar":true,"page_title":"BBPS - Reports API","path":"reports","order":6},{"name":"Additional resources","visible_in_sidebar":true,"page_title":"BBPS - Additional Resources","path":"resources","order":8,"children":[{"name":"Errors","visible_in_sidebar":true,"page_title":"BBPS error codes","path":"errors","order":4},{"name":"JWT authentication","visible_in_sidebar":true,"page_title":"UPI Deeplinks JWT authentication","path":"jwt","order":2},{"name":"OAuth 2.0","visible_in_sidebar":true,"page_title":"BBPS OAuth 2.0","path":"oauth","order":1},{"name":"Settlement object","visible_in_sidebar":true,"page_title":"UPI Deeplinks settlement object","path":"settlement-object","order":3}]}]},{"name":"BBPS BillPay","path":"billpay","order":1,"versions":["v1","v2"],"default_version":"v2","visible_in_sidebar":true,"children":[{"name":"API integration","visible_in_sidebar":true,"page_title":"BBPS Billpay API integration","path":"api-integration","order":1,"children":[{"name":"API payload encryption (optional)","visible_in_sidebar":true,"page_title":"BBPS COU - API payload encryption (AES-CBC)","path":"api-encryption","order":9},{"name":"API reference","visible_in_sidebar":true,"page_title":"COU Direct Connectivity API reference","path":"api-reference","order":10},{"name":"List of APIs","visible_in_sidebar":true,"page_title":"BBPS COU - List of APIs","path":"apis","order":2},{"name":"FX Retail","visible_in_sidebar":true,"page_title":"BBPS COU - FX Retail (Forex category)","path":"fx-retail","order":8},{"name":"Harmonization of TAT","visible_in_sidebar":true,"page_title":"Harmonization Of TAT (Disputes API)","path":"harmonization_of_tat","order":6},{"name":"Objects","visible_in_sidebar":true,"page_title":"BBPS COU - Objects","path":"objects","order":11},{"name":"Paying Bills","visible_in_sidebar":true,"page_title":"Paying Bills","path":"paying-bills","order":4,"children":[{"name":"Paying for options","visible_in_sidebar":true,"page_title":"Paying for alternative options","path":"bill-payment-options","order":4},{"name":"Passing CCF","visible_in_sidebar":true,"page_title":"Customer Convenience Fee (CCF) Integration Guide","path":"customer-convenience-fee","order":3},{"name":"Paying multiple bills","visible_in_sidebar":true,"page_title":"Paying for multiple bills","path":"multi-bill-processing","order":5},{"name":"Paying for plans","visible_in_sidebar":true,"page_title":"Paying for plans","path":"plan-mdm-integration","order":6},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"BBPS Bill Payment Integration Guide","path":"quickstart","order":1},{"name":"Remitter Details","visible_in_sidebar":true,"page_title":"Passing Remitter Details","path":"remittance_flows_guide","order":2}]},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"BBPS COU - API integration","path":"quickstart","order":1},{"name":"Integrating with UPMS","visible_in_sidebar":true,"page_title":"BBPS COU - Integrating with UPMS","path":"upms","order":7},{"name":"Migration Guide to v2","visible_in_sidebar":true,"page_title":"BBPS COU - Migration Guide to v2","path":"v2-migration","order":5},{"name":"Webhooks","visible_in_sidebar":true,"page_title":"BBPS COU - Webhooks","path":"webhooks","order":3}]},{"name":"API reference","visible_in_sidebar":false,"page_title":"BillPay API reference","path":"api-reference","order":5},{"name":null,"visible_in_sidebar":null,"page_title":null,"path":"mcp","order":null,"children":[{"name":"Integration Guide","visible_in_sidebar":false,"page_title":"MCP Server for Bill Payments - Integration Guide","path":"integration-guide","order":1},{"name":"Tools and Prompts","visible_in_sidebar":false,"page_title":"MCP Server for Bill Payments - Tools and Prompts","path":"tools-and-prompts","order":2}]},{"name":"Prepaid Recharge","visible_in_sidebar":true,"page_title":"BBPS Billpay Prepaid Recharge APIs","path":"mobile-prepaid-recharge","order":3,"children":[{"name":"API reference","visible_in_sidebar":true,"page_title":"Mobile Prepaid Recharge API reference","path":"api-reference","order":2},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"Mobile Prepaid Recharge quickstart","path":"quickstart","order":1},{"name":"Webhooks","visible_in_sidebar":true,"page_title":"Mobile Prepaid Recharge Webhooks","path":"webhooks","order":3}]},{"name":"Overview","visible_in_sidebar":true,"page_title":"BBPS Billpay Overview","path":"overview","order":0},{"name":"Pre-built screens","visible_in_sidebar":true,"page_title":"BBPS Billpay pre-built screens","path":"pre-built-screens","order":2,"children":[{"name":"API reference","visible_in_sidebar":false,"page_title":"BBPS Billpay API reference","path":"api-reference-wl","order":4},{"name":"API reference","visible_in_sidebar":true,"page_title":"BBPS Billpay API reference","path":"api-reference","order":5},{"name":"Custom payment","visible_in_sidebar":true,"page_title":"BBPS Billpay custom payment","path":"custom-payment","order":2,"children":[{"name":"Android","visible_in_sidebar":true,"page_title":"BBPS Billpay android integration for custom payment","path":"android","order":3},{"name":"Required APIs","visible_in_sidebar":true,"page_title":"BBPS Billpay APIs for custom payment","path":"apis","order":1},{"name":"Cross platform","visible_in_sidebar":true,"page_title":"BBPS Billpay cross-platform integration for custom payment","path":"cross-platform","order":3},{"name":"iOS","visible_in_sidebar":true,"page_title":"BBPS Billpay iOS integration for custom payment","path":"iOS","order":4},{"name":"Website","visible_in_sidebar":true,"page_title":"BBPS Billpay website integration for custom payment","path":"website","order":2}]},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"BBPS Billpay Quickstart","path":"quickstart","order":1,"children":[{"name":"Android","visible_in_sidebar":true,"page_title":"BBPS Billpay Android integration","path":"android","order":2},{"name":"API","visible_in_sidebar":true,"page_title":"BBPS Billpay API","path":"api","order":2},{"name":"Cross platform","visible_in_sidebar":true,"page_title":"BBPS Billpay cross platform integration","path":"cross-platform","order":4},{"name":"iOS","visible_in_sidebar":true,"page_title":"BBPS Billpay iOS integration","path":"iOS","order":3},{"name":"Website","visible_in_sidebar":true,"page_title":"BBPS Billpay website","path":"website","order":1}]},{"name":"Remitter Details","visible_in_sidebar":true,"page_title":"Remitter Details For Bill Payments Integration Guide","path":"remitter-details","order":4},{"name":"Webhooks","visible_in_sidebar":true,"page_title":"BBPS Billpay webhooks","path":"webhooks","order":2}]},{"name":null,"visible_in_sidebar":null,"page_title":null,"path":"v1","order":null,"children":[{"name":"API integration","visible_in_sidebar":true,"page_title":"BBPS Billpay API integration","path":"api-integration","order":1,"children":[{"name":"API reference","visible_in_sidebar":true,"page_title":"COU Direct Connectivity API reference","path":"api-reference","order":5},{"name":"List of APIs","visible_in_sidebar":true,"page_title":"BBPS COU - List of APIs","path":"apis","order":2},{"name":"Deprecated APIs","visible_in_sidebar":false,"page_title":"BBPS COU - API integration (deprecated)","path":"deprecated","order":4,"children":[{"name":"Mock environment","visible_in_sidebar":false,"page_title":"BBPS Billpay Mock environment","path":"mock-environment","order":2},{"name":"Polling","visible_in_sidebar":false,"page_title":"BBPS Billpay polling","path":"polling","order":2}]},{"name":"Objects","visible_in_sidebar":true,"page_title":"BBPS COU - Objects","path":"objects","order":3},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"BBPS COU - API integration","path":"quickstart","order":1},{"name":"Webhooks","visible_in_sidebar":true,"page_title":"BBPS COU - Webhooks","path":"webhooks","order":4}]},{"name":"API reference","visible_in_sidebar":false,"page_title":"BillPay API reference","path":"api-reference","order":5},{"name":"Overview","visible_in_sidebar":true,"page_title":"BBPS Billpay Overview","path":"overview","order":0},{"name":"Pre-built screens","visible_in_sidebar":true,"page_title":"BBPS Billpay pre-built screens","path":"pre-built-screens","order":2,"children":[{"name":"API reference","visible_in_sidebar":false,"page_title":"BBPS Billpay API reference","path":"api-reference-wl","order":4},{"name":"API reference","visible_in_sidebar":true,"page_title":"BBPS Billpay API reference","path":"api-reference","order":4},{"name":"Custom payment","visible_in_sidebar":true,"page_title":"BBPS Billpay custom payment","path":"custom-payment","order":2,"children":[{"name":"Android","visible_in_sidebar":true,"page_title":"BBPS Billpay android integration for custom payment","path":"android","order":3},{"name":"Required APIs","visible_in_sidebar":true,"page_title":"BBPS Billpay APIs for custom payment","path":"apis","order":1},{"name":"Cross platform","visible_in_sidebar":true,"page_title":"BBPS Billpay cross-platform integration for custom payment","path":"cross-platform","order":3},{"name":"iOS","visible_in_sidebar":true,"page_title":"BBPS Billpay iOS integration for custom payment","path":"iOS","order":4},{"name":"Website","visible_in_sidebar":true,"page_title":"BBPS Billpay website integration for custom payment","path":"website","order":2}]},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"BBPS Billpay Quickstart","path":"quickstart","order":1,"children":[{"name":"Android","visible_in_sidebar":true,"page_title":"BBPS Billpay Android integration","path":"android","order":2},{"name":"API","visible_in_sidebar":true,"page_title":"BBPS Billpay API","path":"api","order":2},{"name":"Cross platform","visible_in_sidebar":true,"page_title":"BBPS Billpay cross platform integration","path":"cross-platform","order":4},{"name":"iOS","visible_in_sidebar":true,"page_title":"BBPS Billpay iOS integration","path":"iOS","order":3},{"name":"Website","visible_in_sidebar":true,"page_title":"BBPS Billpay website","path":"website","order":1}]},{"name":"Webhooks","visible_in_sidebar":true,"page_title":"BBPS Billpay webhooks","path":"webhooks","order":2}]}]}]},{"name":"WhatsApp Collect","path":"whatsapp-collect","order":3,"visible_in_sidebar":true,"children":[{"name":"API Integration","visible_in_sidebar":true,"page_title":"WhatsApp Collect API Integration","path":"api-integration","order":3},{"name":"API reference","visible_in_sidebar":true,"page_title":"WhatsApp Collect API reference","path":"api-reference","order":5},{"name":"Error codes","visible_in_sidebar":true,"page_title":"WhatsApp Collect error codes","path":"errors","order":4},{"name":"Collection journey","visible_in_sidebar":true,"page_title":"WhatsApp Collect Journey","path":"journey","order":1},{"name":"Overview","visible_in_sidebar":true,"page_title":"WhatsApp Collect Overview","path":"overview","order":0},{"name":"Collection reminders","visible_in_sidebar":true,"page_title":"WhatsApp Collect reminders","path":"reminders","order":2}]},{"name":"UPI DeepLinks","path":"upi-deeplinks","order":4,"visible_in_sidebar":true,"children":[{"name":"API reference","visible_in_sidebar":true,"page_title":"UPI Deeplinks API reference","path":"api-reference","order":8},{"name":"Notifications","visible_in_sidebar":true,"page_title":"UPI Deeplinks Notifications","path":"notifications","order":6},{"name":"Overview","visible_in_sidebar":true,"page_title":"UPI Deeplinks Overview","path":"overview","order":0},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"UPI Deeplinks quickstart","path":"quickstart","order":1,"children":[{"name":"Go Live","visible_in_sidebar":true,"page_title":"UPI Deeplinks go live","path":"go-live","order":1}]},{"name":"Refunds","visible_in_sidebar":true,"page_title":"UPI Deeplinks Refunds","path":"refunds","order":4},{"name":"Reports API","visible_in_sidebar":true,"page_title":"UPI Deeplinks Reports API","path":"reports","order":5},{"name":"Additional resources","visible_in_sidebar":true,"page_title":"UPI Deeplinks additonal resources","path":"resources","order":6,"children":[{"name":"JWT authentication","visible_in_sidebar":true,"page_title":"UPI Deeplinks JWT authentication","path":"jwt","order":2},{"name":"OAuth 2.0","visible_in_sidebar":true,"page_title":"UPI Deeplinks OAuth 2.0","path":"oauth","order":1},{"name":"Settlement object","visible_in_sidebar":true,"page_title":"UPI Deeplinks settlement object","path":"settlement-object","order":3}]},{"name":"SDKs","visible_in_sidebar":true,"page_title":"UPI Deeplinks SDKs","path":"sdks","order":3},{"name":"Third party verification","visible_in_sidebar":true,"page_title":"UPI Deeplinks third party verification","path":"third-party-verification","order":3}]},{"name":"UPI Setu","path":"umap","order":7,"visible_in_sidebar":true,"children":[{"name":"API integration","visible_in_sidebar":true,"page_title":"UPI Setu - API integration","path":"api-integration","order":2,"children":[{"name":"Aggregators","visible_in_sidebar":true,"page_title":"UPI Setu - API integration for aggregators","path":"aggregators","order":1},{"name":"Merchants","visible_in_sidebar":true,"page_title":"UPI Setu - API integration for merchants","path":"merchants","order":2}]},{"name":"API reference","visible_in_sidebar":true,"page_title":"UPI Setu - API reference","path":"api-reference","order":8},{"name":"UPI mandates","visible_in_sidebar":true,"page_title":"UPI mandates","path":"mandates","order":4,"children":[{"name":"Mandate operations","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Mandate operations","path":"generic","order":5,"children":[{"name":"Pause","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Pause","path":"pause","order":3},{"name":"Revoke","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Revoke","path":"revoke","order":2},{"name":"Unpause","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Unpause","path":"unpause","order":4},{"name":"Update","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Update","path":"update","order":1}]},{"name":"OneShot","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - OneShot","path":"one-shot","order":1,"children":[{"name":"Check status","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Check payment status","path":"check-status","order":4},{"name":"Create","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Create One Time Mandate","path":"create","order":1},{"name":"Execute","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Execute One Time Mandate","path":"execute","order":3},{"name":"Pre Debit Notify","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Send One Time Mandate Pre Debit Notification","path":"pre-debit-notify","order":2}]},{"name":"Recur","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Recur","path":"recur","order":3,"children":[{"name":"Check payment status","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Check payment status","path":"check-status","order":4},{"name":"Create","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Create recurring mandate","path":"create","order":1},{"name":"Execute","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Execute mandate","path":"execute","order":3},{"name":"Pre Debit Notify","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Send Recurring Mandate Pre Debit Notification","path":"pre-debit-notify","order":2}]},{"name":"Reserve","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Reserve","path":"reserve","order":2,"children":[{"name":"Check status","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Check payment status","path":"check-status","order":4},{"name":"Create","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Create Reserve Mandate","path":"create","order":1},{"name":"Execute","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Execute Reserve Mandate","path":"execute","order":3}]},{"name":"ReservePlus","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - ReservePlus","path":"reserve-plus","order":4,"children":[{"name":"Check status","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Check payment status","path":"check-status","order":3},{"name":"Create","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Create single block multi-debit","path":"create","order":1},{"name":"Execute","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Execute single block multi-debit","path":"execute","order":2}]}]},{"name":"Merchant on-boarding","visible_in_sidebar":true,"page_title":"UPI Setu - Merchant onboarding","path":"merchant-onboarding","order":2,"children":[{"name":"Check VPA availability","visible_in_sidebar":true,"page_title":"UPI Setu - Merchant on-boarding - Check VPA availability","path":"check-vpa-availability-api","order":2},{"name":"Setup a merchant","visible_in_sidebar":true,"page_title":"UPI Setu - Merchant on-boarding - Setup merchant","path":"create-merchant-api","order":1},{"name":"Registering VPA","visible_in_sidebar":true,"page_title":"UPI Setu - Merchant on-boarding - Registering a VPA","path":"create-vpa-api","order":3}]},{"name":"Notifications and alerts","visible_in_sidebar":true,"page_title":"UPI Setu - Notifications and alerts","path":"notifications","order":7,"children":[{"name":"VPA verification","visible_in_sidebar":true,"page_title":"UPI Setu - Notifications and alerts - Customer VPA verification","path":"customer-vpa-verification","order":6},{"name":"Mandates","visible_in_sidebar":true,"page_title":"UPI Setu - Notifications and alerts - Mandates","path":"mandates","order":3,"children":[{"name":"Create","visible_in_sidebar":true,"page_title":"UPI Setu - Notifications and alerts - Creation of mandate","path":"create","order":1},{"name":"Execute","visible_in_sidebar":true,"page_title":"UPI Setu - Notifications and alerts - Mandate execution","path":"execute","order":7},{"name":"Notify","visible_in_sidebar":true,"page_title":"UPI Setu - Notifications and alerts - Mandate pre-debit notifications","path":"notify","order":6},{"name":"Pause","visible_in_sidebar":true,"page_title":"UPI Setu - Notifications and alerts - Pausing mandate","path":"pause","order":4},{"name":"Revoke","visible_in_sidebar":true,"page_title":"UPI Setu - Notifications and alerts - Revoking mandate","path":"revoke","order":3},{"name":"Unpause","visible_in_sidebar":true,"page_title":"UPI Setu - Notifications and alerts - Unpausing mandate","path":"unpause","order":5},{"name":"Update","visible_in_sidebar":true,"page_title":"UPI Setu - Notifications and alerts - Updating mandate","path":"update","order":2}]},{"name":"Payments","visible_in_sidebar":true,"page_title":"UPI Setu - Notifications and alerts - Payments","path":"payments","order":2},{"name":"Refunds","visible_in_sidebar":true,"page_title":"UPI Setu - Notifications and alerts - Refunds","path":"refunds","order":4},{"name":"Verify signature","visible_in_sidebar":true,"page_title":"UMAP - Events and notifications","path":"verify-signature","order":1}]},{"name":"Overview","visible_in_sidebar":true,"page_title":"UPI Setu - Overview","path":"overview","order":0},{"name":"UPI payments","visible_in_sidebar":true,"page_title":"UPI payments","path":"payments","order":3,"children":[{"name":"Collect","visible_in_sidebar":true,"page_title":"UPI payments - Collect","path":"collect","order":2,"children":[{"name":"Check status","visible_in_sidebar":true,"page_title":"UPI Setu payments - Collect request - Check payment status","path":"check-status","order":3},{"name":"Create","visible_in_sidebar":true,"page_title":"UPI Setu payments - Create collect request","path":"create-collect-request","order":2},{"name":"Verify customer VPA","visible_in_sidebar":true,"page_title":"UPI Setu payments - Verify customer VPA","path":"verify-customer-vpa-api","order":1}]},{"name":"Flash","visible_in_sidebar":true,"page_title":"UPI payments - Flash","path":"flash","order":1,"children":[{"name":"Check status","visible_in_sidebar":true,"page_title":"UPI Setu payments - Intent/QR - Check payment status","path":"check-status","order":2},{"name":"Dynamic QR","visible_in_sidebar":true,"page_title":"UPI Setu payments - Create Dynamic QR","path":"create-dqr","order":1},{"name":"Static QR","visible_in_sidebar":true,"page_title":"UPI Setu payments - Create Static QR","path":"create-sqr","order":1}]},{"name":"TPV","visible_in_sidebar":true,"page_title":"UPI payments - TPV","path":"tpv","order":3,"children":[{"name":"Check status","visible_in_sidebar":true,"page_title":"UPI Setu - Payments - TPV - Check payment status","path":"check-status","order":2},{"name":"Create","visible_in_sidebar":true,"page_title":"UPI Setu - Payments - Create TPV API","path":"create-tpv","order":1},{"name":"Payments","visible_in_sidebar":true,"page_title":"UMAP - Notifications and alerts - Payments","path":"life-cycle","order":1}]}]},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"UPI Setu - Quickstart","path":"quickstart","order":1,"children":[{"name":"Aggregators","visible_in_sidebar":true,"page_title":"UPI Setu - Quickstart for aggregators","path":"aggregators","order":1},{"name":"Merchants","visible_in_sidebar":true,"page_title":"UPI Setu - Quickstart for merchants","path":"merchants","order":2}]},{"name":"Refunds and disputes","visible_in_sidebar":true,"page_title":"UPI Setu - Refunds and disputes","path":"refunds-disputes","order":6,"children":[{"name":"Check refund status","visible_in_sidebar":true,"page_title":"UPI Setu - Refunds and disputes - Check refund status API","path":"check-refund-status-api","order":2},{"name":"Create refund","visible_in_sidebar":true,"page_title":"UPI Setu - Refunds and disputes - Create refund API","path":"create-refund-api","order":1},{"name":"Fetch dispute","visible_in_sidebar":true,"page_title":"UPI Setu - Refunds and disputes - Fetch dispute API","path":"fetch-dispute-api","order":3}]},{"name":"Transaction Monitoring","visible_in_sidebar":false,"page_title":"UPI Setu - Transaction Monitoring","path":"transaction-monitoring","order":5,"children":[{"name":"Check status","visible_in_sidebar":true,"page_title":"UPI Setu - Transaction monitoring - Check status API","path":"check-status-api","order":1},{"name":"Check status history","visible_in_sidebar":true,"page_title":"UPI Setu - Transaction monitoring - Check status sistory API","path":"check-status-history-api","order":2},{"name":"Fetch payment","visible_in_sidebar":true,"page_title":"UPI Setu - Transaction monitoring - Fetch payment API","path":"fetch-payment-api","order":3}]}]},{"name":"UPI Issuance","path":"upi-issuance","order":8,"visible_in_sidebar":true,"children":[{"name":"The API envelope","visible_in_sidebar":true,"page_title":"UPI Issuance API envelope","path":"api-envelope","order":2},{"name":"API reference","visible_in_sidebar":true,"page_title":"UPI Issuance API reference","path":"api-reference","order":6},{"name":"User Onboarding","visible_in_sidebar":true,"page_title":"UPI Issuance User Onboarding","path":"onboarding","order":3,"children":[{"name":"API integration","visible_in_sidebar":true,"page_title":"UPI Issuance API integration","path":"api-integration","order":1,"children":[{"name":"Device binding","visible_in_sidebar":true,"page_title":"UPI Issuance device binding","path":"device-binding","order":0},{"name":"Programs","visible_in_sidebar":true,"page_title":"UPI Issuance programs","path":"programs","order":3},{"name":"OTP verification","visible_in_sidebar":true,"page_title":"UPI Issuance OTP verification","path":"user-otp","order":1},{"name":"VPA management","visible_in_sidebar":true,"page_title":"UPI Issuance VPA management","path":"vpa-management","order":2}]},{"name":"Onboarding states","visible_in_sidebar":true,"page_title":"UPI Issuance onboarding states","path":"onboarding-states","order":0}]},{"name":"Overview","visible_in_sidebar":true,"page_title":"UPI Issuance Overview","path":"overview","order":0},{"name":"Payee blocklist","visible_in_sidebar":true,"page_title":"UPI Issuance payee blocklist","path":"payee-blocklist","order":4},{"name":"Testing on QA env","visible_in_sidebar":true,"page_title":"UPI Issuance QA env testing","path":"qa-testing","order":5},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"UPI Issuance Quickstart","path":"quickstart","order":1}]}]},{"name":"Data","path":"data","order":1,"visible_in_sidebar":true,"children":[{"name":"KYC","path":"kyc","order":0,"visible_in_sidebar":true,"children":[{"name":"Secure Data Add-On","visible_in_sidebar":true,"page_title":"Setu Encrypted APIs","path":"encryption","order":2},{"name":"Overview","visible_in_sidebar":true,"page_title":"Setu KYC Overview","path":"overview","order":1}]},{"name":"PAN verification","path":"pan","order":0,"visible_in_sidebar":false,"children":[{"name":"API reference","visible_in_sidebar":true,"page_title":"PAN verification API reference","path":"api-reference","order":1},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"PAN verification quickstart","path":"quickstart","order":0}]},{"name":"Aadhaar eSign","path":"esign","order":2,"visible_in_sidebar":true,"children":[{"name":"API reference","visible_in_sidebar":true,"page_title":"Aadhaar eSign API reference","path":"api-reference","order":9},{"name":"Error codes","visible_in_sidebar":true,"page_title":"Aadhaar eSign error codes","path":"error-codes","order":8},{"name":"eStamp overview","visible_in_sidebar":true,"page_title":"eStamp overview","path":"estamp","order":2},{"name":"Flexible eSign guide","visible_in_sidebar":true,"page_title":"Integration guide with flexible signature coordinates","path":"flexi-esign","order":4},{"name":"eSign Name Match","visible_in_sidebar":true,"page_title":"Aadhaar eSign Name Match","path":"name-match","order":6},{"name":"Notifications","visible_in_sidebar":true,"page_title":"Aadhaar eSign Notifications","path":"notifications","order":7},{"name":"Overview","visible_in_sidebar":true,"page_title":"Aadhaar eSign overview","path":"overview","order":1},{"name":"PDF templates","visible_in_sidebar":true,"page_title":"Integration guide with pdf templating API's","path":"pdf-templating","order":5},{"name":"Integration guide","visible_in_sidebar":true,"page_title":"Aadhaar eSign integration guide","path":"quickstart","order":3}]},{"name":"DigiLocker","path":"digilocker","order":3,"visible_in_sidebar":false,"children":[{"name":"API reference","visible_in_sidebar":true,"page_title":"Digilocker API reference","path":"api-reference","order":3},{"name":"Error codes","visible_in_sidebar":true,"page_title":"DigiLocker error codes","path":"error-codes","order":4},{"name":"Overview","visible_in_sidebar":true,"page_title":"Digilocker overview","path":"overview","order":0},{"name":"Integration guide","visible_in_sidebar":true,"page_title":"Digilocker quickstart","path":"quickstart","order":1}]},{"name":"AA Gateway","path":"account-aggregator","order":4,"versions":["v1","v2"],"default_version":"v2","visible_in_sidebar":true,"children":[{"name":"API integration","visible_in_sidebar":true,"page_title":"Account Aggregator API integration","path":"api-integration","order":3,"children":[{"name":"Account Availability","visible_in_sidebar":true,"page_title":"Account Aggregator Account Availability","path":"account-availability-apis","order":5},{"name":"Consent flow","visible_in_sidebar":true,"page_title":"Account Aggregator Consent flow","path":"consent-flow","order":1},{"name":"Data flow","visible_in_sidebar":true,"page_title":"Account Aggregator Data flow","path":"data-apis","order":2},{"name":"Active FIPs","visible_in_sidebar":true,"page_title":"Account Aggregator Active FIPs","path":"fip-apis","order":4},{"name":"Notifications","visible_in_sidebar":true,"page_title":"Account Aggregator Notifications","path":"notifications","order":3}]},{"name":"API reference","visible_in_sidebar":true,"page_title":"Account Aggregator API reference","path":"api-reference","order":10},{"name":"Consent object","visible_in_sidebar":true,"page_title":"Account Aggregator consent object","path":"consent-object","order":4},{"name":"Embed Setu screens","visible_in_sidebar":true,"page_title":"Account Aggregator Embed Setu screens","path":"embed-setu-aa","order":7},{"name":"FI data types","visible_in_sidebar":true,"page_title":"Account Aggregator FI data types","path":"fi-data-types","order":5},{"name":"Licenses and go live","visible_in_sidebar":true,"page_title":"Account Aggregator license and go live process","path":"licenses-and-go-live","order":8,"children":[{"name":"Go live","visible_in_sidebar":true,"page_title":"FIU go live process","path":"go-live","order":2},{"name":"Licenses","visible_in_sidebar":true,"page_title":"Licenses required to participate in AA","path":"licenses","order":1},{"name":"Participants in AA","visible_in_sidebar":true,"page_title":"Participants in AA","path":"participants-in-aa","order":0}]},{"name":"Multi AA gateway","visible_in_sidebar":true,"page_title":"Account Aggregator multi-AA gateway","path":"multi-aa-gateway","order":2},{"name":"Overview","visible_in_sidebar":true,"page_title":"Account Aggregator overview","path":"overview","order":0},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"Account Aggregator quickstart","path":"quickstart","order":1},{"name":null,"visible_in_sidebar":null,"page_title":null,"path":"v1","order":null,"children":[{"name":"API integration","visible_in_sidebar":true,"page_title":"Account Aggregator API integration","path":"api-integration","order":3,"children":[{"name":"Consent flow","visible_in_sidebar":true,"page_title":"Account Aggregator Consent flow","path":"consent-flow","order":1},{"name":"Data flow","visible_in_sidebar":true,"page_title":"Account Aggregator Data flow","path":"data-apis","order":2},{"name":"Active FIPs","visible_in_sidebar":true,"page_title":"Account Aggregator Active FIPs","path":"fip-apis","order":4},{"name":"Notifications","visible_in_sidebar":true,"page_title":"Account Aggregator Notifications","path":"notifications","order":3}]},{"name":"API reference","visible_in_sidebar":true,"page_title":"Account Aggregator API reference","path":"api-reference","order":10},{"name":"Consent object","visible_in_sidebar":true,"page_title":"Account Aggregator Consent object","path":"consent-object","order":4},{"name":"Embed Setu screens","visible_in_sidebar":true,"page_title":"Account Aggregator Embed Setu screens","path":"embed-setu-aa","order":7},{"name":"End-to-end encryption","visible_in_sidebar":false,"page_title":"Account Aggregator End-to-end encryption","path":"encryption","order":1},{"name":"FI data types","visible_in_sidebar":true,"page_title":"Account Aggregator FI data types","path":"fi-data-types","order":5},{"name":"Get started","visible_in_sidebar":false,"page_title":"Account Aggregator getting started","path":"get-started","order":0},{"name":"Licenses and go live","visible_in_sidebar":true,"page_title":"Account Aggregator license and go live process","path":"licenses-and-go-live","order":8,"children":[{"name":"Go live","visible_in_sidebar":true,"page_title":"FIU go live process","path":"go-live","order":2},{"name":"Licenses","visible_in_sidebar":true,"page_title":"Licenses required to participate in AA","path":"licenses","order":1},{"name":"Participants in AA","visible_in_sidebar":true,"page_title":"Participants in AA","path":"participants-in-aa","order":0}]},{"name":"Migration guide","visible_in_sidebar":true,"page_title":"Account Aggregator Migration Guide","path":"migration-guide","order":6,"children":[{"name":"Consent flow","visible_in_sidebar":true,"page_title":"Account Aggregator Consent flow","path":"consent-flow","order":1},{"name":"Data flow","visible_in_sidebar":true,"page_title":"Account Aggregator Data flow","path":"data-flow","order":2},{"name":"Notifications","visible_in_sidebar":true,"page_title":"Account Aggregator Notifications","path":"notifications","order":3}]},{"name":"Overview","visible_in_sidebar":true,"page_title":"Account Aggregator overview","path":"overview","order":0},{"name":"Postman integration","visible_in_sidebar":true,"page_title":"Account Aggregator Postman integration","path":"postman","order":2},{"name":"Quickstart","visible_in_sidebar":false,"page_title":"Account Aggregator quickstart","path":"quickstart-v1","order":1},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"Account Aggregator quickstart","path":"quickstart","order":1},{"name":"Request signing","visible_in_sidebar":false,"page_title":"Account Aggregator Request signing","path":"request-signing","order":1}]}]},{"name":"Bank account verification","path":"bav","order":5,"visible_in_sidebar":false,"children":[{"name":"Bundled BAV","visible_in_sidebar":true,"page_title":"Bundled Bank Account Verification","path":"bundled-bav","order":2,"children":[{"name":"API integration","visible_in_sidebar":true,"page_title":"Bundled BAV API integration","path":"api-integration","order":2},{"name":"API reference","visible_in_sidebar":true,"page_title":"Bundled BAV API reference","path":"api-reference","order":3},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"Quickstart for Bundled BAV","path":"quickstart","order":1}]},{"name":"Penny drop","visible_in_sidebar":true,"page_title":"BAV using penny drop","path":"penny-drop","order":1,"children":[{"name":"API integration","visible_in_sidebar":true,"page_title":"BAV API integration","path":"api-integration","order":1,"children":[{"name":"Async API","visible_in_sidebar":true,"page_title":"BAV Async API integration","path":"async","order":2},{"name":"Sync API","visible_in_sidebar":true,"page_title":"BAV Sync API integration","path":"sync","order":1}]},{"name":"API reference","visible_in_sidebar":true,"page_title":"BAV API reference","path":"api-reference","order":3},{"name":"Notifications","visible_in_sidebar":true,"page_title":"BAV Async Penny drop Notifications","path":"notifications","order":2},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"BAV quickstart","path":"quickstart","order":0}]},{"name":"Penny drop + PennyLess","visible_in_sidebar":true,"page_title":"Bank account verification using Penny drop + PennyLess","path":"pennydrop-pennyless","order":2,"children":[{"name":"API Integration","visible_in_sidebar":true,"page_title":"Penny drop + PennyLess API Integration","path":"api-integration","order":1},{"name":"API reference","visible_in_sidebar":true,"page_title":"Pennydrop-pennyless API reference","path":"api-reference","order":3},{"name":"Notifications","visible_in_sidebar":true,"page_title":"Penny drop + PennyLess Notifications","path":"notifications","order":2}]},{"name":"PennyLess Drop","visible_in_sidebar":true,"page_title":"BAV using PennyLess Drop API","path":"pennyless-drop","order":4,"children":[{"name":"API reference","visible_in_sidebar":true,"page_title":"BAV Pennyless API reference","path":"api-reference","order":2},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"Quickstart for PennyLess drop API","path":"quickstart","order":1}]},{"name":"Reverse Penny drop","visible_in_sidebar":true,"page_title":"BAV using reverse penny drop","path":"reverse-penny-drop","order":3,"children":[{"name":"API integration","visible_in_sidebar":true,"page_title":"RPD API integration","path":"api-integration","order":2},{"name":"API reference","visible_in_sidebar":true,"page_title":"BAV RPD API reference","path":"api-reference","order":4},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"Quickstart for reverse penny drop","path":"quickstart","order":1},{"name":"Webhook Auth","visible_in_sidebar":true,"page_title":"Webhook Authentication","path":"webhook-authentication","order":3}]}]},{"name":"Insights","path":"insights","order":5,"versions":["v1","v2","v3"],"default_version":"v3","visible_in_sidebar":true,"children":[{"name":"API reference","visible_in_sidebar":true,"page_title":"Setu Insights API reference","path":"api-reference","order":4},{"name":"Error codes","visible_in_sidebar":true,"page_title":"Setu Insights error codes","path":"error-code","order":5},{"name":"List of insights","visible_in_sidebar":true,"page_title":"All Setu insights","path":"insights","order":2},{"name":"Notifications","visible_in_sidebar":true,"page_title":"Setu Insights notifications","path":"notifications","order":3},{"name":"Overview","visible_in_sidebar":true,"page_title":"Setu Insights overview","path":"overview","order":0},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"Setu Insights quickstart","path":"quickstart","order":1,"children":[{"name":"API integration","visible_in_sidebar":true,"page_title":"Setu Insights Postman integration","path":"api-integration","order":1},{"name":"Postman integration","visible_in_sidebar":true,"page_title":"Setu Insights Postman integration","path":"postman","order":0}]},{"name":null,"visible_in_sidebar":null,"page_title":null,"path":"v1","order":null,"children":[{"name":"API reference","visible_in_sidebar":true,"page_title":"Setu Insights API reference","path":"api-reference","order":4},{"name":"List of insights","visible_in_sidebar":true,"page_title":"All Setu insights","path":"insights","order":2},{"name":"Notifications","visible_in_sidebar":true,"page_title":"Setu Insights notifications","path":"notifications","order":3},{"name":"Overview","visible_in_sidebar":true,"page_title":"Setu Insights overview","path":"overview","order":0},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"Setu Insights quickstart","path":"quickstart","order":1,"children":[{"name":"API integration","visible_in_sidebar":true,"page_title":"Setu Insights Postman integration","path":"api-integration","order":1},{"name":"Postman integration","visible_in_sidebar":true,"page_title":"Setu Insights Postman integration","path":"postman","order":0}]}]},{"name":null,"visible_in_sidebar":null,"page_title":null,"path":"v2","order":null,"children":[{"name":"API reference","visible_in_sidebar":true,"page_title":"Setu Insights API reference","path":"api-reference","order":4},{"name":"List of insights","visible_in_sidebar":true,"page_title":"All Setu insights","path":"insights","order":2},{"name":"Notifications","visible_in_sidebar":true,"page_title":"Setu Insights notifications","path":"notifications","order":3},{"name":"Overview","visible_in_sidebar":true,"page_title":"Setu Insights overview","path":"overview","order":0},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"Setu Insights quickstart","path":"quickstart","order":1}]}]},{"name":"Signal IQ","path":"signal-iq","order":6,"visible_in_sidebar":true,"children":[{"name":"AA Flow","visible_in_sidebar":true,"page_title":"Signal IQ - AA Flow","path":"aa-flow","order":1},{"name":"Bring Your Own FI Data","visible_in_sidebar":true,"page_title":"Signal IQ - Bring Your Own FI Data","path":"bring-your-own-fi-data","order":4},{"name":"Overview","visible_in_sidebar":true,"page_title":"Signal IQ overview","path":"overview","order":0},{"name":"PDF Flow","visible_in_sidebar":true,"page_title":"Signal IQ - PDF Flow","path":"pdf-flow","order":2},{"name":"API reference","visible_in_sidebar":true,"page_title":"Signal IQ - API reference","path":"report-apis","order":5,"children":[{"name":"API reference","visible_in_sidebar":false,"page_title":"Signal IQ - API reference","path":"api-reference","order":1}]}]},{"name":"ULI","path":"uli","order":6,"visible_in_sidebar":false,"children":[{"name":"API reference","visible_in_sidebar":true,"page_title":"GSTIN verification API reference","path":"api-reference","order":2},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"GST Verification quickstart","path":"quickstart","order":1}]},{"name":"GST verification","path":"gst","order":6,"visible_in_sidebar":false,"children":[{"name":"API reference","visible_in_sidebar":true,"page_title":"GSTIN verification API reference","path":"api-reference","order":2},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"GST Verification quickstart","path":"quickstart","order":1}]},{"name":"Match APIs","path":"match-apis","order":7,"visible_in_sidebar":false,"children":[{"name":"Name match","visible_in_sidebar":true,"page_title":"Name match APIs","path":"name-match","order":1,"children":[{"name":"API reference","visible_in_sidebar":true,"page_title":"Name Match API reference","path":"api-reference","order":4},{"name":"Examples","visible_in_sidebar":true,"page_title":"Name Match API response examples","path":"examples","order":3},{"name":"Overview","visible_in_sidebar":true,"page_title":"Name Match API overview","path":"overview","order":1},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"Name Match API quickstart","path":"quickstart","order":2}]}]},{"name":"eKYC","path":"ekyc","order":8,"visible_in_sidebar":false,"children":[{"name":"API reference","visible_in_sidebar":true,"page_title":"eKYC API reference","path":"api-reference","order":2},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"PAN verification quickstart","path":"quickstart","order":1}]}]},{"name":"Dev tools","path":"dev-tools","order":2,"visible_in_sidebar":true,"children":[{"name":"The Bridge","path":"bridge","order":0,"versions":["v1","v2"],"default_version":"v2","visible_in_sidebar":true,"children":[{"name":"Analytics and reports","visible_in_sidebar":true,"page_title":"Bridge analytics and reports","path":"analytics-and-reports","order":3},{"name":"Configure products","visible_in_sidebar":true,"page_title":"Bridge explore and configure products","path":"explore-and-configure-products","order":2},{"name":"Glossary","visible_in_sidebar":true,"page_title":"Bridge glossary","path":"glossary","order":1},{"name":"Overview","visible_in_sidebar":true,"page_title":"Bridge overview","path":"overview","order":0},{"name":"Settings","visible_in_sidebar":true,"page_title":"Bridge settings","path":"settings","order":4},{"name":"User profile","visible_in_sidebar":true,"page_title":"Bridge user profile","path":"user-profile","order":5},{"name":null,"visible_in_sidebar":null,"page_title":null,"path":"v1","order":null,"children":[{"name":"Bridge configuration","visible_in_sidebar":false,"page_title":"Bridge configuration","path":"configure","order":6},{"name":"Generate Token","visible_in_sidebar":false,"page_title":"Bridge generate token","path":"generate-token","order":4},{"name":"Org settings","visible_in_sidebar":true,"page_title":"Bridge org settings","path":"org-settings","order":3,"children":[{"name":"API keys","visible_in_sidebar":true,"page_title":"API keys","path":"api-keys","order":2,"children":[{"name":"JWT Auth","visible_in_sidebar":false,"page_title":"JWT Auth","path":"jwt-auth","order":3},{"name":"JWT","visible_in_sidebar":true,"page_title":"JWT","path":"jwt","order":1},{"name":"OAuth","visible_in_sidebar":true,"page_title":"OAuth","path":"oauth","order":2}]},{"name":"People","visible_in_sidebar":true,"page_title":"People","path":"people","order":1}]},{"name":"Overview","visible_in_sidebar":true,"page_title":"Bridge overview","path":"overview","order":0},{"name":"Reports","visible_in_sidebar":true,"page_title":"Bridge reports","path":"reports","order":1,"children":[{"name":"Types","visible_in_sidebar":false,"page_title":"Report types","path":"types","order":1}]},{"name":"Reports API","visible_in_sidebar":false,"page_title":"Reports API","path":"reports-api","order":5}]}]}]},{"name":"Sample Category","path":"sample-category","order":3,"visible_in_sidebar":false,"children":[{"name":"Sample Product","path":"sample-product","order":0,"visible_in_sidebar":false,"children":[{"name":"Sample Page","visible_in_sidebar":false,"page_title":"Docs sample page","path":"sample-page","order":0}]}]}]} +{"home":[{"name":"Payments","path":"payments","order":0,"visible_in_sidebar":true,"api_reference":true,"children":[{"name":"BBPS BillCollect","path":"bbps","order":0,"visible_in_sidebar":true,"children":[{"name":"API reference","visible_in_sidebar":true,"page_title":"BBPS API reference","path":"api-reference","order":9},{"name":"Axis BBPS","visible_in_sidebar":false,"page_title":"Axis BBPS API Approach Document","path":"axis","order":10},{"name":"Bill Structure","visible_in_sidebar":true,"page_title":"BBPS - Bill Structure","path":"bill-structure","order":5,"children":[{"name":"Special cases","visible_in_sidebar":true,"page_title":"BBPS - Bill Structure special cases","path":"special-cases","order":1}]},{"name":"Go live","visible_in_sidebar":true,"page_title":"BBPS - Go live","path":"go-live","order":3},{"name":"Notifications","visible_in_sidebar":true,"page_title":"BBPS - Notifications","path":"notifications","order":4},{"name":"Overview","visible_in_sidebar":true,"page_title":"BBPS - Overview","path":"overview","order":1},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"BBPS - Quickstart","path":"quickstart","order":2,"children":[{"name":"API integration","visible_in_sidebar":true,"page_title":"BBPS - API integration","path":"api-integration","order":2},{"name":"No-code CSV","visible_in_sidebar":true,"page_title":"BBPS - No-code CSV","path":"no-code-integration","order":1},{"name":"Share bills","visible_in_sidebar":false,"page_title":"BBPS - Share bills","path":"share-biils","order":1}]},{"name":"Reports API","visible_in_sidebar":true,"page_title":"BBPS - Reports API","path":"reports","order":6},{"name":"Additional resources","visible_in_sidebar":true,"page_title":"BBPS - Additional Resources","path":"resources","order":8,"children":[{"name":"Errors","visible_in_sidebar":true,"page_title":"BBPS error codes","path":"errors","order":4},{"name":"JWT authentication","visible_in_sidebar":true,"page_title":"UPI Deeplinks JWT authentication","path":"jwt","order":2},{"name":"OAuth 2.0","visible_in_sidebar":true,"page_title":"BBPS OAuth 2.0","path":"oauth","order":1},{"name":"Settlement object","visible_in_sidebar":true,"page_title":"UPI Deeplinks settlement object","path":"settlement-object","order":3}]}]},{"name":"BBPS BillPay","path":"billpay","order":1,"versions":["v1","v2"],"default_version":"v2","visible_in_sidebar":true,"children":[{"name":"API integration","visible_in_sidebar":true,"page_title":"BBPS Billpay API integration","path":"api-integration","order":1,"children":[{"name":"API payload encryption (optional)","visible_in_sidebar":true,"page_title":"BBPS COU - API payload encryption (AES-CBC)","path":"api-encryption","order":9},{"name":"API reference","visible_in_sidebar":true,"page_title":"COU Direct Connectivity API reference","path":"api-reference","order":10},{"name":"List of APIs","visible_in_sidebar":true,"page_title":"BBPS COU - List of APIs","path":"apis","order":2},{"name":"FX Retail","visible_in_sidebar":true,"page_title":"BBPS COU - FX Retail (Forex category)","path":"fx-retail","order":8},{"name":"Harmonization of TAT","visible_in_sidebar":true,"page_title":"Harmonization Of TAT (Disputes API)","path":"harmonization_of_tat","order":6},{"name":"Objects","visible_in_sidebar":true,"page_title":"BBPS COU - Objects","path":"objects","order":11},{"name":"Paying Bills","visible_in_sidebar":true,"page_title":"Paying Bills","path":"paying-bills","order":4,"children":[{"name":"Paying for options","visible_in_sidebar":true,"page_title":"Paying for alternative options","path":"bill-payment-options","order":4},{"name":"Passing CCF","visible_in_sidebar":true,"page_title":"Customer Convenience Fee (CCF) Integration Guide","path":"customer-convenience-fee","order":3},{"name":"Paying multiple bills","visible_in_sidebar":true,"page_title":"Paying for multiple bills","path":"multi-bill-processing","order":5},{"name":"Paying for plans","visible_in_sidebar":true,"page_title":"Paying for plans","path":"plan-mdm-integration","order":6},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"BBPS Bill Payment Integration Guide","path":"quickstart","order":1},{"name":"Remitter Details","visible_in_sidebar":true,"page_title":"Passing Remitter Details","path":"remittance_flows_guide","order":2}]},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"BBPS COU - API integration","path":"quickstart","order":1},{"name":"Integrating with UPMS","visible_in_sidebar":true,"page_title":"BBPS COU - Integrating with UPMS","path":"upms","order":7},{"name":"Migration Guide to v2","visible_in_sidebar":true,"page_title":"BBPS COU - Migration Guide to v2","path":"v2-migration","order":5},{"name":"Webhooks","visible_in_sidebar":true,"page_title":"BBPS COU - Webhooks","path":"webhooks","order":3}]},{"name":"API reference","visible_in_sidebar":false,"page_title":"BillPay API reference","path":"api-reference","order":5},{"name":null,"visible_in_sidebar":null,"page_title":null,"path":"mcp","order":null,"children":[{"name":"Integration Guide","visible_in_sidebar":false,"page_title":"MCP Server for Bill Payments - Integration Guide","path":"integration-guide","order":1},{"name":"Tools and Prompts","visible_in_sidebar":false,"page_title":"MCP Server for Bill Payments - Tools and Prompts","path":"tools-and-prompts","order":2}]},{"name":"Prepaid Recharge","visible_in_sidebar":true,"page_title":"BBPS Billpay Prepaid Recharge APIs","path":"mobile-prepaid-recharge","order":3,"children":[{"name":"API reference","visible_in_sidebar":true,"page_title":"Mobile Prepaid Recharge API reference","path":"api-reference","order":2},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"Mobile Prepaid Recharge quickstart","path":"quickstart","order":1},{"name":"Webhooks","visible_in_sidebar":true,"page_title":"Mobile Prepaid Recharge Webhooks","path":"webhooks","order":3}]},{"name":"Overview","visible_in_sidebar":true,"page_title":"BBPS Billpay Overview","path":"overview","order":0},{"name":"Pre-built screens","visible_in_sidebar":true,"page_title":"BBPS Billpay pre-built screens","path":"pre-built-screens","order":2,"children":[{"name":"API reference","visible_in_sidebar":false,"page_title":"BBPS Billpay API reference","path":"api-reference-wl","order":4},{"name":"API reference","visible_in_sidebar":true,"page_title":"BBPS Billpay API reference","path":"api-reference","order":5},{"name":"Custom payment","visible_in_sidebar":true,"page_title":"BBPS Billpay custom payment","path":"custom-payment","order":2,"children":[{"name":"Android","visible_in_sidebar":true,"page_title":"BBPS Billpay android integration for custom payment","path":"android","order":3},{"name":"Required APIs","visible_in_sidebar":true,"page_title":"BBPS Billpay APIs for custom payment","path":"apis","order":1},{"name":"Cross platform","visible_in_sidebar":true,"page_title":"BBPS Billpay cross-platform integration for custom payment","path":"cross-platform","order":3},{"name":"iOS","visible_in_sidebar":true,"page_title":"BBPS Billpay iOS integration for custom payment","path":"iOS","order":4},{"name":"Website","visible_in_sidebar":true,"page_title":"BBPS Billpay website integration for custom payment","path":"website","order":2}]},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"BBPS Billpay Quickstart","path":"quickstart","order":1,"children":[{"name":"Android","visible_in_sidebar":true,"page_title":"BBPS Billpay Android integration","path":"android","order":2},{"name":"API","visible_in_sidebar":true,"page_title":"BBPS Billpay API","path":"api","order":2},{"name":"Cross platform","visible_in_sidebar":true,"page_title":"BBPS Billpay cross platform integration","path":"cross-platform","order":4},{"name":"iOS","visible_in_sidebar":true,"page_title":"BBPS Billpay iOS integration","path":"iOS","order":3},{"name":"Website","visible_in_sidebar":true,"page_title":"BBPS Billpay website","path":"website","order":1}]},{"name":"Remitter Details","visible_in_sidebar":true,"page_title":"Remitter Details For Bill Payments Integration Guide","path":"remitter-details","order":4},{"name":"Webhooks","visible_in_sidebar":true,"page_title":"BBPS Billpay webhooks","path":"webhooks","order":2}]},{"name":null,"visible_in_sidebar":null,"page_title":null,"path":"v1","order":null,"children":[{"name":"API integration","visible_in_sidebar":true,"page_title":"BBPS Billpay API integration","path":"api-integration","order":1,"children":[{"name":"API reference","visible_in_sidebar":true,"page_title":"COU Direct Connectivity API reference","path":"api-reference","order":5},{"name":"List of APIs","visible_in_sidebar":true,"page_title":"BBPS COU - List of APIs","path":"apis","order":2},{"name":"Deprecated APIs","visible_in_sidebar":false,"page_title":"BBPS COU - API integration (deprecated)","path":"deprecated","order":4,"children":[{"name":"Mock environment","visible_in_sidebar":false,"page_title":"BBPS Billpay Mock environment","path":"mock-environment","order":2},{"name":"Polling","visible_in_sidebar":false,"page_title":"BBPS Billpay polling","path":"polling","order":2}]},{"name":"Objects","visible_in_sidebar":true,"page_title":"BBPS COU - Objects","path":"objects","order":3},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"BBPS COU - API integration","path":"quickstart","order":1},{"name":"Webhooks","visible_in_sidebar":true,"page_title":"BBPS COU - Webhooks","path":"webhooks","order":4}]},{"name":"API reference","visible_in_sidebar":false,"page_title":"BillPay API reference","path":"api-reference","order":5},{"name":"Overview","visible_in_sidebar":true,"page_title":"BBPS Billpay Overview","path":"overview","order":0},{"name":"Pre-built screens","visible_in_sidebar":true,"page_title":"BBPS Billpay pre-built screens","path":"pre-built-screens","order":2,"children":[{"name":"API reference","visible_in_sidebar":false,"page_title":"BBPS Billpay API reference","path":"api-reference-wl","order":4},{"name":"API reference","visible_in_sidebar":true,"page_title":"BBPS Billpay API reference","path":"api-reference","order":4},{"name":"Custom payment","visible_in_sidebar":true,"page_title":"BBPS Billpay custom payment","path":"custom-payment","order":2,"children":[{"name":"Android","visible_in_sidebar":true,"page_title":"BBPS Billpay android integration for custom payment","path":"android","order":3},{"name":"Required APIs","visible_in_sidebar":true,"page_title":"BBPS Billpay APIs for custom payment","path":"apis","order":1},{"name":"Cross platform","visible_in_sidebar":true,"page_title":"BBPS Billpay cross-platform integration for custom payment","path":"cross-platform","order":3},{"name":"iOS","visible_in_sidebar":true,"page_title":"BBPS Billpay iOS integration for custom payment","path":"iOS","order":4},{"name":"Website","visible_in_sidebar":true,"page_title":"BBPS Billpay website integration for custom payment","path":"website","order":2}]},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"BBPS Billpay Quickstart","path":"quickstart","order":1,"children":[{"name":"Android","visible_in_sidebar":true,"page_title":"BBPS Billpay Android integration","path":"android","order":2},{"name":"API","visible_in_sidebar":true,"page_title":"BBPS Billpay API","path":"api","order":2},{"name":"Cross platform","visible_in_sidebar":true,"page_title":"BBPS Billpay cross platform integration","path":"cross-platform","order":4},{"name":"iOS","visible_in_sidebar":true,"page_title":"BBPS Billpay iOS integration","path":"iOS","order":3},{"name":"Website","visible_in_sidebar":true,"page_title":"BBPS Billpay website","path":"website","order":1}]},{"name":"Webhooks","visible_in_sidebar":true,"page_title":"BBPS Billpay webhooks","path":"webhooks","order":2}]}]}]},{"name":"WhatsApp Collect","path":"whatsapp-collect","order":3,"visible_in_sidebar":true,"children":[{"name":"API Integration","visible_in_sidebar":true,"page_title":"WhatsApp Collect API Integration","path":"api-integration","order":3},{"name":"API reference","visible_in_sidebar":true,"page_title":"WhatsApp Collect API reference","path":"api-reference","order":5},{"name":"Error codes","visible_in_sidebar":true,"page_title":"WhatsApp Collect error codes","path":"errors","order":4},{"name":"Collection journey","visible_in_sidebar":true,"page_title":"WhatsApp Collect Journey","path":"journey","order":1},{"name":"Overview","visible_in_sidebar":true,"page_title":"WhatsApp Collect Overview","path":"overview","order":0},{"name":"Collection reminders","visible_in_sidebar":true,"page_title":"WhatsApp Collect reminders","path":"reminders","order":2}]},{"name":"UPI DeepLinks","path":"upi-deeplinks","order":4,"visible_in_sidebar":true,"children":[{"name":"API reference","visible_in_sidebar":true,"page_title":"UPI Deeplinks API reference","path":"api-reference","order":8},{"name":"Notifications","visible_in_sidebar":true,"page_title":"UPI Deeplinks Notifications","path":"notifications","order":6},{"name":"Overview","visible_in_sidebar":true,"page_title":"UPI Deeplinks Overview","path":"overview","order":0},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"UPI Deeplinks quickstart","path":"quickstart","order":1,"children":[{"name":"Go Live","visible_in_sidebar":true,"page_title":"UPI Deeplinks go live","path":"go-live","order":1}]},{"name":"Refunds","visible_in_sidebar":true,"page_title":"UPI Deeplinks Refunds","path":"refunds","order":4},{"name":"Reports API","visible_in_sidebar":true,"page_title":"UPI Deeplinks Reports API","path":"reports","order":5},{"name":"Additional resources","visible_in_sidebar":true,"page_title":"UPI Deeplinks additonal resources","path":"resources","order":6,"children":[{"name":"JWT authentication","visible_in_sidebar":true,"page_title":"UPI Deeplinks JWT authentication","path":"jwt","order":2},{"name":"OAuth 2.0","visible_in_sidebar":true,"page_title":"UPI Deeplinks OAuth 2.0","path":"oauth","order":1},{"name":"Settlement object","visible_in_sidebar":true,"page_title":"UPI Deeplinks settlement object","path":"settlement-object","order":3}]},{"name":"SDKs","visible_in_sidebar":true,"page_title":"UPI Deeplinks SDKs","path":"sdks","order":3},{"name":"Third party verification","visible_in_sidebar":true,"page_title":"UPI Deeplinks third party verification","path":"third-party-verification","order":3}]},{"name":"UPI Setu","path":"umap","order":7,"visible_in_sidebar":true,"children":[{"name":"API integration","visible_in_sidebar":true,"page_title":"UPI Setu - API integration","path":"api-integration","order":2,"children":[{"name":"Aggregators","visible_in_sidebar":true,"page_title":"UPI Setu - API integration for aggregators","path":"aggregators","order":1},{"name":"Merchants","visible_in_sidebar":true,"page_title":"UPI Setu - API integration for merchants","path":"merchants","order":2}]},{"name":"API reference","visible_in_sidebar":true,"page_title":"UPI Setu - API reference","path":"api-reference","order":8},{"name":"UPI mandates","visible_in_sidebar":true,"page_title":"UPI mandates","path":"mandates","order":4,"children":[{"name":"Mandate operations","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Mandate operations","path":"generic","order":5,"children":[{"name":"Pause","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Pause","path":"pause","order":3},{"name":"Revoke","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Revoke","path":"revoke","order":2},{"name":"Unpause","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Unpause","path":"unpause","order":4},{"name":"Update","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Update","path":"update","order":1}]},{"name":"OneShot","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - OneShot","path":"one-shot","order":1,"children":[{"name":"Check status","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Check payment status","path":"check-status","order":4},{"name":"Create","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Create One Time Mandate","path":"create","order":1},{"name":"Execute","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Execute One Time Mandate","path":"execute","order":3},{"name":"Pre Debit Notify","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Send One Time Mandate Pre Debit Notification","path":"pre-debit-notify","order":2}]},{"name":"Recur","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Recur","path":"recur","order":3,"children":[{"name":"Check payment status","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Check payment status","path":"check-status","order":4},{"name":"Create","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Create recurring mandate","path":"create","order":1},{"name":"Execute","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Execute mandate","path":"execute","order":3},{"name":"Pre Debit Notify","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Send Recurring Mandate Pre Debit Notification","path":"pre-debit-notify","order":2}]},{"name":"Reserve","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Reserve","path":"reserve","order":2,"children":[{"name":"Check status","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Check payment status","path":"check-status","order":4},{"name":"Create","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Create Reserve Mandate","path":"create","order":1},{"name":"Execute","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Execute Reserve Mandate","path":"execute","order":3}]},{"name":"ReservePlus","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - ReservePlus","path":"reserve-plus","order":4,"children":[{"name":"Check status","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Check payment status","path":"check-status","order":3},{"name":"Create","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Create single block multi-debit","path":"create","order":1},{"name":"Execute","visible_in_sidebar":true,"page_title":"UPI Setu - Mandates - Execute single block multi-debit","path":"execute","order":2}]}]},{"name":"Merchant on-boarding","visible_in_sidebar":true,"page_title":"UPI Setu - Merchant onboarding","path":"merchant-onboarding","order":2,"children":[{"name":"Check VPA availability","visible_in_sidebar":true,"page_title":"UPI Setu - Merchant on-boarding - Check VPA availability","path":"check-vpa-availability-api","order":2},{"name":"Setup a merchant","visible_in_sidebar":true,"page_title":"UPI Setu - Merchant on-boarding - Setup merchant","path":"create-merchant-api","order":1},{"name":"Registering VPA","visible_in_sidebar":true,"page_title":"UPI Setu - Merchant on-boarding - Registering a VPA","path":"create-vpa-api","order":3}]},{"name":"Notifications and alerts","visible_in_sidebar":true,"page_title":"UPI Setu - Notifications and alerts","path":"notifications","order":7,"children":[{"name":"VPA verification","visible_in_sidebar":true,"page_title":"UPI Setu - Notifications and alerts - Customer VPA verification","path":"customer-vpa-verification","order":6},{"name":"Mandates","visible_in_sidebar":true,"page_title":"UPI Setu - Notifications and alerts - Mandates","path":"mandates","order":3,"children":[{"name":"Create","visible_in_sidebar":true,"page_title":"UPI Setu - Notifications and alerts - Creation of mandate","path":"create","order":1},{"name":"Execute","visible_in_sidebar":true,"page_title":"UPI Setu - Notifications and alerts - Mandate execution","path":"execute","order":7},{"name":"Notify","visible_in_sidebar":true,"page_title":"UPI Setu - Notifications and alerts - Mandate pre-debit notifications","path":"notify","order":6},{"name":"Pause","visible_in_sidebar":true,"page_title":"UPI Setu - Notifications and alerts - Pausing mandate","path":"pause","order":4},{"name":"Revoke","visible_in_sidebar":true,"page_title":"UPI Setu - Notifications and alerts - Revoking mandate","path":"revoke","order":3},{"name":"Unpause","visible_in_sidebar":true,"page_title":"UPI Setu - Notifications and alerts - Unpausing mandate","path":"unpause","order":5},{"name":"Update","visible_in_sidebar":true,"page_title":"UPI Setu - Notifications and alerts - Updating mandate","path":"update","order":2}]},{"name":"Payments","visible_in_sidebar":true,"page_title":"UPI Setu - Notifications and alerts - Payments","path":"payments","order":2},{"name":"Refunds","visible_in_sidebar":true,"page_title":"UPI Setu - Notifications and alerts - Refunds","path":"refunds","order":4},{"name":"Verify signature","visible_in_sidebar":true,"page_title":"UMAP - Events and notifications","path":"verify-signature","order":1}]},{"name":"Overview","visible_in_sidebar":true,"page_title":"UPI Setu - Overview","path":"overview","order":0},{"name":"UPI payments","visible_in_sidebar":true,"page_title":"UPI payments","path":"payments","order":3,"children":[{"name":"Collect","visible_in_sidebar":true,"page_title":"UPI payments - Collect","path":"collect","order":2,"children":[{"name":"Check status","visible_in_sidebar":true,"page_title":"UPI Setu payments - Collect request - Check payment status","path":"check-status","order":3},{"name":"Create","visible_in_sidebar":true,"page_title":"UPI Setu payments - Create collect request","path":"create-collect-request","order":2},{"name":"Verify customer VPA","visible_in_sidebar":true,"page_title":"UPI Setu payments - Verify customer VPA","path":"verify-customer-vpa-api","order":1}]},{"name":"Flash","visible_in_sidebar":true,"page_title":"UPI payments - Flash","path":"flash","order":1,"children":[{"name":"Check status","visible_in_sidebar":true,"page_title":"UPI Setu payments - Intent/QR - Check payment status","path":"check-status","order":2},{"name":"Dynamic QR","visible_in_sidebar":true,"page_title":"UPI Setu payments - Create Dynamic QR","path":"create-dqr","order":1},{"name":"Static QR","visible_in_sidebar":true,"page_title":"UPI Setu payments - Create Static QR","path":"create-sqr","order":1}]},{"name":"TPV","visible_in_sidebar":true,"page_title":"UPI payments - TPV","path":"tpv","order":3,"children":[{"name":"Check status","visible_in_sidebar":true,"page_title":"UPI Setu - Payments - TPV - Check payment status","path":"check-status","order":2},{"name":"Create","visible_in_sidebar":true,"page_title":"UPI Setu - Payments - Create TPV API","path":"create-tpv","order":1},{"name":"Payments","visible_in_sidebar":true,"page_title":"UMAP - Notifications and alerts - Payments","path":"life-cycle","order":1}]}]},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"UPI Setu - Quickstart","path":"quickstart","order":1,"children":[{"name":"Aggregators","visible_in_sidebar":true,"page_title":"UPI Setu - Quickstart for aggregators","path":"aggregators","order":1},{"name":"Merchants","visible_in_sidebar":true,"page_title":"UPI Setu - Quickstart for merchants","path":"merchants","order":2}]},{"name":"Refunds and disputes","visible_in_sidebar":true,"page_title":"UPI Setu - Refunds and disputes","path":"refunds-disputes","order":6,"children":[{"name":"Check refund status","visible_in_sidebar":true,"page_title":"UPI Setu - Refunds and disputes - Check refund status API","path":"check-refund-status-api","order":2},{"name":"Create refund","visible_in_sidebar":true,"page_title":"UPI Setu - Refunds and disputes - Create refund API","path":"create-refund-api","order":1},{"name":"Fetch dispute","visible_in_sidebar":true,"page_title":"UPI Setu - Refunds and disputes - Fetch dispute API","path":"fetch-dispute-api","order":3}]},{"name":"Transaction Monitoring","visible_in_sidebar":false,"page_title":"UPI Setu - Transaction Monitoring","path":"transaction-monitoring","order":5,"children":[{"name":"Check status","visible_in_sidebar":true,"page_title":"UPI Setu - Transaction monitoring - Check status API","path":"check-status-api","order":1},{"name":"Check status history","visible_in_sidebar":true,"page_title":"UPI Setu - Transaction monitoring - Check status sistory API","path":"check-status-history-api","order":2},{"name":"Fetch payment","visible_in_sidebar":true,"page_title":"UPI Setu - Transaction monitoring - Fetch payment API","path":"fetch-payment-api","order":3}]}]},{"name":"UPI Issuance","path":"upi-issuance","order":8,"visible_in_sidebar":true,"children":[{"name":"Overview","visible_in_sidebar":true,"page_title":"UPI Issuance Overview","path":"overview","order":0},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"UPI Issuance Quickstart","path":"quickstart","order":1},{"name":"The API envelope","visible_in_sidebar":true,"page_title":"UPI Issuance API envelope","path":"api-envelope","order":2},{"name":"User Onboarding","visible_in_sidebar":true,"page_title":"UPI Issuance User Onboarding","path":"onboarding","order":3,"children":[{"name":"API integration","visible_in_sidebar":true,"page_title":"UPI Issuance API integration","path":"api-integration","order":1,"children":[{"name":"Device binding","visible_in_sidebar":true,"page_title":"UPI Issuance device binding","path":"device-binding","order":0},{"name":"Programs","visible_in_sidebar":true,"page_title":"UPI Issuance programs","path":"programs","order":3},{"name":"OTP verification","visible_in_sidebar":true,"page_title":"UPI Issuance OTP verification","path":"user-otp","order":1},{"name":"VPA management","visible_in_sidebar":true,"page_title":"UPI Issuance VPA management","path":"vpa-management","order":2}]},{"name":"Onboarding states","visible_in_sidebar":true,"page_title":"UPI Issuance onboarding states","path":"onboarding-states","order":0}]},{"name":"Payee blocklist","visible_in_sidebar":true,"page_title":"UPI Issuance payee blocklist","path":"payee-blocklist","order":4},{"name":"VPA resolution","visible_in_sidebar":true,"page_title":"UPI Issuance VPA resolution","path":"vpa-resolution","order":5},{"name":"Testing on QA env","visible_in_sidebar":true,"page_title":"UPI Issuance QA env testing","path":"qa-testing","order":6},{"name":"API reference","visible_in_sidebar":true,"page_title":"UPI Issuance API reference","path":"api-reference","order":7}]}]},{"name":"Data","path":"data","order":1,"visible_in_sidebar":true,"children":[{"name":"KYC","path":"kyc","order":0,"visible_in_sidebar":true,"children":[{"name":"Secure Data Add-On","visible_in_sidebar":true,"page_title":"Setu Encrypted APIs","path":"encryption","order":2},{"name":"Overview","visible_in_sidebar":true,"page_title":"Setu KYC Overview","path":"overview","order":1}]},{"name":"PAN verification","path":"pan","order":0,"visible_in_sidebar":false,"children":[{"name":"API reference","visible_in_sidebar":true,"page_title":"PAN verification API reference","path":"api-reference","order":1},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"PAN verification quickstart","path":"quickstart","order":0}]},{"name":"Aadhaar eSign","path":"esign","order":2,"visible_in_sidebar":true,"children":[{"name":"API reference","visible_in_sidebar":true,"page_title":"Aadhaar eSign API reference","path":"api-reference","order":9},{"name":"Error codes","visible_in_sidebar":true,"page_title":"Aadhaar eSign error codes","path":"error-codes","order":8},{"name":"eStamp overview","visible_in_sidebar":true,"page_title":"eStamp overview","path":"estamp","order":2},{"name":"Flexible eSign guide","visible_in_sidebar":true,"page_title":"Integration guide with flexible signature coordinates","path":"flexi-esign","order":4},{"name":"eSign Name Match","visible_in_sidebar":true,"page_title":"Aadhaar eSign Name Match","path":"name-match","order":6},{"name":"Notifications","visible_in_sidebar":true,"page_title":"Aadhaar eSign Notifications","path":"notifications","order":7},{"name":"Overview","visible_in_sidebar":true,"page_title":"Aadhaar eSign overview","path":"overview","order":1},{"name":"PDF templates","visible_in_sidebar":true,"page_title":"Integration guide with pdf templating API's","path":"pdf-templating","order":5},{"name":"Integration guide","visible_in_sidebar":true,"page_title":"Aadhaar eSign integration guide","path":"quickstart","order":3}]},{"name":"DigiLocker","path":"digilocker","order":3,"visible_in_sidebar":false,"children":[{"name":"API reference","visible_in_sidebar":true,"page_title":"Digilocker API reference","path":"api-reference","order":3},{"name":"Error codes","visible_in_sidebar":true,"page_title":"DigiLocker error codes","path":"error-codes","order":4},{"name":"Overview","visible_in_sidebar":true,"page_title":"Digilocker overview","path":"overview","order":0},{"name":"Integration guide","visible_in_sidebar":true,"page_title":"Digilocker quickstart","path":"quickstart","order":1}]},{"name":"AA Gateway","path":"account-aggregator","order":4,"versions":["v1","v2"],"default_version":"v2","visible_in_sidebar":true,"children":[{"name":"API integration","visible_in_sidebar":true,"page_title":"Account Aggregator API integration","path":"api-integration","order":3,"children":[{"name":"Account Availability","visible_in_sidebar":true,"page_title":"Account Aggregator Account Availability","path":"account-availability-apis","order":5},{"name":"Consent flow","visible_in_sidebar":true,"page_title":"Account Aggregator Consent flow","path":"consent-flow","order":1},{"name":"Data flow","visible_in_sidebar":true,"page_title":"Account Aggregator Data flow","path":"data-apis","order":2},{"name":"Active FIPs","visible_in_sidebar":true,"page_title":"Account Aggregator Active FIPs","path":"fip-apis","order":4},{"name":"Notifications","visible_in_sidebar":true,"page_title":"Account Aggregator Notifications","path":"notifications","order":3}]},{"name":"API reference","visible_in_sidebar":true,"page_title":"Account Aggregator API reference","path":"api-reference","order":10},{"name":"Consent object","visible_in_sidebar":true,"page_title":"Account Aggregator consent object","path":"consent-object","order":4},{"name":"Embed Setu screens","visible_in_sidebar":true,"page_title":"Account Aggregator Embed Setu screens","path":"embed-setu-aa","order":7},{"name":"FI data types","visible_in_sidebar":true,"page_title":"Account Aggregator FI data types","path":"fi-data-types","order":5},{"name":"Licenses and go live","visible_in_sidebar":true,"page_title":"Account Aggregator license and go live process","path":"licenses-and-go-live","order":8,"children":[{"name":"Go live","visible_in_sidebar":true,"page_title":"FIU go live process","path":"go-live","order":2},{"name":"Licenses","visible_in_sidebar":true,"page_title":"Licenses required to participate in AA","path":"licenses","order":1},{"name":"Participants in AA","visible_in_sidebar":true,"page_title":"Participants in AA","path":"participants-in-aa","order":0}]},{"name":"Multi AA gateway","visible_in_sidebar":true,"page_title":"Account Aggregator multi-AA gateway","path":"multi-aa-gateway","order":2},{"name":"Overview","visible_in_sidebar":true,"page_title":"Account Aggregator overview","path":"overview","order":0},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"Account Aggregator quickstart","path":"quickstart","order":1},{"name":null,"visible_in_sidebar":null,"page_title":null,"path":"v1","order":null,"children":[{"name":"API integration","visible_in_sidebar":true,"page_title":"Account Aggregator API integration","path":"api-integration","order":3,"children":[{"name":"Consent flow","visible_in_sidebar":true,"page_title":"Account Aggregator Consent flow","path":"consent-flow","order":1},{"name":"Data flow","visible_in_sidebar":true,"page_title":"Account Aggregator Data flow","path":"data-apis","order":2},{"name":"Active FIPs","visible_in_sidebar":true,"page_title":"Account Aggregator Active FIPs","path":"fip-apis","order":4},{"name":"Notifications","visible_in_sidebar":true,"page_title":"Account Aggregator Notifications","path":"notifications","order":3}]},{"name":"API reference","visible_in_sidebar":true,"page_title":"Account Aggregator API reference","path":"api-reference","order":10},{"name":"Consent object","visible_in_sidebar":true,"page_title":"Account Aggregator Consent object","path":"consent-object","order":4},{"name":"Embed Setu screens","visible_in_sidebar":true,"page_title":"Account Aggregator Embed Setu screens","path":"embed-setu-aa","order":7},{"name":"End-to-end encryption","visible_in_sidebar":false,"page_title":"Account Aggregator End-to-end encryption","path":"encryption","order":1},{"name":"FI data types","visible_in_sidebar":true,"page_title":"Account Aggregator FI data types","path":"fi-data-types","order":5},{"name":"Get started","visible_in_sidebar":false,"page_title":"Account Aggregator getting started","path":"get-started","order":0},{"name":"Licenses and go live","visible_in_sidebar":true,"page_title":"Account Aggregator license and go live process","path":"licenses-and-go-live","order":8,"children":[{"name":"Go live","visible_in_sidebar":true,"page_title":"FIU go live process","path":"go-live","order":2},{"name":"Licenses","visible_in_sidebar":true,"page_title":"Licenses required to participate in AA","path":"licenses","order":1},{"name":"Participants in AA","visible_in_sidebar":true,"page_title":"Participants in AA","path":"participants-in-aa","order":0}]},{"name":"Migration guide","visible_in_sidebar":true,"page_title":"Account Aggregator Migration Guide","path":"migration-guide","order":6,"children":[{"name":"Consent flow","visible_in_sidebar":true,"page_title":"Account Aggregator Consent flow","path":"consent-flow","order":1},{"name":"Data flow","visible_in_sidebar":true,"page_title":"Account Aggregator Data flow","path":"data-flow","order":2},{"name":"Notifications","visible_in_sidebar":true,"page_title":"Account Aggregator Notifications","path":"notifications","order":3}]},{"name":"Overview","visible_in_sidebar":true,"page_title":"Account Aggregator overview","path":"overview","order":0},{"name":"Postman integration","visible_in_sidebar":true,"page_title":"Account Aggregator Postman integration","path":"postman","order":2},{"name":"Quickstart","visible_in_sidebar":false,"page_title":"Account Aggregator quickstart","path":"quickstart-v1","order":1},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"Account Aggregator quickstart","path":"quickstart","order":1},{"name":"Request signing","visible_in_sidebar":false,"page_title":"Account Aggregator Request signing","path":"request-signing","order":1}]}]},{"name":"Bank account verification","path":"bav","order":5,"visible_in_sidebar":false,"children":[{"name":"Bundled BAV","visible_in_sidebar":true,"page_title":"Bundled Bank Account Verification","path":"bundled-bav","order":2,"children":[{"name":"API integration","visible_in_sidebar":true,"page_title":"Bundled BAV API integration","path":"api-integration","order":2},{"name":"API reference","visible_in_sidebar":true,"page_title":"Bundled BAV API reference","path":"api-reference","order":3},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"Quickstart for Bundled BAV","path":"quickstart","order":1}]},{"name":"Penny drop","visible_in_sidebar":true,"page_title":"BAV using penny drop","path":"penny-drop","order":1,"children":[{"name":"API integration","visible_in_sidebar":true,"page_title":"BAV API integration","path":"api-integration","order":1,"children":[{"name":"Async API","visible_in_sidebar":true,"page_title":"BAV Async API integration","path":"async","order":2},{"name":"Sync API","visible_in_sidebar":true,"page_title":"BAV Sync API integration","path":"sync","order":1}]},{"name":"API reference","visible_in_sidebar":true,"page_title":"BAV API reference","path":"api-reference","order":3},{"name":"Notifications","visible_in_sidebar":true,"page_title":"BAV Async Penny drop Notifications","path":"notifications","order":2},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"BAV quickstart","path":"quickstart","order":0}]},{"name":"Penny drop + PennyLess","visible_in_sidebar":true,"page_title":"Bank account verification using Penny drop + PennyLess","path":"pennydrop-pennyless","order":2,"children":[{"name":"API Integration","visible_in_sidebar":true,"page_title":"Penny drop + PennyLess API Integration","path":"api-integration","order":1},{"name":"API reference","visible_in_sidebar":true,"page_title":"Pennydrop-pennyless API reference","path":"api-reference","order":3},{"name":"Notifications","visible_in_sidebar":true,"page_title":"Penny drop + PennyLess Notifications","path":"notifications","order":2}]},{"name":"PennyLess Drop","visible_in_sidebar":true,"page_title":"BAV using PennyLess Drop API","path":"pennyless-drop","order":4,"children":[{"name":"API reference","visible_in_sidebar":true,"page_title":"BAV Pennyless API reference","path":"api-reference","order":2},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"Quickstart for PennyLess drop API","path":"quickstart","order":1}]},{"name":"Reverse Penny drop","visible_in_sidebar":true,"page_title":"BAV using reverse penny drop","path":"reverse-penny-drop","order":3,"children":[{"name":"API integration","visible_in_sidebar":true,"page_title":"RPD API integration","path":"api-integration","order":2},{"name":"API reference","visible_in_sidebar":true,"page_title":"BAV RPD API reference","path":"api-reference","order":4},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"Quickstart for reverse penny drop","path":"quickstart","order":1},{"name":"Webhook Auth","visible_in_sidebar":true,"page_title":"Webhook Authentication","path":"webhook-authentication","order":3}]}]},{"name":"Insights","path":"insights","order":5,"versions":["v1","v2","v3"],"default_version":"v3","visible_in_sidebar":true,"children":[{"name":"API reference","visible_in_sidebar":true,"page_title":"Setu Insights API reference","path":"api-reference","order":4},{"name":"Error codes","visible_in_sidebar":true,"page_title":"Setu Insights error codes","path":"error-code","order":5},{"name":"List of insights","visible_in_sidebar":true,"page_title":"All Setu insights","path":"insights","order":2},{"name":"Notifications","visible_in_sidebar":true,"page_title":"Setu Insights notifications","path":"notifications","order":3},{"name":"Overview","visible_in_sidebar":true,"page_title":"Setu Insights overview","path":"overview","order":0},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"Setu Insights quickstart","path":"quickstart","order":1,"children":[{"name":"API integration","visible_in_sidebar":true,"page_title":"Setu Insights Postman integration","path":"api-integration","order":1},{"name":"Postman integration","visible_in_sidebar":true,"page_title":"Setu Insights Postman integration","path":"postman","order":0}]},{"name":null,"visible_in_sidebar":null,"page_title":null,"path":"v1","order":null,"children":[{"name":"API reference","visible_in_sidebar":true,"page_title":"Setu Insights API reference","path":"api-reference","order":4},{"name":"List of insights","visible_in_sidebar":true,"page_title":"All Setu insights","path":"insights","order":2},{"name":"Notifications","visible_in_sidebar":true,"page_title":"Setu Insights notifications","path":"notifications","order":3},{"name":"Overview","visible_in_sidebar":true,"page_title":"Setu Insights overview","path":"overview","order":0},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"Setu Insights quickstart","path":"quickstart","order":1,"children":[{"name":"API integration","visible_in_sidebar":true,"page_title":"Setu Insights Postman integration","path":"api-integration","order":1},{"name":"Postman integration","visible_in_sidebar":true,"page_title":"Setu Insights Postman integration","path":"postman","order":0}]}]},{"name":null,"visible_in_sidebar":null,"page_title":null,"path":"v2","order":null,"children":[{"name":"API reference","visible_in_sidebar":true,"page_title":"Setu Insights API reference","path":"api-reference","order":4},{"name":"List of insights","visible_in_sidebar":true,"page_title":"All Setu insights","path":"insights","order":2},{"name":"Notifications","visible_in_sidebar":true,"page_title":"Setu Insights notifications","path":"notifications","order":3},{"name":"Overview","visible_in_sidebar":true,"page_title":"Setu Insights overview","path":"overview","order":0},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"Setu Insights quickstart","path":"quickstart","order":1}]}]},{"name":"Signal IQ","path":"signal-iq","order":6,"visible_in_sidebar":true,"children":[{"name":"AA Flow","visible_in_sidebar":true,"page_title":"Signal IQ - AA Flow","path":"aa-flow","order":1},{"name":"Bring Your Own FI Data","visible_in_sidebar":true,"page_title":"Signal IQ - Bring Your Own FI Data","path":"bring-your-own-fi-data","order":4},{"name":"Overview","visible_in_sidebar":true,"page_title":"Signal IQ overview","path":"overview","order":0},{"name":"PDF Flow","visible_in_sidebar":true,"page_title":"Signal IQ - PDF Flow","path":"pdf-flow","order":2},{"name":"API reference","visible_in_sidebar":true,"page_title":"Signal IQ - API reference","path":"report-apis","order":5,"children":[{"name":"API reference","visible_in_sidebar":false,"page_title":"Signal IQ - API reference","path":"api-reference","order":1}]}]},{"name":"ULI","path":"uli","order":6,"visible_in_sidebar":false,"children":[{"name":"API reference","visible_in_sidebar":true,"page_title":"GSTIN verification API reference","path":"api-reference","order":2},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"GST Verification quickstart","path":"quickstart","order":1}]},{"name":"GST verification","path":"gst","order":6,"visible_in_sidebar":false,"children":[{"name":"API reference","visible_in_sidebar":true,"page_title":"GSTIN verification API reference","path":"api-reference","order":2},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"GST Verification quickstart","path":"quickstart","order":1}]},{"name":"Match APIs","path":"match-apis","order":7,"visible_in_sidebar":false,"children":[{"name":"Name match","visible_in_sidebar":true,"page_title":"Name match APIs","path":"name-match","order":1,"children":[{"name":"API reference","visible_in_sidebar":true,"page_title":"Name Match API reference","path":"api-reference","order":4},{"name":"Examples","visible_in_sidebar":true,"page_title":"Name Match API response examples","path":"examples","order":3},{"name":"Overview","visible_in_sidebar":true,"page_title":"Name Match API overview","path":"overview","order":1},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"Name Match API quickstart","path":"quickstart","order":2}]}]},{"name":"eKYC","path":"ekyc","order":8,"visible_in_sidebar":false,"children":[{"name":"API reference","visible_in_sidebar":true,"page_title":"eKYC API reference","path":"api-reference","order":2},{"name":"Quickstart","visible_in_sidebar":true,"page_title":"PAN verification quickstart","path":"quickstart","order":1}]}]},{"name":"Dev tools","path":"dev-tools","order":2,"visible_in_sidebar":true,"children":[{"name":"The Bridge","path":"bridge","order":0,"versions":["v1","v2"],"default_version":"v2","visible_in_sidebar":true,"children":[{"name":"Analytics and reports","visible_in_sidebar":true,"page_title":"Bridge analytics and reports","path":"analytics-and-reports","order":3},{"name":"Configure products","visible_in_sidebar":true,"page_title":"Bridge explore and configure products","path":"explore-and-configure-products","order":2},{"name":"Glossary","visible_in_sidebar":true,"page_title":"Bridge glossary","path":"glossary","order":1},{"name":"Overview","visible_in_sidebar":true,"page_title":"Bridge overview","path":"overview","order":0},{"name":"Settings","visible_in_sidebar":true,"page_title":"Bridge settings","path":"settings","order":4},{"name":"User profile","visible_in_sidebar":true,"page_title":"Bridge user profile","path":"user-profile","order":5},{"name":null,"visible_in_sidebar":null,"page_title":null,"path":"v1","order":null,"children":[{"name":"Bridge configuration","visible_in_sidebar":false,"page_title":"Bridge configuration","path":"configure","order":6},{"name":"Generate Token","visible_in_sidebar":false,"page_title":"Bridge generate token","path":"generate-token","order":4},{"name":"Org settings","visible_in_sidebar":true,"page_title":"Bridge org settings","path":"org-settings","order":3,"children":[{"name":"API keys","visible_in_sidebar":true,"page_title":"API keys","path":"api-keys","order":2,"children":[{"name":"JWT Auth","visible_in_sidebar":false,"page_title":"JWT Auth","path":"jwt-auth","order":3},{"name":"JWT","visible_in_sidebar":true,"page_title":"JWT","path":"jwt","order":1},{"name":"OAuth","visible_in_sidebar":true,"page_title":"OAuth","path":"oauth","order":2}]},{"name":"People","visible_in_sidebar":true,"page_title":"People","path":"people","order":1}]},{"name":"Overview","visible_in_sidebar":true,"page_title":"Bridge overview","path":"overview","order":0},{"name":"Reports","visible_in_sidebar":true,"page_title":"Bridge reports","path":"reports","order":1,"children":[{"name":"Types","visible_in_sidebar":false,"page_title":"Report types","path":"types","order":1}]},{"name":"Reports API","visible_in_sidebar":false,"page_title":"Reports API","path":"reports-api","order":5}]}]}]},{"name":"Sample Category","path":"sample-category","order":3,"visible_in_sidebar":false,"children":[{"name":"Sample Product","path":"sample-product","order":0,"visible_in_sidebar":false,"children":[{"name":"Sample Page","visible_in_sidebar":false,"page_title":"Docs sample page","path":"sample-page","order":0}]}]}]} \ No newline at end of file From 50e3fa53064cbb4b16e9ead228ba5e365bb93b4a Mon Sep 17 00:00:00 2001 From: Anindya Pandey Date: Mon, 27 Jul 2026 02:10:40 +0530 Subject: [PATCH 11/27] docs(UPIIS-69): tighten the VPA resolution intro "money" -> "payment", and drop the envelope / /api/v1 / active-user / idempotencyKey preamble: the route line below already states all of it, and idempotencyKey is carried by the OpenAPI spec and api-envelope. Co-Authored-By: Claude Opus 5 (1M context) --- content/payments/upi-issuance/vpa-resolution.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/payments/upi-issuance/vpa-resolution.mdx b/content/payments/upi-issuance/vpa-resolution.mdx index ef454150..59b2447b 100644 --- a/content/payments/upi-issuance/vpa-resolution.mdx +++ b/content/payments/upi-issuance/vpa-resolution.mdx @@ -7,7 +7,7 @@ visible_in_sidebar: true ## VPA resolution -Before an `active` user pays a payee, the TPAP / Issuing App resolves the payee's VPA to confirm it is valid and to show who the money is going to. The route is enveloped, under `/api/v1`, and requires an `active` user. `idempotencyKey` is an optional request header. +Before an `active` user pays a payee, the TPAP / Issuing App resolves the payee's VPA to confirm it is valid and to show who the payment is going to. ### How resolution works From 1a305ec9522d3ca9a5165fc73895a08b5e762f28 Mon Sep 17 00:00:00 2001 From: Anindya Pandey Date: Mon, 27 Jul 2026 02:22:51 +0530 Subject: [PATCH 12/27] docs(UPIIS-69): give VPA resolution its own API reference tag resolveVPA was tagged "VPA management", so it rendered as one item inside that section rather than mirroring the guides, where VPA resolution is a page of its own. Adds a "VPA resolution" tag and retags the operation. Co-Authored-By: Claude Opus 5 (1M context) --- api-references/payments/upi-issuance.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/api-references/payments/upi-issuance.json b/api-references/payments/upi-issuance.json index 5d1f53be..e3adae15 100644 --- a/api-references/payments/upi-issuance.json +++ b/api-references/payments/upi-issuance.json @@ -1188,7 +1188,7 @@ "/api/v1/vpa/resolve": { "post": { "tags": [ - "VPA management" + "VPA resolution" ], "summary": "Resolve a payee VPA", "description": "Resolve a payee VPA on the payer side. A VPA under our handle is answered immediately from local records; a foreign-handle VPA triggers an async NPCI ReqValAdd and returns PENDING until the result arrives. Poll by repeating this same POST. PENDING is not an error.", @@ -4963,6 +4963,9 @@ }, { "name": "Payee blocklist" + }, + { + "name": "VPA resolution" } ] } \ No newline at end of file From e8d8a9be047c68ed7489e3eab44a5655941b6ea5 Mon Sep 17 00:00:00 2001 From: Anindya Pandey Date: Mon, 27 Jul 2026 02:27:01 +0530 Subject: [PATCH 13/27] docs(UPIIS-69): drop NPCI protocol operation names from the spec Two descriptions on the VPA resolve operation named the NPCI protocol call (ReqValAdd) directly. Replaced with a plain statement that resolution happens via NPCI. Error codes and NPCI itself stay. Co-Authored-By: Claude Opus 5 (1M context) --- api-references/payments/upi-issuance.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api-references/payments/upi-issuance.json b/api-references/payments/upi-issuance.json index e3adae15..4b4e56bb 100644 --- a/api-references/payments/upi-issuance.json +++ b/api-references/payments/upi-issuance.json @@ -1191,7 +1191,7 @@ "VPA resolution" ], "summary": "Resolve a payee VPA", - "description": "Resolve a payee VPA on the payer side. A VPA under our handle is answered immediately from local records; a foreign-handle VPA triggers an async NPCI ReqValAdd and returns PENDING until the result arrives. Poll by repeating this same POST. PENDING is not an error.", + "description": "Resolve a payee VPA on the payer side. A VPA under our handle is answered immediately from local records; a foreign-handle VPA triggers an async resolution with NPCI and returns PENDING until the result arrives. Poll by repeating this same POST. PENDING is not an error.", "operationId": "resolveVPA", "parameters": [ { @@ -3932,7 +3932,7 @@ "pattern": "^([A-Za-z0-9.-]+)@([A-Za-z0-9.-]+)$", "minLength": 1, "maxLength": 255, - "description": "The payee VPA to resolve (prefix@handle). Resolved locally when the handle is ours, otherwise via an NPCI ReqValAdd.", + "description": "The payee VPA to resolve (prefix@handle). Resolved locally when the handle is ours, otherwise via NPCI.", "example": "someone@ybl" } }, From 0b346d2e9dc3bbb59d6d04ec204e902f19f8ce61 Mon Sep 17 00:00:00 2001 From: Anindya Pandey Date: Mon, 27 Jul 2026 02:39:03 +0530 Subject: [PATCH 14/27] docs(UPIIS-69): clarify VPA resolution response field descriptions - status: spell out what RESOLVED / PENDING / FAILED each mean rather than the terse "(details present)" / "(error present)" shorthand - entityType: PERSON is a non-merchant VPA, ENTITY a merchant VPA - "Present on RESOLVED" -> "Present in the API response in case of successful resolution" - "Present when verified" -> "Present when the payee VPA is a whitelisted / verified VPA" - "Present for a resolved merchant" -> "...merchant VPA" - verifiedUrl is a brand url, not a callback url Applied to both the OpenAPI spec and the guide page so they match. Co-Authored-By: Claude Opus 5 (1M context) --- api-references/payments/upi-issuance.json | 32 +++++++++---------- .../payments/upi-issuance/vpa-resolution.mdx | 24 +++++++------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/api-references/payments/upi-issuance.json b/api-references/payments/upi-issuance.json index 4b4e56bb..cbd7c2d1 100644 --- a/api-references/payments/upi-issuance.json +++ b/api-references/payments/upi-issuance.json @@ -4646,17 +4646,17 @@ "properties": { "accountType": { "type": "string", - "description": "The payee's account type. Present on RESOLVED.", + "description": "The payee's account type. Present in the API response in case of successful resolution.", "example": "SAVINGS" }, "brandName": { "type": "string", - "description": "Merchant brand name. Present for a resolved merchant.", + "description": "Merchant brand name. Present for a resolved merchant VPA.", "example": "Brand-X" }, "entityType": { "type": "string", - "description": "The payee entity type: PERSON or ENTITY. Present on RESOLVED.", + "description": "The payee entity type: PERSON for a non-merchant VPA, ENTITY for a merchant VPA. Present in the API response in case of successful resolution.", "example": "PERSON" }, "errorCode": { @@ -4671,42 +4671,42 @@ }, "franchiseName": { "type": "string", - "description": "Merchant franchise name. Present for a resolved merchant.", + "description": "Merchant franchise name. Present for a resolved merchant VPA.", "example": "Brand-X Franchise" }, "genre": { "type": "string", - "description": "Merchant genre: ONLINE or OFFLINE. Present for a resolved merchant.", + "description": "Merchant genre: ONLINE or OFFLINE. Present for a resolved merchant VPA.", "example": "ONLINE" }, "ifsc": { "type": "string", - "description": "The payee's IFSC. Present on RESOLVED.", + "description": "The payee's IFSC. Present in the API response in case of successful resolution.", "example": "ICIC0000052" }, "legalName": { "type": "string", - "description": "Merchant legal name. Present for a resolved merchant.", + "description": "Merchant legal name. Present for a resolved merchant VPA.", "example": "Acme Pvt Ltd" }, "mcc": { "type": "string", - "description": "Merchant category code. Present for a resolved merchant.", + "description": "Merchant category code. Present for a resolved merchant VPA.", "example": "5732" }, "merchantType": { "type": "string", - "description": "Merchant type: SMALL or LARGE. Present for a resolved merchant.", + "description": "Merchant type: SMALL or LARGE. Present for a resolved merchant VPA.", "example": "LARGE" }, "name": { "type": "string", - "description": "The payee's masked account-holder name. Present on RESOLVED.", + "description": "The payee's masked account-holder name. Present in the API response in case of successful resolution.", "example": "ANI********DEY" }, "ownership": { "type": "string", - "description": "Merchant ownership type: PROPRIETARY, PARTNERSHIP, PRIVATE, PUBLIC or OTHERS. Present for a resolved merchant.", + "description": "Merchant ownership type: PROPRIETARY, PARTNERSHIP, PRIVATE, PUBLIC or OTHERS. Present for a resolved merchant VPA.", "example": "PUBLIC" }, "status": { @@ -4716,7 +4716,7 @@ "PENDING", "FAILED" ], - "description": "Resolution outcome: RESOLVED (details present), PENDING (poll again), or FAILED (error present).", + "description": "Resolution outcome. RESOLVED indicates that the payee VPA is valid and the API response carries the payee VPA details. PENDING indicates that the payee VPA resolution is in progress. FAILED indicates that the payee VPA is invalid and the API response indicates the failure reason.", "example": "RESOLVED" }, "traceId": { @@ -4726,22 +4726,22 @@ }, "verified": { "type": "boolean", - "description": "Whether the resolved payee VPA is in the verified/whitelisted registry. Present on RESOLVED.", + "description": "Whether the resolved payee VPA is in the verified/whitelisted registry. Present in the API response in case of successful resolution.", "example": true }, "verifiedLogo": { "type": "string", - "description": "The registered logo url/ref from the verified registry. Present when verified.", + "description": "The registered logo url/ref from the verified registry. Present when the payee VPA is a whitelisted / verified VPA.", "example": "https://cdn.example/brandx.png" }, "verifiedName": { "type": "string", - "description": "The registered display name from the verified registry. Present when verified.", + "description": "The registered display name from the verified registry. Present when the payee VPA is a whitelisted / verified VPA.", "example": "Brand-X" }, "verifiedUrl": { "type": "string", - "description": "The registered brand/callback url from the verified registry. Present when verified.", + "description": "The registered brand url from the verified registry. Present when the payee VPA is a whitelisted / verified VPA.", "example": "https://brandx.example" }, "vpa": { diff --git a/content/payments/upi-issuance/vpa-resolution.mdx b/content/payments/upi-issuance/vpa-resolution.mdx index 59b2447b..ead3c145 100644 --- a/content/payments/upi-issuance/vpa-resolution.mdx +++ b/content/payments/upi-issuance/vpa-resolution.mdx @@ -17,9 +17,9 @@ The `status` field carries the outcome; the HTTP status stays `200` for all thre | `status` | Meaning | | :--- | :--- | -| `RESOLVED` | The payee is valid; the payee details are included. | -| `PENDING` | Resolution is in progress — send the same request again to poll. | -| `FAILED` | The payee VPA could not be resolved; `errorCode` + `errorMessage` explain why. | +| `RESOLVED` | The payee VPA is valid, and the API response carries the payee VPA details. | +| `PENDING` | The payee VPA resolution is in progress — send the same request again to poll. | +| `FAILED` | The payee VPA is invalid, and the API response indicates the failure reason. |
@@ -54,18 +54,18 @@ The `status` field carries the outcome; the HTTP status stays `200` for all thre | `traceId` | string | Trace handle for this call (ULID). | | `status` | string | `RESOLVED`, `PENDING`, or `FAILED`. | | `vpa` | string | The payee VPA that was resolved (echo of the request). | -| `name` | string | The payee's account-holder name. Present on `RESOLVED`. | -| `accountType` | string | The payee's account type (e.g. `SAVINGS`). Present on `RESOLVED`. | -| `ifsc` | string | The payee's IFSC. Present on `RESOLVED`. | -| `entityType` | string | `PERSON` or `ENTITY` (merchant). Present on `RESOLVED`. | -| `verified` | boolean | `true` when the payee is a verified / whitelisted address. Present (with the fields below) only when verified. | -| `verifiedName` | string | The registered display name from the verified registry. | -| `verifiedLogo` | string | The registered logo url. | -| `verifiedUrl` | string | The registered brand / callback url. | +| `name` | string | The payee's account-holder name. Present in the API response in case of successful resolution. | +| `accountType` | string | The payee's account type (e.g. `SAVINGS`). Present in the API response in case of successful resolution. | +| `ifsc` | string | The payee's IFSC. Present in the API response in case of successful resolution. | +| `entityType` | string | `PERSON` for a non-merchant VPA, `ENTITY` for a merchant VPA. Present in the API response in case of successful resolution. | +| `verified` | boolean | `true` when the payee VPA is a whitelisted / verified VPA. Present in the API response in case of successful resolution. | +| `verifiedName` | string | The registered display name from the verified registry. Present when the payee VPA is a whitelisted / verified VPA. | +| `verifiedLogo` | string | The registered logo url. Present when the payee VPA is a whitelisted / verified VPA. | +| `verifiedUrl` | string | The registered brand url. Present when the payee VPA is a whitelisted / verified VPA. | | `errorCode` | string | Network error code (e.g. `ZH`). Present on `FAILED`. | | `errorMessage` | string | Human-readable failure reason. Present on `FAILED`. | -A **merchant** payee (`entityType: ENTITY`) additionally carries a merchant block on `RESOLVED`: +A **merchant** payee (`entityType: ENTITY`) additionally carries a merchant block. These fields are present for a resolved merchant VPA: | Field | Type | Notes | | :--- | :--- | :--- | From 4a79b3b98820bb40e987a9cd8b3a0b6927d499f2 Mon Sep 17 00:00:00 2001 From: Anindya Pandey Date: Mon, 27 Jul 2026 02:46:39 +0530 Subject: [PATCH 15/27] docs(UPIIS-69): complete the VPA resolution response samples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The merchant sample omitted franchiseName and verifiedLogo, and the person sample omitted verified — all three are documented in the field tables directly above. The spec's 200 example was also self-contradictory: entityType PERSON carrying verifiedName "Brand-X" and a brand logo. Replaced with a complete, consistent verified-merchant response covering every field the schema defines apart from the failure-only pair. Co-Authored-By: Claude Opus 5 (1M context) --- api-references/payments/upi-issuance.json | 31 ++++++++++++------- .../payments/upi-issuance/vpa-resolution.mdx | 5 ++- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/api-references/payments/upi-issuance.json b/api-references/payments/upi-issuance.json index cbd7c2d1..d345bd82 100644 --- a/api-references/payments/upi-issuance.json +++ b/api-references/payments/upi-issuance.json @@ -1245,18 +1245,25 @@ "$ref": "#/components/schemas/VpaResolveResponse" }, "example": { - "accountType": "SAVINGS", - "entityType": "PERSON", - "ifsc": "ICIC0000052", - "name": "ANI********DEY", - "status": "RESOLVED", - "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", - "verified": true, - "verifiedLogo": "https://cdn.example/brandx.png", - "verifiedName": "Brand-X", - "verifiedUrl": "https://brandx.example", - "vpa": "someone@ybl" - } + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "status": "RESOLVED", + "vpa": "brandx@ybl", + "name": "BRAND****X", + "accountType": "CURRENT", + "ifsc": "ICIC0000052", + "entityType": "ENTITY", + "genre": "ONLINE", + "mcc": "5732", + "merchantType": "LARGE", + "brandName": "Brand-X", + "franchiseName": "Brand-X Koramangala", + "legalName": "Acme Pvt Ltd", + "ownership": "PUBLIC", + "verified": true, + "verifiedName": "Brand-X", + "verifiedLogo": "https://cdn.example/brandx.png", + "verifiedUrl": "https://brandx.example" + } } } }, diff --git a/content/payments/upi-issuance/vpa-resolution.mdx b/content/payments/upi-issuance/vpa-resolution.mdx index ead3c145..5fd86676 100644 --- a/content/payments/upi-issuance/vpa-resolution.mdx +++ b/content/payments/upi-issuance/vpa-resolution.mdx @@ -87,7 +87,8 @@ A **merchant** payee (`entityType: ENTITY`) additionally carries a merchant bloc "name": "ANI********DEY", "accountType": "SAVINGS", "ifsc": "ICIC0000052", - "entityType": "PERSON" + "entityType": "PERSON", + "verified": false }`} @@ -106,10 +107,12 @@ A **merchant** payee (`entityType: ENTITY`) additionally carries a merchant bloc "mcc": "5732", "merchantType": "LARGE", "brandName": "Brand-X", + "franchiseName": "Brand-X Koramangala", "legalName": "Acme Pvt Ltd", "ownership": "PUBLIC", "verified": true, "verifiedName": "Brand-X", + "verifiedLogo": "https://cdn.example/brandx.png", "verifiedUrl": "https://brandx.example" }`} From 0d2a1754bc643d4df352da216dd36d907fceeb55 Mon Sep 17 00:00:00 2001 From: Anindya Pandey Date: Mon, 27 Jul 2026 02:59:04 +0530 Subject: [PATCH 16/27] docs(UPIIS-69): correct name-masking and verified claims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Checked against the implementation in setu-upi-issuance: - name is NOT guaranteed masked. On our own handle it is the decrypted account name on record (resolve.go returns derefOr(v.AccountName); the integration test asserts "Test Holder"). On a foreign handle it is whatever the payee PSP put in the RespValAdd maskName attribute, which by convention carries the real name — our own outbound holderName() returns it in the clear, masking it only in logs. - verified is omitted when false: the handler only sets it via `if res.Verified`. So it is present only for a verified payee, not on every successful resolution. Reverts the "verified": false line added to the person sample in 4a79b3b. Co-Authored-By: Claude Opus 5 (1M context) --- api-references/payments/upi-issuance.json | 4 ++-- content/payments/upi-issuance/vpa-resolution.mdx | 7 +++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/api-references/payments/upi-issuance.json b/api-references/payments/upi-issuance.json index d345bd82..8d04beb8 100644 --- a/api-references/payments/upi-issuance.json +++ b/api-references/payments/upi-issuance.json @@ -4708,7 +4708,7 @@ }, "name": { "type": "string", - "description": "The payee's masked account-holder name. Present in the API response in case of successful resolution.", + "description": "The payee's account-holder name. For a VPA on our handle this is the account name on record; for a foreign handle it is the name as reported by the payee PSP. Present in the API response in case of successful resolution.", "example": "ANI********DEY" }, "ownership": { @@ -4733,7 +4733,7 @@ }, "verified": { "type": "boolean", - "description": "Whether the resolved payee VPA is in the verified/whitelisted registry. Present in the API response in case of successful resolution.", + "description": "true when the resolved payee VPA is in the verified/whitelisted registry. Present only when the payee VPA is a whitelisted / verified VPA — omitted otherwise.", "example": true }, "verifiedLogo": { diff --git a/content/payments/upi-issuance/vpa-resolution.mdx b/content/payments/upi-issuance/vpa-resolution.mdx index 5fd86676..3bc0d54c 100644 --- a/content/payments/upi-issuance/vpa-resolution.mdx +++ b/content/payments/upi-issuance/vpa-resolution.mdx @@ -54,11 +54,11 @@ The `status` field carries the outcome; the HTTP status stays `200` for all thre | `traceId` | string | Trace handle for this call (ULID). | | `status` | string | `RESOLVED`, `PENDING`, or `FAILED`. | | `vpa` | string | The payee VPA that was resolved (echo of the request). | -| `name` | string | The payee's account-holder name. Present in the API response in case of successful resolution. | +| `name` | string | The payee's account-holder name — the name on record for a VPA on our handle, or the name as reported by the payee PSP for a foreign handle. Present in the API response in case of successful resolution. | | `accountType` | string | The payee's account type (e.g. `SAVINGS`). Present in the API response in case of successful resolution. | | `ifsc` | string | The payee's IFSC. Present in the API response in case of successful resolution. | | `entityType` | string | `PERSON` for a non-merchant VPA, `ENTITY` for a merchant VPA. Present in the API response in case of successful resolution. | -| `verified` | boolean | `true` when the payee VPA is a whitelisted / verified VPA. Present in the API response in case of successful resolution. | +| `verified` | boolean | `true` when the payee VPA is a whitelisted / verified VPA. Present only when the payee VPA is verified — omitted otherwise. | | `verifiedName` | string | The registered display name from the verified registry. Present when the payee VPA is a whitelisted / verified VPA. | | `verifiedLogo` | string | The registered logo url. Present when the payee VPA is a whitelisted / verified VPA. | | `verifiedUrl` | string | The registered brand url. Present when the payee VPA is a whitelisted / verified VPA. | @@ -87,8 +87,7 @@ A **merchant** payee (`entityType: ENTITY`) additionally carries a merchant bloc "name": "ANI********DEY", "accountType": "SAVINGS", "ifsc": "ICIC0000052", - "entityType": "PERSON", - "verified": false + "entityType": "PERSON" }`} From 4cb71baa8973de764e6acf812e941b0135d6b85c Mon Sep 17 00:00:00 2001 From: Anindya Pandey Date: Mon, 27 Jul 2026 03:00:43 +0530 Subject: [PATCH 17/27] docs(UPIIS-69): drop the last "masked" name claim from qa-testing Co-Authored-By: Claude Opus 5 (1M context) --- content/payments/upi-issuance/qa-testing.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/payments/upi-issuance/qa-testing.mdx b/content/payments/upi-issuance/qa-testing.mdx index 61e1cbb2..56c6f712 100644 --- a/content/payments/upi-issuance/qa-testing.mdx +++ b/content/payments/upi-issuance/qa-testing.mdx @@ -62,7 +62,7 @@ Every field is optional; anything you leave out falls back to a default, and wit | `outcome` | `success` or `failure` | `success` | | `errorCode` | error code returned for a `failure` | `ZH` | | `entityType` | `PERSON` or `ENTITY` (a `merchant` block also implies `ENTITY`) | `PERSON` | -| `name` | masked account-holder name | a default | +| `name` | account-holder name | a default | | `accountType` | e.g. `SAVINGS` / `CURRENT` | a default | | `ifsc` | payee IFSC | a default | | `merchant.mcc` / `.brandName` / `.legalName` / `.franchiseName` / `.merchantType` / `.genre` / `.ownership` | merchant detail for an `ENTITY` payee | defaults | From 2fa63dc0a71545aa63fb1b878c71ec8d2fc4b90f Mon Sep 17 00:00:00 2001 From: Anindya Pandey Date: Mon, 27 Jul 2026 03:17:09 +0530 Subject: [PATCH 18/27] docs(UPIIS-69): use the lowercase resolution status values The implementation emits lowercase status values. vparesolve.State defines pending / resolved / failed, and the comment there calls the spelling the wire contract: "The values are LOWERCASE, matching every other status the switch puts on the wire (app_user and vpa both carry active / deregistered), so a client never has to remember which surface shouts and which does not." The integration test asserts res["status"] == "resolved". Docs said RESOLVED / PENDING / FAILED throughout. Lowercased in the status enum, its description and example, the response examples, the errorCode / errorMessage notes, the guide's status table and samples, and the qa-testing reference to the polling status. Only these three status values changed. The uppercase enums (PERSON, ENTITY, SAVINGS, CURRENT, SMALL, LARGE, ONLINE, OFFLINE, and the ownership values) are uppercase in the implementation and are untouched. Co-Authored-By: Claude Opus 5 (1M context) --- api-references/payments/upi-issuance.json | 24 +++++++++---------- content/payments/upi-issuance/qa-testing.mdx | 2 +- .../payments/upi-issuance/vpa-resolution.mdx | 24 +++++++++---------- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/api-references/payments/upi-issuance.json b/api-references/payments/upi-issuance.json index 8d04beb8..28b047ca 100644 --- a/api-references/payments/upi-issuance.json +++ b/api-references/payments/upi-issuance.json @@ -1191,7 +1191,7 @@ "VPA resolution" ], "summary": "Resolve a payee VPA", - "description": "Resolve a payee VPA on the payer side. A VPA under our handle is answered immediately from local records; a foreign-handle VPA triggers an async resolution with NPCI and returns PENDING until the result arrives. Poll by repeating this same POST. PENDING is not an error.", + "description": "Resolve a payee VPA on the payer side. A VPA under our handle is answered immediately from local records; a foreign-handle VPA triggers an async resolution with NPCI and returns pending until the result arrives. Poll by repeating this same POST. pending is not an error.", "operationId": "resolveVPA", "parameters": [ { @@ -1210,11 +1210,11 @@ { "name": "X-Sim-Resolution", "in": "header", - "description": "**QA / sandbox only.** Pins the values this resolution returns, so a person, a merchant, a verified payee, or a failure can be reproduced deterministically instead of depending on what a real payee PSP reports. The value is a JSON object of the fields you expect back, e.g. {\"name\":\"Test Merchant\",\"entityType\":\"ENTITY\",\"verified\":true} or {\"status\":\"FAILED\",\"errorCode\":\"ZH\"}. Send it on every poll \u2014 it is read on the poll that returns RESOLVED. Ignored on production.", + "description": "**QA / sandbox only.** Pins the values this resolution returns, so a person, a merchant, a verified payee, or a failure can be reproduced deterministically instead of depending on what a real payee PSP reports. The value is a JSON object of the fields you expect back, e.g. {\"name\":\"Test Merchant\",\"entityType\":\"ENTITY\",\"verified\":true} or {\"status\":\"FAILED\",\"errorCode\":\"ZH\"}. Send it on every poll \u2014 it is read on the poll that returns resolved. Ignored on production.", "allowEmptyValue": true, "schema": { "type": "string", - "description": "**QA / sandbox only.** Pins the values this resolution returns, so a person, a merchant, a verified payee, or a failure can be reproduced deterministically instead of depending on what a real payee PSP reports. The value is a JSON object of the fields you expect back, e.g. {\"name\":\"Test Merchant\",\"entityType\":\"ENTITY\",\"verified\":true} or {\"status\":\"FAILED\",\"errorCode\":\"ZH\"}. Send it on every poll \u2014 it is read on the poll that returns RESOLVED. Ignored on production.", + "description": "**QA / sandbox only.** Pins the values this resolution returns, so a person, a merchant, a verified payee, or a failure can be reproduced deterministically instead of depending on what a real payee PSP reports. The value is a JSON object of the fields you expect back, e.g. {\"name\":\"Test Merchant\",\"entityType\":\"ENTITY\",\"verified\":true} or {\"status\":\"FAILED\",\"errorCode\":\"ZH\"}. Send it on every poll \u2014 it is read on the poll that returns resolved. Ignored on production.", "example": "{\"name\":\"Test Merchant\",\"entityType\":\"ENTITY\",\"verified\":true}" }, "example": "{\"name\":\"Test Merchant\",\"entityType\":\"ENTITY\",\"verified\":true}" @@ -1246,7 +1246,7 @@ }, "example": { "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", - "status": "RESOLVED", + "status": "resolved", "vpa": "brandx@ybl", "name": "BRAND****X", "accountType": "CURRENT", @@ -4668,12 +4668,12 @@ }, "errorCode": { "type": "string", - "description": "NPCI error code. Present on FAILED (e.g. ZH for an invalid address).", + "description": "NPCI error code. Present on failed (e.g. ZH for an invalid address).", "example": "ZH" }, "errorMessage": { "type": "string", - "description": "Human-readable failure reason. Present on FAILED.", + "description": "Human-readable failure reason. Present on failed.", "example": "invalid virtual address" }, "franchiseName": { @@ -4719,12 +4719,12 @@ "status": { "type": "string", "enum": [ - "RESOLVED", - "PENDING", - "FAILED" + "resolved", + "pending", + "failed" ], - "description": "Resolution outcome. RESOLVED indicates that the payee VPA is valid and the API response carries the payee VPA details. PENDING indicates that the payee VPA resolution is in progress. FAILED indicates that the payee VPA is invalid and the API response indicates the failure reason.", - "example": "RESOLVED" + "description": "Resolution outcome. The value resolved indicates that the payee VPA is valid and the API response carries the payee VPA details. The value pending indicates that the payee VPA resolution is in progress. The value failed indicates that the payee VPA is invalid and the API response indicates the failure reason.", + "example": "resolved" }, "traceId": { "type": "string", @@ -4762,7 +4762,7 @@ "entityType": "PERSON", "ifsc": "ICIC0000052", "name": "ANI********DEY", - "status": "RESOLVED", + "status": "resolved", "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", "verified": true, "verifiedLogo": "https://cdn.example/brandx.png", diff --git a/content/payments/upi-issuance/qa-testing.mdx b/content/payments/upi-issuance/qa-testing.mdx index 56c6f712..c5a2536a 100644 --- a/content/payments/upi-issuance/qa-testing.mdx +++ b/content/payments/upi-issuance/qa-testing.mdx @@ -53,7 +53,7 @@ On the QA env there is no telecom, so no OTP SMS is sent. To pass verification, #### VPA resolution -On production, resolving a foreign-handle payee returns whatever the payee PSP reports. On the QA env you can pin the resolved values so you can reproduce every shape — a person, a merchant, verified or not, and failures — deterministically. Send an optional `X-Sim-Resolution` header on `POST /vpa/resolve` whose value is a JSON object of the values you expect back. Send it on every poll (it is read on the poll that returns `RESOLVED`). +On production, resolving a foreign-handle payee returns whatever the payee PSP reports. On the QA env you can pin the resolved values so you can reproduce every shape — a person, a merchant, verified or not, and failures — deterministically. Send an optional `X-Sim-Resolution` header on `POST /vpa/resolve` whose value is a JSON object of the values you expect back. Send it on every poll (it is read on the poll that returns `resolved`). Every field is optional; anything you leave out falls back to a default, and with no header at all the resolution uses default values. Failures carry no payee detail, so `entityType`, the merchant fields and `verified` apply only to a success. diff --git a/content/payments/upi-issuance/vpa-resolution.mdx b/content/payments/upi-issuance/vpa-resolution.mdx index 3bc0d54c..13c849fd 100644 --- a/content/payments/upi-issuance/vpa-resolution.mdx +++ b/content/payments/upi-issuance/vpa-resolution.mdx @@ -11,15 +11,15 @@ Before an `active` user pays a payee, the TPAP / Issuing App resolves the payee' ### How resolution works -Resolution is a **single API, client-polled** call. A payee VPA on Setu's own handle resolves immediately; a **foreign-handle** VPA (any other PSP, e.g. `someone@ybl`) is resolved with the UPI network, which is asynchronous — so the first call returns `PENDING` and the app **polls by sending the same request again** until it settles. +Resolution is a **single API, client-polled** call. A payee VPA on Setu's own handle resolves immediately; a **foreign-handle** VPA (any other PSP, e.g. `someone@ybl`) is resolved with the UPI network, which is asynchronous — so the first call returns `pending` and the app **polls by sending the same request again** until it settles. The `status` field carries the outcome; the HTTP status stays `200` for all three: | `status` | Meaning | | :--- | :--- | -| `RESOLVED` | The payee VPA is valid, and the API response carries the payee VPA details. | -| `PENDING` | The payee VPA resolution is in progress — send the same request again to poll. | -| `FAILED` | The payee VPA is invalid, and the API response indicates the failure reason. | +| `resolved` | The payee VPA is valid, and the API response carries the payee VPA details. | +| `pending` | The payee VPA resolution is in progress — send the same request again to poll. | +| `failed` | The payee VPA is invalid, and the API response indicates the failure reason. |
@@ -52,7 +52,7 @@ The `status` field carries the outcome; the HTTP status stays `200` for all thre | Field | Type | Notes | | :--- | :--- | :--- | | `traceId` | string | Trace handle for this call (ULID). | -| `status` | string | `RESOLVED`, `PENDING`, or `FAILED`. | +| `status` | string | `resolved`, `pending`, or `failed`. | | `vpa` | string | The payee VPA that was resolved (echo of the request). | | `name` | string | The payee's account-holder name — the name on record for a VPA on our handle, or the name as reported by the payee PSP for a foreign handle. Present in the API response in case of successful resolution. | | `accountType` | string | The payee's account type (e.g. `SAVINGS`). Present in the API response in case of successful resolution. | @@ -62,8 +62,8 @@ The `status` field carries the outcome; the HTTP status stays `200` for all thre | `verifiedName` | string | The registered display name from the verified registry. Present when the payee VPA is a whitelisted / verified VPA. | | `verifiedLogo` | string | The registered logo url. Present when the payee VPA is a whitelisted / verified VPA. | | `verifiedUrl` | string | The registered brand url. Present when the payee VPA is a whitelisted / verified VPA. | -| `errorCode` | string | Network error code (e.g. `ZH`). Present on `FAILED`. | -| `errorMessage` | string | Human-readable failure reason. Present on `FAILED`. | +| `errorCode` | string | Network error code (e.g. `ZH`). Present on `failed`. | +| `errorMessage` | string | Human-readable failure reason. Present on `failed`. | A **merchant** payee (`entityType: ENTITY`) additionally carries a merchant block. These fields are present for a resolved merchant VPA: @@ -82,7 +82,7 @@ A **merchant** payee (`entityType: ENTITY`) additionally carries a merchant bloc {`{ "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", - "status": "RESOLVED", + "status": "resolved", "vpa": "someone@ybl", "name": "ANI********DEY", "accountType": "SAVINGS", @@ -96,7 +96,7 @@ A **merchant** payee (`entityType: ENTITY`) additionally carries a merchant bloc {`{ "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", - "status": "RESOLVED", + "status": "resolved", "vpa": "brandx@ybl", "name": "BRAND****X", "accountType": "CURRENT", @@ -121,7 +121,7 @@ A **merchant** payee (`entityType: ENTITY`) additionally carries a merchant bloc {`{ "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", - "status": "PENDING", + "status": "pending", "vpa": "someone@ybl" }`} @@ -131,7 +131,7 @@ A **merchant** payee (`entityType: ENTITY`) additionally carries a merchant bloc {`{ "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", - "status": "FAILED", + "status": "failed", "vpa": "someone@ybl", "errorCode": "ZH", "errorMessage": "invalid virtual address" @@ -140,7 +140,7 @@ A **merchant** payee (`entityType: ENTITY`) additionally carries a merchant bloc ##### Errors -`PENDING` and `FAILED` are **outcomes**, not HTTP errors — they come back as `200`. The HTTP error statuses are: +`pending` and `failed` are **outcomes**, not HTTP errors — they come back as `200`. The HTTP error statuses are: | Status | Code | When | | :--- | :--- | :--- | From d0bedae853cc6044f34d40f25f03500b4793e884 Mon Sep 17 00:00:00 2001 From: Anindya Pandey Date: Mon, 27 Jul 2026 03:20:11 +0530 Subject: [PATCH 19/27] docs(UPIIS-69): align X-Sim-Resolution docs with the renamed directive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The directive now spells the outcome field `status`, carrying the same resolved / failed vocabulary as the resolve response (simresolve.go: "An earlier revision spelled this field `outcome` with success / failure, which meant a client pinning a failure wrote one word and read back another"). The legacy `outcome` spelling is still accepted but is never emitted, so document the current name. - qa-testing: `outcome` (success|failure) -> `status` (resolved|failed), and the forced-failure example now sends {"status":"failed",...} - spec X-Sim-Resolution: same, plus `verified` is an object ({name,url,logo}) whose presence flags the payee as verified — the description and both examples had it as a boolean Co-Authored-By: Claude Opus 5 (1M context) --- api-references/payments/upi-issuance.json | 8 ++++---- content/payments/upi-issuance/qa-testing.mdx | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/api-references/payments/upi-issuance.json b/api-references/payments/upi-issuance.json index 28b047ca..becefff8 100644 --- a/api-references/payments/upi-issuance.json +++ b/api-references/payments/upi-issuance.json @@ -1210,14 +1210,14 @@ { "name": "X-Sim-Resolution", "in": "header", - "description": "**QA / sandbox only.** Pins the values this resolution returns, so a person, a merchant, a verified payee, or a failure can be reproduced deterministically instead of depending on what a real payee PSP reports. The value is a JSON object of the fields you expect back, e.g. {\"name\":\"Test Merchant\",\"entityType\":\"ENTITY\",\"verified\":true} or {\"status\":\"FAILED\",\"errorCode\":\"ZH\"}. Send it on every poll \u2014 it is read on the poll that returns resolved. Ignored on production.", + "description": "**QA / sandbox only.** Pins the values this resolution returns, so a person, a merchant, a verified payee, or a failure can be reproduced deterministically instead of depending on what a real payee PSP reports. The value is a JSON object of the fields you expect back, e.g. {\"name\":\"Test Merchant\",\"entityType\":\"ENTITY\",\"verified\":{\"name\":\"Test Merchant\"}} or {\"status\":\"failed\",\"errorCode\":\"ZH\"}. Send it on every poll \u2014 it is read on the poll that returns resolved. Ignored on production.", "allowEmptyValue": true, "schema": { "type": "string", - "description": "**QA / sandbox only.** Pins the values this resolution returns, so a person, a merchant, a verified payee, or a failure can be reproduced deterministically instead of depending on what a real payee PSP reports. The value is a JSON object of the fields you expect back, e.g. {\"name\":\"Test Merchant\",\"entityType\":\"ENTITY\",\"verified\":true} or {\"status\":\"FAILED\",\"errorCode\":\"ZH\"}. Send it on every poll \u2014 it is read on the poll that returns resolved. Ignored on production.", - "example": "{\"name\":\"Test Merchant\",\"entityType\":\"ENTITY\",\"verified\":true}" + "description": "**QA / sandbox only.** Pins the values this resolution returns, so a person, a merchant, a verified payee, or a failure can be reproduced deterministically instead of depending on what a real payee PSP reports. The value is a JSON object of the fields you expect back, e.g. {\"name\":\"Test Merchant\",\"entityType\":\"ENTITY\",\"verified\":{\"name\":\"Test Merchant\"}} or {\"status\":\"failed\",\"errorCode\":\"ZH\"}. Send it on every poll \u2014 it is read on the poll that returns resolved. Ignored on production.", + "example": "{\"name\":\"Test Merchant\",\"entityType\":\"ENTITY\",\"verified\":{\"name\":\"Test Merchant\"}}" }, - "example": "{\"name\":\"Test Merchant\",\"entityType\":\"ENTITY\",\"verified\":true}" + "example": "{\"name\":\"Test Merchant\",\"entityType\":\"ENTITY\",\"verified\":{\"name\":\"Test Merchant\"}}" } ], "requestBody": { diff --git a/content/payments/upi-issuance/qa-testing.mdx b/content/payments/upi-issuance/qa-testing.mdx index c5a2536a..5a479f1c 100644 --- a/content/payments/upi-issuance/qa-testing.mdx +++ b/content/payments/upi-issuance/qa-testing.mdx @@ -59,8 +59,8 @@ Every field is optional; anything you leave out falls back to a default, and wit | field | meaning | default | | :--- | :--- | :--- | -| `outcome` | `success` or `failure` | `success` | -| `errorCode` | error code returned for a `failure` | `ZH` | +| `status` | `resolved` or `failed` — the same vocabulary the response carries | `resolved` | +| `errorCode` | error code returned for a `failed` resolution | `ZH` | | `entityType` | `PERSON` or `ENTITY` (a `merchant` block also implies `ENTITY`) | `PERSON` | | `name` | account-holder name | a default | | `accountType` | e.g. `SAVINGS` / `CURRENT` | a default | @@ -79,7 +79,7 @@ curl https:///api/v1/vpa/resolve \ A forced failure: ``` -X-Sim-Resolution: {"outcome":"failure","errorCode":"ZH"} +X-Sim-Resolution: {"status":"failed","errorCode":"ZH"} ``` From af89c411119be0b94fe578656db38ecabc49e581 Mon Sep 17 00:00:00 2001 From: Anindya Pandey Date: Mon, 27 Jul 2026 03:22:55 +0530 Subject: [PATCH 20/27] docs(UPIIS-69): clarify errorCode wording and space the merchant block - errorCode: "Network error code" -> "VPA resolution failure error code" - add a line break above the merchant-block paragraph Co-Authored-By: Claude Opus 5 (1M context) --- content/payments/upi-issuance/vpa-resolution.mdx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/content/payments/upi-issuance/vpa-resolution.mdx b/content/payments/upi-issuance/vpa-resolution.mdx index 13c849fd..f4a27fe6 100644 --- a/content/payments/upi-issuance/vpa-resolution.mdx +++ b/content/payments/upi-issuance/vpa-resolution.mdx @@ -62,9 +62,11 @@ The `status` field carries the outcome; the HTTP status stays `200` for all thre | `verifiedName` | string | The registered display name from the verified registry. Present when the payee VPA is a whitelisted / verified VPA. | | `verifiedLogo` | string | The registered logo url. Present when the payee VPA is a whitelisted / verified VPA. | | `verifiedUrl` | string | The registered brand url. Present when the payee VPA is a whitelisted / verified VPA. | -| `errorCode` | string | Network error code (e.g. `ZH`). Present on `failed`. | +| `errorCode` | string | VPA resolution failure error code (e.g. `ZH`). Present on `failed`. | | `errorMessage` | string | Human-readable failure reason. Present on `failed`. | +
+ A **merchant** payee (`entityType: ENTITY`) additionally carries a merchant block. These fields are present for a resolved merchant VPA: | Field | Type | Notes | From 0ceec9e7c0a12b03568c95ef27e56e4b0699fcd8 Mon Sep 17 00:00:00 2001 From: Anindya Pandey Date: Mon, 27 Jul 2026 03:25:03 +0530 Subject: [PATCH 21/27] docs(UPIIS-69): state idempotencyKey on the VPA resolution page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 50e3fa5 dropped it along with the rest of the intro preamble, which left this the only feature page not mentioning it — payee-blocklist, vpa-management, device-binding and user-otp all do. Restored in the per-operation position those pages use (after the request field table) rather than back in the intro. Co-Authored-By: Claude Opus 5 (1M context) --- content/payments/upi-issuance/vpa-resolution.mdx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/content/payments/upi-issuance/vpa-resolution.mdx b/content/payments/upi-issuance/vpa-resolution.mdx index f4a27fe6..9be6dcdf 100644 --- a/content/payments/upi-issuance/vpa-resolution.mdx +++ b/content/payments/upi-issuance/vpa-resolution.mdx @@ -34,6 +34,10 @@ The `status` field carries the outcome; the HTTP status stays `200` for all thre | `vpa` | string | yes | The payee VPA to resolve (`prefix@handle`; any handle). Max 255 characters — `^([A-Za-z0-9.-]+)@([A-Za-z0-9.-]+)$`. | | `geocode` | string | no | The device geocode as `lat,long` (e.g. `12.9716,77.5946`); forwarded with the network request. | +
+ +`idempotencyKey` is an optional request header. + ##### Sample request From 87d7f14b042c3f0cdcd978fb730f58e80c4bc021 Mon Sep 17 00:00:00 2001 From: Anindya Pandey Date: Mon, 27 Jul 2026 03:33:02 +0530 Subject: [PATCH 22/27] docs(UPIIS-69): document the QA sim defaults, code blocks and run steps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fill in the actual X-Sim-Resolution defaults from the upstream mock (buildRespValAddInner) instead of saying "a default": they differ by entityType — MOCK****PAYEE/SAVINGS for a PERSON, BRAND****X/CURRENT plus the Brand-X merchant block for an ENTITY, ICIC0000052 either way. - Document the no-header behaviour: the outcome is driven by the payee VPA (fail/invalid -> failed, merchant/brand -> ENTITY, else PERSON), not by a fixed default response. - Convert the two plain fenced blocks to CodeBlockWithCopy — this was the only UPI Issuance page not using the component. - Add the two vpa/resolve steps to the full QA env run table. Co-Authored-By: Claude Opus 5 (1M context) --- content/payments/upi-issuance/qa-testing.mdx | 30 +++++++++++--------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/content/payments/upi-issuance/qa-testing.mdx b/content/payments/upi-issuance/qa-testing.mdx index 5a479f1c..7b4b93d7 100644 --- a/content/payments/upi-issuance/qa-testing.mdx +++ b/content/payments/upi-issuance/qa-testing.mdx @@ -55,32 +55,34 @@ On the QA env there is no telecom, so no OTP SMS is sent. To pass verification, On production, resolving a foreign-handle payee returns whatever the payee PSP reports. On the QA env you can pin the resolved values so you can reproduce every shape — a person, a merchant, verified or not, and failures — deterministically. Send an optional `X-Sim-Resolution` header on `POST /vpa/resolve` whose value is a JSON object of the values you expect back. Send it on every poll (it is read on the poll that returns `resolved`). -Every field is optional; anything you leave out falls back to a default, and with no header at all the resolution uses default values. Failures carry no payee detail, so `entityType`, the merchant fields and `verified` apply only to a success. +Every field is optional; anything you leave out falls back to a default. Failures carry no payee detail, so `entityType`, the merchant fields and `verified` apply only to a success. | field | meaning | default | | :--- | :--- | :--- | | `status` | `resolved` or `failed` — the same vocabulary the response carries | `resolved` | | `errorCode` | error code returned for a `failed` resolution | `ZH` | | `entityType` | `PERSON` or `ENTITY` (a `merchant` block also implies `ENTITY`) | `PERSON` | -| `name` | account-holder name | a default | -| `accountType` | e.g. `SAVINGS` / `CURRENT` | a default | -| `ifsc` | payee IFSC | a default | -| `merchant.mcc` / `.brandName` / `.legalName` / `.franchiseName` / `.merchantType` / `.genre` / `.ownership` | merchant detail for an `ENTITY` payee | defaults | +| `name` | account-holder name | `MOCK****PAYEE`, or `BRAND****X` for an `ENTITY` | +| `accountType` | e.g. `SAVINGS` / `CURRENT` | `SAVINGS`, or `CURRENT` for an `ENTITY` | +| `ifsc` | payee IFSC | `ICIC0000052` | +| `merchant.mcc` / `.brandName` / `.legalName` / `.franchiseName` / `.merchantType` / `.genre` / `.ownership` | merchant detail for an `ENTITY` payee | `5732` / `Brand-X` / `Acme Pvt Ltd` / `Brand-X Franchise` / `LARGE` / `ONLINE` / `PUBLIC` | | `verified.name` / `.url` / `.logo` | verified/whitelisted enrichment; its presence makes the payee resolve `verified: true` | not verified | +
+ +With **no header at all**, the outcome is driven by the payee VPA itself: an address containing `fail` or `invalid` resolves `failed` with `ZH`, one containing `merchant` or `brand` resolves as an `ENTITY`, and anything else resolves as a `PERSON` — each with the defaults above. + A verified merchant with pinned values: -```bash -curl https:///api/v1/vpa/resolve \ - -H 'X-Sim-Resolution: {"entityType":"ENTITY","name":"ACME****PVT","accountType":"CURRENT","ifsc":"HDFC0000123","merchant":{"mcc":"5411","brandName":"MyBrand","legalName":"Acme Pvt Ltd","merchantType":"SMALL"},"verified":{"name":"MyBrand","url":"https://mybrand.example"}}' \ - -H 'Content-Type: application/json' -d '' -``` + + {`curl https:///api/v1/vpa/resolve -H 'Content-Type: application/json' -H 'X-Sim-Resolution: {"entityType":"ENTITY","name":"ACME****PVT","accountType":"CURRENT","ifsc":"HDFC0000123","merchant":{"mcc":"5411","brandName":"MyBrand","legalName":"Acme Pvt Ltd","merchantType":"SMALL"},"verified":{"name":"MyBrand","url":"https://mybrand.example"}}' -d ''`} + A forced failure: -``` -X-Sim-Resolution: {"status":"failed","errorCode":"ZH"} -``` + + {`X-Sim-Resolution: {"status":"failed","errorCode":"ZH"}`} + X-Sim-Resolution is honoured only on the QA env. On production the @@ -119,6 +121,8 @@ Everything else is reproduced with ordinary requests. A sampling: | 4 | `create-vpa` | `otpRequired: true` (the Android path) | VPA → `pending-verification` | | 5 | `otp/request` | With the VPA | `202` accepted | | 6 | `otp/verify` | `idempotencyKey: sim.otp-ok`, with the VPA | VPA → `active` | +| 7 | `vpa/resolve` | A payee on our handle | `resolved` with the payee details | +| 8 | `vpa/resolve` | A foreign-handle payee, `X-Sim-Resolution` pinning the values | `pending` on the first call, then `resolved` with the pinned values | ### Next From d63ab07223338fa9ed90f4ad33534c4f00459895 Mon Sep 17 00:00:00 2001 From: Anindya Pandey Date: Mon, 27 Jul 2026 03:54:31 +0530 Subject: [PATCH 23/27] docs(UPIIS-72): document the vpa.resolve resolution notification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the webhook section to the VPA resolution guide: the key material the TPAP supplies, the X-Setu-Event event, the sealed envelope, the receiver's validation order, and Setu's retry behaviour. Verified against setu-upi-issuance: - event name / header notify/ports: EventVPAResolve, EventHeader - body is the resolve response field for field, terminal status only - envelope ct/sk/iv/api/clientId/sig, api = the event name; RSA-OAEP (SHA-1) wrap of a 32-byte key, AES-256-CBC, sig = HMAC-SHA256 over the plaintext envelope/outbound.go: Seal - 3 attempts, 500ms doubling backoff, 5s per attempt, retry on 5xx/408/ 429 only configs/config.json + notify.retryableStatus Also fixes two examples that showed entityType PERSON alongside verified:true with Brand-X branding — a person carrying merchant brand verification. The webhook sample is now a plain PERSON matching the poll's person sample, and the spec's schema-level example is synced to the path-level one so the two no longer disagree. Co-Authored-By: Claude Opus 5 (1M context) --- api-references/payments/upi-issuance.json | 23 ++-- .../payments/upi-issuance/vpa-resolution.mdx | 101 ++++++++++++++++++ 2 files changed, 116 insertions(+), 8 deletions(-) diff --git a/api-references/payments/upi-issuance.json b/api-references/payments/upi-issuance.json index becefff8..02ab14ff 100644 --- a/api-references/payments/upi-issuance.json +++ b/api-references/payments/upi-issuance.json @@ -4758,17 +4758,24 @@ } }, "example": { - "accountType": "SAVINGS", - "entityType": "PERSON", - "ifsc": "ICIC0000052", - "name": "ANI********DEY", - "status": "resolved", "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "status": "resolved", + "vpa": "brandx@ybl", + "name": "BRAND****X", + "accountType": "CURRENT", + "ifsc": "ICIC0000052", + "entityType": "ENTITY", + "genre": "ONLINE", + "mcc": "5732", + "merchantType": "LARGE", + "brandName": "Brand-X", + "franchiseName": "Brand-X Koramangala", + "legalName": "Acme Pvt Ltd", + "ownership": "PUBLIC", "verified": true, - "verifiedLogo": "https://cdn.example/brandx.png", "verifiedName": "Brand-X", - "verifiedUrl": "https://brandx.example", - "vpa": "someone@ybl" + "verifiedLogo": "https://cdn.example/brandx.png", + "verifiedUrl": "https://brandx.example" }, "required": [ "traceId", diff --git a/content/payments/upi-issuance/vpa-resolution.mdx b/content/payments/upi-issuance/vpa-resolution.mdx index 9be6dcdf..33a1df3f 100644 --- a/content/payments/upi-issuance/vpa-resolution.mdx +++ b/content/payments/upi-issuance/vpa-resolution.mdx @@ -156,6 +156,107 @@ A **merchant** payee (`entityType: ENTITY`) additionally carries a merchant bloc | 409 | `invalid-user-state` | The user is not `active`, or has no active VPA to resolve from. | | 500 | `internal-error` | Something went wrong on Setu's side. | +
+ +### Resolution notification (webhook) + +Polling works, but it makes the app wait. Setu can also **push** the outcome of a foreign-handle resolution to an endpoint you host, the moment the UPI network answers — so the app can react at once instead of on its next poll. + +The notification is an **optimisation, not a delivery guarantee**. Polling `POST /api/v1/vpa/resolve` remains the source of truth: if a notification is missed, the next poll still returns the same result. Nothing is lost, so you never have to build a recovery path around a webhook that did not arrive. + +#### What you give Setu + +The notification is configured **per event**, and you supply everything about it. Setu does not generate any of it. + +| What | Notes | +| :--- | :--- | +| **Target** | The absolute HTTPS URL Setu POSTs the event to, e.g. `https://your-app.example.com/webhooks/upi/vpa-resolve`. One complete URL, not a base plus a path. | +| **Public key** | An RSA public key, PEM-encoded (PKIX or PKCS#1), min 2048-bit. **You generate the keypair and keep the private key** — Setu only ever holds the public half and never sees the private one. Setu encrypts every notification to this key. | +| **Signing key** | A shared secret **you choose**. Setu signs every notification with it and you verify the signature with the same value. Because you already hold it, no further exchange is needed. Treat it as a credential: min 32 bytes of entropy, and rotate it by sending Setu a new one. | +| **Client id** | The identifier you know Setu by. It is echoed on every notification as `clientId`, so your receiver can select which verification secret to use — useful once you accept notifications from more than one source. | + + + This is the **mirror image** of the inbound direction. For your calls *to* Setu, Setu issues the key material (the envelope public key and your client secret). For Setu's calls *to you*, you issue it. Same crypto, opposite roles — so your receiver runs the code you already wrote, with the keys swapped. + + +Send these to your Setu point of contact for each environment. The signing key is stored encrypted and is bound to that environment, so **it cannot be copied between environments**: sandbox and production each need their own. + +#### The event + +`POST ` + +| Header | Value | +| :--- | :--- | +| `X-Setu-Event` | `vpa.resolve` | +| `Content-Type` | `application/json` | + +**The body is the `POST /api/v1/vpa/resolve` response body, field for field.** That is deliberate: the notification exists to save you the wait, not to give you a second contract. Deserialise it with the **same type** you already use for the poll response, and the code that renders a resolved payee works unchanged either way. + +Two differences from a poll response, both because a notification is only ever sent once there is something to say: + +- `status` is only ever `resolved` or `failed`. **`pending` is never pushed.** +- It is sent only for a **foreign-handle** payee. A VPA on Setu's own handle resolves synchronously on your original call, so there is nothing to announce. + +##### Sample notification (decrypted payload) + + + {`{ + "traceId": "01ARZ3NDEKTSV4RRFFQ69G5FAV", + "status": "resolved", + "vpa": "someone@ybl", + "name": "ANI********DEY", + "accountType": "SAVINGS", + "ifsc": "ICIC0000052", + "entityType": "PERSON" +}`} + + +#### What your receiver must do + +The body arrives as the **same crypto envelope** your own requests use, with the key roles reversed: Setu encrypts to your public key and signs with your signing key. + + + {`{ + "ct": "", + "sk": "", + "iv": "", + "api": "vpa.resolve", + "clientId": "", + "sig": "" +}`} + + +Validate in this order, and reject as soon as a step fails: + +1. **Check the event.** Read `X-Setu-Event`. Ignore an event name you do not handle. +2. **Select the secret.** Read `clientId` from the envelope and pick the matching verification secret. Reject an unknown `clientId`. +3. **Recover the session key.** RSA-OAEP-SHA1 decrypt `sk` with your private key. It must yield exactly **32 bytes**. +4. **Decrypt.** AES-256-CBC decrypt `ct` with that key and the base64-decoded `iv` (which must be exactly **16 bytes**), then strip PKCS#7 padding. +5. **Verify the signature.** Compute HMAC-SHA256 of the **decrypted plaintext** under your signing key, base64-encode it, and compare with `sig` in **constant time**. Do not skip this when `sig` is absent — treat a missing signature as a failed verification. +6. **Parse the payload** and act on it. +7. **Acknowledge.** Respond `2xx` with an empty body. Setu does not read the response body. + + + Verify the signature over the **decrypted plaintext**, never over the ciphertext, and always **after** decrypting. This is the same rule Setu applies to your inbound requests. + + +**Be idempotent on `(X-Setu-Event, traceId)`.** Delivery is at-least-once: a network failure between Setu sending and recording success causes a retry, so the same notification can arrive more than once. It always carries the same terminal answer, so de-duplicating on that pair is sufficient. + +#### How Setu responds to your response + +| Your response | What Setu does | +| :--- | :--- | +| `2xx` | Delivered. Done. | +| `5xx`, `408`, `429` | Retries with exponential backoff, up to **3 attempts total** (500 ms, then 1 s). | +| Any other `4xx` | **Does not retry.** A rejected signature or a malformed payload would be rejected identically on a second attempt, so Setu stops and raises an internal alert. | +| No response / timeout | Treated as a transient failure and retried, within the same 3-attempt budget. Each attempt has a 5 s timeout. | + +If every attempt fails, Setu raises an operational alert and **the notification is not redelivered later**. This is safe precisely because polling is authoritative: the result is still available from `POST /api/v1/vpa/resolve` at any time. If your receiver has been down, poll for anything you may have missed. + + + Until you have supplied a target, no notification is sent and nothing changes — polling alone is a complete integration. Add the webhook when you want to remove the wait. + + ### Next - **[Payee blocklist](/payments/upi-issuance/payee-blocklist)** — block payees a user never wants to transact with. From fd36dfa8365c759ea0fd36b3cdac4a497ff6ffb5 Mon Sep 17 00:00:00 2001 From: Anindya Pandey Date: Mon, 27 Jul 2026 03:55:48 +0530 Subject: [PATCH 24/27] docs(UPIIS-72): drop the "optimisation, not a delivery guarantee" line Co-Authored-By: Claude Opus 5 (1M context) --- content/payments/upi-issuance/vpa-resolution.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/payments/upi-issuance/vpa-resolution.mdx b/content/payments/upi-issuance/vpa-resolution.mdx index 33a1df3f..befd7878 100644 --- a/content/payments/upi-issuance/vpa-resolution.mdx +++ b/content/payments/upi-issuance/vpa-resolution.mdx @@ -162,7 +162,7 @@ A **merchant** payee (`entityType: ENTITY`) additionally carries a merchant bloc Polling works, but it makes the app wait. Setu can also **push** the outcome of a foreign-handle resolution to an endpoint you host, the moment the UPI network answers — so the app can react at once instead of on its next poll. -The notification is an **optimisation, not a delivery guarantee**. Polling `POST /api/v1/vpa/resolve` remains the source of truth: if a notification is missed, the next poll still returns the same result. Nothing is lost, so you never have to build a recovery path around a webhook that did not arrive. +Polling `POST /api/v1/vpa/resolve` remains the source of truth: if a notification is missed, the next poll still returns the same result. Nothing is lost, so you never have to build a recovery path around a webhook that did not arrive. #### What you give Setu From 798208305da2447eebe6ee40c05f9276ce327cb9 Mon Sep 17 00:00:00 2001 From: Anindya Pandey Date: Mon, 27 Jul 2026 04:00:45 +0530 Subject: [PATCH 25/27] docs(UPIIS-72): third-person voice, and fix markup inside callouts - Drop second-person voice from the notification section and the QA resolution notes. Every other UPI Issuance page is written in third person around "the TPAP / Issuing App"; these two were the only ones using you/your (41 and 4 occurrences). - Markdown is not processed inside , so **mirror image** and **decrypted plaintext** rendered literally. Switched to /, the pattern the other callouts already use. Same bug fixed in api-envelope, where `clientSecret` in a callout was showing its backticks. - "so there is nothing to announce" -> "so a notification is not needed" - Sample target url now tpap.example.com, matching the API spec's own example, instead of your-app.example.com Co-Authored-By: Claude Opus 5 (1M context) --- .../payments/upi-issuance/api-envelope.mdx | 2 +- content/payments/upi-issuance/qa-testing.mdx | 4 +- .../payments/upi-issuance/vpa-resolution.mdx | 52 +++++++++---------- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/content/payments/upi-issuance/api-envelope.mdx b/content/payments/upi-issuance/api-envelope.mdx index f2089ca5..24a765aa 100644 --- a/content/payments/upi-issuance/api-envelope.mdx +++ b/content/payments/upi-issuance/api-envelope.mdx @@ -46,7 +46,7 @@ Setu issues the TPAP / Issuing App two credentials: a **`clientId`** and a **`cl Setu verifies the signature after decrypting the request. A missing signature, a signature that does not verify, or a `clientId` that does not belong to the TPAP / Issuing App is rejected with **`401`** and the error code **`invalid-signature`**. - Keep the `clientSecret` secret: it lives only on the TPAP / Issuing App's backend, never in a mobile app or browser. Sign on the server, then send the envelope. If the TPAP / Issuing App rotates the secret with Setu, the previous secret keeps working through the overlap window, so the cut-over happens without downtime. + Keep the clientSecret secret: it lives only on the TPAP / Issuing App's backend, never in a mobile app or browser. Sign on the server, then send the envelope. If the TPAP / Issuing App rotates the secret with Setu, the previous secret keeps working through the overlap window, so the cut-over happens without downtime. ### Response format diff --git a/content/payments/upi-issuance/qa-testing.mdx b/content/payments/upi-issuance/qa-testing.mdx index 7b4b93d7..77e18d27 100644 --- a/content/payments/upi-issuance/qa-testing.mdx +++ b/content/payments/upi-issuance/qa-testing.mdx @@ -53,9 +53,9 @@ On the QA env there is no telecom, so no OTP SMS is sent. To pass verification, #### VPA resolution -On production, resolving a foreign-handle payee returns whatever the payee PSP reports. On the QA env you can pin the resolved values so you can reproduce every shape — a person, a merchant, verified or not, and failures — deterministically. Send an optional `X-Sim-Resolution` header on `POST /vpa/resolve` whose value is a JSON object of the values you expect back. Send it on every poll (it is read on the poll that returns `resolved`). +On production, resolving a foreign-handle payee returns whatever the payee PSP reports. On the QA env the resolved values can be pinned, so every shape — a person, a merchant, verified or not, and failures — is reproducible deterministically. Send an optional `X-Sim-Resolution` header on `POST /vpa/resolve` whose value is a JSON object of the expected values. Send it on every poll (it is read on the poll that returns `resolved`). -Every field is optional; anything you leave out falls back to a default. Failures carry no payee detail, so `entityType`, the merchant fields and `verified` apply only to a success. +Every field is optional; anything left out falls back to a default. Failures carry no payee detail, so `entityType`, the merchant fields and `verified` apply only to a success. | field | meaning | default | | :--- | :--- | :--- | diff --git a/content/payments/upi-issuance/vpa-resolution.mdx b/content/payments/upi-issuance/vpa-resolution.mdx index befd7878..85c5dbfc 100644 --- a/content/payments/upi-issuance/vpa-resolution.mdx +++ b/content/payments/upi-issuance/vpa-resolution.mdx @@ -160,42 +160,42 @@ A **merchant** payee (`entityType: ENTITY`) additionally carries a merchant bloc ### Resolution notification (webhook) -Polling works, but it makes the app wait. Setu can also **push** the outcome of a foreign-handle resolution to an endpoint you host, the moment the UPI network answers — so the app can react at once instead of on its next poll. +Polling works, but it makes the app wait. Setu can also **push** the outcome of a foreign-handle resolution to an endpoint the TPAP / Issuing App hosts, the moment the UPI network answers — so the app can react at once instead of on its next poll. -Polling `POST /api/v1/vpa/resolve` remains the source of truth: if a notification is missed, the next poll still returns the same result. Nothing is lost, so you never have to build a recovery path around a webhook that did not arrive. +Polling `POST /api/v1/vpa/resolve` remains the source of truth: if a notification is missed, the next poll still returns the same result. Nothing is lost, so there is no need to build a recovery path around a webhook that did not arrive. -#### What you give Setu +#### What the TPAP / Issuing App supplies -The notification is configured **per event**, and you supply everything about it. Setu does not generate any of it. +The notification is configured **per event**, and the TPAP / Issuing App supplies everything about it. Setu does not generate any of it. | What | Notes | | :--- | :--- | -| **Target** | The absolute HTTPS URL Setu POSTs the event to, e.g. `https://your-app.example.com/webhooks/upi/vpa-resolve`. One complete URL, not a base plus a path. | -| **Public key** | An RSA public key, PEM-encoded (PKIX or PKCS#1), min 2048-bit. **You generate the keypair and keep the private key** — Setu only ever holds the public half and never sees the private one. Setu encrypts every notification to this key. | -| **Signing key** | A shared secret **you choose**. Setu signs every notification with it and you verify the signature with the same value. Because you already hold it, no further exchange is needed. Treat it as a credential: min 32 bytes of entropy, and rotate it by sending Setu a new one. | -| **Client id** | The identifier you know Setu by. It is echoed on every notification as `clientId`, so your receiver can select which verification secret to use — useful once you accept notifications from more than one source. | +| **Target** | The absolute HTTPS URL Setu POSTs the event to, e.g. `https://tpap.example.com/webhooks/upi/vpa-resolve`. One complete URL, not a base plus a path. | +| **Public key** | An RSA public key, PEM-encoded (PKIX or PKCS#1), min 2048-bit. **The TPAP / Issuing App generates the keypair and keeps the private key** — Setu only ever holds the public half and never sees the private one. Setu encrypts every notification to this key. | +| **Signing key** | A shared secret **chosen by the TPAP / Issuing App**. Setu signs every notification with it, and the same value verifies the signature. Because the TPAP / Issuing App already holds it, no further exchange is needed. Treat it as a credential: min 32 bytes of entropy, and rotate it by sending Setu a new one. | +| **Client id** | The identifier the TPAP / Issuing App knows Setu by. It is echoed on every notification as `clientId`, so the receiver can select which verification secret to use — useful once it accepts notifications from more than one source. | - This is the **mirror image** of the inbound direction. For your calls *to* Setu, Setu issues the key material (the envelope public key and your client secret). For Setu's calls *to you*, you issue it. Same crypto, opposite roles — so your receiver runs the code you already wrote, with the keys swapped. + This is the mirror image of the inbound direction. For calls to Setu, Setu issues the key material (the envelope public key and the client secret). For Setu's calls to the TPAP / Issuing App, the TPAP / Issuing App issues it. Same crypto, opposite roles — so the receiver runs the code already written for the envelope, with the keys swapped. -Send these to your Setu point of contact for each environment. The signing key is stored encrypted and is bound to that environment, so **it cannot be copied between environments**: sandbox and production each need their own. +Send these to the Setu point of contact for each environment. The signing key is stored encrypted and is bound to that environment, so **it cannot be copied between environments**: sandbox and production each need their own. #### The event -`POST ` +`POST ` | Header | Value | | :--- | :--- | | `X-Setu-Event` | `vpa.resolve` | | `Content-Type` | `application/json` | -**The body is the `POST /api/v1/vpa/resolve` response body, field for field.** That is deliberate: the notification exists to save you the wait, not to give you a second contract. Deserialise it with the **same type** you already use for the poll response, and the code that renders a resolved payee works unchanged either way. +**The body is the `POST /api/v1/vpa/resolve` response body, field for field.** That is deliberate: the notification exists to save the wait, not to introduce a second contract. It deserialises with the **same type** as the poll response, so the code that renders a resolved payee works unchanged either way. Two differences from a poll response, both because a notification is only ever sent once there is something to say: - `status` is only ever `resolved` or `failed`. **`pending` is never pushed.** -- It is sent only for a **foreign-handle** payee. A VPA on Setu's own handle resolves synchronously on your original call, so there is nothing to announce. +- It is sent only for a **foreign-handle** payee. A VPA on Setu's own handle resolves synchronously on the original call, so a notification is not needed. ##### Sample notification (decrypted payload) @@ -211,50 +211,50 @@ Two differences from a poll response, both because a notification is only ever s }`}
-#### What your receiver must do +#### What the receiver must do -The body arrives as the **same crypto envelope** your own requests use, with the key roles reversed: Setu encrypts to your public key and signs with your signing key. +The body arrives as the **same crypto envelope** as inbound requests, with the key roles reversed: Setu encrypts to the TPAP / Issuing App's public key and signs with its signing key. {`{ "ct": "", - "sk": "", + "sk": "", "iv": "", "api": "vpa.resolve", - "clientId": "", - "sig": "" + "clientId": "", + "sig": "" }`} Validate in this order, and reject as soon as a step fails: -1. **Check the event.** Read `X-Setu-Event`. Ignore an event name you do not handle. +1. **Check the event.** Read `X-Setu-Event`. Ignore an unhandled event name. 2. **Select the secret.** Read `clientId` from the envelope and pick the matching verification secret. Reject an unknown `clientId`. -3. **Recover the session key.** RSA-OAEP-SHA1 decrypt `sk` with your private key. It must yield exactly **32 bytes**. +3. **Recover the session key.** RSA-OAEP-SHA1 decrypt `sk` with the private key. It must yield exactly **32 bytes**. 4. **Decrypt.** AES-256-CBC decrypt `ct` with that key and the base64-decoded `iv` (which must be exactly **16 bytes**), then strip PKCS#7 padding. -5. **Verify the signature.** Compute HMAC-SHA256 of the **decrypted plaintext** under your signing key, base64-encode it, and compare with `sig` in **constant time**. Do not skip this when `sig` is absent — treat a missing signature as a failed verification. +5. **Verify the signature.** Compute HMAC-SHA256 of the **decrypted plaintext** under the signing key, base64-encode it, and compare with `sig` in **constant time**. Do not skip this when `sig` is absent — treat a missing signature as a failed verification. 6. **Parse the payload** and act on it. 7. **Acknowledge.** Respond `2xx` with an empty body. Setu does not read the response body. - Verify the signature over the **decrypted plaintext**, never over the ciphertext, and always **after** decrypting. This is the same rule Setu applies to your inbound requests. + Verify the signature over the decrypted plaintext, never over the ciphertext, and always after decrypting. This is the same rule Setu applies to inbound requests. **Be idempotent on `(X-Setu-Event, traceId)`.** Delivery is at-least-once: a network failure between Setu sending and recording success causes a retry, so the same notification can arrive more than once. It always carries the same terminal answer, so de-duplicating on that pair is sufficient. -#### How Setu responds to your response +#### How Setu handles the response -| Your response | What Setu does | +| Receiver response | What Setu does | | :--- | :--- | | `2xx` | Delivered. Done. | | `5xx`, `408`, `429` | Retries with exponential backoff, up to **3 attempts total** (500 ms, then 1 s). | | Any other `4xx` | **Does not retry.** A rejected signature or a malformed payload would be rejected identically on a second attempt, so Setu stops and raises an internal alert. | | No response / timeout | Treated as a transient failure and retried, within the same 3-attempt budget. Each attempt has a 5 s timeout. | -If every attempt fails, Setu raises an operational alert and **the notification is not redelivered later**. This is safe precisely because polling is authoritative: the result is still available from `POST /api/v1/vpa/resolve` at any time. If your receiver has been down, poll for anything you may have missed. +If every attempt fails, Setu raises an operational alert and **the notification is not redelivered later**. This is safe precisely because polling is authoritative: the result is still available from `POST /api/v1/vpa/resolve` at any time. If the receiver has been down, poll for anything missed. - Until you have supplied a target, no notification is sent and nothing changes — polling alone is a complete integration. Add the webhook when you want to remove the wait. + Until a target is supplied, no notification is sent and nothing changes — polling alone is a complete integration. Add the webhook to remove the wait. ### Next From b10809d88839a0eaf112fba87050985810fc6adb Mon Sep 17 00:00:00 2001 From: Anindya Pandey Date: Mon, 27 Jul 2026 04:01:42 +0530 Subject: [PATCH 26/27] docs(UPIIS-72): space out the notification section Line breaks before the mirror-image callout, the per-environment note, the idempotency note and the all-attempts-failed note. Co-Authored-By: Claude Opus 5 (1M context) --- content/payments/upi-issuance/vpa-resolution.mdx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/content/payments/upi-issuance/vpa-resolution.mdx b/content/payments/upi-issuance/vpa-resolution.mdx index 85c5dbfc..1b2542ed 100644 --- a/content/payments/upi-issuance/vpa-resolution.mdx +++ b/content/payments/upi-issuance/vpa-resolution.mdx @@ -175,10 +175,14 @@ The notification is configured **per event**, and the TPAP / Issuing App supplie | **Signing key** | A shared secret **chosen by the TPAP / Issuing App**. Setu signs every notification with it, and the same value verifies the signature. Because the TPAP / Issuing App already holds it, no further exchange is needed. Treat it as a credential: min 32 bytes of entropy, and rotate it by sending Setu a new one. | | **Client id** | The identifier the TPAP / Issuing App knows Setu by. It is echoed on every notification as `clientId`, so the receiver can select which verification secret to use — useful once it accepts notifications from more than one source. | +
+ This is the mirror image of the inbound direction. For calls to Setu, Setu issues the key material (the envelope public key and the client secret). For Setu's calls to the TPAP / Issuing App, the TPAP / Issuing App issues it. Same crypto, opposite roles — so the receiver runs the code already written for the envelope, with the keys swapped. +
+ Send these to the Setu point of contact for each environment. The signing key is stored encrypted and is bound to that environment, so **it cannot be copied between environments**: sandbox and production each need their own. #### The event @@ -240,6 +244,8 @@ Validate in this order, and reject as soon as a step fails: Verify the signature over the decrypted plaintext, never over the ciphertext, and always after decrypting. This is the same rule Setu applies to inbound requests.
+
+ **Be idempotent on `(X-Setu-Event, traceId)`.** Delivery is at-least-once: a network failure between Setu sending and recording success causes a retry, so the same notification can arrive more than once. It always carries the same terminal answer, so de-duplicating on that pair is sufficient. #### How Setu handles the response @@ -251,6 +257,8 @@ Validate in this order, and reject as soon as a step fails: | Any other `4xx` | **Does not retry.** A rejected signature or a malformed payload would be rejected identically on a second attempt, so Setu stops and raises an internal alert. | | No response / timeout | Treated as a transient failure and retried, within the same 3-attempt budget. Each attempt has a 5 s timeout. | +
+ If every attempt fails, Setu raises an operational alert and **the notification is not redelivered later**. This is safe precisely because polling is authoritative: the result is still available from `POST /api/v1/vpa/resolve` at any time. If the receiver has been down, poll for anything missed. From 4b5bdb9e39e3e5c34d7a071ac62bc7f53d39151a Mon Sep 17 00:00:00 2001 From: Anindya Pandey Date: Mon, 27 Jul 2026 04:05:09 +0530 Subject: [PATCH 27/27] docs(UPIIS-72): break before the body paragraph, receiver -> TPAP / Issuing App "receiver" was the last actor name in the notification section that did not match the house term used everywhere else in these docs. Co-Authored-By: Claude Opus 5 (1M context) --- content/payments/upi-issuance/vpa-resolution.mdx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/content/payments/upi-issuance/vpa-resolution.mdx b/content/payments/upi-issuance/vpa-resolution.mdx index 1b2542ed..4e5e12c4 100644 --- a/content/payments/upi-issuance/vpa-resolution.mdx +++ b/content/payments/upi-issuance/vpa-resolution.mdx @@ -173,12 +173,12 @@ The notification is configured **per event**, and the TPAP / Issuing App supplie | **Target** | The absolute HTTPS URL Setu POSTs the event to, e.g. `https://tpap.example.com/webhooks/upi/vpa-resolve`. One complete URL, not a base plus a path. | | **Public key** | An RSA public key, PEM-encoded (PKIX or PKCS#1), min 2048-bit. **The TPAP / Issuing App generates the keypair and keeps the private key** — Setu only ever holds the public half and never sees the private one. Setu encrypts every notification to this key. | | **Signing key** | A shared secret **chosen by the TPAP / Issuing App**. Setu signs every notification with it, and the same value verifies the signature. Because the TPAP / Issuing App already holds it, no further exchange is needed. Treat it as a credential: min 32 bytes of entropy, and rotate it by sending Setu a new one. | -| **Client id** | The identifier the TPAP / Issuing App knows Setu by. It is echoed on every notification as `clientId`, so the receiver can select which verification secret to use — useful once it accepts notifications from more than one source. | +| **Client id** | The identifier the TPAP / Issuing App knows Setu by. It is echoed on every notification as `clientId`, so the TPAP / Issuing App can select which verification secret to use — useful once it accepts notifications from more than one source. |
- This is the mirror image of the inbound direction. For calls to Setu, Setu issues the key material (the envelope public key and the client secret). For Setu's calls to the TPAP / Issuing App, the TPAP / Issuing App issues it. Same crypto, opposite roles — so the receiver runs the code already written for the envelope, with the keys swapped. + This is the mirror image of the inbound direction. For calls to Setu, Setu issues the key material (the envelope public key and the client secret). For Setu's calls to the TPAP / Issuing App, the TPAP / Issuing App issues it. Same crypto, opposite roles — so the TPAP / Issuing App runs the code already written for the envelope, with the keys swapped.
@@ -194,6 +194,8 @@ Send these to the Setu point of contact for each environment. The signing key is | `X-Setu-Event` | `vpa.resolve` | | `Content-Type` | `application/json` | +
+ **The body is the `POST /api/v1/vpa/resolve` response body, field for field.** That is deliberate: the notification exists to save the wait, not to introduce a second contract. It deserialises with the **same type** as the poll response, so the code that renders a resolved payee works unchanged either way. Two differences from a poll response, both because a notification is only ever sent once there is something to say: @@ -215,7 +217,7 @@ Two differences from a poll response, both because a notification is only ever s }`}
-#### What the receiver must do +#### What the TPAP / Issuing App must do The body arrives as the **same crypto envelope** as inbound requests, with the key roles reversed: Setu encrypts to the TPAP / Issuing App's public key and signs with its signing key. @@ -250,7 +252,7 @@ Validate in this order, and reject as soon as a step fails: #### How Setu handles the response -| Receiver response | What Setu does | +| TPAP / Issuing App response | What Setu does | | :--- | :--- | | `2xx` | Delivered. Done. | | `5xx`, `408`, `429` | Retries with exponential backoff, up to **3 attempts total** (500 ms, then 1 s). | @@ -259,7 +261,7 @@ Validate in this order, and reject as soon as a step fails:
-If every attempt fails, Setu raises an operational alert and **the notification is not redelivered later**. This is safe precisely because polling is authoritative: the result is still available from `POST /api/v1/vpa/resolve` at any time. If the receiver has been down, poll for anything missed. +If every attempt fails, Setu raises an operational alert and **the notification is not redelivered later**. This is safe precisely because polling is authoritative: the result is still available from `POST /api/v1/vpa/resolve` at any time. If the TPAP / Issuing App has been down, poll for anything missed. Until a target is supplied, no notification is sent and nothing changes — polling alone is a complete integration. Add the webhook to remove the wait.