{"data":{"openapi":"3.1.0","info":{"title":"Sente Rails API","version":"1.1.0","summary":"Government revenue collection and orchestration rail for Uganda.","description":"Sente Rails is the public API for the Sente Rails revenue collection\nrail \u2014 citizen identification, service catalog, multi-MDA assessment,\naggregator-level payment splitting, fiscal receipting, and end-of-\nshift reconciliation.\n\nSente Rails NEVER holds public money (PFMA \u00a743): split rules live on\nPayment Intent; the aggregator (MoMo / Airtel / Pesapal / Flutterwave)\nexecutes the split directly into each MDA's collection account.\nSente Rails records the event with proof and propagates downstream\nwebhooks.\n\n## Three interaction modes\n\nEndpoints serve three classes of consumer (per ARCHITECTURE.md \u00a73):\n  - **Mode A \u2014 System of Record.** MDAs without an existing digital\n    revenue system. Clerks transact directly through these endpoints.\n  - **Mode B \u2014 Integration.** External MDA systems (URA EFRIS, URSB\n    OBRS, NIRA via UGHub) are *called* by Sente Rails through internal\n    adapters. They push events back via `/v1/webhooks/{provider}`.\n  - **Mode C \u2014 Oversight.** Read-only consumers (OAG, MoFPED, UBOS)\n    will consume `/v1/oversight/*` (planned, not yet shipped).\n\n## URL scheme\n\nAll endpoints are versioned under `/v1/`. Canonical URLs work both\nin development (against the local bench) and in production (where\nnginx serves them directly to the same handlers).\n\n## Auth\n\nBearer key \u2014 issued at `/signup` and managed at `/dashboard/keys`:\n  `Authorization: Bearer <key>`\n\nEach key carries a set of scopes (`citizens.read`, `assessments.write`,\netc.). Calls that don't carry a key are rejected with `401`; calls that\ncarry a key without the required scope are rejected with `403`. Per-MDA\nscoped JWT tokens are a v2 hardening.\n\n## Response shape\n\nAll successful responses use:\n  `{ \"data\": <body> }`\n\nAll error responses use:\n  `{ \"error\": { \"code\": \"validation_failed\", \"message\": \"...\" } }`\n\nRFC 7807 problem-details migration is a v1 follow-up.\n","contact":{"name":"ASAT LABS","url":"https://github.com/asatlabs/sente-rails","email":"opensource@asatlabs.org"},"license":{"name":"Apache 2.0","url":"https://www.apache.org/licenses/LICENSE-2.0"},"x-built-by":"ASAT LABS \u2014 Geoffrey Oketwangwu (Gulu, Uganda)"},"servers":[{"url":"https://sente-rails.space","description":"Public sandbox \u2014 live"},{"url":"http://localhost:8000","description":"Local development bench"}],"security":[{"bearerToken":[]}],"tags":[{"name":"Identity","description":"Citizens, identity lookup, NIRA cascade."},{"name":"Catalog","description":"MDAs (Ministries / Departments / Agencies / LGs) and the services they offer."},{"name":"Transactions","description":"Assessments \u2014 multi-line, multi-MDA, lifecycle-managed."},{"name":"Payments","description":"Payment Intents (with split rules) and Payment Events (proof of receipt)."},{"name":"Settlement","description":"Counter Shifts \u2014 clerk open / transact / close with cash variance."},{"name":"Integrations","description":"Adapter dispatch status \u2014 per-country, per-domain."},{"name":"Sign up","description":"Self-serve sandbox account creation with email OTP verification.\nThree calls \u2014 `POST /v1/signup` \u2192 `POST /v1/signup/verify` \u2192 plaintext\nsandbox key. No human in the loop.\n"},{"name":"Authentication","description":"Magic-link sign-in to the integrator dashboard. The link is single-use\nand lives 15 minutes. A successful consume sets a `sente_session`\ncookie that authenticates `/v1/me/*` calls from the browser; the same\nsession also accepts a Bearer key for headless tooling.\n"},{"name":"Account","description":"Integrator self-service \u2014 profile read/write, contact info, anticipated\nvolume. The signed-in integrator only ever sees their own row.\n"},{"name":"API keys","description":"Per-integrator key lifecycle \u2014 list, rotate (with grace window),\nrevoke. Plaintext is shown exactly once at issue or rotate; lose it\nand the only path forward is rotate.\n"},{"name":"Audit logs","description":"90-day hot window over every `/v1` request your account made. Filter\nby endpoint, event class (`api.auth.granted` / `api.auth.denied` /\n`api.handler.error`), and minimum HTTP status. Cold-storage retention\nruns to 7 years per Tax Procedures Code Act \u00a773A\u2013B.\n"},{"name":"Counter Stations","description":"Kiosk-style endpoints used by MDA counter clerks at physical\nterminals. The full clerk workflow lives here: shift open \u2192\ncitizen lookup \u2192 multi-line assessment \u2192 payment intent \u2192 close\nwith cash variance. Mirrors the /v1 core surface but auth'd via\nPlatform `sid` cookie + the `Sente Rails Clerk`, `Sente Rails\nSupervisor`, or `Sente Rails Admin` role (never via Bearer key).\n"},{"name":"Counter Stations \u2014 Supervisor","description":"Variance management surface \u2014 approve, reject, or escalate a\nclosed shift's cash variance. Requires `Sente Rails Supervisor`\n(or admin) role on top of the platform session.\n"},{"name":"Ops Console","description":"Sente Rails staff administration \u2014 MDAs + services catalog\nmanagement, integrator + key lifecycle, audit log, fleet-wide\nshifts view, adapter health, system status. Auth'd via the platform\n`sid` cookie. Read endpoints accept `Sente Rails Admin`,\n`System Manager`, or `Sente Rails OAG` roles; write endpoints\n(PATCH / suspend / reactivate / revoke) require Admin or\nSystem Manager.\n"},{"name":"Ops Console \u2014 Oversight","description":"Aggregated / anomaly views for oversight bodies (OAG, MoFPED,\nUBOS). Read-only. Role-gated to `Sente Rails OAG` and admins.\nThese power the OAG dashboards that surface anomaly flags,\npayment-event traces, citizen-consent timelines, and fleet\nstatistics for accountability oversight.\n"},{"name":"Webhooks","description":"Inbound provider callbacks. Money-movement aggregators (MoMo,\nAirtel, Pesapal) and fiscal partners (EFRIS) POST here with\nstatus updates on payment intents Sente Rails dispatched.\nSigned and verified \u2014 endpoints reject unsigned callbacks in\nlive mode. Idempotent: replays return 200 with the cached\ndecision.\n"},{"name":"Supervisor","description":"Bearer-key surface for integrators automating cash-variance\nreview across counter shifts. Mirrors the workbench's\nSupervisor surface but consumable by external finance / audit\nsystems. Scoped to `assessments.read` (dashboard) and\n`assessments.write` (approve / reject / escalate).\n"},{"name":"Oversight (Mode C)","description":"Read-only API for oversight bodies \u2014 Office of the Auditor\nGeneral (OAG), Ministry of Finance (MoFPED), Uganda Bureau of\nStatistics (UBOS). Bearer key + `oversight.read` scope. Returns\naggregated revenue, anomaly flags, citizen-consent counts,\npayment-event streams, and UBOS-shaped statistics. Strictly\nadditive \u2014 no row carries citizen NIN, msisdn, or other PII.\n"},{"name":"Meta","description":"Spec + discovery endpoints. Useful for tooling that wants to\nkeep its client SDK / Postman collection in lockstep with the\nlive API surface.\n"},{"name":"Service notices","description":"Operator-curated public announcements: planned MDA downtime, new\nMDA onboarding, SDK breaking changes, security advisories. The\nlist endpoint is `allow_guest` and is consumed by the marketing\nlanding page + by the dashboard top-bar. Notices are managed in\nthe back-office Desk (`Service Notice` doctype) by System Manager\nor Sente Rails Admin.\n"}],"paths":{"/v1/citizens":{"get":{"tags":["Identity"],"summary":"List / search citizens","parameters":[{"name":"q","in":"query","schema":{"type":"string"},"description":"substring across full_name + nin + phone"},{"name":"nin","in":"query","schema":{"type":"string"},"description":"exact NIN match (uppercased server-side)"},{"name":"phone","in":"query","schema":{"type":"string"},"description":"exact phone match"},{"name":"start","in":"query","schema":{"type":"integer","default":0}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}}],"responses":{"200":{"description":"List of citizens (summary fields only).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CitizenSummary"}}}}}}}}},"post":{"tags":["Identity"],"summary":"Create a new citizen","description":"Persist a citizen into the local rail registry. **This does not\noriginate a legal identity** \u2014 only NIRA (the National\nIdentification and Registration Authority) does that. This\nendpoint records, on the rail, a citizen the rail needs to\ntransact with (an assessment payer, licence holder, counter\nwalk-in), linked back to NIRA by `nin`. New records are created\n`verified: false` until confirmed against NIRA.\n\n**Privileged scope.** Requires `citizens.write`, which is granted\nto counter-station and admin actors \u2014 **not** to integrator API\nkeys (which hold `citizens.read` only). Integrators *read*\nidentity and *resolve* it via NIRA cascade\n(`GET /v1/citizens/search`); they do not mint citizens. A key\nwithout the scope receives `403 forbidden`. This is the intended\ngovernance boundary, not an error.\n\nTypical flow: `GET /v1/citizens/search?nin=\u2026` returns\n`source: \"nira\"` for someone known to NIRA but not yet on the\nrail \u2192 a privileged caller POSTs that record here so future\nlookups resolve `source: \"local\"` with no NIRA round-trip.\n\n**If you click \"Send\" with a standard sandbox key:** expect\n`403 forbidden` \u2014 the sandbox key holds `citizens.read`, not\n`citizens.write`. That is the governance boundary working, not a\nbug. Prefer `POST /v1/citizens/register` (resolve-by-NIN) over\nraw create for real flows.\n","responses":{"200":{"description":"Citizen created.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Citizen"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/v1/citizens/{name}":{"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","pattern":"^CITIZEN-\\d{4}-\\d{6}$"},"example":"CITIZEN-2026-000002"}],"get":{"tags":["Identity"],"summary":"Get one citizen by docname","responses":{"200":{"description":"The citizen.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Citizen"}}}}}}}}},"/v1/citizens/register":{"post":{"tags":["Identity"],"summary":"Register (find-or-create) a citizen from a NIN","description":"Write-scoped companion to `GET /v1/citizens/search`. Resolves a\nNIN (local first, then NIRA) and persists a NIRA hit into the\nlocal registry so it can anchor an assessment; an\nalready-registered NIN returns the existing record (idempotent).\nRequires the privileged `citizens.write` scope \u2014 integrator keys\nhold `citizens.read` only and receive 403.\n\n**Where it sits in the flow:** the first step of a counter or\nbilling pipeline \u2014 `**register (here)** \u2192 create assessment \u2192\n:assess \u2192 pay`. Persisting the NIRA hit gives you the local\n`CITIZEN-\u2026` docname that the assessment's `citizen` field\nrequires.\n\n**If you click \"Send\" with a standard sandbox key:** expect\n`403 forbidden` (no `citizens.write`). At a real counter this is\ncalled server-side under a staff session via\n`POST /v1/work/citizens`, which is where the clerk's in-person\naction authorises it.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["nin"],"properties":{"nin":{"type":"string","example":"CM78001234ABCD"},"mda":{"type":"string","example":"GULU"}}}}}},"responses":{"200":{"description":"Citizen registered (or already on the rail).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"citizen":{"$ref":"#/components/schemas/Citizen"},"created":{"type":"boolean"},"source":{"type":"string","enum":["local","nira"]},"consent_event":{"type":["string","null"]}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/citizens/search":{"get":{"tags":["Identity"],"summary":"Resolve a citizen by NIN","description":"Lookup order: local Citizen registry first, then NIRA via the\ncountry's identity adapter. Response carries `source`\n(`local` | `nira` | `not_found`) and `stub: true` when the NIRA\nadapter is still in stub mode.\n","parameters":[{"name":"nin","in":"query","required":true,"schema":{"type":"string"},"example":"CM78001234ABCD"}],"responses":{"200":{"description":"Lookup result.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"source":{"type":"string","enum":["local","nira","not_found"]},"stub":{"type":"boolean"},"citizen":{"$ref":"#/components/schemas/Citizen"}}}}}}}}}}},"/v1/mdas":{"get":{"tags":["Catalog"],"summary":"List MDAs","parameters":[{"name":"mode","in":"query","schema":{"type":"string","enum":["A","B","C"]},"description":"interaction mode"},{"name":"country","in":"query","schema":{"type":"string","example":"UG"},"description":"Country Profile code"},{"name":"mda_type","in":"query","schema":{"type":"string","example":"City Authority"}},{"name":"status","in":"query","schema":{"type":"string","default":"Active"}},{"name":"start","in":"query","schema":{"type":"integer","default":0}},{"name":"limit","in":"query","schema":{"type":"integer","default":100,"maximum":500}}],"responses":{"200":{"description":"List of MDAs.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/MDA"}}}}}}}}}},"/v1/mdas/{name}":{"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"},"example":"GULU"}],"get":{"tags":["Catalog"],"summary":"Get one MDA by short_code","responses":{"200":{"description":"The MDA.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/MDA"}}}}}}}}},"/v1/services":{"get":{"tags":["Catalog"],"summary":"List services in the catalog","parameters":[{"name":"mda","in":"query","schema":{"type":"string","example":"GULU"}},{"name":"sector","in":"query","schema":{"type":"string","example":"Revenue"}},{"name":"family","in":"query","schema":{"type":"string","example":"Trading Licenses"}},{"name":"code","in":"query","schema":{"type":"string","example":"TL-RENEW"}},{"name":"q","in":"query","schema":{"type":"string"},"description":"substring across service_name + code"},{"name":"status","in":"query","schema":{"type":"string","default":"Active"}},{"name":"start","in":"query","schema":{"type":"integer","default":0}},{"name":"limit","in":"query","schema":{"type":"integer","default":100,"maximum":500}}],"responses":{"200":{"description":"List of services.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Service"}}}}}}}}}},"/v1/services/{name}":{"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","pattern":"^SVC-\\d{4}-\\d{6}$"},"example":"SVC-2026-000004"}],"get":{"tags":["Catalog"],"summary":"Get one service by docname","responses":{"200":{"description":"The service.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Service"}}}}}}}}},"/v1/assessments":{"get":{"tags":["Transactions"],"summary":"List / filter assessments","parameters":[{"name":"citizen","in":"query","schema":{"type":"string"}},{"name":"status","in":"query","schema":{"type":"string","enum":["Draft","Assessed","Paid","Cancelled"]}},{"name":"shift","in":"query","schema":{"type":"string"}},{"name":"from_date","in":"query","schema":{"type":"string","format":"date"}},{"name":"to_date","in":"query","schema":{"type":"string","format":"date"}},{"name":"start","in":"query","schema":{"type":"integer","default":0}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}}],"responses":{"200":{"description":"List of assessments (summary fields).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AssessmentSummary"}}}}}}}}},"post":{"tags":["Transactions"],"summary":"Create a new Draft assessment (multi-line, multi-MDA)","description":"**Who calls this:** a back-office integrator system (billing\nengine, MDA portal) holding an API key with the\n`assessments.write` scope. **Not** an interactive \"click Send\"\naction \u2014 it is a programmatic step in a billing pipeline.\n\n**Where it sits in the flow:**\n`resolve citizen \u2192 **create assessment (here)** \u2192 :assess (lock\ntotals) \u2192 create payment-intent \u2192 settle`. The created assessment\nstarts in `Draft`; nothing is owed until `:assess` locks it.\n\n**Server is authoritative on price.** You send *which* services\nand *how many* \u2014 the rail prices each line from the official fee\nschedule (`Service.fee_amount`); a `rate` you pass is ignored.\n`citizen` must already exist on the rail (resolve/register first).\n\n**If you click \"Send\" in this explorer:** with a sandbox key that\ncarries `assessments.write` it will genuinely create a Draft\nassessment \u2014 *provided* `citizen` and each `lines[].service` are\nreal docnames. The placeholder example values resolve against the\nlive sandbox, so swap in IDs from `GET /v1/citizens` and\n`GET /v1/services` first, or you'll get `422 \u2014 Could not find\nCitizen/Service`.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssessmentCreate"}}}},"responses":{"200":{"description":"Assessment created in Draft status.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Assessment"}}}}}}}}},"/v1/assessments/{name}":{"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","pattern":"^ASMT-\\d{4}-\\d{2}-\\d{6}$"},"example":"ASMT-2026-05-000022"}],"get":{"tags":["Transactions"],"summary":"Get one assessment with its lines","responses":{"200":{"description":"The assessment.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Assessment"}}}}}}}}},"/v1/assessments/{name}:assess":{"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"},"example":"ASMT-2026-05-000022"}],"post":{"tags":["Transactions"],"summary":"Transition Draft \u2192 Assessed","description":"**Who calls this:** the same `assessments.write` integrator\nsystem, immediately after `POST /v1/assessments`. Programmatic \u2014\nit locks the bill so a payment can be raised against a fixed\ntotal.\n\n**Where it sits in the flow:**\n`create assessment \u2192 **:assess (here)** \u2192 create payment-intent`.\nFreezes line amounts + grand total; an `Assessed` assessment is\nthe only state a payment-intent can attach to. Re-running on an\nalready-`Assessed` assessment is a safe no-op.\n\n**If you click \"Send\":** supply the `name` of a real `Draft`\nassessment (one you just created). A sandbox key with\n`assessments.write` will succeed; a stale/paid/cancelled id is\nrejected by the status-transition guard, and a missing id returns\n`404`.\n","responses":{"200":{"description":"Assessment now in Assessed status.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Assessment"}}}}}}}}},"/v1/assessments/{name}:cancel":{"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"}}],"post":{"tags":["Transactions"],"summary":"Cancel an assessment","description":"**Who calls this:** a privileged actor holding the separate\n`assessments.cancel` scope \u2014 deliberately **not** bundled with\n`assessments.write`. Voiding an obligation is a higher-trust act\nthan raising one (clerk error, citizen walks away), so it is\ngated apart and typically held by supervisors/admin, not the\nbilling integrator.\n\n**Where it sits in the flow:** off the happy path \u2014 a corrective\naction from `Draft` or `Assessed` (and `Paid`, for refund-style\nreversals). The status-transition guard enforces which states may\ncancel.\n\n**If you click \"Send\" with a standard sandbox key:** expect\n`403 forbidden` \u2014 the default sandbox key carries\n`assessments.write` but **not** `assessments.cancel`. That 403 is\nthe governance boundary working, not a bug; it demonstrates that\ncreating and voiding revenue obligations are separately\nauthorised.\n","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","example":"Citizen left counter"}}}}}},"responses":{"200":{"description":"Assessment moved to Cancelled.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Assessment"}}}}}}}}},"/v1/payment-intents":{"post":{"tags":["Payments"],"summary":"Create a Payment Intent (auto-derives splits from Assessment lines if omitted)","description":"Create a Payment Intent for an assessed Assessment. Splits are\nauto-derived per MDA from the assessment lines unless explicitly\nprovided. **Sente Rails never holds funds** \u2014 the aggregator\nexecutes the split directly into each MDA collection account\n(PFMA \u00a743).\n\n**Who calls this:** a `payments.initiate`-scoped integrator system,\nright after the assessment is `:assess`-ed. A programmatic step, not\na click \u2014 it's how a billing front-end turns a fixed bill into a\npayable charge.\n\n**Where it sits in the flow:**\n`create assessment \u2192 :assess \u2192 **create payment-intent (here)** \u2192\n:initiate \u2192 :confirm (or provider webhook) \u2192 settle`. The intent\nopens `Pending`; `amount` comes from the assessment total and cannot\nbe set here.\n\n**If you click \"Send\":** the standard sandbox key *does* carry\n`payments.initiate`, so a valid body creates a Pending intent.\n`assessment` must be a real **assessed** docname \u2014 a draft /\ncancelled / missing one is rejected. An empty body returns a\nvalidation error (`assessment` + `channel` are required).\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentIntentCreate"}}}},"responses":{"200":{"description":"Payment Intent created in Pending status.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PaymentIntent"}}}}}}}}},"/v1/payment-intents/{name}":{"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","pattern":"^PI-\\d{4}-\\d{2}-\\d{6}$"},"example":"PI-2026-05-000023"}],"get":{"tags":["Payments"],"summary":"Get one Payment Intent","responses":{"200":{"description":"The Payment Intent.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PaymentIntent"}}}}}}}}},"/v1/payment-intents/{name}:initiate":{"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"}}],"post":{"tags":["Payments"],"summary":"Send the payment instruction via the channel-specific adapter","description":"Calls the country's payment adapter for the intent's channel\n(e.g. MTN MoMo STK push). On stub adapters returns a stub-shaped\naggregator_reference. Transitions intent: Pending \u2192 Sent.\n\n**Who calls this:** the same `payments.initiate` integrator,\nimmediately after creating the intent. This is the step that\nactually reaches out to the mobile-money / card aggregator and\ntriggers the citizen's payment prompt.\n\n**Where it sits in the flow:**\n`create payment-intent \u2192 **:initiate (here)** \u2192 :confirm`.\nOnly valid from `Pending`; calling it on an already-Sent/Confirmed\nintent is rejected by the state guard (`422`).\n\n**If you click \"Send\":** supply the `name` of a real **Pending**\nintent. The sandbox runs against MoMo sandbox creds, so this\ngenuinely calls the aggregator and stamps the intent `Sent`.\n","responses":{"200":{"description":"Initiation result with intent + adapter response.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"intent":{"$ref":"#/components/schemas/PaymentIntent"},"adapter_response":{"type":"object","description":"Channel-specific aggregator response"}}}}}}}}}}},"/v1/payment-intents/{name}:confirm":{"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"}}],"post":{"tags":["Payments"],"summary":"Verify + confirm settlement; materialise Payment Events per split","description":"Synchronous confirmation path (production uses\n`/v1/webhooks/{provider}` for the same materialisation).\nVerifies via the channel adapter, on success creates one Payment\nEvent per split rule, marks parent Assessment Paid.\n\n**Who calls this \u2014 and the important nuance:** in production this is\nrarely called directly. The aggregator's **webhook**\n(`/v1/webhooks/{provider}`) drives the same materialisation\nautomatically when the citizen completes payment. This endpoint is\nthe **manual / poll-confirm fallback** (a cash counter, or any flow\nthat reconciles without a webhook) for a `payments.initiate`-scoped\ncaller.\n\n**Where it sits in the flow:**\n`:initiate \u2192 [citizen pays] \u2192 **:confirm (here) OR provider webhook**\n\u2192 Payment Events created, Assessment marked Paid`. Only valid from\n`Sent`; on success it is idempotent against the parent assessment's\npaid state.\n\n**If you click \"Send\":** use a real **Sent** intent. The adapter is\nre-queried; if the sandbox reports the payment complete, events are\nmaterialised and the assessment flips to Paid. A non-Sent intent is\nrejected (`422`).\n","responses":{"200":{"description":"Confirmation result with intent, events, assessment.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"intent":{"$ref":"#/components/schemas/PaymentIntent"},"events":{"type":"array","items":{"$ref":"#/components/schemas/PaymentEvent"}},"assessment":{"$ref":"#/components/schemas/Assessment"}}}}}}}}}}},"/v1/payment-intents/{name}/trace":{"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"},"example":"PI-2026-05-000051"}],"get":{"tags":["Payments"],"summary":"Unified audit-trail timeline for a Payment Intent","description":"Assembles the assessment context, intent lifecycle, split rules,\npayment events, and verbatim adapter request/response payloads\ninto a single document. Designed for OAG-grade audit evidence\nand on-demand reconciliation.\n\nEvery state change and every adapter call is rendered as one\nrow in the `timeline` array, sorted chronologically. Adapter\nrequest/response payloads are inlined for inspection (Bearer\ntokens stripped).\n","responses":{"200":{"description":"Trace bundle.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"assessment":{"type":"object"},"intent":{"type":"object"},"events":{"type":"array","items":{"type":"object"}},"timeline":{"type":"array","items":{"type":"object","properties":{"at":{"type":"string","format":"date-time"},"kind":{"type":"string","enum":["assessment.update","payment_intent.update","adapter.initiate","adapter.verify","payment_event.created"]},"actor":{"type":"string"},"summary":{"type":"string"},"doc":{"type":["string","null"]},"detail":{"type":"object"}}}}}}}}}}}}}},"/v1/payment-intents/{name}/live-status":{"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"},"example":"PI-2026-05-000051"}],"get":{"tags":["Payments"],"summary":"Re-query the aggregator LIVE for current status","description":"Read-only \u2014 polls the aggregator (MTN MoMo, Airtel, etc.) right\nnow and returns its response next to what Sente Rails has stored.\nDoes NOT mutate any Sente Rails state. Built for on-demand\n\"ask the aggregator live\" reconciliation.\n\nReturns a `match: bool` field so consumers can immediately tell\nwhether stored and live agree.\n","responses":{"200":{"description":"Live status comparison.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"queried_at":{"type":"string","format":"date-time"},"aggregator":{"type":"string","example":"MTN"},"aggregator_reference":{"type":"string"},"stored":{"type":"object","properties":{"status":{"type":"string"},"confirmed_at":{"type":["string","null"],"format":"date-time"},"amount":{"type":"number"},"currency":{"type":"string"}}},"live":{"type":"object","properties":{"status":{"type":"string"},"txn_id":{"type":["string","null"]},"amount":{"type":"number"},"currency":{"type":"string"},"settled_at":{"type":["string","null"],"format":"date-time"},"stub":{"type":"boolean"},"raw_response":{"type":"object"}}},"match":{"type":"boolean"}}}}}}}}}}},"/v1/payment-events":{"get":{"tags":["Payments"],"summary":"List Payment Events","parameters":[{"name":"intent","in":"query","schema":{"type":"string"}},{"name":"mda","in":"query","schema":{"type":"string"}},{"name":"from_date","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"to_date","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"start","in":"query","schema":{"type":"integer","default":0}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}}],"responses":{"200":{"description":"List of Payment Events.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PaymentEvent"}}}}}}}}}},"/v1/counter-shifts":{"get":{"tags":["Settlement"],"summary":"List shifts","parameters":[{"name":"clerk","in":"query","schema":{"type":"string"}},{"name":"mda","in":"query","schema":{"type":"string"}},{"name":"status","in":"query","schema":{"type":"string","enum":["Open","Closed","Cancelled"]}},{"name":"from_date","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"to_date","in":"query","schema":{"type":"string","format":"date-time"}},{"name":"start","in":"query","schema":{"type":"integer","default":0}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}}],"responses":{"200":{"description":"List of shifts (summary fields).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CounterShiftSummary"}}}}}}}}},"post":{"tags":["Settlement"],"summary":"Open a new shift","description":"Open a counter shift before collecting at an MDA window \u2014 the\ncash-handling day's first step. Assessments + payments are scoped\nto the open shift, and end-of-day reconciliation (cash expected vs\ncounted, variance) is computed against it.\n\n**Who calls this:** an `assessments.write`-scoped integrator, or a\nclerk via the counter station. Single-open-shift-per-(clerk, mda)\nis enforced \u2014 close any existing open shift first.\n\n**Where it sits in the flow:**\n`**open shift (here)** \u2192 assess + collect (many) \u2192 close shift`.\n\n**If you click \"Send\":** the sandbox key carries\n`assessments.write`, so this opens a real shift \u2014 unless the clerk\nalready has one open at that MDA, which returns a validation error.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["mda"],"properties":{"mda":{"type":"string","example":"GULU"},"counter_label":{"type":"string","example":"Counter 3 \u2014 City Hall"},"opening_float":{"type":"number","default":0,"example":100000},"currency":{"type":"string","default":"UGX"},"opening_notes":{"type":"string"}}}}}},"responses":{"200":{"description":"Shift opened.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/CounterShift"}}}}}}}}},"/v1/counter-shifts/active":{"get":{"tags":["Settlement"],"summary":"Get the current user's open shift on a given MDA (or null)","description":"Used by the Clerk UI as the \"is there an open shift?\" gate at counter-app load.","parameters":[{"name":"mda","in":"query","required":true,"schema":{"type":"string"},"example":"GULU"}],"responses":{"200":{"description":"The active shift or null.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"oneOf":[{"$ref":"#/components/schemas/CounterShift"},{"type":"null"}]}}}}}}}}},"/v1/counter-shifts/{name}":{"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","pattern":"^SHIFT-\\d{4}-\\d{2}-\\d{2}-\\d{3}$"},"example":"SHIFT-2026-05-21-021"}],"get":{"tags":["Settlement"],"summary":"Get one shift","responses":{"200":{"description":"The shift.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/CounterShift"}}}}}}}}},"/v1/counter-shifts/{name}:close":{"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"}}],"post":{"tags":["Settlement"],"summary":"Close a shift with a counted-cash value","description":"Triggers `refresh_aggregates()` then transitions Open \u2192 Closed.\nIf |variance| > 0 and no `variance_reason` is provided, returns 422.\n\n**Who calls this:** the `assessments.write` actor (clerk / counter\nstation) at end of day \u2014 the reconciliation step. It freezes the\nshift's collected totals and records any cash discrepancy; a\nmaterial variance raises an Anomaly Flag for supervisor review.\n\n**Where it sits in the flow:**\n`open shift \u2192 assess + collect (many) \u2192 **close shift (here)**`.\nOnly valid on an `Open` shift.\n\n**If you click \"Send\":** supply the `name` of a real Open shift and\na `cash_counted` total. If counted \u2260 expected you must also pass\n`variance_reason`, or the close is rejected \u2014 that forced\nexplanation is the audit control, not an error.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["cash_counted"],"properties":{"cash_counted":{"type":"number","example":100000},"variance_reason":{"type":"string","example":"Customer paid in coins, drawer over by 500"},"closing_notes":{"type":"string"}}}}}},"responses":{"200":{"description":"Shift closed with reconciliation aggregates filled.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/CounterShift"}}}}}}}}},"/v1/counter-shifts/{name}:refresh":{"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"}}],"post":{"tags":["Settlement"],"summary":"Recompute aggregates without closing","description":"Useful as a Clerk-UI \"refresh stats\" button on an open shift.","responses":{"200":{"description":"Shift with refreshed aggregates.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/CounterShift"}}}}}}}}},"/v1/integrations":{"get":{"tags":["Integrations"],"summary":"Per-country integration status snapshot","description":"Per-country integration status: `country \u2192 capability \u2192 status`,\nwhere status is `live` (real credentials wired), `sandbox`\n(adapter present, running in STUB mode), or `unavailable`\n(referenced in a Country Profile but not yet built / MoU-pending).\nSingle-adapter capabilities (`identity`, `fiscal`) are one\n`{status}` object; `payment` is a LIST of provider entries, each\n`{status, channels}`. Internal adapter class paths are never\nexposed.\n\n**Who calls this:** any integrator key (`catalogue.read` scope) \u2014\ndiscovery metadata, like the catalogue. The operator view with\nimportability + class-path detail is `/v1/ops/adapters`.\n\nSurfaces, at a glance, which integrations are live vs sandbox vs\nMoU-pending across the rail.\n","security":[{"bearerToken":[]}],"responses":{"200":{"description":"Per-country integration status map.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","description":"country \u2192 capability \u2192 status; payment is a list of {status, channels}.","example":{"UG":{"identity":{"status":"sandbox"},"fiscal":{"status":"sandbox"},"payment":[{"status":"live","channels":["MTN MoMo"]},{"status":"sandbox","channels":["Airtel Money"]},{"status":"live","channels":["Cash"]}]}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/signup":{"post":{"tags":["Sign up"],"summary":"Start a sandbox signup","description":"Step 1 of the self-serve sandbox flow. Creates a `PendingEmail`\nintegrator row + dispatches a 6-digit OTP to the supplied email.\n`tos_accepted_version` must equal the value from `GET /v1/signup/tos`\n(currently `sandbox-tos-v1-2026-05-25`) \u2014 anything else is a 422.\n\nAn email already registered to an integrator is rejected with 409\n(`duplicate_email`) \u2014 sign in via the magic-link flow instead. Use\n`POST /v1/signup/resend-otp` to re-issue an OTP for a pending signup.\n\nRate limits: at most one OTP per 60 seconds per integrator,\n5 OTPs per UTC day total across initial signup + resends.\n","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupRequest"},"examples":{"minimal":{"summary":"Minimum viable signup","value":{"full_name":"Jane Builder","email":"jane@acmefintech.test","organisation":"Acme Fintech","tos_accepted_version":"sandbox-tos-v1-2026-05-25"}}}}}},"responses":{"200":{"description":"OTP dispatched. Carry `integrator_id` to `/v1/signup/verify`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SignupResponse"}}}}}},"409":{"description":"An integrator is already registered for that email.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X POST https://sente-rails.space/v1/signup \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"full_name\": \"Jane Builder\",\n    \"email\": \"jane@acmefintech.test\",\n    \"organisation\": \"Acme Fintech\",\n    \"tos_accepted_version\": \"sandbox-tos-v1-2026-05-25\"\n  }'"},{"lang":"Python","label":"Python","source":"import requests\ntos = requests.get(\"https://sente-rails.space/v1/signup/tos\").json()[\"data\"]\nr = requests.post(\n    \"https://sente-rails.space/v1/signup\",\n    json={\n        \"full_name\": \"Jane Builder\",\n        \"email\": \"jane@acmefintech.test\",\n        \"organisation\": \"Acme Fintech\",\n        \"tos_accepted_version\": tos[\"version\"],\n    },\n)\nr.raise_for_status()\nprint(r.json()[\"data\"])  # \u2192 {\"integrator_id\": \"ACME-FINTECH-XXXXXX\", ...}"}]}},"/v1/signup/verify":{"post":{"tags":["Sign up"],"summary":"Submit the OTP and receive the sandbox key plaintext","description":"Step 2 of signup. On success: the integrator flips to\n`status=Active` + `email_verified=1`, OTP fields clear, the first\nsandbox key is issued. **The plaintext is shown exactly once** \u2014\nstore it before the response leaves the network.\n\nFailed OTP entries increment `otp_attempts_today`; after the\nconfigured cap the integrator must wait or contact ops.\n","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignupVerifyRequest"}}}},"responses":{"200":{"description":"Verified. Plaintext key in the response \u2014 copy now.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SignupVerifyResponse"}}}}}},"401":{"description":"OTP not correct.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"No signup in progress for that integrator id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"OTP not issued or already consumed \u2014 request a new one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"$ref":"#/components/responses/ValidationFailed"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X POST https://sente-rails.space/v1/signup/verify \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"integrator_id\": \"ACME-FINTECH-XXXXXX\", \"otp\": \"123456\"}'"},{"lang":"Python","label":"Python","source":"import requests\nr = requests.post(\n    \"https://sente-rails.space/v1/signup/verify\",\n    json={\"integrator_id\": \"ACME-FINTECH-XXXXXX\", \"otp\": \"123456\"},\n)\nr.raise_for_status()\ndata = r.json()[\"data\"]\nprint(\"Save this plaintext:\", data[\"plaintext\"])"}]}},"/v1/signup/resend-otp":{"post":{"tags":["Sign up"],"summary":"Re-issue an OTP for a pending signup","description":"Rate-limited: at most one send per 60 seconds, 5 sends per UTC\nday across initial signup + resends. Each call resets the\n15-minute TTL on the active OTP.\n","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["integrator_id"],"properties":{"integrator_id":{"type":"string","example":"ACME-FINTECH-XXXXXX"}}}}}},"responses":{"200":{"description":"New OTP dispatched.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"integrator_id":{"type":"string"},"message":{"type":"string"},"expires_at_iso":{"type":"string","format":"date-time"},"sends_remaining_today":{"type":"integer"}}}}}}}},"404":{"description":"No signup in progress for that integrator id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X POST https://sente-rails.space/v1/signup/resend-otp \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"integrator_id\": \"ACME-FINTECH-XXXXXX\"}'"},{"lang":"Python","label":"Python","source":"import requests\nr = requests.post(\n    \"https://sente-rails.space/v1/signup/resend-otp\",\n    json={\"integrator_id\": \"ACME-FINTECH-XXXXXX\"},\n)\nprint(r.json()[\"data\"])"}]}},"/v1/signup/tos":{"get":{"tags":["Sign up"],"summary":"Get the current Sandbox Terms of Service version","description":"Returns the version + summary + a URL to the full text. The signup\nform pins `tos_version` to the value returned here when the user\naccepts; spec changes that bump the version invalidate signups\nin flight only after a deliberate rollover.\n","security":[],"responses":{"200":{"description":"Current Sandbox ToS metadata.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SignupTos"}}}}}}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl https://sente-rails.space/v1/signup/tos"},{"lang":"Python","label":"Python","source":"import requests\ntos = requests.get(\"https://sente-rails.space/v1/signup/tos\").json()[\"data\"]\nprint(tos[\"version\"], tos[\"document_url\"])"}]}},"/v1/login/request":{"post":{"tags":["Authentication"],"summary":"Send a magic-link to an active integrator","description":"Step 1 of magic-link sign-in. The response is **deliberately\nuniform** \u2014 whether the email is registered, registered-but-\nunverified, or suspended, the caller sees the same 200 body. This\ndenies account-existence enumeration via timing or shape side\nchannels.\n\nRate limits: at most one link per 60 seconds per integrator,\n5 links per UTC day.\n\n**Dev-only convenience**: when the site has\n`sente_dev_reveal_magic_link: 1` in `site_config.json`, the\nresponse data additionally includes a `dev_consume_url` field for\nreal-email-less testing. Real deployments leave this off.\n","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}}},"responses":{"200":{"description":"Always returned. Body is uniform regardless of match.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"message":{"type":"string"},"dev_consume_url":{"type":"string","format":"uri","description":"Dev-only \u2014 present only when site_config enables reveal."}}}}}}}},"422":{"$ref":"#/components/responses/ValidationFailed"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X POST https://sente-rails.space/v1/login/request \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"email\": \"jane@acmefintech.test\"}'"},{"lang":"Python","label":"Python","source":"import requests\nrequests.post(\n    \"https://sente-rails.space/v1/login/request\",\n    json={\"email\": \"jane@acmefintech.test\"},\n)"}]}},"/v1/login/consume":{"get":{"tags":["Authentication"],"summary":"Exchange a magic-link token for a session cookie","description":"Step 2 of magic-link sign-in. Validates the single-use token\nencoded in the URL. On success: sets the `sente_session` cookie\n(HttpOnly, Secure, SameSite=Lax, 14-day life) and redirects to\n`/dashboard`. On any failure (bad token, expired, already used,\nintegrator suspended, email unverified): redirects to\n`/signin/expired` with no cookie set \u2014 failure paths share a\ntarget so attackers can't probe which tokens existed.\n\nThis endpoint is typically not called directly \u2014 it's the target\nof the URL inside the magic-link email.\n","security":[],"parameters":[{"name":"token","in":"query","required":true,"schema":{"type":"string","pattern":"^[A-Z0-9-]{3,64}\\.[A-Z2-9]{32}$"},"example":"ACME-FINTECH-XXXXXX.A7F3GHKL..."}],"responses":{"302":{"description":"Redirect \u2014 either to `/dashboard` (success) or `/signin/expired`.","headers":{"Location":{"schema":{"type":"string"}},"Set-Cookie":{"schema":{"type":"string"},"description":"Sets `sente_session=...` on success only."}}}}}},"/v1/logout":{"post":{"tags":["Authentication"],"summary":"Clear the active session","description":"Idempotent \u2014 calling without a session set returns the same 200.\nThe cookie is cleared on every path so a malformed/stale cookie\nalso gets wiped from the browser.\n","security":[{"sessionCookie":[]},{"bearerToken":[]}],"responses":{"200":{"description":"Session cleared (or nothing to clear).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"message":{"type":"string","example":"Signed out."}}}}}}}}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X POST https://sente-rails.space/v1/logout \\\n  -H 'Cookie: sente_session=<your-session>'"},{"lang":"Python","label":"Python","source":"import requests\ns = requests.Session()\ns.cookies.set(\"sente_session\", \"<your-session>\")\ns.post(\"https://sente-rails.space/v1/logout\")"}]}},"/v1/session":{"get":{"tags":["Authentication"],"summary":"Probe the current session","description":"Returns `{authenticated: true, integrator: {code, display_name,\ncontact_email, tier, pricing_tier, last_login_at}}` when a valid\nsession cookie OR Bearer key is present, else `{authenticated:\nfalse}`. Used by the workbench to hide/show signed-in chrome\nwithout falling over on logged-out visitors.\n","security":[{"sessionCookie":[]},{"bearerToken":[]},{}],"responses":{"200":{"description":"Session probe result.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SessionInfo"}}}}}}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl https://sente-rails.space/v1/session \\\n  -H 'Cookie: sente_session=<...>'"},{"lang":"Python","label":"Python","source":"import requests\nr = requests.get(\"https://sente-rails.space/v1/session\",\n                 cookies={\"sente_session\": \"<...>\"})\nprint(r.json()[\"data\"])"}]}},"/v1/me":{"get":{"tags":["Account"],"summary":"Get the signed-in integrator's profile","description":"Returns the profile + live counters (active key count, total key\ncount, last-7-day request count). The endpoint reads the\nrequest-local integrator identity set by the auth pipeline \u2014 so the\ncaller can only ever see their own row.\n","security":[{"sessionCookie":[]},{"bearerToken":[]}],"responses":{"200":{"description":"The signed-in integrator's profile.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Integrator"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl https://sente-rails.space/v1/me \\\n  -H 'Authorization: Bearer sk_sandbox_...'"},{"lang":"Python","label":"Python","source":"import requests\nme = requests.get(\n    \"https://sente-rails.space/v1/me\",\n    headers={\"Authorization\": \"Bearer sk_sandbox_...\"},\n).json()[\"data\"]\nprint(me[\"display_name\"], me[\"status\"])"}]},"patch":{"tags":["Account"],"summary":"Patch a writable subset of the profile","description":"Only the listed fields are writable here. `contact_email` changes\nare deliberately out of scope \u2014 rotating the email requires re-\nrunning OTP verification against the new address, which is an ops\nflow until self-serve email change ships.\n","security":[{"sessionCookie":[]},{"bearerToken":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegratorPatch"}}}},"responses":{"200":{"description":"Updated profile.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Integrator"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"$ref":"#/components/responses/ValidationFailed"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X PATCH https://sente-rails.space/v1/me \\\n  -H 'Authorization: Bearer sk_sandbox_...' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"webhook_endpoint\": \"https://acmefintech.test/sente-hook\", \"anticipated_volume_monthly\": 250000}'"},{"lang":"Python","label":"Python","source":"import requests\nrequests.patch(\n    \"https://sente-rails.space/v1/me\",\n    headers={\"Authorization\": \"Bearer sk_sandbox_...\"},\n    json={\"webhook_endpoint\": \"https://acmefintech.test/sente-hook\"},\n)"}]},"post":{"tags":["Account"],"summary":"Alias for PATCH /v1/me","description":"Some HTTP clients can't issue PATCH. POST against `/v1/me` with\nthe same body produces the same effect.\n","security":[{"sessionCookie":[]},{"bearerToken":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegratorPatch"}}}},"responses":{"200":{"description":"Updated profile.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Integrator"}}}}}}}}},"/v1/me/keys":{"get":{"tags":["API keys"],"summary":"List every key the caller owns","description":"Returns keys newest-first, including revoked and rolling ones, so\nthe dashboard can render the full lifecycle. Plaintext is never\nin this response \u2014 only `prefix`, `last4`, and metadata.\n","security":[{"sessionCookie":[]},{"bearerToken":[]}],"responses":{"200":{"description":"All keys owned by the signed-in integrator.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ApiKey"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl https://sente-rails.space/v1/me/keys \\\n  -H 'Authorization: Bearer sk_sandbox_...'"},{"lang":"Python","label":"Python","source":"import requests\nkeys = requests.get(\n    \"https://sente-rails.space/v1/me/keys\",\n    headers={\"Authorization\": \"Bearer sk_sandbox_...\"},\n).json()[\"data\"]\nfor k in keys:\n    print(k[\"name\"], k[\"status\"], k[\"last_used_at\"])"}]}},"/v1/me/keys/{name}:rotate":{"post":{"tags":["API keys"],"summary":"Rotate a key","description":"Mints a fresh key with the same scopes. The old key flips to\n`rolling` and remains live for `grace_hours` (1\u2013168, default 24),\ngiving callers a window to update applications. After the window\nthe old key expires and only the new one accepts traffic.\n\n**The plaintext of the new key is shown exactly once** in this\nresponse. Lose it, and the only remedy is to rotate again.\n","security":[{"sessionCookie":[]},{"bearerToken":[]}],"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","pattern":"^KEY-\\d{4}-\\d{6}$"},"example":"KEY-2026-000002"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"grace_hours":{"type":"integer","default":24,"minimum":1,"maximum":168}}}}}},"responses":{"200":{"description":"Rotated. Plaintext returned once \u2014 store immediately.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ApiKeyRotateResult"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Key not found \u2014 or owned by a different integrator. The two\ncases are deliberately indistinguishable (both 404, no 403\ndifferential) so a caller can't probe for other integrators'\nkey ids.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X POST https://sente-rails.space/v1/me/keys/KEY-2026-000002:rotate \\\n  -H 'Authorization: Bearer sk_sandbox_...' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"grace_hours\": 24}'"},{"lang":"Python","label":"Python","source":"import requests\nr = requests.post(\n    \"https://sente-rails.space/v1/me/keys/KEY-2026-000002:rotate\",\n    headers={\"Authorization\": \"Bearer sk_sandbox_...\"},\n    json={\"grace_hours\": 24},\n)\ndata = r.json()[\"data\"]\nprint(\"Save now:\", data[\"plaintext\"])"}]}},"/v1/me/keys/{name}:revoke":{"post":{"tags":["API keys"],"summary":"Revoke a key permanently","description":"Immediate, non-reversible. The key flips to `status=revoked` and\nevery subsequent request carrying it receives `401`. The supplied\n`reason` is recorded in the audit log; it shows up later under\nthe `revoked_reason` field on key listings.\n\nTo restore service, rotate a *different* key (or contact ops if\nthe revoked one was your last).\n","security":[{"sessionCookie":[]},{"bearerToken":[]}],"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","pattern":"^KEY-\\d{4}-\\d{6}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["reason"],"properties":{"reason":{"type":"string","maxLength":280,"example":"Suspected leak in CI logs"}}}}}},"responses":{"200":{"description":"Revoked. Subsequent requests with the key receive 401.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ApiKey"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Key not found \u2014 or owned by a different integrator. The two\ncases are deliberately indistinguishable (both 404, no 403\ndifferential) so a caller can't probe for other integrators'\nkey ids.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"$ref":"#/components/responses/ValidationFailed"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X POST https://sente-rails.space/v1/me/keys/KEY-2026-000001:revoke \\\n  -H 'Authorization: Bearer sk_sandbox_...' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"reason\": \"rotated out of legacy pipeline\"}'"},{"lang":"Python","label":"Python","source":"import requests\nrequests.post(\n    \"https://sente-rails.space/v1/me/keys/KEY-2026-000001:revoke\",\n    headers={\"Authorization\": \"Bearer sk_sandbox_...\"},\n    json={\"reason\": \"rotated out of legacy pipeline\"},\n)"}]}},"/v1/me/logs":{"get":{"tags":["Audit logs"],"summary":"List the caller's recent /v1 audit events","description":"90-day hot window \u2014 newest first. Filter by substring on\n`endpoint`, exact `event` class, and minimum HTTP status. Each\nrow carries a `request_id` you can quote when reporting issues.\n\nEvents emitted today:\n- `api.auth.granted` \u2014 request authenticated + scoped successfully.\n- `api.auth.denied` \u2014 auth or scope check refused (4xx).\n- `api.handler.error` \u2014 handler raised (5xx).\n","security":[{"sessionCookie":[]},{"bearerToken":[]}],"parameters":[{"name":"endpoint","in":"query","schema":{"type":"string"},"description":"Substring match on the endpoint path.","example":"/v1/citizens"},{"name":"event","in":"query","schema":{"type":"string","enum":["api.auth.granted","api.auth.denied","api.handler.error"]}},{"name":"min_status","in":"query","schema":{"type":"integer"},"description":"Minimum HTTP status to include (e.g. 400 \u2192 errors only)."},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"minimum":1,"maximum":200}}],"responses":{"200":{"description":"Matching audit rows.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AuditLogEntry"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl 'https://sente-rails.space/v1/me/logs?event=api.auth.denied&limit=50' \\\n  -H 'Authorization: Bearer sk_sandbox_...'"},{"lang":"Python","label":"Python","source":"import requests\nrows = requests.get(\n    \"https://sente-rails.space/v1/me/logs\",\n    params={\"event\": \"api.auth.denied\", \"limit\": 50},\n    headers={\"Authorization\": \"Bearer sk_sandbox_...\"},\n).json()[\"data\"]\nfor r in rows:\n    print(r[\"ts\"], r[\"http_status\"], r[\"endpoint\"], r[\"request_id\"])"}]}},"/v1/work/whoami":{"get":{"tags":["Counter Stations"],"summary":"Return the signed-in staff user's identity + role flags","description":"Allow-guest endpoint that returns `{authenticated: false}` when no\nvalid platform session is present. When authenticated, returns the\nuser row plus role booleans the kiosk uses to render its chrome.\n\nThis is the only `/v1/work/*` endpoint that doesn't 401 for\nunauthenticated callers \u2014 needed so the kiosk's auth-check flow\ncan probe state without bouncing the user to `/login`.\n","security":[{"staffSession":[]},{}],"responses":{"200":{"description":"Authentication probe result.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/WorkWhoami"}}}}}}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl https://sente-rails.space/v1/work/whoami \\\n  -H 'Cookie: sid=<staff-session-id>'"},{"lang":"Python","label":"Python","source":"import requests\nr = requests.get(\"https://sente-rails.space/v1/work/whoami\",\n                 cookies={\"sid\": \"<staff-session-id>\"})\nme = r.json()[\"data\"]\nprint(me[\"user\"][\"full_name\"], me[\"roles\"])"}]}},"/v1/work/mdas":{"get":{"tags":["Counter Stations"],"summary":"List MDAs available at the kiosk","description":"Mirrors `/v1/mdas` but gated by Counter Stations auth. The kiosk's\nMDA picker reads from here on shift-open. No filters today \u2014\nreturns the full active catalogue.\n","security":[{"staffSession":[]}],"responses":{"200":{"description":"Active MDAs.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/MdaSummary"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl https://sente-rails.space/v1/work/mdas \\\n  -H 'Cookie: sid=<staff-session-id>'"},{"lang":"Python","label":"Python","source":"import requests\nr = requests.get(\"https://sente-rails.space/v1/work/mdas\",\n                 cookies={\"sid\": \"<staff-session-id>\"})\nfor m in r.json()[\"data\"]:\n    print(m[\"name\"], m[\"full_name\"])"}]}},"/v1/work/services":{"get":{"tags":["Counter Stations"],"summary":"List active services for an MDA","description":"Powers the kiosk's service picker mid-assessment. The `mda` query\nparam is required \u2014 services are scoped to their owning MDA.\n","security":[{"staffSession":[]}],"parameters":[{"name":"mda","in":"query","required":true,"schema":{"type":"string"},"example":"GULU"}],"responses":{"200":{"description":"Active services for the given MDA.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Service"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"$ref":"#/components/responses/ValidationFailed"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl 'https://sente-rails.space/v1/work/services?mda=GULU' \\\n  -H 'Cookie: sid=<staff-session-id>'"},{"lang":"Python","label":"Python","source":"import requests\nr = requests.get(\"https://sente-rails.space/v1/work/services\",\n                 params={\"mda\": \"GULU\"},\n                 cookies={\"sid\": \"<staff-session-id>\"})\nfor svc in r.json()[\"data\"]:\n    print(svc[\"code\"], svc[\"service_name\"], svc[\"fee_amount\"])"}]}},"/v1/work/citizens":{"post":{"tags":["Counter Stations"],"summary":"Register a citizen at the counter","description":"Find-or-create a local Citizen from a NIN, resolving via NIRA.\nThe counter companion to `GET /v1/work/citizens/search`: a\nNIRA-only hit has no local docname and cannot anchor an\nassessment, so this persists it into the local registry and\ncaptures an Identity Verification consent event (the clerk\nregistering a citizen who is present at the counter is the\nin-person consent gesture). Idempotent \u2014 an already-registered\nNIN returns the existing record. Auth: staff session\n(Clerk / Supervisor).\n\n**Who calls this:** a signed-in clerk at a counter station \u2014 via\na browser **session cookie** (`sid`), not an API key. It is\ndriven by the workbench `/work/assess` screen, not by integrators.\n\n**If you click \"Send\" in this explorer:** expect `401` unless you\nare signed in as staff \u2014 this endpoint takes a staff session, and\nthe API key on the explorer page does not satisfy it. That is by\ndesign: only counter staff, with the citizen present, may register\nthem.\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["nin"],"properties":{"nin":{"type":"string","example":"CM78001234ABCD"},"mda":{"type":"string","example":"GULU","description":"Consent attribution; defaults to the clerk's assigned MDA."}}}}}},"responses":{"200":{"description":"Citizen registered (or already on the rail).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"citizen":{"$ref":"#/components/schemas/Citizen"},"created":{"type":"boolean"},"source":{"type":"string","enum":["local","nira"]},"consent_event":{"type":["string","null"]}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/work/citizens/search":{"get":{"tags":["Counter Stations"],"summary":"Resolve a citizen by NIN","description":"Two-layer lookup: local Citizen registry first, falling through to\nthe NIRA adapter (and caching the result locally). Response\n`source` tells the caller which layer answered.\n\nReturns `{source: \"miss\", citizen: null}` when no match is found \u2014\nthe kiosk falls back to its manual entry flow.\n","security":[{"staffSession":[]}],"parameters":[{"name":"nin","in":"query","required":true,"schema":{"type":"string"},"description":"National Identification Number (case-insensitive \u2014 uppercased server-side).","example":"CM78001234ABCD"}],"responses":{"200":{"description":"Citizen lookup result.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/CitizenSearchResult"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"$ref":"#/components/responses/ValidationFailed"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl 'https://sente-rails.space/v1/work/citizens/search?nin=CM78001234ABCD' \\\n  -H 'Cookie: sid=<staff-session-id>'"},{"lang":"Python","label":"Python","source":"import requests\nr = requests.get(\"https://sente-rails.space/v1/work/citizens/search\",\n                 params={\"nin\": \"CM78001234ABCD\"},\n                 cookies={\"sid\": \"<staff-session-id>\"})\nresult = r.json()[\"data\"]\nprint(result[\"source\"], result[\"citizen\"])"}]}},"/v1/work/shift/active":{"get":{"tags":["Counter Stations"],"summary":"Get the clerk's currently open shift at an MDA","description":"Returns the open `Counter Shift` row for the signed-in clerk\nscoped to the given MDA, or `null` if no shift is open. The\nkiosk hits this on every page load to know whether to show the\n\"open shift\" CTA or the active workflow.\n","security":[{"staffSession":[]}],"parameters":[{"name":"mda","in":"query","required":true,"schema":{"type":"string"},"example":"GULU"}],"responses":{"200":{"description":"The open shift, or null.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"oneOf":[{"$ref":"#/components/schemas/CounterShift"},{"type":"null"}]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl 'https://sente-rails.space/v1/work/shift/active?mda=GULU' \\\n  -H 'Cookie: sid=<staff-session-id>'"},{"lang":"Python","label":"Python","source":"import requests\nshift = requests.get(\n    \"https://sente-rails.space/v1/work/shift/active\",\n    params={\"mda\": \"GULU\"},\n    cookies={\"sid\": \"<staff-session-id>\"},\n).json()[\"data\"]\nprint(\"open shift:\", shift[\"name\"] if shift else \"none\")"}]}},"/v1/work/shifts":{"get":{"tags":["Counter Stations"],"summary":"List the clerk's shifts (newest first)","description":"History view for the kiosk's \"previous shifts\" tab. Filter by\nstatus (Open / Closed / Variance) if needed.\n","security":[{"staffSession":[]}],"parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["Open","Closed","Variance"]}},{"name":"limit","in":"query","schema":{"type":"integer","default":20,"minimum":1,"maximum":100}}],"responses":{"200":{"description":"Shifts owned by the signed-in clerk.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CounterShift"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl 'https://sente-rails.space/v1/work/shifts?status=Closed&limit=10' \\\n  -H 'Cookie: sid=<staff-session-id>'"},{"lang":"Python","label":"Python","source":"import requests\nshifts = requests.get(\n    \"https://sente-rails.space/v1/work/shifts\",\n    params={\"status\": \"Closed\", \"limit\": 10},\n    cookies={\"sid\": \"<staff-session-id>\"},\n).json()[\"data\"]\nfor s in shifts:\n    print(s[\"name\"], s[\"status\"], s[\"cash_counted\"])"}]}},"/v1/work/shift":{"post":{"tags":["Counter Stations"],"summary":"Open a new shift","description":"Creates a `Counter Shift` row in `Open` state owned by the signed-in\nclerk. Rejects if the clerk already has an open shift at this MDA \u2014\nonly one open shift per (clerk, MDA) pair at a time.\n","security":[{"staffSession":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShiftOpenRequest"}}}},"responses":{"200":{"description":"Shift opened.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/CounterShift"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"Clerk already has an open shift at this MDA.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"$ref":"#/components/responses/ValidationFailed"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X POST https://sente-rails.space/v1/work/shift \\\n  -H 'Cookie: sid=<staff-session-id>' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"mda\": \"GULU\", \"counter_label\": \"Window 2\", \"opening_cash\": 50000}'"},{"lang":"Python","label":"Python","source":"import requests\nshift = requests.post(\n    \"https://sente-rails.space/v1/work/shift\",\n    cookies={\"sid\": \"<staff-session-id>\"},\n    json={\"mda\": \"GULU\", \"counter_label\": \"Window 2\", \"opening_cash\": 50000},\n).json()[\"data\"]\nprint(\"opened:\", shift[\"name\"])"}]}},"/v1/work/shift/{name}:close":{"post":{"tags":["Counter Stations"],"summary":"Close a shift with a cash count","description":"Submits the clerk's end-of-shift cash count. The server computes\nvariance against expected (`opening_cash + cash receipts -\ncash refunds`) and writes it to the shift row. If non-zero, the\nshift flips to `Variance` state and surfaces in the supervisor\nqueue; otherwise it flips to `Closed`.\n\nOnly the shift's owning clerk (or an admin) may close it.\n","security":[{"staffSession":[]}],"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","pattern":"^SHIFT-\\d{4}-\\d{2}-\\d{2}-\\d{3}$"},"example":"SHIFT-2026-05-28-204"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShiftCloseRequest"}}}},"responses":{"200":{"description":"Shift closed. Inspect `status` for `Closed` vs `Variance`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/CounterShift"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Caller does not own this shift.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X POST 'https://sente-rails.space/v1/work/shift/SHIFT-2026-05-28-204:close' \\\n  -H 'Cookie: sid=<staff-session-id>' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"cash_counted\": 1450000, \"note\": \"Drawer ran short by petty change\"}'"},{"lang":"Python","label":"Python","source":"import requests\nr = requests.post(\n    \"https://sente-rails.space/v1/work/shift/SHIFT-2026-05-28-204:close\",\n    cookies={\"sid\": \"<staff-session-id>\"},\n    json={\"cash_counted\": 1450000, \"note\": \"Drawer ran short\"},\n)\nprint(r.json()[\"data\"][\"status\"])  # \u2192 \"Closed\" or \"Variance\""}]}},"/v1/work/assessments":{"post":{"tags":["Counter Stations"],"summary":"Create a draft assessment","description":"Builds an `Assessment` in `Draft` state with one or more\n`Assessment Line` rows. Each line resolves its rate from the\nService catalogue; line `quantity` and `explicit_amount` overrides\nare honoured. The assessment is scoped to the clerk's currently\nopen shift \u2014 open one via `POST /v1/work/shift` first.\n","security":[{"staffSession":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkAssessmentCreate"}}}},"responses":{"200":{"description":"Assessment created in Draft.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Assessment"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"$ref":"#/components/responses/ValidationFailed"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X POST https://sente-rails.space/v1/work/assessments \\\n  -H 'Cookie: sid=<staff-session-id>' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"citizen\": \"CITIZEN-2026-000002\",\n    \"mda_default\": \"GULU\",\n    \"lines\": [{\"service\": \"SVC-2026-000004\", \"quantity\": 1}]\n  }'"},{"lang":"Python","label":"Python","source":"import requests\nr = requests.post(\n    \"https://sente-rails.space/v1/work/assessments\",\n    cookies={\"sid\": \"<staff-session-id>\"},\n    json={\n        \"citizen\": \"CITIZEN-2026-000002\",\n        \"mda_default\": \"GULU\",\n        \"lines\": [{\"service\": \"SVC-2026-000004\", \"quantity\": 1}],\n    },\n)\nprint(r.json()[\"data\"][\"name\"])"}]}},"/v1/work/assessments/{name}":{"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","pattern":"^ASMT-\\d{4}-\\d{2}-\\d{6}$"},"example":"ASMT-2026-05-000123"}],"get":{"tags":["Counter Stations"],"summary":"Fetch an assessment by docname","description":"Returns the full assessment including all lines, totals,\ncurrency, payment status, and links to the issuing clerk + shift.\n","security":[{"staffSession":[]}],"responses":{"200":{"description":"The assessment.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Assessment"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl https://sente-rails.space/v1/work/assessments/ASMT-2026-05-000123 \\\n  -H 'Cookie: sid=<staff-session-id>'"},{"lang":"Python","label":"Python","source":"import requests\nasmt = requests.get(\n    \"https://sente-rails.space/v1/work/assessments/ASMT-2026-05-000123\",\n    cookies={\"sid\": \"<staff-session-id>\"},\n).json()[\"data\"]\nprint(asmt[\"status\"], asmt[\"total_amount\"])"}]}},"/v1/work/assessments/{name}:assess":{"post":{"tags":["Counter Stations"],"summary":"Lock totals on a draft assessment","description":"Transitions a `Draft` assessment to `Assessed`, freezing line\ntotals + the grand total. Required before a payment intent can\nbe created against the assessment. Re-running on an already-\nassessed assessment is a no-op.\n","security":[{"staffSession":[]}],"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","pattern":"^ASMT-\\d{4}-\\d{2}-\\d{6}$"}}],"responses":{"200":{"description":"Assessment locked.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Assessment"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X POST 'https://sente-rails.space/v1/work/assessments/ASMT-2026-05-000123:assess' \\\n  -H 'Cookie: sid=<staff-session-id>'"},{"lang":"Python","label":"Python","source":"import requests\nrequests.post(\n    \"https://sente-rails.space/v1/work/assessments/ASMT-2026-05-000123:assess\",\n    cookies={\"sid\": \"<staff-session-id>\"},\n)"}]}},"/v1/work/payment-intents":{"post":{"tags":["Counter Stations"],"summary":"Create a payment intent for an assessed assessment","description":"Spawns a `Payment Intent` linked to the named assessment, carrying\nthe chosen channel + (for mobile money) the citizen's MSISDN.\nSplit rules are inherited from the assessment's per-line MDA\nbreakdown; Sente Rails never holds public money \u2014 the aggregator\nexecutes splits directly into each MDA's collection account.\n\nThe intent is `Pending` until `:initiate` fires.\n","security":[{"staffSession":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkPaymentIntentCreate"}}}},"responses":{"200":{"description":"Payment intent created in Pending.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PaymentIntent"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"$ref":"#/components/responses/ValidationFailed"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X POST https://sente-rails.space/v1/work/payment-intents \\\n  -H 'Cookie: sid=<staff-session-id>' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"assessment\": \"ASMT-2026-05-000123\", \"channel\": \"MTN MoMo\", \"citizen_msisdn\": \"+256772123456\"}'"},{"lang":"Python","label":"Python","source":"import requests\npi = requests.post(\n    \"https://sente-rails.space/v1/work/payment-intents\",\n    cookies={\"sid\": \"<staff-session-id>\"},\n    json={\n        \"assessment\": \"ASMT-2026-05-000123\",\n        \"channel\": \"MTN MoMo\",\n        \"citizen_msisdn\": \"+256772123456\",\n    },\n).json()[\"data\"]\nprint(pi[\"name\"], pi[\"status\"])"}]}},"/v1/work/payment-intents/{name}:initiate":{"post":{"tags":["Counter Stations"],"summary":"Dispatch the payment intent to the aggregator","description":"Fires the adapter for the chosen channel (MoMo / Airtel / Pesapal\n/ etc.) and stores the `aggregator_reference`. The intent moves\nfrom `Pending` \u2192 `Sent`. The aggregator pushes confirmation back\nvia `/v1/webhooks/{provider}`; the kiosk can poll\n`/v1/work/payment-intents/{name}/live-status` while waiting.\n\nFor `Cash` channel intents, this transitions straight to\n`Confirmed`.\n","security":[{"staffSession":[]}],"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","pattern":"^PI-\\d{4}-\\d{2}-\\d{6}$"},"example":"PI-2026-05-000087"}],"responses":{"200":{"description":"Adapter dispatched. Inspect `status` + `aggregator_reference`.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PaymentIntent"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X POST 'https://sente-rails.space/v1/work/payment-intents/PI-2026-05-000087:initiate' \\\n  -H 'Cookie: sid=<staff-session-id>'"},{"lang":"Python","label":"Python","source":"import requests\nrequests.post(\n    \"https://sente-rails.space/v1/work/payment-intents/PI-2026-05-000087:initiate\",\n    cookies={\"sid\": \"<staff-session-id>\"},\n)"}]}},"/v1/work/payment-intents/{name}:confirm":{"post":{"tags":["Counter Stations"],"summary":"Manually confirm a payment intent","description":"Operator-side confirmation \u2014 used when the aggregator's webhook\nis delayed or missing (e.g. cash, or a manual aggregator backfill).\nTransitions the intent to `Confirmed` + the assessment's\n`payment_status` to `Confirmed`, fires the success-side hooks\n(receipt, EFRIS, downstream webhook).\n","security":[{"staffSession":[]}],"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","pattern":"^PI-\\d{4}-\\d{2}-\\d{6}$"}}],"responses":{"200":{"description":"Intent confirmed.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PaymentIntent"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X POST 'https://sente-rails.space/v1/work/payment-intents/PI-2026-05-000087:confirm' \\\n  -H 'Cookie: sid=<staff-session-id>'"},{"lang":"Python","label":"Python","source":"import requests\nrequests.post(\n    \"https://sente-rails.space/v1/work/payment-intents/PI-2026-05-000087:confirm\",\n    cookies={\"sid\": \"<staff-session-id>\"},\n)"}]}},"/v1/work/payment-intents/{name}/live-status":{"get":{"tags":["Counter Stations"],"summary":"Poll the aggregator for current payment status","description":"Calls the aggregator's status endpoint live (no caching) and\nreturns the latest known state. The kiosk polls this every\nfew seconds while a payment is in flight to drive the spinner.\n","security":[{"staffSession":[]}],"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","pattern":"^PI-\\d{4}-\\d{2}-\\d{6}$"}}],"responses":{"200":{"description":"Live status snapshot.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","additionalProperties":true,"properties":{"status":{"type":"string"},"aggregator_reference":{"type":["string","null"]},"raw":{"type":"object","description":"Aggregator-native payload (shape varies).","additionalProperties":true}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl 'https://sente-rails.space/v1/work/payment-intents/PI-2026-05-000087/live-status' \\\n  -H 'Cookie: sid=<staff-session-id>'"},{"lang":"Python","label":"Python","source":"import requests\nr = requests.get(\n    \"https://sente-rails.space/v1/work/payment-intents/PI-2026-05-000087/live-status\",\n    cookies={\"sid\": \"<staff-session-id>\"},\n)\nprint(r.json()[\"data\"][\"status\"])"}]}},"/v1/work/payment-intents/{name}/trace":{"get":{"tags":["Counter Stations"],"summary":"Get the full event trace for a payment intent","description":"Returns the chronological list of `Payment Event` rows attached\nto the intent \u2014 adapter dispatch, webhook receipt, status flips,\nEFRIS issuance, downstream webhook to the integrator. Useful for\ndebugging stuck payments.\n","security":[{"staffSession":[]}],"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","pattern":"^PI-\\d{4}-\\d{2}-\\d{6}$"}}],"responses":{"200":{"description":"Event trace, newest-last.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PaymentEvent"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl 'https://sente-rails.space/v1/work/payment-intents/PI-2026-05-000087/trace' \\\n  -H 'Cookie: sid=<staff-session-id>'"},{"lang":"Python","label":"Python","source":"import requests\nevents = requests.get(\n    \"https://sente-rails.space/v1/work/payment-intents/PI-2026-05-000087/trace\",\n    cookies={\"sid\": \"<staff-session-id>\"},\n).json()[\"data\"]\nfor e in events:\n    print(e[\"ts\"], e[\"event_type\"], e.get(\"note\"))"}]}},"/v1/work/supervisor/dashboard":{"get":{"tags":["Counter Stations \u2014 Supervisor"],"summary":"Supervisor variance queue + counters","description":"Returns the pending-variance queue plus daily counters surfaced\non `/work/supervisor`. Requires the `Sente Rails Supervisor` or\nadmin role on top of the session.\n\nThe exact shape is intentionally open \u2014 downstream code should\ntreat unknown fields as forward-compatible additions.\n","security":[{"staffSession":[]}],"parameters":[{"name":"mda","in":"query","schema":{"type":"string"},"description":"Optional scope to one MDA. Omit for fleet-wide view."}],"responses":{"200":{"description":"Dashboard snapshot.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SupervisorDashboard"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl 'https://sente-rails.space/v1/work/supervisor/dashboard' \\\n  -H 'Cookie: sid=<supervisor-session-id>'"},{"lang":"Python","label":"Python","source":"import requests\ndash = requests.get(\n    \"https://sente-rails.space/v1/work/supervisor/dashboard\",\n    cookies={\"sid\": \"<supervisor-session-id>\"},\n).json()[\"data\"]\nprint(dash[\"pending_variance\"], \"shifts awaiting review\")"}]}},"/v1/work/supervisor/shifts/{name}:approve-variance":{"post":{"tags":["Counter Stations \u2014 Supervisor"],"summary":"Approve a closed shift's variance","description":"Marks the shift's variance as approved (recorded under the\nsupervisor's user id). Closes the variance loop without\nfurther action \u2014 used when the difference is within tolerance\nor has a documented explanation.\n","security":[{"staffSession":[]}],"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","pattern":"^SHIFT-\\d{4}-\\d{2}-\\d{2}-\\d{3}$"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VarianceDecisionRequest"}}}},"responses":{"200":{"description":"Variance approved.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/CounterShift"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X POST 'https://sente-rails.space/v1/work/supervisor/shifts/SHIFT-2026-05-28-204:approve-variance' \\\n  -H 'Cookie: sid=<supervisor-session-id>' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"note\": \"Counted bank deposit matches; within tolerance.\"}'"},{"lang":"Python","label":"Python","source":"import requests\nrequests.post(\n    \"https://sente-rails.space/v1/work/supervisor/shifts/SHIFT-2026-05-28-204:approve-variance\",\n    cookies={\"sid\": \"<supervisor-session-id>\"},\n    json={\"note\": \"Counted bank deposit matches; within tolerance.\"},\n)"}]}},"/v1/work/supervisor/shifts/{name}:reject-variance":{"post":{"tags":["Counter Stations \u2014 Supervisor"],"summary":"Reject a closed shift's variance","description":"Records a rejected variance \u2014 the clerk owes a re-count or an\nexplanation. The shift stays in `Variance` state until the\nclerk resolves and re-submits, or the supervisor escalates.\nRecommended `note` explains what the clerk needs to do next.\n","security":[{"staffSession":[]}],"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","pattern":"^SHIFT-\\d{4}-\\d{2}-\\d{2}-\\d{3}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/VarianceDecisionRequest"},{"required":["note"]}]}}}},"responses":{"200":{"description":"Variance rejected.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/CounterShift"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X POST 'https://sente-rails.space/v1/work/supervisor/shifts/SHIFT-2026-05-28-204:reject-variance' \\\n  -H 'Cookie: sid=<supervisor-session-id>' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"note\": \"Recount the float \u2014 totals don'\\''t match the till tape.\"}'"},{"lang":"Python","label":"Python","source":"import requests\nrequests.post(\n    \"https://sente-rails.space/v1/work/supervisor/shifts/SHIFT-2026-05-28-204:reject-variance\",\n    cookies={\"sid\": \"<supervisor-session-id>\"},\n    json={\"note\": \"Recount the float \u2014 totals don't match till tape\"},\n)"}]}},"/v1/work/supervisor/shifts/{name}:escalate-variance":{"post":{"tags":["Counter Stations \u2014 Supervisor"],"summary":"Escalate a variance to MDA finance","description":"Flags the shift's variance for review beyond the supervisor \u2014\ncreates a row in the escalation queue that MDA finance / audit\nsees. Used for material discrepancies or repeated issues with\nthe same clerk.\n","security":[{"staffSession":[]}],"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","pattern":"^SHIFT-\\d{4}-\\d{2}-\\d{2}-\\d{3}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/VarianceDecisionRequest"},{"required":["note"]}]}}}},"responses":{"200":{"description":"Variance escalated.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/CounterShift"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X POST 'https://sente-rails.space/v1/work/supervisor/shifts/SHIFT-2026-05-28-204:escalate-variance' \\\n  -H 'Cookie: sid=<supervisor-session-id>' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"note\": \"UGX 35,000 short; third week in a row for this clerk.\"}'"},{"lang":"Python","label":"Python","source":"import requests\nrequests.post(\n    \"https://sente-rails.space/v1/work/supervisor/shifts/SHIFT-2026-05-28-204:escalate-variance\",\n    cookies={\"sid\": \"<supervisor-session-id>\"},\n    json={\"note\": \"Material variance; pattern of issues.\"},\n)"}]}},"/v1/ops/whoami":{"get":{"tags":["Ops Console"],"summary":"Return the signed-in operator's identity + role flags","description":"Allow-guest probe \u2014 returns `{authenticated: false}` when no\nvalid platform session is present. Authenticated callers receive\ntheir user row plus the booleans the workbench uses to render\nchrome: `has_ops_access`, `can_write` (admin-only writes),\n`can_read_oversight`.\n","security":[{"staffSession":[]},{}],"responses":{"200":{"description":"Authentication probe result.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/OpsWhoami"}}}}}}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl https://sente-rails.space/v1/ops/whoami \\\n  -H 'Cookie: sid=<admin-session-id>'"},{"lang":"Python","label":"Python","source":"import requests\nme = requests.get(\"https://sente-rails.space/v1/ops/whoami\",\n                  cookies={\"sid\": \"<admin-session-id>\"}).json()[\"data\"]\nprint(me[\"roles\"], \"write:\", me[\"can_write\"])"}]}},"/v1/ops/system":{"get":{"tags":["Ops Console"],"summary":"System health snapshot","description":"Returns a snapshot used by the Ops Console homepage: audit-log\ntable state (row count + oldest/newest timestamps), the last\ndaily key-expiry sweep, the live/sandbox adapter tally, schema\ncounters (integrators / MDAs / services / keys), and the deployed\nbuild's git head. The exact field set is treated as forward-\ncompatible \u2014 unknown fields should be ignored.\n","security":[{"staffSession":[]}],"responses":{"200":{"description":"Health snapshot.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SystemHealth"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl https://sente-rails.space/v1/ops/system \\\n  -H 'Cookie: sid=<admin-session-id>'"},{"lang":"Python","label":"Python","source":"import requests\nh = requests.get(\"https://sente-rails.space/v1/ops/system\",\n                 cookies={\"sid\": \"<admin-session-id>\"}).json()[\"data\"]\nprint(h)"}]}},"/v1/ops/mdas":{"get":{"tags":["Ops Console"],"summary":"List every MDA (including inactive)","description":"Returns up to 500 MDA rows. Unlike the public `/v1/mdas` which\nfilters to `Active` by default, this includes pending/inactive\nrows so the console can render the full management view.\n","security":[{"staffSession":[]}],"parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["Active","Onboarding","Suspended"]}}],"responses":{"200":{"description":"All MDAs matching the filter (or all when none given).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/MdaSummary"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl 'https://sente-rails.space/v1/ops/mdas?status=Active' \\\n  -H 'Cookie: sid=<admin-session-id>'"},{"lang":"Python","label":"Python","source":"import requests\nmdas = requests.get(\n    \"https://sente-rails.space/v1/ops/mdas\",\n    cookies={\"sid\": \"<admin-session-id>\"},\n).json()[\"data\"]\nfor m in mdas:\n    print(m[\"name\"], m[\"status\"])"}]}},"/v1/ops/mdas/{name}":{"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"},"example":"GULU"}],"get":{"tags":["Ops Console"],"summary":"Get a single MDA's full record","description":"Returns the unredacted MDA row \u2014 including the integration config\nthe public read hides: `treasury_account`, `integration_endpoint`,\n`push_webhook_url`, `api_credentials_ref`, `oversight_scopes`, and\nthe `contact_email` / `contact_phone` roster.\n","security":[{"staffSession":[]}],"responses":{"200":{"description":"The MDA.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/OpsMDA"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl https://sente-rails.space/v1/ops/mdas/GULU \\\n  -H 'Cookie: sid=<admin-session-id>'"},{"lang":"Python","label":"Python","source":"import requests\nmda = requests.get(\n    \"https://sente-rails.space/v1/ops/mdas/GULU\",\n    cookies={\"sid\": \"<admin-session-id>\"},\n).json()[\"data\"]\nprint(mda[\"full_name\"], mda[\"treasury_account\"])"}]},"patch":{"tags":["Ops Console"],"summary":"Update MDA configuration","description":"Writes the allowed subset of MDA fields. Requires `Sente Rails\nAdmin` or `System Manager` role \u2014 `Sente Rails OAG` cannot\nwrite. Touches the audit log with the caller's user id.\n","security":[{"staffSession":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MdaPatch"}}}},"responses":{"200":{"description":"Updated MDA row.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/OpsMDA"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X PATCH https://sente-rails.space/v1/ops/mdas/GULU \\\n  -H 'Cookie: sid=<admin-session-id>' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"status\": \"Active\", \"target_endpoint_count\": 14}'"},{"lang":"Python","label":"Python","source":"import requests\nrequests.patch(\n    \"https://sente-rails.space/v1/ops/mdas/GULU\",\n    cookies={\"sid\": \"<admin-session-id>\"},\n    json={\"status\": \"Active\", \"target_endpoint_count\": 14},\n)"}]},"post":{"tags":["Ops Console"],"summary":"Alias for PATCH /v1/ops/mdas/{name}","description":"POST shape for clients that can't issue PATCH. Same body, same\nbehavior, same role gate.\n","security":[{"staffSession":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MdaPatch"}}}},"responses":{"200":{"description":"Updated MDA row.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/OpsMDA"}}}}}}}}},"/v1/ops/services":{"get":{"tags":["Ops Console"],"summary":"List services across the fleet","description":"Admin view over Service rows. Filter by `mda` or `status`.\nIncludes inactive/coming-soon entries that the public catalog\nhides.\n","security":[{"staffSession":[]}],"parameters":[{"name":"mda","in":"query","schema":{"type":"string"},"example":"GULU"},{"name":"status","in":"query","schema":{"type":"string","enum":["Active","Inactive","Coming Soon"]}}],"responses":{"200":{"description":"Matching services.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Service"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl 'https://sente-rails.space/v1/ops/services?mda=GULU' \\\n  -H 'Cookie: sid=<admin-session-id>'"},{"lang":"Python","label":"Python","source":"import requests\nsvcs = requests.get(\n    \"https://sente-rails.space/v1/ops/services\",\n    params={\"mda\": \"GULU\"},\n    cookies={\"sid\": \"<admin-session-id>\"},\n).json()[\"data\"]"}]}},"/v1/ops/services/{name}":{"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","pattern":"^SVC-\\d{4}-\\d{6}$"},"example":"SVC-2026-000004"}],"patch":{"tags":["Ops Console"],"summary":"Update service configuration","description":"Writes the allowed subset of Service fields \u2014 service_name,\nsector, service_family, status, fee_amount, fee_currency,\nfee_basis, vat_rate, vat_applicable, efris_taxable. Admin or\nSystem Manager only.\n","security":[{"staffSession":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServicePatch"}}}},"responses":{"200":{"description":"Updated service.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Service"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X PATCH https://sente-rails.space/v1/ops/services/SVC-2026-000004 \\\n  -H 'Cookie: sid=<admin-session-id>' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"fee_amount\": 75000, \"status\": \"Active\"}'"},{"lang":"Python","label":"Python","source":"import requests\nrequests.patch(\n    \"https://sente-rails.space/v1/ops/services/SVC-2026-000004\",\n    cookies={\"sid\": \"<admin-session-id>\"},\n    json={\"fee_amount\": 75000, \"status\": \"Active\"},\n)"}]},"post":{"tags":["Ops Console"],"summary":"Alias for PATCH /v1/ops/services/{name}","security":[{"staffSession":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServicePatch"}}}},"responses":{"200":{"description":"Updated service.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Service"}}}}}}}}},"/v1/ops/integrators":{"get":{"tags":["Ops Console"],"summary":"List integrators (admin view)","description":"Returns up to 500 integrator rows. Filter by `status`, `tier`,\nor `signup_source`; full-text-ish search via `q` matches against\ndisplay_name, contact_email, and integrator code. Sorted by\ncreation desc.\n","security":[{"staffSession":[]}],"parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["PendingEmail","Active","Suspended"]}},{"name":"tier","in":"query","schema":{"type":"string","enum":["Anonymous","Registered","Onboarding","Production","Restricted-Ops"]}},{"name":"signup_source","in":"query","schema":{"type":"string"}},{"name":"q","in":"query","schema":{"type":"string"},"description":"Substring search across display_name + contact_email + integrator code."},{"name":"limit","in":"query","schema":{"type":"integer","default":200,"minimum":1,"maximum":500}}],"responses":{"200":{"description":"Matching integrators.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/IntegratorAdminListItem"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl 'https://sente-rails.space/v1/ops/integrators?status=Active&q=acme' \\\n  -H 'Cookie: sid=<admin-session-id>'"},{"lang":"Python","label":"Python","source":"import requests\nrows = requests.get(\n    \"https://sente-rails.space/v1/ops/integrators\",\n    params={\"status\": \"Active\", \"q\": \"acme\"},\n    cookies={\"sid\": \"<admin-session-id>\"},\n).json()[\"data\"]"}]}},"/v1/ops/integrators/{name}":{"get":{"tags":["Ops Console"],"summary":"Get a single integrator's full record","description":"Returns the integrator row plus live counters (active vs total\nkeys, requests in the last 7 days). Sensitive transient fields\n(`otp_hash`, `session_token_hash`, `login_link_hash`) are\nstripped before serialization \u2014 admins don't need plaintext\nhashes.\n","security":[{"staffSession":[]}],"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"},"example":"ASAT-LABS"}],"responses":{"200":{"description":"The integrator with counters.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/IntegratorAdminDetail"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl https://sente-rails.space/v1/ops/integrators/ASAT-LABS \\\n  -H 'Cookie: sid=<admin-session-id>'"},{"lang":"Python","label":"Python","source":"import requests\nr = requests.get(\"https://sente-rails.space/v1/ops/integrators/ASAT-LABS\",\n                 cookies={\"sid\": \"<admin-session-id>\"}).json()[\"data\"]\nprint(r[\"display_name\"], r[\"keys\"], r[\"requests_last_7d\"])"}]}},"/v1/ops/integrators/{name}:suspend":{"post":{"tags":["Ops Console"],"summary":"Suspend an integrator","description":"Flips status to `Suspended`. All keys owned by the integrator\nimmediately fail with 401 (the auth middleware checks integrator\nstatus on every request, not just key status). Admin or System\nManager only. `reason` is required and recorded against the\nintegrator's audit notes.\n\nReturns 409 if the integrator is already Suspended.\n","security":[{"staffSession":[]}],"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["reason"],"properties":{"reason":{"type":"string","maxLength":280}}}}}},"responses":{"200":{"description":"Integrator suspended.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/OpsActionResponse"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Integrator already suspended.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"$ref":"#/components/responses/ValidationFailed"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X POST 'https://sente-rails.space/v1/ops/integrators/ACME-FINTECH-XXXXXX:suspend' \\\n  -H 'Cookie: sid=<admin-session-id>' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"reason\": \"Suspicious traffic pattern flagged by anomaly detector.\"}'"},{"lang":"Python","label":"Python","source":"import requests\nrequests.post(\n    \"https://sente-rails.space/v1/ops/integrators/ACME-FINTECH-XXXXXX:suspend\",\n    cookies={\"sid\": \"<admin-session-id>\"},\n    json={\"reason\": \"Repeated 401s flagged by anomaly detector\"},\n)"}]}},"/v1/ops/integrators/{name}:reactivate":{"post":{"tags":["Ops Console"],"summary":"Reactivate a suspended integrator","description":"Restores status to `Active`. Existing keys resume accepting\ntraffic. `reason` is required.\n\nReturns 409 if the integrator is not currently suspended.\n","security":[{"staffSession":[]}],"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["reason"],"properties":{"reason":{"type":"string","maxLength":280}}}}}},"responses":{"200":{"description":"Integrator reactivated.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/OpsActionResponse"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Integrator is already Active.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"$ref":"#/components/responses/ValidationFailed"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X POST 'https://sente-rails.space/v1/ops/integrators/ACME-FINTECH-XXXXXX:reactivate' \\\n  -H 'Cookie: sid=<admin-session-id>' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"reason\": \"Reviewed traffic; false positive.\"}'"},{"lang":"Python","label":"Python","source":"import requests\nrequests.post(\n    \"https://sente-rails.space/v1/ops/integrators/ACME-FINTECH-XXXXXX:reactivate\",\n    cookies={\"sid\": \"<admin-session-id>\"},\n    json={\"reason\": \"Reviewed traffic; false positive.\"},\n)"}]}},"/v1/ops/keys":{"get":{"tags":["Ops Console"],"summary":"List API keys fleet-wide","description":"Returns API keys across all integrators. Filter by integrator,\nenvironment, or status. Plaintext is never returned.\n","security":[{"staffSession":[]}],"parameters":[{"name":"integrator","in":"query","schema":{"type":"string"}},{"name":"environment","in":"query","schema":{"type":"string","enum":["sandbox","live"]}},{"name":"status","in":"query","schema":{"type":"string","enum":["active","rolling","revoked","expired"]}},{"name":"limit","in":"query","schema":{"type":"integer","default":200,"minimum":1,"maximum":500}}],"responses":{"200":{"description":"Matching keys.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/OpsKeyListItem"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl 'https://sente-rails.space/v1/ops/keys?status=active' \\\n  -H 'Cookie: sid=<admin-session-id>'"},{"lang":"Python","label":"Python","source":"import requests\nkeys = requests.get(\n    \"https://sente-rails.space/v1/ops/keys\",\n    params={\"status\": \"active\"},\n    cookies={\"sid\": \"<admin-session-id>\"},\n).json()[\"data\"]"}]}},"/v1/ops/keys/{name}:revoke":{"post":{"tags":["Ops Console"],"summary":"Force-revoke an API key","description":"Admin-issued revoke. Behaves identically to the integrator's\nown `/v1/me/keys/{name}:revoke` but bypasses the ownership\ncheck \u2014 used when an integrator can't (or won't) revoke a\nleaked key themselves. `reason` is required.\n\nReturns 409 if the key is already revoked.\n","security":[{"staffSession":[]}],"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","pattern":"^KEY-\\d{4}-\\d{6}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["reason"],"properties":{"reason":{"type":"string","maxLength":280}}}}}},"responses":{"200":{"description":"Key revoked.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/OpsActionResponse"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Key already revoked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"$ref":"#/components/responses/ValidationFailed"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X POST 'https://sente-rails.space/v1/ops/keys/KEY-2026-000007:revoke' \\\n  -H 'Cookie: sid=<admin-session-id>' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"reason\": \"Key appeared in a public GitHub gist.\"}'"},{"lang":"Python","label":"Python","source":"import requests\nrequests.post(\n    \"https://sente-rails.space/v1/ops/keys/KEY-2026-000007:revoke\",\n    cookies={\"sid\": \"<admin-session-id>\"},\n    json={\"reason\": \"Key appeared in a public GitHub gist.\"},\n)"}]}},"/v1/ops/audit":{"get":{"tags":["Ops Console"],"summary":"Query the audit log fleet-wide","description":"Same underlying `Sente API Audit Log` doctype as `/v1/me/logs`,\nbut unscoped \u2014 admins see every integrator's row. Filter by\n`integrator`, `event`, `min_status`, or `since` (ISO datetime).\n","security":[{"staffSession":[]}],"parameters":[{"name":"integrator","in":"query","schema":{"type":"string"}},{"name":"event","in":"query","schema":{"type":"string","enum":["api.auth.granted","api.auth.denied","api.handler.error"]}},{"name":"endpoint","in":"query","schema":{"type":"string"},"description":"Substring match on the endpoint path."},{"name":"min_status","in":"query","schema":{"type":"integer"}},{"name":"since","in":"query","schema":{"type":"string","format":"date-time"},"description":"ISO datetime \u2014 only rows newer than this are returned."},{"name":"limit","in":"query","schema":{"type":"integer","default":100,"minimum":1,"maximum":500}}],"responses":{"200":{"description":"Matching audit rows.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AuditLogEntry"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"$ref":"#/components/responses/ValidationFailed"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl 'https://sente-rails.space/v1/ops/audit?event=api.handler.error&since=2026-05-20T00:00:00Z' \\\n  -H 'Cookie: sid=<admin-session-id>'"},{"lang":"Python","label":"Python","source":"import requests\nrows = requests.get(\n    \"https://sente-rails.space/v1/ops/audit\",\n    params={\"event\": \"api.handler.error\", \"min_status\": 500},\n    cookies={\"sid\": \"<admin-session-id>\"},\n).json()[\"data\"]"}]}},"/v1/ops/shifts":{"get":{"tags":["Ops Console"],"summary":"Fleet-wide shifts list","description":"All Counter Shifts across MDAs and clerks. Filter by `status`\nor `mda`. Mirrors the supervisor surface but unscoped to the\nwhole fleet \u2014 used by admins to spot operational anomalies.\n","security":[{"staffSession":[]}],"parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["Open","Closed","Cancelled"]}},{"name":"mda","in":"query","schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","default":100,"minimum":1,"maximum":500}}],"responses":{"200":{"description":"Matching shifts.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CounterShift"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl 'https://sente-rails.space/v1/ops/shifts?status=Variance' \\\n  -H 'Cookie: sid=<admin-session-id>'"},{"lang":"Python","label":"Python","source":"import requests\nshifts = requests.get(\n    \"https://sente-rails.space/v1/ops/shifts\",\n    params={\"status\": \"Variance\"},\n    cookies={\"sid\": \"<admin-session-id>\"},\n).json()[\"data\"]"}]}},"/v1/ops/adapters":{"get":{"tags":["Ops Console"],"summary":"Adapter registry + status","description":"Reports the registered adapters (per country, per domain \u2014\nPayment / Identity / Lands / Companies / Fiscal / SMS) along\nwith their STUB-vs-live state, last call latency, and last\nerror if any. Used to spot when an aggregator's sandbox key\nrotated or a partner system went down.\n","security":[{"staffSession":[]}],"responses":{"200":{"description":"Adapter registry snapshot.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AdapterRegistry"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl https://sente-rails.space/v1/ops/adapters \\\n  -H 'Cookie: sid=<admin-session-id>'"},{"lang":"Python","label":"Python","source":"import requests\nadapters = requests.get(\n    \"https://sente-rails.space/v1/ops/adapters\",\n    cookies={\"sid\": \"<admin-session-id>\"},\n).json()[\"data\"]"}]}},"/v1/ops/oversight/aggregates":{"get":{"tags":["Ops Console \u2014 Oversight"],"summary":"Per-MDA + fleet aggregate counters","description":"Returns `{by_mda: [{mda, payments_confirmed, total_amount, ...}],\ntotals: {...}}`. Powers the OAG dashboard's revenue overview.\n","security":[{"staffSession":[]}],"responses":{"200":{"description":"Aggregate snapshot.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/OversightAggregates"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl https://sente-rails.space/v1/ops/oversight/aggregates \\\n  -H 'Cookie: sid=<oag-session-id>'"},{"lang":"Python","label":"Python","source":"import requests\nagg = requests.get(\n    \"https://sente-rails.space/v1/ops/oversight/aggregates\",\n    cookies={\"sid\": \"<oag-session-id>\"},\n).json()[\"data\"]\nprint(agg[\"totals\"])"}]}},"/v1/ops/oversight/anomaly-flags":{"get":{"tags":["Ops Console \u2014 Oversight"],"summary":"Recent anomaly-flagged rows","description":"Returns the latest entries from the `Anomaly Flag` doctype \u2014\nrows where one of the three detectors fired (large cash, large\namount, velocity spike). Newest first.\n","security":[{"staffSession":[]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","default":100,"minimum":1,"maximum":500}}],"responses":{"200":{"description":"Anomaly flag rows.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AnomalyFlag"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl 'https://sente-rails.space/v1/ops/oversight/anomaly-flags?limit=50' \\\n  -H 'Cookie: sid=<oag-session-id>'"},{"lang":"Python","label":"Python","source":"import requests\nflags = requests.get(\n    \"https://sente-rails.space/v1/ops/oversight/anomaly-flags\",\n    params={\"limit\": 50},\n    cookies={\"sid\": \"<oag-session-id>\"},\n).json()[\"data\"]"}]}},"/v1/ops/oversight/payment-events":{"get":{"tags":["Ops Console \u2014 Oversight"],"summary":"Recent payment events (cross-MDA)","description":"Returns the latest entries from the `Payment Event` doctype \u2014\nthe immutable trail of every aggregator callback + status\nflip + EFRIS issuance. Used by OAG to spot stuck or anomalous\npayment flows.\n","security":[{"staffSession":[]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","default":100,"minimum":1,"maximum":500}}],"responses":{"200":{"description":"Payment event rows.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PaymentEvent"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl 'https://sente-rails.space/v1/ops/oversight/payment-events?limit=50' \\\n  -H 'Cookie: sid=<oag-session-id>'"},{"lang":"Python","label":"Python","source":"import requests\nevents = requests.get(\n    \"https://sente-rails.space/v1/ops/oversight/payment-events\",\n    params={\"limit\": 50},\n    cookies={\"sid\": \"<oag-session-id>\"},\n).json()[\"data\"]"}]}},"/v1/ops/oversight/citizen-consent":{"get":{"tags":["Ops Console \u2014 Oversight"],"summary":"Recent citizen-consent events","description":"Returns rows from the `Citizen Consent Event` doctype \u2014 the\nproof-of-consent trail for citizen-data access. Each MDA's\naccess is logged when a clerk pulls a citizen record via NIRA;\nOAG uses this for compliance review.\n","security":[{"staffSession":[]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","default":100,"minimum":1,"maximum":500}}],"responses":{"200":{"description":"Consent event rows.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/CitizenConsentEvent"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl 'https://sente-rails.space/v1/ops/oversight/citizen-consent?limit=50' \\\n  -H 'Cookie: sid=<oag-session-id>'"},{"lang":"Python","label":"Python","source":"import requests\nevents = requests.get(\n    \"https://sente-rails.space/v1/ops/oversight/citizen-consent\",\n    params={\"limit\": 50},\n    cookies={\"sid\": \"<oag-session-id>\"},\n).json()[\"data\"]"}]}},"/v1/ops/oversight/statistics":{"get":{"tags":["Ops Console \u2014 Oversight"],"summary":"Fleet statistics snapshot","description":"High-level counters used on the OAG home \u2014 total MDAs / active\nservices / clerks / shifts / settled volume / outstanding\nvariance \u2014 at this moment. Treat unknown fields as forward-\ncompatible additions.\n","security":[{"staffSession":[]}],"responses":{"200":{"description":"Statistics snapshot.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/OversightStatistics"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl https://sente-rails.space/v1/ops/oversight/statistics \\\n  -H 'Cookie: sid=<oag-session-id>'"},{"lang":"Python","label":"Python","source":"import requests\nstats = requests.get(\n    \"https://sente-rails.space/v1/ops/oversight/statistics\",\n    cookies={\"sid\": \"<oag-session-id>\"},\n).json()[\"data\"]\nprint(stats)"}]}},"/v1/notices":{"get":{"tags":["Service notices"],"summary":"List currently-active service notices","description":"Returns operator-curated announcements ordered by severity\n(Critical \u2192 Warning \u2192 Info), then newest-first by\n`effective_from`. Default behavior (`active=1`) returns only\nnotices that are simultaneously `active=1`, past their\n`effective_from`, and either open-ended or before their\n`effective_to`.\n\nPass `active=0` to see every row regardless of state \u2014 used by\noperators reviewing the queue. Pass `mda=<short_code>` to scope\nto that MDA's notices plus any platform-wide ones (rows with\n`mda IS NULL`).\n\nCurated in the back-office Desk by System Manager or Sente\nRails Admin (Service Notice doctype). No write surface on\n`/v1` today.\n","security":[],"parameters":[{"name":"active","in":"query","schema":{"type":"integer","enum":[0,1],"default":1},"description":"1 = currently-displayable rows only. 0 = every row."},{"name":"mda","in":"query","schema":{"type":"string"},"example":"GULU","description":"Optional MDA scope. Empty = all notices regardless of scope."},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"minimum":1,"maximum":200}}],"responses":{"200":{"description":"Matching notices, ordered by severity then effective_from desc.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ServiceNotice"}}}}}}}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl https://sente-rails.space/v1/notices"},{"lang":"Python","label":"Python","source":"import requests\nnotices = requests.get(\n    \"https://sente-rails.space/v1/notices\",\n    params={\"active\": 1},\n).json()[\"data\"]\nfor n in notices:\n    print(f\"[{n['severity']}] {n['title']}\")"}]}},"/v1/openapi.json":{"get":{"tags":["Meta"],"summary":"Return this OpenAPI 3.1 spec as JSON","description":"Returns the live OpenAPI 3.1 document the explorer reads. The\nserver parses `openapi.yaml` on disk once per worker (mtime-\nkeyed) and serves from RAM thereafter \u2014 re-parsing only when\nthe file changes between deploys. Use this URL as the spec\nsource for client-SDK generators, Postman collection\ngenerators, or anything else that wants to stay in lockstep\nwith the live surface.\n","security":[],"responses":{"200":{"description":"The OpenAPI 3.1 document.","content":{"application/json":{"schema":{"type":"object","description":"OpenAPI 3.1 document wrapped in the standard `{data}` envelope.","properties":{"data":{"type":"object","description":"The OpenAPI 3.1 root object. See https://spec.openapis.org/oas/v3.1.0","additionalProperties":true}}}}}}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl https://sente-rails.space/v1/openapi.json"},{"lang":"Python","label":"Python","source":"import requests\nspec = requests.get(\"https://sente-rails.space/v1/openapi.json\").json()[\"data\"]\nprint(spec[\"info\"][\"title\"], spec[\"info\"][\"version\"])"}]}},"/v1/openapi.postman.json":{"get":{"tags":["Meta"],"summary":"Return the API as a Postman Collection v2.1","description":"The same surface as `/v1/openapi.json`, converted on the fly to a\nPostman Collection v2.1 document \u2014 folders per tag, one request per\noperation, example bodies pre-filled from the schemas.\n\nUnlike every other `/v1` response, this one is **not** wrapped in\nthe `{data}` envelope: Postman's importer rejects anything but the\nbare collection at the top level (`{info, item, ...}`). Point\nPostman's **Import \u2192 Link** at this URL, or save it to a file and\nimport that.\n","security":[],"responses":{"200":{"description":"Postman Collection v2.1 document (bare, no `{data}` envelope).","content":{"application/json":{"schema":{"type":"object","description":"Postman Collection v2.1 root (`info` + `item`). See https://schema.getpostman.com/json/collection/v2.1.0/collection.json","additionalProperties":true}}}}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl https://sente-rails.space/v1/openapi.postman.json -o sente-rails.postman_collection.json"},{"lang":"Python","label":"Python","source":"import requests\ncoll = requests.get(\"https://sente-rails.space/v1/openapi.postman.json\").json()\nprint(coll[\"info\"][\"name\"], \"\u2014\", len(coll[\"item\"]), \"folders\")"}]}},"/v1/payment-intents/{name}/public-summary":{"get":{"tags":["Payments"],"summary":"Citizen-facing receipt verifier","description":"Powers the public `/verify/{ref}` page \u2014 anyone with a printed\nreceipt's QR code can land here and confirm the payment is on\nfile. Returns only non-sensitive fields: status, citizen\n**display name** only (no NIN / phone / msisdn), MDA full names,\nservice line descriptions, amount + currency, channel,\naggregator reference, confirmation timestamp, split summary\nwith MDA names only (no destination account numbers).\n\nUnknown references return 404 \u2014 never reveal whether the ref\nexisted via response shape.\n","security":[],"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","pattern":"^PI-\\d{4}-\\d{2}-\\d{6}$"},"example":"PI-2026-05-000087"}],"responses":{"200":{"description":"Public-safe payment summary.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PublicSummary"}}}}}},"404":{"$ref":"#/components/responses/NotFound"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl https://sente-rails.space/v1/payment-intents/PI-2026-05-000087/public-summary"},{"lang":"Python","label":"Python","source":"import requests\nr = requests.get(\"https://sente-rails.space/v1/payment-intents/PI-2026-05-000087/public-summary\")\nif r.status_code == 200:\n    print(\"Verified:\", r.json()[\"data\"][\"status\"])"}]}},"/v1/webhooks/momo":{"post":{"tags":["Webhooks"],"summary":"MTN Mobile Money callback","description":"MTN MoMo posts here when a payment intent's status changes\n(Pending \u2192 Sent \u2192 Confirmed / Failed). Sente Rails verifies the\nprovider signature (live mode), looks up the Payment Intent by\nthe aggregator reference, writes a Payment Event row, and\npropagates downstream events to the integrator's webhook.\n\nIdempotent \u2014 duplicate callbacks return `{status: \"IGNORED\",\nreason: \"duplicate\"}` with 200. Unknown references return\n`{status: \"IGNORED\", reason: \"unknown_reference\"}` with 200 so\nretries from the provider eventually stop.\n","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookPayload"}}}},"responses":{"200":{"description":"Callback accepted (or idempotently ignored).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/WebhookAck"}}}}}}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X POST https://sente-rails.space/v1/webhooks/momo \\\n  -H 'Content-Type: application/json' \\\n  -H 'X-MoMo-Signature: <signature>' \\\n  -d '{\"referenceId\": \"PI-2026-05-000087\", \"status\": \"SUCCESSFUL\", \"financialTransactionId\": \"MOMO-87xyz\"}'"},{"lang":"Python","label":"Python","source":"# MoMo posts to this endpoint \u2014 you don't normally call it.\n# The shape below mirrors MTN MoMo's collections sandbox.\nimport requests\nrequests.post(\n    \"https://sente-rails.space/v1/webhooks/momo\",\n    headers={\"X-MoMo-Signature\": \"<signature>\"},\n    json={\"referenceId\": \"PI-2026-05-000087\",\n          \"status\": \"SUCCESSFUL\",\n          \"financialTransactionId\": \"MOMO-87xyz\"},\n)"}]}},"/v1/webhooks/airtel":{"post":{"tags":["Webhooks"],"summary":"Airtel Money callback","description":"Airtel Money posts here on payment status updates. Identical\nbehavior to `/v1/webhooks/momo` \u2014 signature-verified,\nidempotent, looks up the Payment Intent by aggregator reference.\n","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookPayload"}}}},"responses":{"200":{"description":"Callback accepted (or idempotently ignored).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/WebhookAck"}}}}}}}}},"/v1/webhooks/pesapal":{"post":{"tags":["Webhooks"],"summary":"Pesapal callback","description":"Pesapal posts here on payment status updates. Pesapal carries\nits own canonical-body signature scheme \u2014 already implemented\non Sente's side. Same idempotent behavior as MoMo / Airtel.\n","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookPayload"}}}},"responses":{"200":{"description":"Callback accepted (or idempotently ignored).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/WebhookAck"}}}}}}}}},"/v1/webhooks/efris":{"post":{"tags":["Webhooks"],"summary":"EFRIS fiscal callback","description":"EFRIS (Uganda's fiscal device platform) posts here when a\nfiscal receipt has been issued for a payment Sente Rails\ntriggered. The callback carries the FDN (Fiscal Document\nNumber); Sente Rails writes it to the Assessment Line so the\nprinted receipt + downstream webhook to the integrator both\ninclude it.\n","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookPayload"}}}},"responses":{"200":{"description":"Callback accepted (or idempotently ignored).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/WebhookAck"}}}}}}}}},"/v1/supervisor/dashboard":{"get":{"tags":["Supervisor"],"summary":"Variance-queue snapshot for finance/audit systems","description":"Bearer-key mirror of the workbench's\n`/v1/work/supervisor/dashboard`. Returns variance summary +\nper-shift detail for a given MDA + date. Default `mda` is\ninferred from the integrator's roster when omitted; default\n`date` is today.\n\nScope: `assessments.read`.\n","security":[{"bearerToken":[]}],"parameters":[{"name":"mda","in":"query","schema":{"type":"string"},"example":"GULU"},{"name":"date","in":"query","schema":{"type":"string","format":"date"},"description":"ISO date (YYYY-MM-DD). Defaults to today (server tz)."}],"responses":{"200":{"description":"Dashboard payload.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SupervisorBearerDashboard"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl 'https://sente-rails.space/v1/supervisor/dashboard?mda=GULU&date=2026-05-28' \\\n  -H 'Authorization: Bearer sk_sandbox_...'"},{"lang":"Python","label":"Python","source":"import requests\ndash = requests.get(\n    \"https://sente-rails.space/v1/supervisor/dashboard\",\n    params={\"mda\": \"GULU\", \"date\": \"2026-05-28\"},\n    headers={\"Authorization\": \"Bearer sk_sandbox_...\"},\n).json()[\"data\"]\nprint(dash)"}]}},"/v1/supervisor/shifts/{name}:approve-variance":{"post":{"tags":["Supervisor"],"summary":"Approve a closed shift's variance","description":"Stamps the shift's variance as approved by the calling\nintegrator. Idempotent \u2014 repeating returns the same stamp.\nScope: `assessments.write`.\n\n**Who calls this:** a supervisor (or a finance/audit system on\ntheir behalf) clearing the variance queue surfaced by\n`GET /v1/supervisor/dashboard`. The chain-of-custody sign-off \u2014 it\nrecords *who* accepted the clerk's stated reason, in the shift's\naudit trail. **If you click \"Send\":** pass a real closed shift\n`name`; the sandbox key carries `assessments.write`, so it stamps a\nlive approval. Unknown shift \u2192 404.\n","security":[{"bearerToken":[]}],"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","pattern":"^SHIFT-\\d{4}-\\d{2}-\\d{2}-\\d{3}$"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"note":{"type":"string","description":"Optional explanation."}}}}}},"responses":{"200":{"description":"Variance stamped approved.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SupervisorActionResponse"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X POST 'https://sente-rails.space/v1/supervisor/shifts/SHIFT-2026-05-28-204:approve-variance' \\\n  -H 'Authorization: Bearer sk_sandbox_...' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"note\": \"Within tolerance, matched deposit slip.\"}'"},{"lang":"Python","label":"Python","source":"import requests\nrequests.post(\n    \"https://sente-rails.space/v1/supervisor/shifts/SHIFT-2026-05-28-204:approve-variance\",\n    headers={\"Authorization\": \"Bearer sk_sandbox_...\"},\n    json={\"note\": \"Within tolerance.\"},\n)"}]}},"/v1/supervisor/shifts/{name}:reject-variance":{"post":{"tags":["Supervisor"],"summary":"Reject a closed shift's variance","description":"Reopens the shift (status \u2192 `Open`) and stamps the rejection\nnote for the clerk to act on. `note` is required.\nScope: `assessments.write`.\n\n**Who calls this:** a supervisor sending a drawer back for a\nre-count when the stated variance reason doesn't hold up. **If you\nclick \"Send\":** pass a real closed shift `name` plus a `note` \u2014 a\nmissing `note` returns a validation error (the reason the clerk\nsees is mandatory), not a server error.\n","security":[{"bearerToken":[]}],"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","pattern":"^SHIFT-\\d{4}-\\d{2}-\\d{2}-\\d{3}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["note"],"properties":{"note":{"type":"string","description":"Required \u2014 what the clerk needs to do."}}}}}},"responses":{"200":{"description":"Variance rejected, shift reopened.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SupervisorActionResponse"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X POST 'https://sente-rails.space/v1/supervisor/shifts/SHIFT-2026-05-28-204:reject-variance' \\\n  -H 'Authorization: Bearer sk_sandbox_...' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"note\": \"Recount the float \u2014 totals do not match till tape.\"}'"},{"lang":"Python","label":"Python","source":"import requests\nrequests.post(\n    \"https://sente-rails.space/v1/supervisor/shifts/SHIFT-2026-05-28-204:reject-variance\",\n    headers={\"Authorization\": \"Bearer sk_sandbox_...\"},\n    json={\"note\": \"Recount the float.\"},\n)"}]}},"/v1/supervisor/shifts/{name}:escalate-variance":{"post":{"tags":["Supervisor"],"summary":"Escalate a variance to MDA finance","description":"Flags the shift for review beyond the supervisor \u2014 creates an\nescalation queue entry that MDA finance / audit consumers\ncan poll. `note` is required.\nScope: `assessments.write`.\n\n**Who calls this:** a supervisor raising a variance they can't\nresolve to the Treasurer / MDA finance tier. The top of the\nescalation ladder (approve = accept, reject = bounce to clerk,\nescalate = push upward). **If you click \"Send\":** real closed shift\n`name` + a `note`; a missing `note` returns a validation error.\n","security":[{"bearerToken":[]}],"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","pattern":"^SHIFT-\\d{4}-\\d{2}-\\d{2}-\\d{3}$"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["note"],"properties":{"note":{"type":"string"}}}}}},"responses":{"200":{"description":"Escalation stamped.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SupervisorActionResponse"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/ValidationFailed"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl -X POST 'https://sente-rails.space/v1/supervisor/shifts/SHIFT-2026-05-28-204:escalate-variance' \\\n  -H 'Authorization: Bearer sk_sandbox_...' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"note\": \"Pattern of shortfalls \u2014 recommend audit.\"}'"},{"lang":"Python","label":"Python","source":"import requests\nrequests.post(\n    \"https://sente-rails.space/v1/supervisor/shifts/SHIFT-2026-05-28-204:escalate-variance\",\n    headers={\"Authorization\": \"Bearer sk_sandbox_...\"},\n    json={\"note\": \"Pattern of shortfalls.\"},\n)"}]}},"/v1/oversight/aggregates":{"get":{"tags":["Oversight (Mode C)"],"summary":"Aggregate revenue + transaction counts grouped by MDA or district","description":"Returns per-group totals over a settable period. `district=1`\ngroups by Citizen.district; otherwise groups by MDA.\nAugments each row with the top 5 services contributing to\nthat group's revenue. Scope: `oversight.read`.\n","security":[{"bearerToken":[]}],"parameters":[{"name":"mda","in":"query","schema":{"type":"string"},"description":"Filter to one MDA."},{"name":"district","in":"query","schema":{"type":"boolean"},"description":"When true, groups by Citizen.district instead of MDA."},{"name":"period_type","in":"query","schema":{"type":"string","enum":["day","week","month","quarter","year","custom"],"default":"month"}},{"name":"period_start","in":"query","schema":{"type":"string","format":"date"},"description":"ISO date (inclusive). Required when `period_type=custom`."},{"name":"period_end","in":"query","schema":{"type":"string","format":"date"},"description":"ISO date (exclusive). Required when `period_type=custom`."}],"responses":{"200":{"description":"Aggregate rows for the window.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/OversightAggregatesQuery"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl 'https://sente-rails.space/v1/oversight/aggregates?period_type=month' \\\n  -H 'Authorization: Bearer sk_sandbox_...'"},{"lang":"Python","label":"Python","source":"import requests\nagg = requests.get(\n    \"https://sente-rails.space/v1/oversight/aggregates\",\n    params={\"period_type\": \"month\", \"district\": \"true\"},\n    headers={\"Authorization\": \"Bearer sk_sandbox_...\"},\n).json()[\"data\"]\nfor r in agg[\"rows\"]:\n    print(r[\"group_key\"], r[\"total_collected\"])"}]}},"/v1/oversight/audit-trail":{"get":{"tags":["Oversight (Mode C)"],"summary":"Document version history (audit trail)","description":"Returns the change log for a named document \u2014 useful for\nproving who changed what when. Backed by the platform's `Version`\nrows. Common doctypes: `Assessment`, `Payment Intent`,\n`Counter Shift`, `MDA`, `Service`. Scope: `oversight.read`.\n","security":[{"bearerToken":[]}],"parameters":[{"name":"doctype","in":"query","required":true,"schema":{"type":"string","example":"Assessment"}},{"name":"name","in":"query","required":true,"schema":{"type":"string","example":"ASMT-2026-05-000123"}}],"responses":{"200":{"description":"Versions for the document.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/OversightAuditTrail"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"$ref":"#/components/responses/ValidationFailed"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl 'https://sente-rails.space/v1/oversight/audit-trail?doctype=Assessment&name=ASMT-2026-05-000123' \\\n  -H 'Authorization: Bearer sk_sandbox_...'"},{"lang":"Python","label":"Python","source":"import requests\ntrail = requests.get(\n    \"https://sente-rails.space/v1/oversight/audit-trail\",\n    params={\"doctype\": \"Assessment\", \"name\": \"ASMT-2026-05-000123\"},\n    headers={\"Authorization\": \"Bearer sk_sandbox_...\"},\n).json()[\"data\"]\nprint(trail[\"version_count\"])"}]}},"/v1/oversight/anomaly-flags":{"get":{"tags":["Oversight (Mode C)"],"summary":"Paginated anomaly flags","description":"Returns rows from the `Anomaly Flag` doctype with pagination\n(`limit` + `offset`). Filter by status, severity, or flag type.\nDetectors today: cash-anomaly, amount-anomaly, velocity-spike.\nScope: `oversight.read`.\n","security":[{"bearerToken":[]}],"parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["Open","Dismissed","Resolved"],"default":"Open"}},{"name":"severity","in":"query","schema":{"type":"string","enum":["Low","Medium","High"]}},{"name":"flag_type","in":"query","schema":{"type":"string","example":"cash_anomaly"}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"minimum":1,"maximum":500}},{"name":"offset","in":"query","schema":{"type":"integer","default":0,"minimum":0}}],"responses":{"200":{"description":"Page of anomaly flags.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/OversightAnomalyFlagsPage"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl 'https://sente-rails.space/v1/oversight/anomaly-flags?severity=High&limit=20' \\\n  -H 'Authorization: Bearer sk_sandbox_...'"},{"lang":"Python","label":"Python","source":"import requests\nflags = requests.get(\n    \"https://sente-rails.space/v1/oversight/anomaly-flags\",\n    params={\"severity\": \"High\", \"limit\": 20},\n    headers={\"Authorization\": \"Bearer sk_sandbox_...\"},\n).json()[\"data\"]\nprint(flags[\"row_count\"], \"high-severity flags open\")"}]}},"/v1/oversight/citizen-consent":{"get":{"tags":["Oversight (Mode C)"],"summary":"Active citizen-consent counts grouped by (mda, purpose)","description":"Active = `granted=1 AND revoked_at IS NULL AND\n(expiry_at IS NULL OR expiry_at >= today)`. Used for\ncompliance review \u2014 OAG can spot MDAs / purposes with\nunusual consent volumes. No row carries citizen identity.\nScope: `oversight.read`.\n","security":[{"bearerToken":[]}],"parameters":[{"name":"mda","in":"query","schema":{"type":"string"}},{"name":"purpose","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Consent counts grouped by (mda, purpose).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/OversightConsentSummary"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl 'https://sente-rails.space/v1/oversight/citizen-consent?mda=GULU' \\\n  -H 'Authorization: Bearer sk_sandbox_...'"},{"lang":"Python","label":"Python","source":"import requests\ncs = requests.get(\n    \"https://sente-rails.space/v1/oversight/citizen-consent\",\n    params={\"mda\": \"GULU\"},\n    headers={\"Authorization\": \"Bearer sk_sandbox_...\"},\n).json()[\"data\"]\nfor r in cs[\"rows\"]:\n    print(r[\"mda\"], r[\"purpose\"], r[\"active_consents\"])"}]}},"/v1/oversight/payment-events":{"get":{"tags":["Oversight (Mode C)"],"summary":"Payment-event stream (cursor-paginated)","description":"Streams `Payment Event` rows newest-first with a stable\ncursor. Pass the previous response's `next_cursor` as\n`after` to fetch the next page. Used by OAG to tail the\nlive event feed without polling holes.\nScope: `oversight.read`.\n","security":[{"bearerToken":[]}],"parameters":[{"name":"after","in":"query","schema":{"type":"string"},"description":"Opaque cursor from a previous response."},{"name":"limit","in":"query","schema":{"type":"integer","default":100,"minimum":1,"maximum":500}}],"responses":{"200":{"description":"One page of payment events.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/OversightPaymentEventsStream"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl 'https://sente-rails.space/v1/oversight/payment-events?limit=50' \\\n  -H 'Authorization: Bearer sk_sandbox_...'"},{"lang":"Python","label":"Python","source":"import requests\ncursor = None\nwhile True:\n    params = {\"limit\": 100}\n    if cursor:\n        params[\"after\"] = cursor\n    page = requests.get(\n        \"https://sente-rails.space/v1/oversight/payment-events\",\n        params=params,\n        headers={\"Authorization\": \"Bearer sk_sandbox_...\"},\n    ).json()[\"data\"]\n    for ev in page[\"rows\"]:\n        print(ev)\n    cursor = page[\"next_cursor\"]\n    if not cursor or page[\"row_count\"] < 100:\n        break"}]}},"/v1/oversight/statistics":{"get":{"tags":["Oversight (Mode C)"],"summary":"UBOS-shaped aggregate statistics","description":"Three metrics today, scoped to a custom window: `revenue_by_sector`\n(totals per Service.sector), `transactions_by_district` (counts\nper Citizen.district), `taxpayer_count` (distinct citizens per\ndistrict). `geography` optionally restricts to a specific\ndistrict. Scope: `oversight.read`.\n","security":[{"bearerToken":[]}],"parameters":[{"name":"metric","in":"query","required":true,"schema":{"type":"string","enum":["revenue_by_sector","transactions_by_district","taxpayer_count"]}},{"name":"period_start","in":"query","required":true,"schema":{"type":"string","format":"date"}},{"name":"period_end","in":"query","required":true,"schema":{"type":"string","format":"date"},"description":"Exclusive \u2014 `period_end - 1 day` is the last day included."},{"name":"geography","in":"query","schema":{"type":"string"},"description":"Optional district filter."}],"responses":{"200":{"description":"The requested metric over the window.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/OversightStatisticsResult"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"$ref":"#/components/responses/ValidationFailed"}},"x-codeSamples":[{"lang":"curl","label":"cURL","source":"curl 'https://sente-rails.space/v1/oversight/statistics?metric=revenue_by_sector&period_start=2026-04-01&period_end=2026-05-01' \\\n  -H 'Authorization: Bearer sk_sandbox_...'"},{"lang":"Python","label":"Python","source":"import requests\nstats = requests.get(\n    \"https://sente-rails.space/v1/oversight/statistics\",\n    params={\n        \"metric\": \"revenue_by_sector\",\n        \"period_start\": \"2026-04-01\",\n        \"period_end\": \"2026-05-01\",\n    },\n    headers={\"Authorization\": \"Bearer sk_sandbox_...\"},\n).json()[\"data\"]\nfor row in stats[\"rows\"]:\n    print(row)"}]}}},"components":{"securitySchemes":{"bearerToken":{"type":"http","scheme":"bearer","bearerFormat":"opaque","description":"Bearer key \u2014 formatted as `Authorization: Bearer <key>`.\nIssued at `/signup`; rotate or revoke at `/dashboard/keys`.\nEach key carries a scope set; calls missing the required scope\nreceive `403`.\n"},"sessionCookie":{"type":"apiKey","in":"cookie","name":"sente_session","description":"Browser session cookie minted by `GET /v1/login/consume` after a\nvalid magic-link click. HttpOnly, Secure, SameSite=Lax, 14-day\nsliding life. Accepted by `/v1/me/*` as an alternative to a\nBearer key.\n"},"staffSession":{"type":"apiKey","in":"cookie","name":"sid","description":"Platform staff session cookie. Used by Sente Rails employees\n(clerks, supervisors, admins, OAG) to authenticate against the\n`/v1/work/*` (Counter Stations) and `/v1/ops/*` (Operations\nConsole) surfaces. Issued by the platform login at `/login`\n(not the integrator magic-link flow). Role gates are enforced\nper-operation on top of the session.\n"}},"responses":{"Unauthorized":{"description":"Missing or invalid credentials.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"error":{"code":"unauthorized","message":"Invalid API key.","request_id":"9d5e6f1c-1a4d-4a2b-9f1e-9a0c2b8e3a40"}}}}},"Forbidden":{"description":"Authenticated but the credential lacks the required scope or role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"error":{"code":"scope_missing","message":"Required scope `citizens.write` is not granted to this key.","request_id":"9d5e6f1c-1a4d-4a2b-9f1e-9a0c2b8e3a40"}}}}},"NotFound":{"description":"The named resource does not exist (or is hidden by permissions).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"ValidationFailed":{"description":"Request body or query failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"error":{"code":"validation_failed","message":"Valid email address is required.","request_id":"9d5e6f1c-1a4d-4a2b-9f1e-9a0c2b8e3a40"}}}}},"RateLimited":{"description":"Per-account rate limit hit. Try again later.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"},"example":{"error":{"code":"rate_limited","message":"Wait 60 seconds before requesting another OTP.","request_id":"9d5e6f1c-1a4d-4a2b-9f1e-9a0c2b8e3a40"}}}}}},"schemas":{"CitizenSummary":{"type":"object","properties":{"name":{"type":"string","example":"CITIZEN-2026-000002"},"nin":{"type":"string","example":"CM78001234ABCD"},"full_name":{"type":"string","example":"Mukasa John Patrick"},"phone":{"type":"string","example":"+256772123456"},"email":{"type":["string","null"],"format":"email"},"district":{"type":["string","null"],"example":"Gulu"},"status":{"type":"string","enum":["Active","Inactive","Deceased","Restricted"]},"verified":{"type":"boolean"},"modified":{"type":"string","format":"date-time"}}},"Citizen":{"allOf":[{"$ref":"#/components/schemas/CitizenSummary"},{"type":"object","properties":{"tin":{"type":["string","null"]},"first_name":{"type":["string","null"]},"middle_name":{"type":["string","null"]},"surname":{"type":["string","null"]},"dob":{"type":["string","null"],"format":"date"},"gender":{"type":["string","null"],"enum":[null,"Male","Female","Other","Prefer not to say"]},"alternate_phone":{"type":["string","null"]},"sub_county":{"type":["string","null"]},"parish":{"type":["string","null"]},"village":{"type":["string","null"]},"address_line":{"type":["string","null"]},"consent_data_sharing":{"type":"boolean"},"consent_recorded_on":{"type":["string","null"],"format":"date-time"},"consent_recorded_by":{"type":["string","null"]},"linked_customer":{"type":["string","null"],"description":"Linked accounting customer record (auto-set on first Assessment)"},"photo":{"type":["string","null"],"description":"Attached file path"}}}]},"CitizenCreate":{"type":"object","required":["full_name"],"properties":{"nin":{"type":"string","description":"14-char NIRA NIN (uppercased server-side); optional"},"tin":{"type":"string"},"full_name":{"type":"string"},"first_name":{"type":"string"},"middle_name":{"type":"string"},"surname":{"type":"string"},"dob":{"type":"string","format":"date"},"gender":{"type":"string","enum":["Male","Female","Other","Prefer not to say"]},"phone":{"type":"string"},"alternate_phone":{"type":"string"},"email":{"type":"string","format":"email"},"district":{"type":"string"},"sub_county":{"type":"string"},"parish":{"type":"string"},"village":{"type":"string"},"address_line":{"type":"string"},"consent_data_sharing":{"type":"boolean","default":false},"status":{"type":"string","enum":["Active","Inactive","Deceased","Restricted"],"default":"Active"}}},"MDA":{"type":"object","properties":{"name":{"type":"string","example":"GULU","description":"Short_code as docname"},"short_code":{"type":"string"},"full_name":{"type":"string","example":"Gulu City Authority"},"mda_type":{"type":"string","enum":["Ministry","Department","Agency","Authority","City Authority","Municipal Council","District Local Government","Sub-County","Division"]},"country":{"type":"string","example":"UG","description":"Country Profile code"},"mode":{"type":"string","enum":["A","B","C"],"description":"A = SoR; B = Integration; C = Oversight"},"status":{"type":"string","enum":["Active","Onboarding","Suspended"]},"parent_authority":{"type":["string","null"]},"treasury_account":{"type":["string","null"]},"integration_endpoint":{"type":["string","null"],"description":"Mode B only"},"push_webhook_url":{"type":["string","null"],"description":"Mode B only"},"api_credentials_ref":{"type":["string","null"],"description":"Mode B only"},"oversight_scopes":{"type":["string","null"],"description":"Mode C only \u2014 comma-separated scopes"},"contact_email":{"type":["string","null"],"format":"email"},"contact_phone":{"type":["string","null"]}}},"Service":{"type":"object","properties":{"name":{"type":"string","pattern":"^SVC-\\d{4}-\\d{6}$"},"mda":{"type":"string","example":"GULU"},"code":{"type":"string","example":"TL-RENEW"},"service_name":{"type":"string","example":"Trading License Renewal"},"status":{"type":"string","enum":["Active","Inactive","Coming Soon"]},"sector":{"type":["string","null"],"example":"Revenue"},"service_family":{"type":["string","null"],"example":"Trading Licenses"},"fee_amount":{"type":"number","example":50000},"fee_currency":{"type":"string","example":"UGX"},"fee_basis":{"type":"string","enum":["Flat","Per-Day","Per-Month","Per-Square-Meter","Tiered-by-Turnover","Percent-of-Value","Manual"]},"fee_schedule_ref":{"type":["string","null"],"example":"Trade Licensing Act 2015 \u00a712(1)"},"efris_taxable":{"type":"boolean"},"vat_applicable":{"type":"boolean"},"vat_rate":{"type":["number","null"]},"gl_account_credit":{"type":["string","null"]},"linked_item":{"type":["string","null"]},"description":{"type":["string","null"]}}},"AssessmentLine":{"type":"object","required":["mda","service"],"properties":{"mda":{"type":"string","example":"GULU"},"service":{"type":"string","example":"SVC-2026-000004"},"service_name":{"type":"string","readOnly":true,"description":"Fetched from Service"},"fee_basis":{"type":"string","readOnly":true,"description":"Fetched from Service"},"quantity":{"type":"number","default":1},"rate":{"type":"number","description":"Defaults to Service.fee_amount"},"amount":{"type":"number","readOnly":true,"description":"qty * rate"},"efris_taxable":{"type":"boolean","readOnly":true,"description":"Fetched from Service"},"fee_schedule_ref":{"type":["string","null"],"readOnly":true},"ura_prn":{"type":["string","null"],"readOnly":true},"efris_fdn":{"type":["string","null"],"readOnly":true},"notes":{"type":["string","null"]}}},"AssessmentSummary":{"type":"object","properties":{"name":{"type":"string","pattern":"^ASMT-\\d{4}-\\d{2}-\\d{6}$"},"citizen":{"type":"string"},"transaction_date":{"type":"string","format":"date"},"status":{"type":"string","enum":["Draft","Assessed","Paid","Cancelled"]},"mda_default":{"type":["string","null"]},"shift":{"type":["string","null"],"description":"Counter Shift name"},"total_amount":{"type":"number","description":"Net payable = gross_amount \u2212 discount_amount."},"gross_amount":{"type":"number","description":"Sum of line amounts before any waiver."},"discount_amount":{"type":"number","description":"Supervisor-authorised fee waiver applied before payment (0 when none)."},"discount_reason":{"type":["string","null"],"description":"Why the waiver was granted."},"currency":{"type":"string"},"payment_status":{"type":"string","enum":["Pending","Initiated","Confirmed","Failed","Refunded"]},"payment_channel":{"type":["string","null"]},"payment_reference":{"type":["string","null"]},"paid_at":{"type":["string","null"],"format":"date-time"}}},"Assessment":{"allOf":[{"$ref":"#/components/schemas/AssessmentSummary"},{"type":"object","properties":{"clerk":{"type":"string"},"assessment_lines":{"type":"array","items":{"$ref":"#/components/schemas/AssessmentLine"}},"idempotency_key":{"type":"string"},"linked_journal_entry":{"type":["string","null"]},"notes":{"type":["string","null"]}}}]},"AssessmentCreate":{"type":"object","required":["citizen","lines"],"properties":{"citizen":{"type":"string","example":"CITIZEN-2026-000002"},"mda_default":{"type":"string","example":"GULU"},"currency":{"type":"string","default":"UGX"},"transaction_date":{"type":"string","format":"date"},"notes":{"type":"string"},"lines":{"type":"array","minItems":1,"items":{"type":"object","required":["mda","service"],"properties":{"mda":{"type":"string","example":"GULU"},"service":{"type":"string","example":"SVC-2026-000004"},"quantity":{"type":"number","default":1},"rate":{"type":"number","description":"Defaults to Service.fee_amount when omitted"},"notes":{"type":"string"}}}}}},"PaymentIntentSplit":{"type":"object","required":["mda","amount"],"properties":{"mda":{"type":"string","example":"GULU"},"amount":{"type":"number","example":50000},"destination_account":{"type":"string","example":"GULU-COLLECTION-001"},"destination_account_type":{"type":"string","enum":["Bank","Mobile Money","Pre-Configured"],"default":"Bank"},"notes":{"type":["string","null"]}}},"PaymentIntent":{"type":"object","properties":{"name":{"type":"string","pattern":"^PI-\\d{4}-\\d{2}-\\d{6}$"},"assessment":{"type":"string"},"channel":{"type":"string","enum":["MTN MoMo","Airtel Money","Card","Bank Transfer","Cash","Voucher"]},"status":{"type":"string","enum":["Pending","Sent","Confirmed","Failed","Refunded"]},"currency":{"type":"string"},"amount":{"type":"number"},"citizen_msisdn":{"type":["string","null"]},"aggregator":{"type":["string","null"]},"aggregator_reference":{"type":["string","null"]},"sent_at":{"type":["string","null"],"format":"date-time"},"confirmed_at":{"type":["string","null"],"format":"date-time"},"failed_at":{"type":["string","null"],"format":"date-time"},"failure_reason":{"type":["string","null"]},"fiscal_status":{"type":"string","enum":["Not Fiscalised","Fiscalised","Failed"],"description":"URA EFRIS fiscalisation state of the settled receipt. The receipt is fiscalised automatically on :confirm."},"fdn":{"type":["string","null"],"description":"Fiscal Document Number issued by URA EFRIS once the payment is confirmed."},"fiscal_verification_code":{"type":["string","null"],"description":"Short code a citizen can use to verify the receipt on the URA EFRIS portal."},"fiscal_qr_payload":{"type":["string","null"],"description":"URL encoded in the receipt's fiscal QR code."},"fiscalised_at":{"type":["string","null"],"format":"date-time"},"refunded_at":{"type":["string","null"],"format":"date-time","description":"Set when a settled payment is reversed at the counter."},"refund_reason":{"type":["string","null"]},"idempotency_key":{"type":"string"},"split_rules":{"type":"array","items":{"$ref":"#/components/schemas/PaymentIntentSplit"}},"notes":{"type":["string","null"]}}},"PaymentIntentCreate":{"type":"object","required":["assessment","channel"],"properties":{"assessment":{"type":"string","example":"ASMT-2026-05-000022"},"channel":{"type":"string","enum":["MTN MoMo","Airtel Money","Card","Bank Transfer","Cash","Voucher"]},"citizen_msisdn":{"type":"string","example":"+256772123456"},"aggregator":{"type":"string","example":"MTN"},"splits":{"type":"array","description":"Optional \u2014 when omitted, auto-derived from Assessment lines (one split per MDA, summed across lines).","items":{"$ref":"#/components/schemas/PaymentIntentSplit"}}}},"PaymentEvent":{"type":"object","properties":{"name":{"type":"string","pattern":"^PE-\\d{4}-\\d{2}-\\d{6}$"},"payment_intent":{"type":"string"},"mda":{"type":"string"},"amount":{"type":"number"},"currency":{"type":"string"},"aggregator":{"type":["string","null"]},"aggregator_txn_id":{"type":"string"},"destination_account":{"type":["string","null"]},"received_at":{"type":"string","format":"date-time"},"proof_payload":{"type":["string","null"],"description":"Verbatim webhook payload + signature verification result (OAG audit evidence)"},"linked_journal_entry":{"type":["string","null"]}}},"CounterShiftSummary":{"type":"object","properties":{"name":{"type":"string","pattern":"^SHIFT-\\d{4}-\\d{2}-\\d{2}-\\d{3}$"},"clerk":{"type":"string"},"mda":{"type":"string"},"counter_label":{"type":["string","null"]},"status":{"type":"string","enum":["Open","Closed","Cancelled"]},"opened_at":{"type":"string","format":"date-time"},"closed_at":{"type":["string","null"],"format":"date-time"},"opening_float":{"type":"number"},"assessment_count":{"type":"integer"},"total_collected":{"type":"number"},"cash_collected":{"type":"number"},"momo_collected":{"type":"number"},"airtel_collected":{"type":"number"},"cash_expected":{"type":"number"},"cash_counted":{"type":"number"},"cash_variance":{"type":"number"}}},"CounterShift":{"allOf":[{"$ref":"#/components/schemas/CounterShiftSummary"},{"type":"object","properties":{"currency":{"type":"string"},"opening_notes":{"type":["string","null"]},"card_collected":{"type":"number"},"bank_collected":{"type":"number"},"voucher_collected":{"type":"number"},"variance_reason":{"type":["string","null"]},"closing_notes":{"type":["string","null"]}}}]},"AdapterStatus":{"type":"object","properties":{"class_path":{"type":["string","null"],"example":"sente_rails.adapters.fiscal.uganda_efris.EFRISAdapter"},"importable":{"type":"boolean"},"stub":{"type":["boolean","null"]},"supported_channels":{"type":["array","null"],"items":{"type":"string"}},"error":{"type":["string","null"],"description":"Set when importable=false"}}},"CountryAdapterSnapshot":{"type":"object","properties":{"identity":{"$ref":"#/components/schemas/AdapterStatus"},"fiscal":{"$ref":"#/components/schemas/AdapterStatus"},"payment":{"type":"array","items":{"$ref":"#/components/schemas/AdapterStatus"}}}},"ServerError":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["validation_failed","forbidden","not_found","conflict","internal_error","method_not_allowed","error"],"example":"validation_failed"},"message":{"type":"string","example":"Variance reason is required when cash count differs from expected"},"details":{"type":"object","description":"Optional structured detail payload (field-level errors etc.)","additionalProperties":true}}}}},"ProblemDetails":{"type":"object","description":"RFC 7807 problem-details (planned for v1). Not currently emitted \u2014\nv0 responses use the ServerError shape.\n","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string","format":"uri"}}},"ErrorEnvelope":{"type":"object","properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","description":"Machine-readable error code (e.g. `validation_failed`, `unauthorized`, `rate_limited`).","example":"validation_failed"},"message":{"type":"string","description":"Human-readable summary.","example":"Valid email address is required."},"request_id":{"type":"string","format":"uuid","description":"Quote this when reporting an issue \u2014 it ties to a row in `/v1/me/logs`.","example":"9d5e6f1c-1a4d-4a2b-9f1e-9a0c2b8e3a40"}}}}},"SignupRequest":{"type":"object","required":["full_name","email","tos_accepted_version"],"properties":{"full_name":{"type":"string","description":"Real name of the human creating the account \u2014 used for the audit trail, not shown publicly.","example":"Jane Builder"},"email":{"type":"string","format":"email","example":"jane@acmefintech.test"},"organisation":{"type":"string","description":"Brand or company name, shown in dashboards. Optional \u2014 the display name falls back to full_name when omitted.","example":"Acme Fintech"},"tos_accepted_version":{"type":"string","description":"The ToS version the user accepted. Must exactly match the value returned by `GET /v1/signup/tos`; any other value is rejected with 422.","example":"sandbox-tos-v1-2026-05-25"},"intended_use":{"type":"string","description":"Optional free-text describing what the integrator plans to build. Used only for ops triage."}}},"SignupResponse":{"type":"object","properties":{"integrator_id":{"type":"string","description":"Carry this to `/v1/signup/verify` along with the OTP.","example":"ACME-FINTECH-XXXXXX"},"email":{"type":"string","format":"email"},"message":{"type":"string"},"expires_at_iso":{"type":"string","format":"date-time","description":"When the OTP expires. Re-issue with `/v1/signup/resend-otp` if needed."},"tos_version":{"type":"string"}}},"SignupVerifyRequest":{"type":"object","required":["integrator_id","otp"],"properties":{"integrator_id":{"type":"string","example":"ACME-FINTECH-XXXXXX"},"otp":{"type":"string","pattern":"^[0-9]{6}$","example":"123456"}}},"SignupVerifyResponse":{"type":"object","properties":{"integrator":{"type":"object","properties":{"code":{"type":"string","example":"ACME-FINTECH-XXXXXX"},"display_name":{"type":"string"},"contact_email":{"type":"string","format":"email"},"tier":{"type":"string","example":"Registered"},"pricing_tier":{"type":"string","example":"Free"},"email_verified":{"type":"boolean","example":true}}},"key":{"type":"object","description":"Metadata about the freshly issued key. Plaintext is on the outer `plaintext` field.","properties":{"name":{"type":"string","pattern":"^KEY-\\d{4}-\\d{6}$"},"prefix":{"type":"string","example":"sk_sandbox_2026"},"last4":{"type":"string","example":"kKr2"},"scopes":{"type":"array","items":{"type":"string"}},"expires_at":{"type":["string","null"],"format":"date-time"}}},"plaintext":{"type":"string","description":"The full bearer key \u2014 shown once, never recoverable.","example":"sk_sandbox_2026_kKr2RPJu84wJjWspIZfQx5XcMK7bjWKS"},"plaintext_warning":{"type":"string"},"next_steps":{"type":"array","items":{"type":"string"}}}},"SignupTos":{"type":"object","properties":{"version":{"type":"string","example":"sandbox-tos-v1-2026-05-25"},"summary":{"type":"string"},"document_url":{"type":"string","format":"uri"}}},"LoginRequest":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email","example":"jane@acmefintech.test"}}},"SessionInfo":{"type":"object","properties":{"authenticated":{"type":"boolean","example":true},"integrator":{"type":"object","description":"The signed-in integrator's basic profile. Absent when unauthenticated.","properties":{"code":{"type":"string","example":"ACME-FINTECH-XXXXXX"},"display_name":{"type":"string"},"contact_email":{"type":"string","format":"email"},"tier":{"type":"string","example":"Registered"},"pricing_tier":{"type":"string","example":"Free"},"last_login_at":{"type":["string","null"],"format":"date-time"}}}}},"Integrator":{"type":"object","description":"Self-view of the signed-in integrator \u2014 full profile plus live counters.","properties":{"name":{"type":"string","example":"ACME-FINTECH-XXXXXX"},"display_name":{"type":"string","example":"Acme Fintech"},"type":{"type":"string","enum":["MDA","Partner","Developer","Civic","Academic"],"example":"Developer"},"tier":{"type":"string","enum":["Anonymous","Registered","Onboarding","Production","Restricted-Ops"],"example":"Registered"},"pricing_tier":{"type":"string","enum":["Free","Developer","Business","Enterprise","MDA"],"example":"Free"},"status":{"type":"string","enum":["PendingEmail","Active","Suspended"]},"contact_email":{"type":"string","format":"email"},"technical_lead_user":{"type":["string","null"]},"webhook_endpoint":{"type":["string","null"],"format":"uri","description":"Where Sente delivers `/v1` event notifications. Must be HTTPS in production."},"mou_status":{"type":["string","null"]},"kyc_status":{"type":["string","null"]},"ip_allowlist":{"type":["string","null"],"description":"Comma-separated CIDR ranges. Empty/null accepts any source."},"tos_accepted_on":{"type":["string","null"],"format":"date-time"},"tos_accepted_version":{"type":["string","null"]},"signup_source":{"type":["string","null"]},"email_verified":{"type":"boolean"},"last_login_at":{"type":["string","null"],"format":"date-time"},"anticipated_volume_daily":{"type":"integer","example":5000},"anticipated_volume_monthly":{"type":"integer","example":150000},"keys":{"type":"object","properties":{"total":{"type":"integer","example":2},"active":{"type":"integer","example":1}}},"requests_last_7d":{"type":"integer","example":1284}}},"IntegratorPatch":{"type":"object","description":"Writable subset of the integrator profile. All fields optional \u2014 send only what you're changing.","properties":{"display_name":{"type":"string","maxLength":140},"webhook_endpoint":{"type":["string","null"],"format":"uri","description":"Send `null` (or omit) to clear."},"ip_allowlist":{"type":["string","null"],"example":"10.0.0.0/8, 203.0.113.42/32"},"anticipated_volume_daily":{"type":"integer","minimum":0},"anticipated_volume_monthly":{"type":"integer","minimum":0}}},"ApiKey":{"type":"object","properties":{"name":{"type":"string","pattern":"^KEY-\\d{4}-\\d{6}$","example":"KEY-2026-000002"},"prefix":{"type":"string","example":"sk_sandbox_2026"},"last4":{"type":"string","example":"kKr2"},"environment":{"type":"string","enum":["sandbox","live"]},"key_type":{"type":"string","enum":["sk","rk","pk","whsec"],"example":"sk"},"status":{"type":"string","enum":["active","rolling","revoked","expired"]},"scopes":{"type":"array","items":{"type":"string"},"example":["citizens.read","citizens.write","assessments.write","payment-intents.write"]},"created_at":{"type":"string","format":"date-time"},"expires_at":{"type":["string","null"],"format":"date-time"},"last_used_at":{"type":["string","null"],"format":"date-time"},"last_used_ip":{"type":["string","null"]},"usage_count":{"type":"integer","minimum":0},"revoked_at":{"type":["string","null"],"format":"date-time"},"revoked_by":{"type":["string","null"]},"revoked_reason":{"type":["string","null"]},"rolling_until":{"type":["string","null"],"format":"date-time","description":"When `status=rolling`, the moment after which this key stops accepting traffic."},"rolled_to":{"type":["string","null"],"description":"Name of the successor key when this one is rolling/expired."},"description":{"type":["string","null"]}}},"ApiKeyRotateResult":{"type":"object","properties":{"old_key":{"type":"object","properties":{"name":{"type":"string"},"status":{"type":"string","example":"rolling"},"rolling_until":{"type":"string","format":"date-time"}}},"new_key":{"$ref":"#/components/schemas/ApiKey"},"plaintext":{"type":"string","description":"Full bearer for the new key. Shown once, never recoverable."},"plaintext_warning":{"type":"string"}}},"AuditLogEntry":{"type":"object","properties":{"name":{"type":"string","description":"Doctype row id \u2014 opaque."},"ts":{"type":"string","format":"date-time"},"event":{"type":"string","enum":["api.auth.granted","api.auth.denied","api.handler.error"]},"request_id":{"type":"string","format":"uuid"},"http_method":{"type":"string","enum":["GET","POST","PATCH","PUT","DELETE"],"example":"POST"},"endpoint":{"type":"string","example":"/v1/payment-intents"},"http_status":{"type":"integer","example":200},"error_code":{"type":["string","null"],"description":"Populated on `denied`/`handler.error`. Cross-references `ErrorEnvelope.code`."},"api_key":{"type":["string","null"],"description":"Name of the key that made the call. Plaintext never appears here."},"source_ip":{"type":["string","null"]},"required_scopes":{"type":"array","items":{"type":"string"}},"granted_scopes":{"type":"array","items":{"type":"string"}},"latency_ms":{"type":"integer","example":47}}},"WorkWhoami":{"type":"object","description":"Identity + role snapshot for the signed-in staff user. Drives the\nkiosk UI's visibility logic \u2014 which buttons render, which tabs\nunlock, whether the supervisor variance panel is reachable.\n","properties":{"authenticated":{"type":"boolean","example":true},"user":{"type":"object","properties":{"name":{"type":"string","example":"clerk.gulu@sente-rails.space"},"full_name":{"type":"string","example":"Akello Susan"},"email":{"type":"string","format":"email"}}},"roles":{"type":"array","items":{"type":"string"},"example":["Sente Rails Clerk","Guest"]},"is_clerk":{"type":"boolean"},"is_supervisor":{"type":"boolean"},"is_admin":{"type":"boolean"},"has_work_access":{"type":"boolean","description":"True iff the user holds at least one of clerk/supervisor/admin role."}}},"CitizenSearchResult":{"type":"object","description":"Citizen lookup result. The `source` field tells the caller whether\nthe row was already in the local registry or had to be pulled\nfrom the NIRA cascade.\n","properties":{"source":{"type":"string","enum":["local","nira","miss"],"description":"`local` = found in Sente registry. `nira` = freshly resolved\nand cached. `miss` = no match (`citizen` is null).\n","example":"local"},"citizen":{"oneOf":[{"$ref":"#/components/schemas/Citizen"},{"type":"null"}]}}},"ShiftOpenRequest":{"type":"object","required":["mda"],"properties":{"mda":{"type":"string","description":"MDA code the shift is opening against.","example":"GULU"},"counter_label":{"type":"string","description":"Free-text label for the physical counter (e.g. \"Window 2\").","example":"Window 2"},"opening_cash":{"type":"number","description":"Float of cash on hand at the start of the shift.","default":0,"example":50000}}},"ShiftCloseRequest":{"type":"object","required":["cash_counted"],"properties":{"cash_counted":{"type":"number","description":"Final cash count at close. Compared against expected to compute variance.","example":1450000},"note":{"type":"string","description":"Optional close-time comment appended to the shift's notes."}}},"WorkAssessmentLineInput":{"type":"object","required":["service"],"properties":{"service":{"type":"string","description":"Service docname (resolves to MDA + fee_basis + rate via the catalogue).","example":"SVC-2026-000004"},"quantity":{"type":"number","description":"Defaults to 1 if omitted.","default":1},"explicit_amount":{"type":["number","null"],"description":"Optional override of the catalogue rate \u00d7 quantity."},"notes":{"type":"string"}}},"WorkAssessmentCreate":{"type":"object","required":["citizen","lines"],"properties":{"citizen":{"type":"string","description":"Citizen docname returned from `/v1/work/citizens/search`.","example":"CITIZEN-2026-000002"},"lines":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/WorkAssessmentLineInput"},"description":"Either a JSON array OR a JSON-encoded string (the kiosk sends\nboth shapes \u2014 the server JSON-parses strings).\n"},"mda_default":{"type":"string","description":"MDA code that owns lines without their own `mda`.","example":"GULU"},"notes":{"type":"string"}}},"WorkPaymentIntentCreate":{"type":"object","required":["assessment","channel"],"properties":{"assessment":{"type":"string","description":"Assessment docname (must be in `Assessed` state).","example":"ASMT-2026-05-000123"},"channel":{"type":"string","enum":["MTN MoMo","Airtel Money","Pesapal","Card","Bank Transfer","Cash","Voucher"],"example":"MTN MoMo"},"citizen_msisdn":{"type":"string","description":"Required for mobile-money channels.","example":"+256772123456"},"notes":{"type":"string"}}},"VarianceDecisionRequest":{"type":"object","properties":{"note":{"type":"string","description":"Comment recorded against the shift's variance row. Required\nfor `reject` and `escalate`; optional but recommended for\n`approve`.\n","example":"Counted bank deposit matches; difference is small-change loss."}}},"SupervisorDashboard":{"type":"object","description":"Variance-queue + at-a-glance counters surfaced on\n`/work/supervisor`. The exact shape is intentionally open \u2014\ndownstream consumers should treat unknown fields as forward-\ncompatible additions.\n","additionalProperties":true,"properties":{"pending_variance":{"type":"integer","description":"Shifts closed with variance awaiting supervisor action."},"approved_today":{"type":"integer"},"escalated_today":{"type":"integer"},"queue":{"type":"array","items":{"type":"object","additionalProperties":true,"properties":{"shift":{"type":"string"},"clerk":{"type":"string"},"mda":{"type":"string"},"variance":{"type":"number"},"closed_at":{"type":"string","format":"date-time"}}}}}},"OpsWhoami":{"type":"object","description":"Identity + role snapshot for the signed-in operator. Drives the\nOps Console's visibility logic \u2014 which tabs render, which\nwrite-buttons unlock, whether oversight panels are reachable.\n","properties":{"authenticated":{"type":"boolean","example":true},"user":{"type":"object","properties":{"name":{"type":"string","example":"admin@sente-rails.space"},"full_name":{"type":"string","example":"Asiimwe K."},"email":{"type":"string","format":"email"}}},"roles":{"type":"array","items":{"type":"string"},"example":["Sente Rails Admin","System Manager"]},"has_ops_access":{"type":"boolean"},"can_write":{"type":"boolean","description":"True iff the user holds `Sente Rails Admin` or `System Manager`."},"can_read_oversight":{"type":"boolean","description":"True iff the user holds `Sente Rails OAG` or admin."}}},"SystemHealth":{"type":"object","description":"Ops Console homepage snapshot \u2014 audit-log table state, scheduler\nlast-run, adapter live/stub tally, schema counters, and the live\nbuild's git head. Forward-compatible \u2014 treat unknown fields as\nadditions.\n","additionalProperties":true,"properties":{"audit_log":{"type":"object","properties":{"row_count":{"type":"integer"},"oldest_ts":{"type":["string","null"],"format":"date-time"},"newest_ts":{"type":["string","null"],"format":"date-time"}}},"scheduler":{"type":"object","additionalProperties":true,"properties":{"last_daily_expiry_sweep":{"type":["string","null"],"format":"date-time"}}},"adapters":{"type":"object","description":"Live vs sandbox(stub) adapter counts for the active country; both null if the registry can't be read.","properties":{"live":{"type":["integer","null"]},"stub":{"type":["integer","null"]}}},"counts":{"type":"object","properties":{"integrators":{"type":"integer"},"mdas":{"type":"integer"},"services":{"type":"integer"},"keys_active":{"type":"integer"},"keys_total":{"type":"integer"}}},"build":{"type":"object","properties":{"git_head":{"type":["string","null"],"description":"Short SHA of the deployed app build","or null off-git.":null}}}}},"MdaSummary":{"type":"object","description":"Compact MDA row for list views (kiosk picker, ops console).","additionalProperties":true,"properties":{"name":{"type":"string","example":"GULU"},"short_code":{"type":"string","example":"GULU"},"full_name":{"type":"string","example":"Gulu City Council"},"mda_type":{"type":["string","null"],"example":"City Authority"},"status":{"type":"string","enum":["Active","Onboarding","Suspended"]},"country":{"type":"string","example":"UG"},"mode":{"type":"string","enum":["A","B","C"]},"sector":{"type":["string","null"]},"integration_status":{"type":["string","null"],"enum":["Live","Sandbox","Planned","Inquiry"]},"target_endpoint_count":{"type":["integer","null"]},"endpoint_count":{"type":"integer","description":"Active Service rows registered for this MDA today."},"display_endpoint_count":{"type":"integer","description":"endpoint_count","or target_endpoint_count when nothing is live yet.":null},"contact_email":{"type":["string","null"],"format":"email"},"contact_phone":{"type":["string","null"]}}},"OpsMDA":{"type":"object","description":"Full MDA record (admin view; the raw doc including treasury + integration-config refs).","additionalProperties":true,"properties":{"name":{"type":"string","example":"GULU"},"short_code":{"type":"string","example":"GULU"},"full_name":{"type":"string","example":"Gulu City Council"},"mda_type":{"type":["string","null"]},"status":{"type":"string","enum":["Active","Onboarding","Suspended"]},"mode":{"type":"string","enum":["A","B","C"]},"country":{"type":"string","example":"UG"},"sector":{"type":["string","null"]},"integration_status":{"type":["string","null"],"enum":["Live","Sandbox","Planned","Inquiry"]},"parent_authority":{"type":["string","null"]},"treasury_account":{"type":["string","null"],"example":"GULU-TREASURY-001"},"integration_endpoint":{"type":["string","null"]},"push_webhook_url":{"type":["string","null"]},"api_credentials_ref":{"type":["string","null"],"description":"Pointer to the stored credential record \u2014 never the secret itself."},"oversight_scopes":{"type":["string","null"]},"contact_email":{"type":["string","null"],"format":"email"},"contact_phone":{"type":["string","null"]},"target_endpoint_count":{"type":["integer","null"],"minimum":0}}},"MdaPatch":{"type":"object","description":"Writable subset of MDA fields. All optional \u2014 send only what you're changing.","properties":{"full_name":{"type":"string"},"mda_type":{"type":"string"},"mode":{"type":"string","enum":["A","B","C"]},"status":{"type":"string","enum":["Active","Onboarding","Suspended"]},"parent_authority":{"type":["string","null"]},"treasury_account":{"type":["string","null"]},"sector":{"type":["string","null"]},"integration_status":{"type":"string","enum":["Live","Sandbox","Planned","Inquiry"]},"target_endpoint_count":{"type":"integer","minimum":0}}},"ServicePatch":{"type":"object","description":"Writable subset of Service fields. All optional.","properties":{"service_name":{"type":"string"},"sector":{"type":["string","null"]},"service_family":{"type":["string","null"]},"status":{"type":"string","enum":["Active","Inactive","Coming Soon"]},"fee_amount":{"type":"number","minimum":0},"fee_currency":{"type":"string","example":"UGX"},"fee_basis":{"type":"string","enum":["Flat","Per-Day","Per-Month","Per-Square-Meter","Tiered","Tiered-by-Turnover","Percent-of-Value","Manual"]},"vat_applicable":{"type":"boolean"},"vat_rate":{"type":["number","null"]},"efris_taxable":{"type":"boolean"}}},"IntegratorAdminListItem":{"type":"object","description":"Lightweight integrator row used in admin list views.","properties":{"name":{"type":"string","example":"ACME-FINTECH-XXXXXX"},"display_name":{"type":"string"},"contact_email":{"type":"string","format":"email"},"type":{"type":"string","enum":["MDA","Partner","Developer","Civic","Academic"]},"status":{"type":"string","enum":["PendingEmail","Active","Suspended"]},"tier":{"type":"string","enum":["Anonymous","Registered","Onboarding","Production","Restricted-Ops"]},"pricing_tier":{"type":"string","enum":["Free","Developer","Business","Enterprise","MDA"]},"signup_source":{"type":["string","null"]},"email_verified":{"type":"boolean"},"mou_status":{"type":["string","null"]},"kyc_status":{"type":["string","null"]},"last_login_at":{"type":["string","null"],"format":"date-time"},"creation":{"type":"string","format":"date-time"}}},"IntegratorAdminDetail":{"allOf":[{"$ref":"#/components/schemas/Integrator"},{"type":"object","description":"Admin view of the integrator \u2014 everything the integrator\nsees in `/v1/me` PLUS internal-only fields (creation,\nmodified, suspension notes). Sensitive transient hashes\nare stripped: `otp_hash`, `session_token_hash`,\n`login_link_hash` never appear in this response.\n","additionalProperties":true,"properties":{"creation":{"type":"string","format":"date-time"},"modified":{"type":"string","format":"date-time"},"notes":{"type":["string","null"]},"keys":{"type":"object","description":"Live key counters for this integrator.","properties":{"total":{"type":"integer"},"active":{"type":"integer"}}},"requests_last_7d":{"type":"integer","description":"Audit-log request count for this integrator over the trailing 7 days."}}}]},"OpsActionResponse":{"type":"object","description":"Uniform shape for admin action responses (suspend / reactivate / revoke).","properties":{"name":{"type":"string"},"status":{"type":"string","example":"Suspended"}}},"OpsKeyListItem":{"allOf":[{"$ref":"#/components/schemas/ApiKey"},{"type":"object","description":"Admin view of a key \u2014 adds the owning integrator id.","properties":{"integrator":{"type":"string","description":"Owning integrator code.","example":"ACME-FINTECH-XXXXXX"}}}]},"AdapterRegistry":{"type":"object","description":"Per-adapter status snapshot \u2014 the same shaped registry as\n`GET /v1/integrations`, role-gated for the ops view. Top-level\nkeys are country codes (`UG`, `KE`, `TZ` once those ship); each\nvalue maps a capability (`payment`, `identity`, `fiscal`, \u2026) to a\nstatus entry, or \u2014 for `payment` \u2014 a list of them. Each entry is\n`{status: live|sandbox|unavailable, channels?: [...]}`; the\ninternal adapter class path is never exposed.\n","additionalProperties":true,"example":{"UG":{"identity":{"status":"sandbox"},"fiscal":{"status":"sandbox"},"payment":[{"status":"live","channels":["MTN MoMo"]},{"status":"sandbox","channels":["Airtel Money"]}]}}},"OversightAggregates":{"type":"object","description":"Settled Payment Event volume by MDA over a trailing 30-day window.","properties":{"by_mda":{"type":"array","items":{"type":"object","additionalProperties":true,"properties":{"mda":{"type":"string","example":"GULU"},"total_amount":{"type":"number"},"event_count":{"type":"integer"}}}},"totals":{"type":"object","additionalProperties":true,"properties":{"window_days":{"type":"integer","example":30},"total_amount":{"type":"number"},"event_count":{"type":"integer"}}}}},"AnomalyFlag":{"type":"object","additionalProperties":true,"properties":{"name":{"type":"string"},"flag_type":{"type":"string","enum":["Cash Variance","Duplicate Assessment","Unusual Amount","Timing Anomaly","Permission Misuse","Velocity Spike","Cross-MDA Inconsistency"]},"severity":{"type":"string","enum":["Low","Medium","High","Critical"]},"status":{"type":"string","enum":["Open","Investigating","Resolved","False Positive","Escalated"]},"flagged_at":{"type":"string","format":"date-time"},"reference_doctype":{"type":["string","null"],"description":"The flagged subject's doctype (e.g. Payment Intent","Counter Shift).":null},"reference_name":{"type":["string","null"],"description":"The flagged subject's record id."},"detection_rule":{"type":["string","null"]},"signal_value":{"type":["number","null"]},"threshold":{"type":["number","null"]},"description":{"type":["string","null"]},"flagged_by":{"type":["string","null"]},"assigned_to":{"type":["string","null"]},"resolved_at":{"type":["string","null"],"format":"date-time"}}},"CitizenConsentEvent":{"type":"object","additionalProperties":true,"properties":{"name":{"type":"string"},"citizen":{"type":"string"},"mda":{"type":["string","null"]},"purpose":{"type":"string"},"granted":{"type":"boolean"},"granted_at":{"type":["string","null"],"format":"date-time"},"expiry_at":{"type":["string","null"],"format":"date-time"},"revoked_at":{"type":["string","null"],"format":"date-time"},"evidence_type":{"type":["string","null"]},"captured_by":{"type":["string","null"],"description":"User id of the clerk who recorded the access."}}},"OversightStatistics":{"type":"object","description":"Flat running totals across the rail. Forward-compatible \u2014 treat\nunknown fields as additions. Anomaly + payment-event counters are\npresent only when those doctypes are installed.\n","additionalProperties":true,"properties":{"citizens_total":{"type":"integer"},"integrators_total":{"type":"integer"},"integrators_active":{"type":"integer"},"mdas_total":{"type":"integer"},"services_total":{"type":"integer"},"keys_active":{"type":"integer"},"audit_total":{"type":"integer"},"audit_7d":{"type":"integer"},"anomaly_flags_total":{"type":"integer"},"anomaly_flags_open":{"type":"integer"},"payment_events_total":{"type":"integer"}}},"PublicSummary":{"type":"object","description":"Citizen-facing receipt verifier payload. Strictly redacted \u2014\nno NIN, no msisdn, no phone, no destination account numbers.\n","properties":{"name":{"type":"string","pattern":"^PI-\\d{4}-\\d{2}-\\d{6}$"},"status":{"type":"string","enum":["Confirmed","Pending","Failed"]},"citizen_display_name":{"type":"string","example":"Mukasa John P."},"currency":{"type":"string","example":"UGX"},"amount":{"type":"number","example":50000},"channel":{"type":"string","example":"MTN MoMo"},"aggregator_reference":{"type":["string","null"],"example":"MOMO-87xyz"},"confirmed_at":{"type":["string","null"],"format":"date-time"},"lines":{"type":"array","description":"Service line summary \u2014 MDA + service display names + amount.","items":{"type":"object","properties":{"mda":{"type":"string","example":"GULU"},"mda_name":{"type":"string","example":"Gulu City Council"},"service":{"type":"string","example":"SVC-2026-000004"},"service_name":{"type":"string","example":"Trading License Renewal"},"amount":{"type":"number","example":50000}}}},"split_summary":{"type":"array","description":"Per-MDA split with NAMES only \u2014 no destination account numbers.","items":{"type":"object","properties":{"mda":{"type":"string"},"mda_name":{"type":"string"},"amount":{"type":"number"}}}}}},"WebhookPayload":{"type":"object","description":"Provider-specific callback body. The shape varies per\nprovider (MoMo / Airtel use different field names than\nPesapal / EFRIS); the table below documents the union of\nuseful fields. Sente Rails parses and resolves the payment\nintent via the provider-specific reference field.\n","additionalProperties":true,"properties":{"referenceId":{"type":"string","description":"MoMo / Airtel \u2014 the aggregator reference we set on dispatch."},"reference":{"type":"string","description":"Pesapal \u2014 the merchant reference we set on dispatch."},"invoiceId":{"type":"string","description":"EFRIS \u2014 the invoice/FDN reference for the fiscal receipt."},"status":{"type":"string","description":"Provider-native status string (SUCCESSFUL / FAILED / PENDING / etc).","example":"SUCCESSFUL"},"financialTransactionId":{"type":"string","description":"MoMo / Airtel \u2014 the provider's own transaction id."},"fdn":{"type":"string","description":"EFRIS \u2014 the Fiscal Document Number issued for the receipt."}}},"WebhookAck":{"type":"object","description":"Sente Rails's response to a callback. `ACCEPTED` means the\nevent landed in the Payment Event table + the intent moved\nstates. `IGNORED` means the callback was a duplicate or\nreferenced an unknown reference \u2014 either way the provider\nshould stop retrying.\n","properties":{"status":{"type":"string","enum":["ACCEPTED","IGNORED"]},"reason":{"type":["string","null"],"description":"Set on `IGNORED` \u2014 `duplicate`, `unknown_reference`, `signature_invalid`, or `internal_error`.","example":"duplicate"},"payment_intent":{"type":["string","null"],"description":"Set on `ACCEPTED` \u2014 the resolved Payment Intent."},"payment_event":{"type":["string","null"],"description":"Set on `ACCEPTED` \u2014 the freshly-written Payment Event row."},"new_status":{"type":["string","null"],"description":"Set on `ACCEPTED` \u2014 the new state of the Payment Intent.","example":"Confirmed"}}},"SupervisorBearerDashboard":{"type":"object","description":"Bearer-key supervisor dashboard payload. Mirrors the\nworkbench's `/v1/work/supervisor/dashboard` but accessed via\nBearer key with `assessments.read` scope.\n","additionalProperties":true,"properties":{"mda":{"type":["string","null"]},"date":{"type":"string","format":"date"},"totals":{"type":"object","additionalProperties":true,"properties":{"total_collected":{"type":"number"},"shift_count":{"type":"integer"},"variance_count":{"type":"integer"},"outstanding_variance":{"type":"number"}}},"shifts":{"type":"array","items":{"$ref":"#/components/schemas/CounterShift"}},"by_service":{"type":"array","items":{"type":"object","additionalProperties":true,"properties":{"service":{"type":"string"},"service_name":{"type":"string"},"count":{"type":"integer"},"total":{"type":"number"}}}},"by_channel":{"type":"array","items":{"type":"object","additionalProperties":true,"properties":{"channel":{"type":"string"},"count":{"type":"integer"},"total":{"type":"number"}}}}}},"SupervisorActionResponse":{"type":"object","properties":{"name":{"type":"string","example":"SHIFT-2026-05-28-204"},"action":{"type":"string","enum":["approved","rejected","escalated"]},"stamped":{"type":"object","description":"Audit stamp (who + when + note).","properties":{"by":{"type":"string","description":"Integrator code that took the action."},"at":{"type":"string","format":"date-time"},"note":{"type":["string","null"]}}}}},"OversightAggregatesQuery":{"type":"object","description":"Per-group aggregate response (MDA or district).","properties":{"period_start":{"type":"string","format":"date"},"period_end":{"type":"string","format":"date"},"period_type":{"type":"string","enum":["day","week","month","quarter","year","custom"]},"grouped_by":{"type":"string","enum":["mda","district"]},"mda_filter":{"type":"string"},"row_count":{"type":"integer"},"rows":{"type":"array","items":{"type":"object","additionalProperties":true,"properties":{"group_key":{"type":"string","example":"GULU"},"total_collected":{"type":"number"},"transaction_count":{"type":"integer"},"average_amount":{"type":"number"},"distinct_assessments":{"type":"integer"},"distinct_citizens":{"type":"integer"},"top_services":{"type":"array","items":{"type":"object","properties":{"service":{"type":"string"},"service_name":{"type":"string"},"total":{"type":"number"}}}}}}}}},"OversightAuditTrail":{"type":"object","properties":{"doctype":{"type":"string","example":"Assessment"},"name":{"type":"string","example":"ASMT-2026-05-000123"},"version_count":{"type":"integer"},"versions":{"type":"array","items":{"type":"object","additionalProperties":true,"properties":{"name":{"type":"string"},"owner":{"type":"string"},"creation":{"type":"string","format":"date-time"},"modified":{"type":"string","format":"date-time"},"data":{"type":"string","description":"Platform-encoded diff payload."}}}}}},"OversightAnomalyFlagsPage":{"type":"object","properties":{"limit":{"type":"integer"},"offset":{"type":"integer"},"row_count":{"type":"integer"},"has_more":{"type":"boolean"},"rows":{"type":"array","items":{"$ref":"#/components/schemas/AnomalyFlag"}}}},"OversightConsentSummary":{"type":"object","properties":{"mda_filter":{"type":"string"},"purpose_filter":{"type":"string"},"as_of":{"type":"string","format":"date"},"row_count":{"type":"integer"},"rows":{"type":"array","items":{"type":"object","properties":{"mda":{"type":"string"},"purpose":{"type":"string"},"active_consents":{"type":"integer"}}}}}},"OversightPaymentEventsStream":{"type":"object","description":"Cursor-paginated stream of Payment Event rows.","properties":{"limit":{"type":"integer"},"after":{"type":"string","description":"The cursor passed in, echoed back."},"row_count":{"type":"integer"},"next_cursor":{"type":["string","null"],"description":"Pass as `after` on the next call. `null` when no more rows."},"rows":{"type":"array","items":{"$ref":"#/components/schemas/PaymentEvent"}}}},"OversightStatisticsResult":{"type":"object","description":"UBOS-shaped metric result.","properties":{"metric":{"type":"string","enum":["revenue_by_sector","transactions_by_district","taxpayer_count"]},"period_start":{"type":"string","format":"date"},"period_end":{"type":"string","format":"date"},"geography_filter":{"type":"string"},"rows":{"type":"array","description":"Row shape varies by metric \u2014 see field reference per metric.","items":{"type":"object","additionalProperties":true}}}},"ServiceNotice":{"type":"object","description":"Operator-curated announcement. Surfaced on the marketing\nlanding page + inside the dashboard. Body is plain text;\nnewlines render as paragraph breaks.\n","properties":{"name":{"type":"string","pattern":"^SN-\\d{4}-\\d{2}-\\d{6}$","example":"SN-2026-05-000001"},"title":{"type":"string","example":"GULU EFRIS sandbox refresh tomorrow 22:00\u201323:00 UTC"},"body":{"type":"string","example":"We're rotating the GULU EFRIS sandbox credentials at 22:00 UTC.\nPayment intents created during the window may return PENDING\nuntil the new credentials are accepted on our side. Expect\nfull restoration within 60 minutes.\n"},"severity":{"type":"string","enum":["Critical","Warning","Info"],"example":"Warning"},"mda":{"type":["string","null"],"description":"Optional scope. `null` means platform-wide; a short_code\nmeans the notice is targeted to integrators / clerks of\nthat MDA only.\n","example":"GULU"},"effective_from":{"type":"string","format":"date-time"},"effective_to":{"type":["string","null"],"format":"date-time"},"active":{"type":"integer","enum":[0,1],"description":"Operator kill-switch. Independent of the effective-window filter."}}}}}}}