Owner Burhan

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 unique operation_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). The v1_client_payload_encryption exclusion lists cover only auth-login paths plus /v1/health and /v1/webhooks/; app_actions.py:234 matches path.startswith("/auth/") only.

Scope

MVP — add new-convention URLs for the 5 non-compliant endpoints, keep the old ones live:

  1. 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 via add_api_route.
  2. Payments history renamed (1)GET /v1/payments gains the named operation GET /v1/payments/o/user-payments (it returns the authenticated user’s billing history — hence the action name). Same handler re-registered.
  3. Old URLs untouched — every old route stays live, byte-identical, not deprecated (wave-1 policy above).
  4. 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=True flags, 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 routeMethodOld operation_idNew operation_id
1GET /v1/dockets/syncGET /v1/dockets/o/syncGETv1_docket_syncv1_docket_o_sync
2GET /v1/tags/syncGET /v1/tags/o/syncGETv1_tag_syncv1_tag_o_sync
3GET /v1/taxonomy/syncGET /v1/taxonomy/o/syncGETv1_taxonomy_syncv1_taxonomy_o_sync
4GET /v1/years/syncGET /v1/years/o/syncGETv1_years_syncv1_years_o_sync
5GET /v1/paymentsGET /v1/payments/o/user-paymentsGETv1_payment_get_billing_historyv1_payment_o_user_payments

Audited, zero change (already convention-compliant):

#RouteWhy compliant
6GET /v1/flowchart/{_id}Wire URL unchanged (param rename is label-only); plural was a typo.
7GET /v1/table/{_id}Same as flowchart.
8GET /v1/plansAlready 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

#MilestoneOutcomeStatusPlan
1Sync & payments surface renamed5 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_route re-registration mechanism; single milestone; new operation_id names binding as tabled above (any build-time change must update the M1 table and touch-list in the as-built pass).

Risks

RiskLikelihoodImpactMitigation
operation_id collision. A duplicate op-id across old+new routes breaks OpenAPI/MCP generation at boot.MedHigh — app fails to boot / docs breakUnique 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.LowHigh — 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.LowMed — contract drift between old and new URLEach 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.LowMed — unintended admin-surface exposureNew 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 usersDual-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 termCode
Dockets synclist_by_updated_at_sync, APIRouter(prefix="/dockets")src/api/v1/docket/routes.py:24,30
Tags synclist_by_updated_at, APIRouter(prefix="/tags")src/api/v1/tag/routes.py:24,30
Taxonomy synclist_by_updated_at, APIRouter(prefix="/taxonomy")src/api/v1/taxonomy/routes.py:29,35
Years synclist_by_updated_at, APIRouter(prefix="/years")src/api/v1/years/routes.py:22,28
Payments historylist_billing_history, APIRouter(prefix="/payments"), @router.get("")src/api/v1/payment/routes.py:17,23
Zero-change triosrc/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 routerprotected_router with _linear_client_dependenciessrc/routes/v1.py:162-225; per-handler Depends(client_jwt_validator) in signatures
Version mountapp.mount("/v1", v1_app)src/main.py:90
Aliasing mechanismrouter.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

SurfaceRoutesNotes
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-paymentsNew 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/paymentsLive, byte-identical, not deprecated (wave-1 policy).
v1 (audited, no change)GET /v1/flowchart/{_id}, GET /v1/table/{_id}, GET /v1/plansAlready convention-compliant; zero diff.
  • No new error codes. No middleware changes (verified — see Context).

Backend (keystone) — files to touch (pre-build touch-list)

LayerFileChange
Routessrc/api/v1/docket/routes_o.pyNew. /dockets router + add_api_route("/o/sync", list_by_updated_at_sync, …, operation_id="v1_docket_o_sync")
Routessrc/api/v1/tag/routes_o.pyNew. Same pattern, v1_tag_o_sync
Routessrc/api/v1/taxonomy/routes_o.pyNew. Same pattern, v1_taxonomy_o_sync
Routessrc/api/v1/years/routes_o.pyNew. Same pattern, v1_years_o_sync
Routessrc/api/v1/payment/routes_o.pyNew. /payments router + add_api_route("/o/user-payments", list_billing_history, …, operation_id="v1_payment_o_user_payments")
Wiringsrc/routes/v1.py+5 imports, +5 include_router on protected_router (beside the existing pairs at ~L188-202)
Testssrc/tests/v1/test_v1_url_rename_sync_payments.pyNew. 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
Brunobruno v1 collectionsNew requests for the 5 new URLs (wave-1 precedent, commit c8f3a5fc)
Untouchedsrc/api/v1/{docket,tag,taxonomy,years,payment,flowchart,table,plan}/routes.py, src/routes/av1.py, all middlewaresZero-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 deprecated flagging remain deferred to the adoption-gated retirement PRD (shared with wave 1).