{"openapi":"3.0.3","info":{"title":"STRUCTVATE Public API","version":"1.0.0","summary":"Field, tracking, and export APIs for tenant integrations","description":"Interactive contract for external developers integrating with STRUCTVATE.\n\n## Authentication\n- **ApiKeyAuth** — header `x-api-key: sv_live_…` (create keys in Settings → API Keys).\n- **BearerAuth** — `Authorization: Bearer <access_token>` (user JWT / Field mobile).\n\n## Tenant awareness\nAPI keys are **bound to a single tenant** at issuance. Examples use placeholders:\n`YOUR_TENANT_ID`, `YOUR_API_KEY`, `YOUR_DELIVERY_ID`, `YOUR_TRACKING_TOKEN`.\n\n## HTTP status policy (B-009)\n| Status | Meaning | When |\n|--------|---------|------|\n| **401 Unauthorized** | Not authenticated | Missing/invalid JWT, cookie, or API key |\n| **403 Forbidden** | Authenticated but denied | Wrong scope/role/plan, billing lock, bad tracking HMAC |\n| **429 Too Many Requests** | Rate limited | Exceeded plan quota (Mission 48) |\n\nNever conflate 401 and 403: a 403 means credentials were accepted but **authorization** failed.\n\n## OpenAPI download\nRaw spec: [`GET /api/openapi.json`](/api/openapi.json) — static baseline.\nTenant-aware spec: [`GET /api/v1/openapi`](/api/v1/openapi) — session or `x-api-key` injects your custom fields.","contact":{"name":"STRUCTVATE Platform","url":"https://structvate.vercel.app"}},"servers":[{"url":"https://structvate.vercel.app","description":"Production"},{"url":"http://localhost:3000","description":"Local development"}],"tags":[{"name":"Exports","description":"Owner / cashier CSV exports (API key scopes: `orders.read` | `exports.read`)."},{"name":"Field","description":"Mobile Field BFF — Bearer JWT only (cookies rejected)."},{"name":"Tracking","description":"Public customer tracking — HMAC token required (query or header)."}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"apiKey","in":"header","name":"x-api-key","description":"Tenant-scoped developer key (`sv_live_…`). Issued under Settings → API Keys for `YOUR_TENANT_ID`."},"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Supabase / STRUCTVATE access token. Required for Field GPS; also accepted on export routes."}},"schemas":{"ErrorBody":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"code":{"type":"string","nullable":true},"details":{"nullable":true}}},"Product":{"type":"object","description":"Catalog product. `metadata` holds tenant custom fields (Mission 76/84).","properties":{"id":{"type":"string","format":"uuid","example":"YOUR_PRODUCT_ID"},"name":{"type":"string","example":"Sac ciment CPJ 45"},"sku":{"type":"string","example":"SKU-CEMENT-45"},"base_price_cents":{"type":"integer","example":8500},"metadata":{"type":"object","description":"Tenant-defined custom fields — keys injected dynamically per tenant in GET /api/v1/openapi.","additionalProperties":true,"properties":{}}}},"Customer":{"type":"object","description":"CRM customer. `metadata` holds tenant custom fields (Mission 76/84).","properties":{"id":{"type":"string","format":"uuid","example":"YOUR_CUSTOMER_ID"},"name":{"type":"string","example":"SARL Atlas BTP"},"phone":{"type":"string","nullable":true,"example":"+212600000000"},"ice":{"type":"string","nullable":true,"description":"Morocco ICE (15 digits, B-029)."},"metadata":{"type":"object","description":"Tenant-defined custom fields — keys injected dynamically per tenant in GET /api/v1/openapi.","additionalProperties":true,"properties":{}}}},"Delivery":{"type":"object","description":"Dispatch delivery row. `metadata` holds tenant custom fields (Mission 76/84).","properties":{"id":{"type":"string","format":"uuid","example":"YOUR_DELIVERY_ID"},"status":{"type":"string","example":"in_transit"},"driver_name":{"type":"string","nullable":true},"metadata":{"type":"object","description":"Tenant-defined custom fields — keys injected dynamically per tenant in GET /api/v1/openapi.","additionalProperties":true,"properties":{}}}},"GpsLocationRequest":{"type":"object","description":"Field GPS ping. Rejects Null Island (0,0). Provide `delivery_id` or `dossier_id`.","required":["lat","lng"],"properties":{"lat":{"type":"number","minimum":-90,"maximum":90,"example":33.5731,"description":"Latitude (or use `latitude`)."},"lng":{"type":"number","minimum":-180,"maximum":180,"example":-7.5898,"description":"Longitude (or use `longitude`)."},"latitude":{"type":"number","minimum":-90,"maximum":90},"longitude":{"type":"number","minimum":-180,"maximum":180},"accuracy":{"type":"number","minimum":0,"default":0,"example":12.5},"delivery_id":{"type":"string","format":"uuid","example":"YOUR_DELIVERY_ID"},"deliveryId":{"type":"string","format":"uuid"},"dossier_id":{"type":"string","format":"uuid","example":"YOUR_DOSSIER_ID"},"dossierId":{"type":"string","format":"uuid"},"timestamp":{"type":"string","format":"date-time","example":"2026-09-03T12:00:00.000Z"}}},"GpsLocationResponse":{"type":"object","properties":{"ok":{"type":"boolean","example":true},"success":{"type":"boolean","example":true},"recorded_at":{"type":"string","format":"date-time"},"delivery_id":{"type":"string","format":"uuid"},"upserted":{"type":"boolean"},"skipped":{"type":"boolean"},"location":{"type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"},"accuracy":{"type":"number"},"updated_at":{"type":"string","format":"date-time"},"updated_by":{"type":"string","format":"uuid"}}},"data":{"type":"object","properties":{"deliveryId":{"type":"string","format":"uuid"},"lat":{"type":"number"},"lng":{"type":"number"},"accuracy":{"type":"number"},"upserted":{"type":"boolean"}}}}},"PublicTrackingResponse":{"type":"object","properties":{"delivery":{"nullable":true,"type":"object","properties":{"driver_name":{"type":"string","nullable":true},"driver_phone":{"type":"string","nullable":true},"truck_plate":{"type":"string","nullable":true},"dispatched_at":{"type":"string","format":"date-time","nullable":true}}},"order":{"type":"object","properties":{"id":{"type":"string","format":"uuid","example":"YOUR_DOSSIER_ID"},"reference":{"type":"string","example":"BL-2026-0042"},"status":{"type":"string","example":"in_transit"},"amount_cents":{"type":"integer","description":"Amount in centimes (SoT).","example":1250000},"delivery_city":{"type":"string","nullable":true},"payment_method":{"type":"string","nullable":true},"customer_name":{"type":"string","nullable":true}}},"location":{"nullable":true,"description":"Live GPS only when City Ops + live_gps_enabled + is_gps_public.","type":"object","properties":{"lat":{"type":"number"},"lng":{"type":"number"}}}}}},"responses":{"Unauthorized":{"description":"Unauthorized (B-009) — missing or invalid credentials. The client is not authenticated (no valid Bearer JWT, session cookie, or `x-api-key`). Fix: send `Authorization: Bearer <token>` or `x-api-key: sv_live_…`.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"code":{"type":"string","nullable":true},"details":{"nullable":true}}},"example":{"error":"Unauthorized","code":"UNAUTHORIZED"}}}},"Forbidden":{"description":"Forbidden (B-009) — the caller is authenticated but not permitted. Examples: API key missing required scope, role lacks capability, plan gate (City Ops), billing lock, or invalid tracking HMAC. Do **not** treat as “retry login” — fix scopes / plan / token.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"code":{"type":"string","nullable":true},"details":{"nullable":true}}},"example":{"error":"Forbidden","code":"ACCESS_DENIED"}}}},"RateLimited":{"description":"Too Many Requests — plan-aware rate limit (Mission 48). Retry after `Retry-After` / rate-limit headers.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"code":{"type":"string","nullable":true},"details":{"nullable":true}}},"example":{"error":"Too Many Requests","code":"RATE_LIMITED"}}},"headers":{"Retry-After":{"schema":{"type":"string"},"description":"Seconds until the bucket resets (when provided)."},"X-RateLimit-Limit":{"schema":{"type":"string"}},"X-RateLimit-Remaining":{"schema":{"type":"string"}}}}}},"paths":{"/api/v1/exports/unpaid":{"get":{"tags":["Exports"],"operationId":"exportUnpaidDeliveries","summary":"List unpaid deliveries (CSV)","description":"Downloads a CSV of unpaid commercial dossiers for the authenticated tenant.\n\n**Auth:** `x-api-key` (scopes `orders.read` or `exports.read`) **or** Bearer/cookie with `field.read`.\n\nTenant is inferred from the API key / session — never pass `YOUR_TENANT_ID` in the path.","security":[{"ApiKeyAuth":[]},{"BearerAuth":[]}],"responses":{"200":{"description":"CSV attachment (`unpaid_deliveries_YYYY-MM-DD.csv`).","content":{"text/csv":{"schema":{"type":"string","format":"binary"},"example":"reference,customer,amount_due_cents,status\nBL-2026-0042,Chantier Al Amal,1700000,overdue\n"}},"headers":{"Content-Disposition":{"schema":{"type":"string"},"example":"attachment; filename=\"unpaid_deliveries_2026-09-03.csv\""}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/field/location":{"post":{"tags":["Field"],"operationId":"updateDriverGps","summary":"Update driver GPS","description":"Upserts the latest GPS point on a delivery (single-row overwrite — no ping history bloat).\n\n**Auth:** Bearer JWT only (Field mobile). Cookies are rejected.\n\n**Plan:** City Ops + `live_gps_enabled`. Roles: chauffeur / warehouse / owner / admin / … (`LOCATION_ROLES`).\n\nOptional header `x-client-type: mobile-background` enables movement/time throttling.","security":[{"BearerAuth":[]}],"parameters":[{"name":"x-client-type","in":"header","required":false,"schema":{"type":"string","enum":["mobile-background"]},"description":"Background clients may be throttled (~45s / 20m)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GpsLocationRequest"},"examples":{"deliveryPing":{"summary":"Ping by delivery id","value":{"lat":33.5731,"lng":-7.5898,"accuracy":12.5,"delivery_id":"YOUR_DELIVERY_ID","timestamp":"2026-09-03T12:00:00.000Z"}},"dossierPing":{"summary":"Ping by commercial dossier id","value":{"latitude":33.5731,"longitude":-7.5898,"dossier_id":"YOUR_DOSSIER_ID"}}}}}},"responses":{"200":{"description":"Location recorded (or skipped when throttled).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GpsLocationResponse"}}}},"400":{"description":"Invalid GPS payload (Zod / Null Island).","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"code":{"type":"string","nullable":true},"details":{"nullable":true}}},"example":{"success":false,"ok":false,"error":"Invalid GPS payload","code":"INVALID_GPS_PAYLOAD"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"description":"Payment required — tenant billing lock.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"code":{"type":"string","nullable":true},"details":{"nullable":true}}},"example":{"error":"Payment Required","code":"PAYMENT_REQUIRED"}}}},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/api/v1/tracking/{id}":{"get":{"tags":["Tracking"],"operationId":"getPublicTracking","summary":"Public tracking info","description":"Public order/delivery tracking JSON. `id` may be a `commercial_dossiers` UUID or a `deliveries` UUID.\n\n**Auth:** HMAC tracking token via `?token=` or header `x-tracking-token` (no API key).\n\nGPS `location` is null unless City Ops + live GPS public flag.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Dossier or delivery UUID for `YOUR_TENANT_ID`.","example":"YOUR_DOSSIER_ID"},{"name":"token","in":"query","required":false,"schema":{"type":"string"},"description":"HMAC tracking token (`YOUR_TRACKING_TOKEN`)."},{"name":"x-tracking-token","in":"header","required":false,"schema":{"type":"string"},"description":"Alternate header for the tracking HMAC."}],"responses":{"200":{"description":"Tracking payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicTrackingResponse"}}}},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Tracking information not found.","content":{"application/json":{"schema":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"code":{"type":"string","nullable":true},"details":{"nullable":true}}},"example":{"error":"Tracking information not found"}}}},"429":{"$ref":"#/components/responses/RateLimited"}}}}}}