v1 Sync & Payments URL Rename (Wave 2) — PRD
Context
The 8 endpoints in this wave already ship — they are part of the v1 mobile API
(Android, iOS), all mounted on the JWT/session-gated protected_router in
src/routes/v1.py and all returning encrypted payloads. This is wave 2 of the
v1 URL-rename effort; wave 1 (v1-url-rename-prd.md, 14
auth/user endpoints) shipped in keystone PR #804 and locked the conventions this
wave inherits. The same migration was already done for mcq/block/attrs/content-rating
via sibling routes_o.py files, so the target pattern is well established in-repo.
This PRD is a routing delta, not a feature. Five endpoints get a second, new-convention URL added alongside the current one; both URLs serve the identical handler function. It does not re-spec any endpoint’s behaviour, request/response schemas, pagination, payload encryption, or the av1/v2 surfaces.
Wave-1 policy inherited (decided). Dual-serve, remove nothing: old URLs stay live; existing route files are left completely untouched (zero-line diff), so old routes are not flagged
deprecated=True; deprecation + removal are a separate future PRD gated on mobile-build adoption (x-build-version). Every route (old + new) keeps a globally uniqueoperation_id(collisions break OpenAPI/MCP at boot).
Zero-change rows (decided). Three of the 8 audited endpoints are already convention-compliant — nothing is built for them:
GET /v1/flowchart/{_id},GET /v1/table/{_id},GET /v1/plans. The plural forms (/flowcharts,/tables) in the original request table were typos (owner-confirmed) — both stay singular. The{flowchart_id}/{table_id}→{_id}param rename is an OpenAPI label only, not a wire change (the URL on the wire is identical), and relabelling would require editing the untouched route files — so no change.
No middleware changes (verified). Unlike wave 1’s SSO allowlist trap, no path-keyed middleware, allowlist, or config references any of these 8 paths (grepped keystone
src/outside routes/tests: only docstrings and Razorpay’s own unrelated API docs match). Thev1_client_payload_encryptionexclusion lists cover only auth-login paths plus/v1/healthand/v1/webhooks/;app_actions.py:234matchespath.startswith("/auth/")only.
Scope
MVP — add new-convention URLs for the 5 non-compliant endpoints, keep the old ones live:
- Sync surface renamed (4) — a pure path rename: insert the
/o/operation segment (/v1/{dockets,tags,taxonomy,years}/o/sync). Same method (GET), same query params, same auth, same handler function re-registered viaadd_api_route. - Payments history renamed (1) —
GET /v1/paymentsgains the named operationGET /v1/payments/o/user-payments(it returns the authenticated user’s billing history — hence the action name). Same handler re-registered. - Old URLs untouched — every old route stays live, byte-identical, not deprecated (wave-1 policy above).
- Already-compliant trio recorded — flowchart, table, plans: audited, zero change, listed here so the wave-2 audit trail covers all 8 requested rows.
Explicit non-goals
- No request/response schema, behaviour, pagination, or status-code changes.
- No
deprecated=Trueflags, no old-URL removal, no client-forced migration. - No av1/v2 surface changes; no data-model change; no migration/backfill.
- No param-label renames (
{flowchart_id}/{table_id}stay as-is in code). - No new URL for
/v1/plans,/v1/flowchart/{_id},/v1/table/{_id}.
M1 — Sync & payments surface renamed (5 new URLs)
Each new URL is an add_api_route re-registration of the existing handler
function in a new sibling routes_o.py file — the wave-1 mechanism (routes_new.py,
PR #804 deviation D-a/D-c), chosen because the original files stay untouched and the
transport is unchanged (same GET + same params, so no thin wrappers are needed —
unlike the mcq routes_o.py wave, where GET→POST transport changes forced them).
Each re-registration replicates all of the old decorator’s kwargs
(response_model, status_code, tags, summary, description) and carries a new
unique operation_id; the handlers’ in-signature
Depends(client_jwt_validator) params carry over automatically with the function.
| # | Old route (stays live, untouched) | New route | Method | Old operation_id | New operation_id |
|---|---|---|---|---|---|
| 1 | GET /v1/dockets/sync | GET /v1/dockets/o/sync | GET | v1_docket_sync | v1_docket_o_sync |
| 2 | GET /v1/tags/sync | GET /v1/tags/o/sync | GET | v1_tag_sync | v1_tag_o_sync |
| 3 | GET /v1/taxonomy/sync | GET /v1/taxonomy/o/sync | GET | v1_taxonomy_sync | v1_taxonomy_o_sync |
| 4 | GET /v1/years/sync | GET /v1/years/o/sync | GET | v1_years_sync | v1_years_o_sync |
| 5 | GET /v1/payments | GET /v1/payments/o/user-payments | GET | v1_payment_get_billing_history | v1_payment_o_user_payments |
Audited, zero change (already convention-compliant):
| # | Route | Why compliant |
|---|---|---|
| 6 | GET /v1/flowchart/{_id} | Wire URL unchanged (param rename is label-only); plural was a typo. |
| 7 | GET /v1/table/{_id} | Same as flowchart. |
| 8 | GET /v1/plans | Already a plain plural collection GET — target URL is identical. |
All 5 new routes mount on protected_router (same _linear_client_dependencies
JWT/session gating as the old URLs) and are v1-only — the new routers are
included only in src/routes/v1.py; src/routes/av1.py is untouched (av1 has its
own separate docket/tag/taxonomy/years/payment routers).
Data Model (requirements-level)
No data-model change — routing only. No collection, document, field, index, or projection is added, removed, or altered. No request or response schema changes. No migration or backfill (see Migration).
Delivery Milestones
| # | Milestone | Outcome | Status | Plan |
|---|---|---|---|---|
| 1 | Sync & payments surface renamed | 5 new /o/ routes live via add_api_route re-registration in new per-module routes_o.py files (unique operation_ids per the table above); old routes byte-identical; new paths absent from av1; tests + Bruno requests added. No behaviour/schema change. | pending | — |
Plan key:
BE= backend plan tasks in keystone (keystone/docs/superpowers/specs/
…/plans/), authored from this PRD via/plan. Single milestone, one PR (wave 1 shipped 14 endpoints in one PR; these 5 are strictly simpler).
Open Questions
- None remaining — all resolved during PRD review: zero-change trio confirmed
(plural forms were typos); wave-1 dual-serve/untouched-files policy inherited;
add_api_routere-registration mechanism; single milestone; newoperation_idnames binding as tabled above (any build-time change must update the M1 table and touch-list in the as-built pass).
Risks
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
operation_id collision. A duplicate op-id across old+new routes breaks OpenAPI/MCP generation at boot. | Med | High — app fails to boot / docs break | Unique op-ids specified per route (table above; verified unused in keystone today); wave-1’s v1_app-scoped uniqueness test already guards this (the new routes are v1-only); import src.main + openapi() smoke. |
New route accidentally ungated. A new router included on public_router instead of protected_router → auth bypass on the new URL. | Low | High — unauthenticated access to user data (payments) | Include on protected_router only; test asserts the new paths reject unauthenticated requests exactly like the old paths. |
Decorator kwargs not mirrored. A missed response_model/kwarg on a re-registration changes the OpenAPI contract or response serialisation on the new URL. | Low | Med — contract drift between old and new URL | Each add_api_route replicates the old decorator’s kwargs (wave-1 D5 discipline); parity test compares old-vs-new responses. |
| New route leaks into av1. | Low | Med — unintended admin-surface exposure | New routers included only in src/routes/v1.py; test asserts the 5 new paths absent from the av1 OpenAPI (wave-1 regression-guard pattern). |
| Client on old build. Renaming would break shipped mobile builds if old URLs stopped working. | High (only if removed) | High — sync/payments fail for un-updated users | Dual-serve; no removal, no edits to old routes in this PRD; retirement deferred to the adoption-gated PRD. |
Technical Details (planned)
To be re-verified and completed as built after the keystone PR ships. Pre-build facts below are (verified) against keystone at PRD-writing time (2026-07-06).
Naming: PRD vs code
| PRD term | Code |
|---|---|
| Dockets sync | list_by_updated_at_sync, APIRouter(prefix="/dockets") — src/api/v1/docket/routes.py:24,30 |
| Tags sync | list_by_updated_at, APIRouter(prefix="/tags") — src/api/v1/tag/routes.py:24,30 |
| Taxonomy sync | list_by_updated_at, APIRouter(prefix="/taxonomy") — src/api/v1/taxonomy/routes.py:29,35 |
| Years sync | list_by_updated_at, APIRouter(prefix="/years") — src/api/v1/years/routes.py:22,28 |
| Payments history | list_billing_history, APIRouter(prefix="/payments"), @router.get("") — src/api/v1/payment/routes.py:17,23 |
| Zero-change trio | src/api/v1/flowchart/routes.py:14,20 (/flowchart/{flowchart_id}), src/api/v1/table/routes.py:14,20 (/table/{table_id}), src/api/v1/plan/routes.py:24,30 (/plans, get_plans) |
| Gating router | protected_router with _linear_client_dependencies — src/routes/v1.py:162-225; per-handler Depends(client_jwt_validator) in signatures |
| Version mount | app.mount("/v1", v1_app) — src/main.py:90 |
| Aliasing mechanism | router.add_api_route(path, existing_fn, methods=["GET"], response_model=..., operation_id=...) — wave-1 idiom (src/api/v1/user/routes_new.py); sibling-file precedent src/api/v1/{mcq,mcq_attrs,block,block_attrs,content_rating}/routes_o.py |
| Response models (mirror on re-registration) | dockets BaseResponseWithPagination[List[DocketSyncListResponse]]; tags …[List[TagDetailsResponse]]; taxonomy …[List[TaxonomyDetailsResponse]]; years …[List[YearsDetailsResponse]]; payments BaseResponse[List[PaymentHistoryRowProjection]] |
Surface & semantics
| Surface | Routes | Notes |
|---|---|---|
| v1 (new) | GET /v1/dockets/o/sync, GET /v1/tags/o/sync, GET /v1/taxonomy/o/sync, GET /v1/years/o/sync, GET /v1/payments/o/user-payments | New per-module routes_o.py routers on protected_router; existing handlers re-registered via add_api_route; encrypted payloads as today. |
| v1 (old, unchanged) | GET /v1/dockets/sync, GET /v1/tags/sync, GET /v1/taxonomy/sync, GET /v1/years/sync, GET /v1/payments | Live, byte-identical, not deprecated (wave-1 policy). |
| v1 (audited, no change) | GET /v1/flowchart/{_id}, GET /v1/table/{_id}, GET /v1/plans | Already convention-compliant; zero diff. |
- No new error codes. No middleware changes (verified — see Context).
Backend (keystone) — files to touch (pre-build touch-list)
| Layer | File | Change |
|---|---|---|
| Routes | src/api/v1/docket/routes_o.py | New. /dockets router + add_api_route("/o/sync", list_by_updated_at_sync, …, operation_id="v1_docket_o_sync") |
| Routes | src/api/v1/tag/routes_o.py | New. Same pattern, v1_tag_o_sync |
| Routes | src/api/v1/taxonomy/routes_o.py | New. Same pattern, v1_taxonomy_o_sync |
| Routes | src/api/v1/years/routes_o.py | New. Same pattern, v1_years_o_sync |
| Routes | src/api/v1/payment/routes_o.py | New. /payments router + add_api_route("/o/user-payments", list_billing_history, …, operation_id="v1_payment_o_user_payments") |
| Wiring | src/routes/v1.py | +5 imports, +5 include_router on protected_router (beside the existing pairs at ~L188-202) |
| Tests | src/tests/v1/test_v1_url_rename_sync_payments.py | New. New paths present on v1; old paths unchanged; app-wide operation_id uniqueness; new paths absent from av1; unauthenticated requests rejected on new paths; old-vs-new response parity |
| Bruno | bruno v1 collections | New requests for the 5 new URLs (wave-1 precedent, commit c8f3a5fc) |
| Untouched | src/api/v1/{docket,tag,taxonomy,years,payment,flowchart,table,plan}/routes.py, src/routes/av1.py, all middlewares | Zero-line diff |
Migration
- Code-only — no data migration, no backfill, no collection change. This is a routing change; storage is untouched.
- No client-forced migration. Old URLs remain live and unflagged; mobile
clients adopt the new URLs on their own schedule. Old-URL removal and
deprecatedflagging remain deferred to the adoption-gated retirement PRD (shared with wave 1).