TransitPin - API Reference¶
Source of truth: the live FastAPI backend on app3 (/opt/transitpin-api/), read
from main.py, billing_routes.py, ops_routes.py, and maps_routes.py on
2026-08-15. There are 40 endpoints total (21 in main.py, 10 in
billing_routes.py, 6 in ops_routes.py, 3 in maps_routes.py).
1. Conventions¶
- Base URL: same-origin
/api/...(nginx proxies/api/to uvicorn on127.0.0.1:8900). The API is not otherwise exposed publicly. - All request and response bodies are JSON.
- Request bodies are
dict(untyped) on most endpoints; the ops endpoints use Pydantic models (AssumeIdentityRequest,ExitRequest,LogActionRequest). - IDs are TEXT: UUID hex strings (
uuid.uuid4().hex), slugs (tenant ids, route ids liker1), or opaque tokens. - Errors are standard FastAPI
HTTPExceptionJSON:{"detail": "<message>"}. - Version reported by
/api/healthis1.0.0.
2. Auth schemes¶
| Scheme | Header | Where used |
|---|---|---|
| none | - | public endpoints (health, theme, public schools, auth, registration, billing rates, provider status, tomtom health, weather) |
| tenant JWT | Authorization: Bearer <token> |
product endpoints (routes, buses, schools, children, registrations, stats, billing). Validated by get_current_user. |
| impersonation token | Authorization: Bearer <token> |
ops impersonated endpoints (get_impersonated_user) |
| platform admin | tenant JWT whose user role is platform_admin or admin |
/api/tenants, /api/ops/assume-identity, /api/ops/audit-log, and cross-tenant /api/tenants/{tenant}/logs |
2.1 get_current_user behavior (tenant JWT)¶
- Requires an
Authorization: Bearer <token>header (else 401). - Calls auth2
GET http://127.0.0.1:8102/api/latest/users/mewith the token in anx-hexclave-access-tokenheader (plusx-hexclave-access-type: clientandx-hexclave-project-id: internal; 5s timeout). On HTTP 200 returns a normalized user{id, email, name, role, tenant_id}whereroleandtenant_idare read from auth2client_metadata(withclient_read_only_metadataas fallback). - If auth2 does not return 200, and the token starts with
demo-, returns a hardcoded user{"id":"demo-user","email":"demo@transitpin.com","name":"Demo User","role":"admin"}(roleadminis a legacy alias for platform_admin). - Otherwise 401
Invalid token.
auth2 session tokens are opaque (not JWTs), so there is no tenant_id JWT
claim. Role and tenant identity live in auth2 client_metadata, set at
registration time by the registering service.
2.2 Impersonation token (ops)¶
A secrets.token_urlsafe(32) value stored in impersonation_sessions with a
15-minute expiry. get_impersonated_user validates it (existence + expiry) and
returns the impersonated identity, never consulting auth2.
2.3 Tenant resolution¶
get_tenant_id (auth-gated endpoints), priority order: (1) tenant_id claim on
the authenticated user, (2) first URL path segment when not api, (3)
X-Tenant-Id request header, (4) default demo.
resolve_public_tenant (no-auth endpoints in main.py), priority order: (1)
tenant or slug query parameter, (2) X-Tenant-Id header, (3) first Host
subdomain label (skipping www, my, app, ops, webmail, and numeric
labels), (4) first path segment when not api, (5) default demo.
POST /api/registrations/full resolves tenant from the payload tenant field,
then the X-Tenant-Id header, then demo (it does NOT read the Host header).
3. Endpoint reference¶
3.1 Health and tenants¶
GET /api/health¶
Auth: none.
Response: {"status":"ok"|"degraded","db":"ok"|"error","version":"1.0.0","time":"<iso>"}.
GET /api/tenants¶
Auth: platform admin.
Response: array of tenants, each with id, name, tier, theme (parsed
object), domain, created_at.
GET /api/theme/{slug}¶
Auth: none.
Response: {"tenant_id","name","tier","theme":{...},"domain"}. 404
Tenant not found for an unknown slug.
3.2 Auth¶
POST /api/auth/login¶
Auth: none. Body: {"email","password"} (both required, else 400).
Behavior: calls auth2 POST /api/latest/auth/password/sign-in with the
publishable client key and client headers, then resolves the user via
GET /users/me. Returns {"access_token","token_type":"Bearer","user":{...}}
where user carries role and tenant_id from client_metadata. Wrong
credentials return 4xx (no demo fallback); the demo fallback fires only if
auth2 is unreachable.
POST /api/auth/register¶
Auth: none. Body: {"email","password","display_name"?,"tenant"?} (email and
password required, else 400).
Behavior: creates a real auth2 parent account via
POST /api/latest/auth/password/sign-up (Stack Auth accepts only
email+password), then applies display_name and client_metadata
(role=parent, tenant_id=<tenant>) via a best-effort PATCH /users/me.
Returns
{"access_token","token_type":"Bearer","user":{...},"metadata_applied":bool}.
Parent enrollment (household, children, invoices) is handled separately by
/api/registrations/full.
3.3 Registrations (legacy list)¶
GET /api/registrations¶
Auth: tenant JWT. Query: status (optional, all or a status), search
(optional, matches name/school/route).
Response: array of registration objects ordered by created_at DESC.
PUT /api/registrations/{reg_id}/approve¶
Auth: tenant JWT.
Behavior: marks the registration approved, promotes the household's pending
registration invoice to sent, and emails the parent the $50 registration
fee payment option (best-effort; a payment link is included when
REGISTRATION_PAY_URL is set). 404 if not found in the tenant.
Response: {"status":"ok","id":"<reg_id>","action":"approved","email_sent":bool}.
PUT /api/registrations/{reg_id}/reject¶
Auth: tenant JWT.
Response: {"status":"ok","id":"<reg_id>","action":"rejected"}. 404 if not
found in the tenant.
POST /api/registrations/full¶
Auth: none (public; the signup form runs before the parent has a token). Body:
{
"tenant": "<slug>",
"parent": {"name","email","phone","relationship","address"},
"parent2": {"name","relationship","phone","email"},
"children": [{"name","transport_need":"morning|afternoon|both","school","school_id","grade","route"}],
"emergency_contacts": [],
"first_pickup_date": "YYYY-MM-DD",
"policy_acknowledged": ["transportation-agreement", "..."]
}
trip_type mapped from transport_need and
rate_weekly set) + a registration invoice of $50.00 (status pending). Also
creates a registrations row (id = household id) so the signup surfaces in the
admin approval queue. Sets first_pickup_date and policy_acknowledged on the
household. The registration-fee email is deferred to admin approval; nothing is
emailed at signup.
Response:
{
"household_id": "...",
"children_count": 2,
"weekly_total": 240.0,
"registration_fee": 50.0,
"registration_fee_invoice_id": "...",
"email_sent": true,
"first_pickup_date": "2026-09-01",
"policy_acknowledged": 5,
"parent2_added": true
}
3.4 Routes¶
GET /api/routes¶
Auth: tenant JWT. Response: array of route objects (no stops array; the
stops field is the denormalized integer count).
POST /api/routes¶
Auth: tenant JWT. Body: {"name" (required), "bus_id", "driver", "mode",
"stops":[{"name","address","lat","lng","time","riders":[]}]}.
Response: the created route merged with its ordered stops (each stop's
riders JSON-decoded).
GET /api/routes/{route_id}¶
Auth: tenant JWT. Response: route merged with stops[]. 404 Route not found.
PUT /api/routes/{route_id}¶
Auth: tenant JWT. Body: any of name, driver, bus_id, mode, status,
miles, and optionally stops[] (which deletes and re-inserts all stops and
re-syncs the stops count).
Response: route merged with stops[]. 404 Route not found.
DELETE /api/routes/{route_id}¶
Auth: tenant JWT. Response: {"ok":true}. 404 Route not found. Also deletes
the route's route_stops.
3.5 Buses¶
GET /api/buses¶
Auth: tenant JWT. Response: array of bus objects.
GET /api/buses/{bus_id}¶
Auth: tenant JWT. Response: one bus object. 404 Bus not found.
PUT /api/buses/{bus_id}/location¶
Auth: tenant JWT. Body: {"lat","lng","heading","speed"}.
Response: {"status":"ok"}. Sets last_update to now.
3.6 Schools¶
GET /api/schools¶
Auth: tenant JWT. Response: array of school objects (all columns).
GET /api/public/schools¶
Auth: none (tenant resolved via resolve_public_tenant).
Response: {"tenant_id":"<slug>","schools":[{"id","name","address","start_time","end_time"}]}.
3.7 Children and stats¶
GET /api/children¶
Auth: tenant JWT. Response: array of active children (all columns, including
the billing extension columns trip_type, household_id, rate_weekly,
school_id).
GET /api/dashboard/stats¶
Auth: tenant JWT.
Response: {"pending_registrations","active_buses","active_children","total_routes"} (integers).
3.8 Audit (main.py)¶
GET /api/tenants/{tenant}/logs¶
Auth: platform admin OR a user whose tenant_id equals tenant.
Response: array of audit_log rows (newest first), with details JSON-decoded.
3.9 Billing¶
GET /api/billing/households¶
Auth: tenant JWT. Response: array of households, each with children[] and a
computed weekly_total.
GET /api/billing/households/{household_id}¶
Auth: tenant JWT. Response: household plus children[], payers[],
allocations[], weekly_total, and payer_splits[] (each
{"payer_id","amount"}). 404 Household not found.
POST /api/billing/households/{household_id}/payers¶
Auth: tenant JWT. Body: {"name","email","phone","allocation":{"child_id","split_type":"percentage|fixed|per_child","value"}}.
Behavior: inserts a non-primary payer and, if allocation is present, a
billing_allocations row.
Response: {"payer":{...},"allocation":{...}|null}.
POST /api/billing/generate¶
Auth: tenant JWT. Body: none.
Behavior: for the current Monday-Sunday cycle, computes each household total and
splits it across payers, creating one draft invoice per payer (provider
square if Square is connected, else none).
Response: {"cycle_start","cycle_end","invoices":[{...}]}.
POST /api/billing/generate-due¶
Auth: tenant JWT. Body: none.
Behavior: generates and emails the FIRST weekly transportation invoice for every
household whose first_pickup_date is within 5 days (or past). Idempotent:
households that already have a transportation invoice are skipped.
Response: {"date","generated":[{...}],"emailed":[{...}],"skipped":["<household_id>",...]}.
POST /api/billing/households/{household_id}/first-pickup¶
Auth: tenant JWT. Body: {"first_pickup_date":"YYYY-MM-DD"} (required).
Response: {"household_id","first_pickup_date"}. 404 Household not found.
GET /api/billing/outstanding¶
Auth: tenant JWT. Response: array grouped by household, each
{"household_id","household_name","total_outstanding","payers":[{...}]}
(invoices with status unpaid, partial, or sent).
GET /api/billing/rates¶
Auth: none.
Response: {"one_way":120.0,"two_way":175.0,"cycle":"weekly","registration_fee":50.0}.
GET /api/billing/provider-status¶
Auth: none. Response: {"square_connected":false,"stripe_connected":false}.
3.10 Ops (assume identity)¶
POST /api/ops/assume-identity¶
Auth: platform admin. Body: {"tenant_id" (required), "user_email", "target_role":"operator"}.
Response: {"token":"<opaque>","expires_at":"<iso>","banner":"Acting as <tenant_id>"}.
Also writes assume_identity_start to audit_log.
POST /api/ops/exit-impersonation¶
Auth: impersonation token via body {"token"} or Bearer header.
Response: {"status":"ok","exited":true,"tenant_id":"<id>"}, or
{"status":"ok","exited":false,"detail":"session not found"}.
GET /api/ops/audit-log¶
Auth: platform admin. Query: tenant_id (optional), limit (default 100,
clamped 1..1000). Response: array of audit_log rows (newest first), details
JSON-decoded.
POST /api/ops/log-action¶
Auth: impersonation token (Bearer header). Body: {"action" (required), "resource", "details":{}}.
Response: {"status":"ok","id":"<audit id>","logged":true}.
GET /api/ops/impersonated/me¶
Auth: impersonation token. Response: {"impersonated_user":{...}}.
GET /api/ops/impersonated/registrations¶
Auth: impersonation token. Response: array of registrations for the impersonated
tenant, and auto-writes a list_registrations audit row.
3.11 Maps, traffic, weather¶
GET /api/health/tomtom¶
Auth: none. Server-side ping to TomTom flowSegmentData.
Response: {"ok":true,"status":200,"detail":"TomTom flowSegmentData reachable"}.
GET /api/weather¶
Auth: none. Query: lat (default 32.0158), lon (default -81.0595).
Response: raw Open-Meteo forecast JSON (current, daily, ...). 502 on
upstream failure.
GET /api/routes/{route_id}/conditions¶
Auth: tenant JWT. Response: {"route_id","route_name","summary":"clear|moderate|heavy|unavailable","incident_count","incidents":[{...}],"schools":[{...}],"bbox":"31.95,-81.20,32.15,-80.95"}.
404 Route not found. Degrades to summary:"unavailable" on TomTom failure.