# 04 · API contract (FastAPI) Canonical REST contract. Reconcile the existing 25+ endpoints in `src/orchestrator/main.py` toward this. JSON in/out, `snake_case`. All amounts in **öre**. Auth: Next.js → FastAPI over a shared internal token (`X-Internal-Token`); customer identity passed as `org_id`/`user_id` resolved from the Auth.js session server-side. ## Conventions - Errors: `{ "error": { "code", "message" } }`, proper HTTP status. Interface-voice messages (Swedish) are produced in the frontend, not here. - Idempotency: accept `Idempotency-Key` on POSTs that create orders/payments. - Pagination: `?limit=&cursor=`. ## Catalog ``` GET /catalog -> [{id,title,markets,entity_type,count,updated,price_ore,confidence}] GET /catalog/{id} -> full dataset (overview, coverage, schema, quality summary) GET /catalog/{id}/sample?n=10 -> sample rows WITH per-field provenance ``` ## Estimate (configurator → price/time/confidence) ``` POST /estimates body: { flow, markets[], entity_type, fields[], enrichment[], volume, frequency, confidence_target, formats[] } -> { total_ore, deposit_ore, balance_ore, est_days, est_confidence, breakdown[] } ``` ## AI-assist (NL → config) ``` POST /ai/parse body: { text } -- "5000 svenska SaaS-bolag med e-post och omsättning" -> { config: , rationale } -- prefilled configurator; uses pgvector match to catalog/fields ``` ## Orders (lifecycle) ``` POST /orders body: { flow, config } -> { order_id, status:'draft'|'quoted' } GET /orders -> [orders for org] GET /orders/{id} -> order + current job + delivery (if any) POST /orders/{id}/scope (custom flow) body:{ notes } -> status:'quoted' (manual quote step) POST /orders/{id}/accept customer approves delivery -> triggers balance release; status:'accepted' POST /orders/{id}/revise body:{ notes } -> status:'revision'; escrow untouched ``` ## Payments (50/50 escrow) ``` POST /payments/{order_id}/deposit -> { checkout_url } (Stripe Checkout for 50%; on success → IN_PRODUCTION) POST /payments/{order_id}/release -> captures/charges the held balance (called on accept) POST /webhooks/stripe (Stripe → here) verify signature; update payments + order status ``` > Implement balance as either a manual capture on a held PaymentIntent, or a second Checkout/invoice keyed to `order_id`. Never charge balance before `accept`. ## Jobs / live status ``` GET /orders/{id}/status -> { stage, pct, per_market[], confidence, eta, rows } GET /orders/{id}/events -> Server-Sent Events stream (preferred for the production tracker) ``` ## Uploads (R2 presigned) ``` POST /uploads body:{ order_id, name, mime, bytes } -> { file_id, put_url } (client PUTs to R2) GET /orders/{id}/files -> [files] ``` ## Customer DB access (read-only grant) ``` POST /connections/db-grant body: { order_id, conn_string, scope, expires_at } -- stored ENCRYPTED; validated read-only -> { grant_id, status:'pending'|'verified' } DELETE /connections/db-grant/{id} -> revoke ``` ## Deliveries ``` GET /orders/{id}/delivery -> { formats, destinations, record_count, avg_confidence, quality_report, files[], provenance_export_url } POST /orders/{id}/delivery/destinations body:{ destinations[] } -> updates targets (CSV/Parquet/JSON/API/NocoDB/Metabase/FiveCRM/Sheets/webhook) ``` ## Sources (the transparency moat) ``` GET /sources -> [{source_id,name,market,access,licence,cadence,confidence}] GET /sources/{market} -> sources + freshness for a market ``` ## Subscriptions / feeds ``` POST /subscriptions body:{ order_id, cadence, webhook_url } -> { sub_id } GET /subscriptions -> [subs] POST /subscriptions/{id}/pause | /resume ``` ## Webhooks out (to customers) - On delivery / feed update: POST to `subscriptions.webhook_url` with `{ order_id, delta_url, summary }`. ## Health ``` GET /health -> { ok, db, workers, queue } ``` ## Existing-code reconciliation Map current endpoints (orders, estimates, ai-assistent, catalog, webhooks, health) onto the names above; rename for consistency, keep behavior. Add: `/orders/{id}/events` (SSE), `/payments/*` escrow split, `/connections/db-grant`, `/sources`, `/subscriptions`. Remove any endpoint that drives a bypass/stealth fetch path.