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