Skip to content

docs(UPI Issuance): VPA resolution + resolution notification, and restore the UPI Issuance doc set - #450

Merged
Anindya-Pandey merged 27 commits into
stagingfrom
UPIIS-69-upi-issuance-docs
Jul 26, 2026
Merged

Anindya-Pandey merged 27 commits into
stagingfrom
UPIIS-69-upi-issuance-docs

Conversation

@Anindya-Pandey

Copy link
Copy Markdown

What

Adds VPA resolution (UPIIS-69) and the vpa.resolve resolution notification
(UPIIS-72) to the UPI Issuance docs, and restores the rest of the UPI Issuance
doc set onto the current staging.

Tickets: UPIIS-47, UPIIS-53, UPIIS-57, UPIIS-69, UPIIS-72

Why it is this large

origin/staging was force-pushed and the rewritten history no longer contains
any UPI Issuance content — api-references/payments/upi-issuance.json and the
whole content/payments/upi-issuance/ tree are gone, along with merged PRs #445
and #447. Oddly, menuItems.json on staging still carries the UPI Issuance
sidebar entries, so the site currently links to pages that do not exist.

This branch is cut from the current origin/staging and replays the UPI Issuance
commits on top, so it reads as one large addition rather than an incremental diff.
Only menuItems.json is modified; everything else is new.

If the force-push was unintentional, that is worth resolving separately — the
pre-rewrite history is preserved locally on staging-pre-force-push.

New in this MR

  • VPA resolution (vpa-resolution.mdx) — the client-polled resolve call,
    resolved / pending / failed outcomes, full response and merchant field
    tables, four worked samples, and the HTTP error table.
  • Resolution notification — the vpa.resolve webhook: the key material the
    TPAP / Issuing App supplies, the sealed envelope, the validation order, and
    Setu's retry behaviour.
  • QA simulation — the X-Sim-Resolution header, its defaults, and the
    no-header behaviour.
  • API reference: resolveVPA operation, its own VPA resolution tag, and the
    API playground entry.

Corrections made against the implementation

Checked against setu-upi-issuance rather than written from the ticket, which
turned up several places where the docs disagreed with the code:

was now
status values RESOLVED / PENDING / FAILED resolved / pending / failed — vparesolve.State calls the lowercase spelling the wire contract
name "masked account-holder name" not masked — own handle returns the decrypted account name, foreign handle whatever the payee PSP reports
verified present on every success omitted when false; the handler only sets it via if res.Verified
X-Sim-Resolution outcome (success/failure), verified boolean status (resolved/failed), verified object — the directive was renamed so
pinning and reading use one vocabulary
examples entityType: PERSON alongside verified: true with Brand-X branding consistent; response samples completed with franchiseName /
verifiedLogo

Retry and crypto claims were verified too: 3 attempts, 500 ms → 1 s backoff, 5 s
per attempt, retry on 5xx/408/429 only, RSA-OAEP-SHA1 + AES-256-CBC + HMAC-SHA256
over plaintext.

Also removed the NPCI protocol operation names (ReqValAdd) from the spec.
NPCI itself and NPCI error codes are kept.

Verified

Rendered locally against docs-mdx with LOCAL_CONTENT_DIR. Every UPI Issuance
page returns 200 and the API reference renders VPA resolution as its own
section. vpa-resolution was 404ing before this branch — the page existed but
had never been added to menuItems.json.

Reviewer notes

  • menuItems.json was edited by hand. The README marks it generated
    ("DO NOT TOUCH"). The edit matches the generator's shape and serves correctly,
    but a regeneration pass through the Docter Preview extension before merge would
    be safer.
  • Two claims in the notification section are not enforced in code: the
    "min 2048-bit" public key and "min 32 bytes of entropy" signing key. Only
    MaxLength is validated. They read as hard requirements — fine if that is
    policy, worth softening to "recommended" if not.

Anindya Pandey and others added 27 commits July 27, 2026 01:46
…rence

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
- 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 <noreply@anthropic.com>
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.
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
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 <noreply@anthropic.com>
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
- 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.
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.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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) <noreply@anthropic.com>
"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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
- 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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
- 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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
- 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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- 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 <Callout>, so **mirror image** and
  **decrypted plaintext** rendered literally. Switched to <b>/<i>, 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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
…ssuing 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) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Checklist to merge a PR 🚀

To merge this pull request, please take time to complete the checklist.

What action did you perform?

Review the corresponding checklist items for the action you performed and mark them done.

Edit an existing content (MDX) page

Checklist

  • Review changes using the MDX preview option
  • If the length of content >15000 chars, use the Content preview portal to view changes
  • If a redirect is needed to the existing page, add a key, value pair in redirects.json

Edit an existing API reference page

Checklist


Add a new content (MDX) page

Checklist

  • Create a .mdx file with the path as its name in the content folder
  • Add frontmatter with all the metadata
  • Review the order of items in Sidebar using the Sidebar preview option
  • Review changes using the MDX preview option
  • If the length of content >15000 chars, use the Content preview portal to view changes
  • Created a folder with the same name, if any children were to be added to the page
  • Once all changes are done, update the menu items by using the Menu Items option
  • Add a key, and value pair in redirects.json if you wish to have a redirect to the new page

Add a new API reference page

Checklist

  • Create a .json file with the product path as its name
  • Create an api-reference.mdx file in the respective product folder inside content folder
  • Add frontmatter with all the metadata
  • Review the order of items in Sidebar using the Sidebar preview option
  • Add API reference in JSON format (OpenAPI or Swagger) into created .json file.
  • Used the Content preview portal to view changes
  • Once all changes are done, update the menu items by using the Menu Items option

@Anindya-Pandey
Anindya-Pandey merged commit 7d1c9ea into staging Jul 26, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants