{"openapi":"3.0.3","info":{"title":"API New Window System (Main Load Data)","version":"0.1.0","description":"Migration target for the legacy ms_main_load_data Django service. URLs match the legacy ones byte-for-byte so existing clients keep working.\n\n**Versioning:** every path is also reachable under `/api/v1/…` (e.g. `/api/v1/consume/zoho/customers/`), gated identically. Pin `/api/v1/` for stability; a future breaking change ships under `/api/v2/`. The unversioned paths remain a permanent alias of v1.\n\n**Note:** paginated list endpoints omit the heavy `raw_data` field by default to keep responses small. Pass `?include_raw=true` to get it back (the by-id detail endpoints always include it).\n\n**Scopes:** a credential may be restricted to a subset of endpoints (e.g. `consume:customers`, `consume:*`, `load:sales_orders`). A request outside the credential's scopes returns **403** (`code: insufficient_scope`, with a `required` field). A credential with no scopes set has full access.\n\n**Errors** use RFC 7807 `application/problem+json`: `{type, title, status, code, detail}` (the token endpoint is the one exception — it uses the OAuth2 `{error, error_description}` shape). Every response carries an `X-Request-Id` header for support/log correlation.\n\n**Idempotency:** the POST `load_*` endpoints accept an `Idempotency-Key` header — the first call runs and its response is cached; a retry with the same key replays it (409 while in flight, 422 if reused on another endpoint)."},"servers":[{"url":"/","description":"current host"}],"security":[{"bearerAuth":[]},{"clientCredentials":[]}],"tags":[{"name":"auth","description":"Machine-to-machine token endpoint."},{"name":"system","description":"Service-level health and meta endpoints."},{"name":"consume:senitron","description":"Read-only Senitron snapshot data."},{"name":"consume:zoho","description":"Read/write Zoho snapshot data."},{"name":"reconciliation","description":"Cross-system reconciliation (Zoho vs Senitron)."},{"name":"analytics","description":"Business aggregations (sales, receivables, inventory)."},{"name":"audit","description":"API usage + security audit (request volume, flagged IPs, credential activity)."},{"name":"freshness","description":"Data freshness monitor (time since each module last synced)."},{"name":"zoho:integration","description":"Zoho OAuth flows and pull commands."},{"name":"manage:auth","description":"RBAC and user administration."}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Send `Authorization: Bearer <token>`. Accepts a per-system API key (`ak_…`), a client-credentials access token from `POST /api/token`, the `SUPABASE_SERVICE_ROLE_KEY` (internal), or a Supabase Auth user JWT. Paste any of these into Authorize."},"clientCredentials":{"type":"oauth2","description":"OAuth2 client-credentials. Authorize with your `client_id` + `client_secret`; Swagger fetches a short-lived token from `/api/token` and sends it as a Bearer token automatically.","flows":{"clientCredentials":{"tokenUrl":"/api/token/","scopes":{}}}}},"parameters":{"Page":{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"default":1},"description":"1-based page index."},"PageSize":{"name":"page_size","in":"query","schema":{"type":"integer","minimum":1,"maximum":1000,"default":10},"description":"Rows per page; capped at 1000 to match the legacy API."},"Sort":{"name":"sort","in":"query","schema":{"type":"string"},"description":"Sort by a column, `-` prefix for descending (e.g. `sort=-date`). Supported on the main list endpoints (customers, items, itemgroups, packages, invoices, purchase_orders); each accepts a curated column set — an unknown column falls back to the endpoint's default order."},"Format":{"name":"format","in":"query","schema":{"type":"string","enum":["json","csv"],"default":"json"},"description":"Response format. `csv` returns the current page as a downloadable CSV attachment (nested fields JSON-encoded per cell) instead of the JSON envelope; combine with `page_size` to export in chunks."},"Count":{"name":"count","in":"query","schema":{"type":"string","enum":["exact","planned","estimated"],"default":"exact"},"description":"Total-count strategy for the `count` field. `exact` (default) is a precise `COUNT(*)`; `planned` returns the query planner's estimate (fast, approximate); `estimated` uses the estimate above a threshold, else exact. Use an approximate count to speed up paging over the large tables; it can make `next`/`previous` slightly inaccurate near the end."},"ItemNumbers":{"name":"item_numbers","in":"query","schema":{"type":"string"},"description":"Comma-separated `item_number` filter."},"OnlyFields":{"name":"only_fields","in":"query","schema":{"type":"string"},"description":"Comma-separated list of columns to return. The `id` column is always included for stable identifiers."},"ZohoOrgId":{"name":"zoho_org_id","in":"query","schema":{"type":"string"},"description":"Tenant filter — the legacy Zoho organization id."},"StartDate":{"name":"start_date","in":"query","schema":{"type":"string","format":"date"},"description":"YYYY-MM-DD lower bound on the entity's primary date column."},"EndDate":{"name":"end_date","in":"query","schema":{"type":"string","format":"date"},"description":"YYYY-MM-DD inclusive upper bound (clamped to 23:59:59)."},"StartLastModifiedTime":{"name":"start_last_modified_time","in":"query","schema":{"type":"string","format":"date"},"description":"YYYY-MM-DD lower bound on `last_modified_time`."},"EndLastModifiedTime":{"name":"end_last_modified_time","in":"query","schema":{"type":"string","format":"date"},"description":"YYYY-MM-DD inclusive upper bound on `last_modified_time`."}},"schemas":{"Problem":{"type":"object","description":"RFC 7807 problem details (served as application/problem+json).","properties":{"type":{"type":"string","default":"about:blank"},"title":{"type":"string","example":"Forbidden"},"status":{"type":"integer","example":403},"code":{"type":"string","example":"insufficient_scope","description":"Stable machine-readable slug."},"detail":{"type":"string","example":"Insufficient scope"}}},"HealthCheck":{"type":"object","required":["status"],"properties":{"status":{"type":"string","example":"ok"}}},"Pagination":{"type":"object","required":["count","next","previous","results"],"properties":{"count":{"type":"integer"},"next":{"type":"string","nullable":true},"previous":{"type":"string","nullable":true},"results":{"type":"array","items":{}}}},"ZohoInventoryItem":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"org_id":{"type":"string"},"item_id":{"type":"string"},"group_id":{"type":"string","nullable":true},"group_name":{"type":"string","nullable":true},"name":{"type":"string","nullable":true},"sku":{"type":"string","nullable":true},"status":{"type":"string","nullable":true},"item_type":{"type":"string","nullable":true},"rate":{"type":"number","nullable":true},"purchase_rate":{"type":"number","nullable":true},"stock_on_hand":{"type":"integer","nullable":true},"available_stock":{"type":"integer","nullable":true},"actual_available_stock":{"type":"integer","nullable":true},"last_modified_time":{"type":"string","format":"date-time","nullable":true}}},"ZohoItemGroup":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"org_id":{"type":"string"},"group_id":{"type":"string"},"group_name":{"type":"string"},"product_type":{"type":"string","nullable":true},"brand":{"type":"string","nullable":true},"manufacturer":{"type":"string","nullable":true},"unit":{"type":"string","nullable":true},"last_modified_time":{"type":"string","format":"date-time","nullable":true}}},"ZohoCustomer":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"org_id":{"type":"string"},"contact_id":{"type":"string"},"contact_name":{"type":"string","nullable":true},"customer_name":{"type":"string","nullable":true},"first_name":{"type":"string","nullable":true},"last_name":{"type":"string","nullable":true},"email":{"type":"string","nullable":true},"phone":{"type":"string","nullable":true},"mobile":{"type":"string","nullable":true},"status":{"type":"string","nullable":true},"last_modified_time":{"type":"string","format":"date-time","nullable":true}}},"ZohoSalesOrder":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"org_id":{"type":"string"},"salesorder_id":{"type":"string"},"salesorder_number":{"type":"string"},"date":{"type":"string","format":"date-time","nullable":true},"status":{"type":"string","nullable":true},"customer_id":{"type":"string","nullable":true},"customer_name":{"type":"string","nullable":true},"total":{"type":"number","nullable":true},"last_modified_time":{"type":"string","format":"date-time","nullable":true}}},"ZohoFullSalesOrder":{"type":"object","description":"ZohoSalesOrder enriched with the matching customer record.","allOf":[{"$ref":"#/components/schemas/ZohoSalesOrder"},{"type":"object","properties":{"customer":{"$ref":"#/components/schemas/ZohoCustomer"}}}]},"ZohoInvoice":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"org_id":{"type":"string"},"invoice_id":{"type":"string"},"invoice_number":{"type":"string"},"date":{"type":"string","format":"date-time","nullable":true},"due_date":{"type":"string","format":"date-time","nullable":true},"status":{"type":"string","nullable":true},"customer_id":{"type":"string","nullable":true},"customer_name":{"type":"string","nullable":true},"salesorder_id":{"type":"string","nullable":true},"salesorder_number":{"type":"string","nullable":true},"total":{"type":"number","nullable":true},"balance":{"type":"number","nullable":true},"last_modified_time":{"type":"string","format":"date-time","nullable":true}}},"ZohoFullInvoice":{"type":"object","description":"ZohoInvoice enriched with the matching customer record.","allOf":[{"$ref":"#/components/schemas/ZohoInvoice"},{"type":"object","properties":{"customer":{"$ref":"#/components/schemas/ZohoCustomer"}}}]},"ZohoPurchaseReceive":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"org_id":{"type":"string"},"receive_id":{"type":"string"},"receive_number":{"type":"string","nullable":true},"status":{"type":"string","nullable":true},"received_status":{"type":"string","nullable":true},"billed_status":{"type":"string","nullable":true},"date":{"type":"string","format":"date-time","nullable":true},"vendor_id":{"type":"string","nullable":true},"vendor_name":{"type":"string","nullable":true},"purchaseorder_id":{"type":"string","nullable":true},"purchaseorder_number":{"type":"string","nullable":true},"total_quantity_received":{"type":"number","nullable":true},"created_time":{"type":"string","format":"date-time","nullable":true},"last_modified_time":{"type":"string","format":"date-time","nullable":true}}},"ZohoFullPurchaseOrder":{"type":"object","description":"ZohoPurchaseOrder enriched with its line_items array.","allOf":[{"$ref":"#/components/schemas/ZohoPurchaseOrder"},{"type":"object","properties":{"line_items":{"type":"array","items":{"type":"object"}}}}]},"ZohoShipmentOrder":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"org_id":{"type":"string"},"shipment_id":{"type":"string"},"shipment_number":{"type":"string","nullable":true},"salesorder_id":{"type":"string","nullable":true},"salesorder_number":{"type":"string","nullable":true},"shipping_date":{"type":"string","format":"date-time","nullable":true},"status":{"type":"string","nullable":true},"carrier":{"type":"string","nullable":true}}},"ZohoApiSettings":{"type":"object","required":["app_config","connected","auth_url","zoho_connection_configured"],"properties":{"app_config":{"type":"object","properties":{"org_id":{"type":"string"},"zoho_client_id":{"type":"string"},"zoho_redirect_uri":{"type":"string"},"zoho_refresh_time":{"type":"string","format":"date-time","nullable":true},"zoho_last_sync_time":{"type":"string","format":"date-time","nullable":true},"zoho_connection_configured":{"type":"boolean"},"zoho_client_secret_vault_id":{"type":"string","format":"uuid","nullable":true},"zoho_refresh_token_vault_id":{"type":"string","format":"uuid","nullable":true}}},"connected":{"type":"boolean"},"auth_url":{"type":"string","nullable":true},"zoho_connection_configured":{"type":"boolean"}}},"ZohoPackage":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"org_id":{"type":"string"},"package_id":{"type":"string"},"package_number":{"type":"string","nullable":true},"shipment_id":{"type":"string","nullable":true},"salesorder_id":{"type":"string","nullable":true},"customer_id":{"type":"string","nullable":true},"status":{"type":"string","nullable":true},"last_modified_time":{"type":"string","format":"date-time","nullable":true}}},"ZohoPurchaseOrder":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"org_id":{"type":"string"},"purchaseorder_id":{"type":"string"},"purchaseorder_number":{"type":"string"},"reference_number":{"type":"string","nullable":true},"status":{"type":"string","nullable":true},"vendor_id":{"type":"string","nullable":true},"vendor_name":{"type":"string","nullable":true},"date":{"type":"string","format":"date-time","nullable":true},"currency_code":{"type":"string","nullable":true},"sub_total":{"type":"number","nullable":true},"tax_total":{"type":"number","nullable":true},"total":{"type":"number","nullable":true},"created_time":{"type":"string","format":"date-time","nullable":true},"last_modified_time":{"type":"string","format":"date-time","nullable":true}}},"SenitronItem":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"org_id":{"type":"string"},"item_number":{"type":"string"},"tags_count":{"type":"integer","nullable":true},"qty":{"type":"integer","nullable":true},"raw_data":{"type":"object","nullable":true}}},"ReconciliationItem":{"type":"object","properties":{"item_id":{"type":"string"},"sku":{"type":"string","nullable":true},"name":{"type":"string","nullable":true},"org_id":{"type":"string"},"zoho_stock_on_hand":{"type":"integer","nullable":true},"senitron_qty":{"type":"integer","nullable":true},"senitron_tags_count":{"type":"integer","nullable":true},"variance":{"type":"integer","description":"zoho_stock_on_hand − senitron_qty."},"status":{"type":"string","enum":["match","over","under","missing_in_zoho"]}}},"ReconciliationSummary":{"type":"object","properties":{"total":{"type":"integer"},"matched":{"type":"integer"},"over":{"type":"integer"},"under":{"type":"integer"},"missing_in_zoho":{"type":"integer"},"total_abs_variance":{"type":"integer"}}},"TimelineItem":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"org_id":{"type":"string"},"item_number":{"type":"string","description":"Zoho item_id."},"previous_stock_on_hand":{"type":"integer","nullable":true},"actual_stock_on_hand":{"type":"integer","nullable":true},"previous_status_zoho":{"type":"string","nullable":true},"actual_status_zoho":{"type":"string","nullable":true},"text":{"type":"string","nullable":true,"description":"Human-readable change summary."},"created_at":{"type":"string","format":"date-time"}}},"SenitronItemAsset":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"serial_number":{"type":"string"},"item_number":{"type":"string","nullable":true},"alt_serial":{"type":"string","nullable":true},"epc":{"type":"string","nullable":true},"status_id":{"type":"string","format":"uuid","nullable":true},"first_seen":{"type":"string","format":"date-time","nullable":true},"last_seen":{"type":"string","format":"date-time","nullable":true},"last_zone":{"type":"string","nullable":true},"last_seen_antenna":{"type":"string","nullable":true},"handheld_reader":{"type":"string","nullable":true},"handheld_last_seen":{"type":"string","format":"date-time","nullable":true},"static_zone":{"type":"string","nullable":true},"current_units":{"type":"number","nullable":true},"adjust_qty":{"type":"integer","nullable":true},"attr1":{"type":"string","nullable":true},"attr2":{"type":"string","nullable":true},"attr3":{"type":"string","nullable":true},"attr4":{"type":"string","nullable":true},"attr5":{"type":"string","nullable":true},"attr6":{"type":"string","nullable":true},"attr7":{"type":"string","nullable":true},"attr8":{"type":"string","nullable":true},"attr9":{"type":"string","nullable":true},"attr10":{"type":"string","nullable":true},"text3":{"type":"string","nullable":true}}},"SenitronItemAssetLog":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"senitron_id":{"type":"integer"},"serial_number":{"type":"string","nullable":true},"item_number":{"type":"string","nullable":true},"alt_serial":{"type":"string","nullable":true},"epc":{"type":"string","nullable":true},"last_seen":{"type":"string","format":"date-time","nullable":true},"last_zone":{"type":"string","nullable":true},"last_status_id":{"type":"integer","nullable":true},"last_status_name":{"type":"string","nullable":true},"current_status_id":{"type":"integer","nullable":true},"current_status_name":{"type":"string","nullable":true},"user":{"type":"string","nullable":true},"reason":{"type":"string","nullable":true},"created_time":{"type":"string","format":"date-time","nullable":true}}}}},"paths":{"/api/token/":{"post":{"tags":["auth"],"security":[],"summary":"Exchange client credentials for a short-lived access token","description":"OAuth2 **client-credentials** grant. Send `client_id` + `client_secret` (HTTP Basic auth, or in the form/JSON body) and get back a Bearer access token (default 1h) to call the data API. Clients issued via the self-service flow also require a matching `client_user` (the API name) — all three values must line up. Register at `/api-access/`; CLI: `node scripts/issue-api-client.mjs`.","requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","properties":{"grant_type":{"type":"string","default":"client_credentials"},"client_id":{"type":"string"},"client_secret":{"type":"string"},"client_user":{"type":"string","description":"Required for clients registered via /api-access/ (the 3rd value)."}}}}}},"responses":{"200":{"description":"The access token.","content":{"application/json":{"schema":{"type":"object","properties":{"access_token":{"type":"string"},"token_type":{"type":"string","example":"Bearer"},"expires_in":{"type":"integer","example":3600}}}}}},"400":{"description":"Unsupported grant_type."},"401":{"description":"Invalid client credentials."},"500":{"description":"Token signing not configured on the server."}}}},"/health-check/":{"get":{"tags":["system"],"security":[],"summary":"Liveness probe","description":"Returns `{status: 'ok'}` if the process is up.","responses":{"200":{"description":"Service is healthy.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthCheck"}}}}}}},"/api/consume/analytics/overview/":{"get":{"tags":["analytics"],"summary":"Business analytics overview","description":"Invoiced sales per month, invoice aging buckets, top items by revenue, and an inventory summary — in one call. Backed by the `analytics_*` SQL functions.","parameters":[{"$ref":"#/components/parameters/ZohoOrgId"},{"name":"months","in":"query","schema":{"type":"integer","default":12},"description":"Window for sales + top items (1–60)."},{"name":"top_limit","in":"query","schema":{"type":"integer","default":10},"description":"How many top items (1–100)."}],"responses":{"200":{"description":"Analytics overview.","content":{"application/json":{"schema":{"type":"object","properties":{"sales_by_month":{"type":"array","items":{"type":"object","properties":{"month":{"type":"string","format":"date"},"invoice_count":{"type":"integer"},"total_amount":{"type":"number"}}}},"invoice_aging":{"type":"array","items":{"type":"object","properties":{"bucket":{"type":"string","enum":["current","1-30","31-60","61-90","90+"]},"invoice_count":{"type":"integer"},"total_balance":{"type":"number"}}}},"top_items":{"type":"array","items":{"type":"object","properties":{"sku":{"type":"string","nullable":true},"name":{"type":"string","nullable":true},"total_revenue":{"type":"number"},"total_qty":{"type":"number"}}}},"inventory_summary":{"type":"object","properties":{"item_count":{"type":"integer"},"total_units":{"type":"integer"},"stock_value":{"type":"number"},"low_stock":{"type":"integer"},"out_of_stock":{"type":"integer"}}}}}}}}}}},"/api/consume/freshness/":{"get":{"tags":["freshness"],"summary":"Data freshness report","description":"Per (module, org), how long since the last sync run, classified `fresh` / `stale` / `critical` / `never` against the FRESHNESS_*_HOURS thresholds. Backed by `integration_metrics`. Scope: `consume:freshness`.","responses":{"200":{"description":"Freshness report.","content":{"application/json":{"schema":{"type":"object","properties":{"generated_at":{"type":"string","format":"date-time"},"warn_hours":{"type":"integer"},"crit_hours":{"type":"integer"},"summary":{"type":"object","properties":{"total":{"type":"integer"},"fresh":{"type":"integer"},"stale":{"type":"integer"},"critical":{"type":"integer"},"never":{"type":"integer"}}},"rows":{"type":"array","items":{"type":"object","properties":{"module":{"type":"string"},"org_id":{"type":"string"},"last_run":{"type":"string","format":"date-time","nullable":true},"last_status":{"type":"string","nullable":true},"age_seconds":{"type":"integer","nullable":true},"status":{"type":"string","enum":["fresh","stale","critical","never"]}}}}}}}}}}}},"/api/consume/audit/overview/":{"get":{"tags":["audit"],"summary":"API usage + security audit overview","description":"Request volume by day (allowed vs denied), top clients and endpoints, flagged IPs (auth failures / rate-limit denials), and per-credential activity — in one call. Backed by the `audit_*` SQL functions (migration 0017); rows are written by the Edge proxy on each gated request.","parameters":[{"name":"days","in":"query","schema":{"type":"integer","default":30,"minimum":1,"maximum":365},"description":"Look-back window in days (1–365)."},{"name":"top_limit","in":"query","schema":{"type":"integer","default":10},"description":"How many top clients / endpoints (1–100)."},{"name":"ip_limit","in":"query","schema":{"type":"integer","default":20},"description":"How many flagged IPs (1–100)."}],"responses":{"200":{"description":"Audit overview.","content":{"application/json":{"schema":{"type":"object","properties":{"window_days":{"type":"integer"},"summary":{"type":"object","properties":{"total":{"type":"integer"},"allowed":{"type":"integer"},"invalid_auth":{"type":"integer"},"forbidden":{"type":"integer"},"rate_limited":{"type":"integer"},"unique_clients":{"type":"integer"},"unique_fail_ips":{"type":"integer"}}},"by_day":{"type":"array","items":{"type":"object","properties":{"day":{"type":"string","format":"date"},"allowed":{"type":"integer"},"denied":{"type":"integer"}}}},"top_clients":{"type":"array","items":{"type":"object","properties":{"rate_key":{"type":"string"},"client_label":{"type":"string"},"requests":{"type":"integer"}}}},"top_paths":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"requests":{"type":"integer"}}}},"security_ips":{"type":"array","items":{"type":"object","properties":{"ip":{"type":"string"},"invalid_auth":{"type":"integer"},"rate_limited":{"type":"integer"},"last_seen":{"type":"string","format":"date-time"}}}},"credentials":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"client_id":{"type":"string","nullable":true},"client_user":{"type":"string","nullable":true},"is_active":{"type":"boolean"},"last_used":{"type":"string","format":"date-time","nullable":true},"requests":{"type":"integer"}}}}}}}}}}}},"/api/consume/timeline/":{"get":{"tags":["consume:zoho"],"summary":"Item stock/status change history","description":"Paginated `timeline_item` — one row per detected stock or status change during an item sync, newest first. Filter by `item_number` (a Zoho item_id) or `item_numbers` (comma-separated), and a `start_date`/`end_date` range on `created_at`.","parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PageSize"},{"$ref":"#/components/parameters/Count"},{"$ref":"#/components/parameters/Format"},{"$ref":"#/components/parameters/ZohoOrgId"},{"$ref":"#/components/parameters/StartDate"},{"$ref":"#/components/parameters/EndDate"},{"$ref":"#/components/parameters/OnlyFields"},{"name":"item_number","in":"query","schema":{"type":"string"},"description":"Single Zoho `item_id`."},{"name":"item_numbers","in":"query","schema":{"type":"string"},"description":"Comma-separated Zoho `item_id`s."}],"responses":{"200":{"description":"Paginated change history.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Pagination"},{"type":"object","properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/TimelineItem"}}}}]}}}}}}},"/api/consume/reconciliation/inventory/":{"get":{"tags":["reconciliation"],"summary":"Reconcile Zoho stock vs Senitron RFID counts","description":"Joins `senitron_items` (RFID physical count) to `zoho_inventory_items` (stock-on-hand) on `item_number = item_id`. Each row has `variance` (zoho − rfid) and `status` (match / over / under / missing_in_zoho), ordered by |variance| desc.","parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PageSize"},{"$ref":"#/components/parameters/Count"},{"$ref":"#/components/parameters/Format"},{"$ref":"#/components/parameters/ZohoOrgId"},{"name":"only_discrepancies","in":"query","schema":{"type":"boolean"},"description":"`true` to omit matched rows."}],"responses":{"200":{"description":"Paginated reconciliation rows.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Pagination"},{"type":"object","properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/ReconciliationItem"}}}}]}}}}}}},"/api/consume/reconciliation/inventory/summary/":{"get":{"tags":["reconciliation"],"summary":"Reconciliation headline counts","description":"Totals by status + total absolute unit variance for a tenant.","parameters":[{"$ref":"#/components/parameters/ZohoOrgId"}],"responses":{"200":{"description":"Summary counts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReconciliationSummary"}}}}}}},"/api/consume/senitron/quantities/":{"get":{"tags":["consume:senitron"],"summary":"List Senitron item quantities","description":"Paginated list of `senitron_items` (per-item tag counts and quantities), populated by `/api/senitron/load_quantities/`. Net-new — the legacy never exposed this feed.","parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PageSize"},{"$ref":"#/components/parameters/Count"},{"$ref":"#/components/parameters/Format"},{"$ref":"#/components/parameters/ItemNumbers"},{"$ref":"#/components/parameters/OnlyFields"}],"responses":{"200":{"description":"Paginated quantities list.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Pagination"},{"type":"object","properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/SenitronItem"}}}}]}}}}}}},"/api/consume/senitron/item_assets/":{"get":{"tags":["consume:senitron"],"summary":"List Senitron item assets","description":"Paginated list of `senitron_item_assets`. Mirrors the legacy `/api/consume/senitron/item_assets/` endpoint.","parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PageSize"},{"$ref":"#/components/parameters/Count"},{"$ref":"#/components/parameters/Format"},{"$ref":"#/components/parameters/ItemNumbers"},{"$ref":"#/components/parameters/OnlyFields"}],"responses":{"200":{"description":"Paginated asset list.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Pagination"},{"type":"object","properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/SenitronItemAsset"}}}}]}}}},"400":{"description":"Invalid query parameters."}}}},"/api/consume/senitron/item_assets_logs/":{"get":{"tags":["consume:senitron"],"summary":"List Senitron item asset logs","description":"Paginated list of `senitron_item_asset_logs`. Mirrors the legacy `/api/consume/senitron/item_assets_logs/` endpoint.","parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PageSize"},{"$ref":"#/components/parameters/Count"},{"$ref":"#/components/parameters/Format"},{"$ref":"#/components/parameters/ItemNumbers"},{"$ref":"#/components/parameters/OnlyFields"}],"responses":{"200":{"description":"Paginated log list.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Pagination"},{"type":"object","properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/SenitronItemAssetLog"}}}}]}}}}}}},"/api/consume/zoho/items/":{"get":{"tags":["consume:zoho"],"summary":"List Zoho inventory items","parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PageSize"},{"$ref":"#/components/parameters/Count"},{"$ref":"#/components/parameters/Format"},{"$ref":"#/components/parameters/ZohoOrgId"},{"$ref":"#/components/parameters/StartLastModifiedTime"},{"$ref":"#/components/parameters/EndLastModifiedTime"},{"$ref":"#/components/parameters/OnlyFields"}],"responses":{"200":{"description":"Paginated item list.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Pagination"},{"type":"object","properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/ZohoInventoryItem"}}}}]}}}},"400":{"description":"Invalid query parameters."}}}},"/api/consume/zoho/itemgroups/":{"get":{"tags":["consume:zoho"],"summary":"List Zoho item groups","parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PageSize"},{"$ref":"#/components/parameters/Count"},{"$ref":"#/components/parameters/Format"},{"$ref":"#/components/parameters/ZohoOrgId"},{"$ref":"#/components/parameters/StartLastModifiedTime"},{"$ref":"#/components/parameters/EndLastModifiedTime"},{"$ref":"#/components/parameters/OnlyFields"}],"responses":{"200":{"description":"Paginated item-group list.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Pagination"},{"type":"object","properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/ZohoItemGroup"}}}}]}}}}}}},"/api/consume/zoho/customers/":{"get":{"tags":["consume:zoho"],"summary":"List Zoho customers","description":"Paginated list. Adds substring filters on first_name, last_name, email, and a combined phone-or-mobile filter.","parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PageSize"},{"$ref":"#/components/parameters/Count"},{"$ref":"#/components/parameters/Format"},{"$ref":"#/components/parameters/ZohoOrgId"},{"$ref":"#/components/parameters/StartLastModifiedTime"},{"$ref":"#/components/parameters/EndLastModifiedTime"},{"$ref":"#/components/parameters/OnlyFields"},{"name":"first_name","in":"query","schema":{"type":"string"},"description":"Case-insensitive substring match."},{"name":"last_name","in":"query","schema":{"type":"string"},"description":"Case-insensitive substring match."},{"name":"email","in":"query","schema":{"type":"string"},"description":"Case-insensitive substring match."},{"name":"phone","in":"query","schema":{"type":"string"},"description":"Substring match against either `phone` or `mobile`."},{"name":"mobile","in":"query","schema":{"type":"string"},"description":"Same effect as `phone`. If both are present, `phone` wins."}],"responses":{"200":{"description":"Paginated customer list.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Pagination"},{"type":"object","properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/ZohoCustomer"}}}}]}}}}}}},"/api/consume/zoho/shipment_orders/":{"get":{"tags":["consume:zoho"],"summary":"List Zoho shipment orders","description":"`start_date`/`end_date` filter `shipping_date`; `start_last_modified_time`/`end_last_modified_time` filter `updated_at`.","parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PageSize"},{"$ref":"#/components/parameters/Count"},{"$ref":"#/components/parameters/Format"},{"$ref":"#/components/parameters/ZohoOrgId"},{"$ref":"#/components/parameters/StartDate"},{"$ref":"#/components/parameters/EndDate"},{"$ref":"#/components/parameters/StartLastModifiedTime"},{"$ref":"#/components/parameters/EndLastModifiedTime"},{"$ref":"#/components/parameters/OnlyFields"}],"responses":{"200":{"description":"Paginated shipment-order list.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Pagination"},{"type":"object","properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/ZohoShipmentOrder"}}}}]}}}}}}},"/api/consume/zoho/packages/":{"get":{"tags":["consume:zoho"],"summary":"List Zoho packages","parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PageSize"},{"$ref":"#/components/parameters/Count"},{"$ref":"#/components/parameters/Format"},{"$ref":"#/components/parameters/ZohoOrgId"},{"$ref":"#/components/parameters/StartLastModifiedTime"},{"$ref":"#/components/parameters/EndLastModifiedTime"},{"$ref":"#/components/parameters/OnlyFields"},{"name":"shipment_ids","in":"query","schema":{"type":"string"},"description":"Comma-separated `shipment_id` filter."}],"responses":{"200":{"description":"Paginated package list.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Pagination"},{"type":"object","properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/ZohoPackage"}}}}]}}}}}}},"/api/consume/zoho/full_purchase_orders/":{"get":{"tags":["consume:zoho"],"summary":"List Zoho purchase orders enriched with their line items","description":"Same filters as `purchase_orders/`, but each row carries a `line_items` array (from `zoho_purchase_order_line_items`). POs have a vendor (not a customer) and there is no vendor snapshot, so \"full\" here means the order plus its lines. `only_fields` is not honored.","parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PageSize"},{"$ref":"#/components/parameters/Count"},{"$ref":"#/components/parameters/Format"},{"$ref":"#/components/parameters/ZohoOrgId"},{"$ref":"#/components/parameters/StartDate"},{"$ref":"#/components/parameters/EndDate"},{"$ref":"#/components/parameters/StartLastModifiedTime"},{"$ref":"#/components/parameters/EndLastModifiedTime"},{"name":"purchaseorders_ids","in":"query","schema":{"type":"string"},"description":"Comma-separated `purchaseorder_id` filter."},{"name":"salesorders_ids","in":"query","schema":{"type":"string"},"description":"Comma-separated linked `salesorder_id` filter."}],"responses":{"200":{"description":"Paginated purchase-order list, each with its line items.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Pagination"},{"type":"object","properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/ZohoFullPurchaseOrder"}}}}]}}}}}}},"/api/consume/zoho/purchase_receives/":{"get":{"tags":["consume:zoho"],"summary":"List Zoho purchase receives","description":"Paginated `zoho_purchase_receives` (goods receipts against POs), populated by `/api/zoho/load_purchase_receives/<org>/`. Net-new. Parent rows only — use the by-id route for line items.","parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PageSize"},{"$ref":"#/components/parameters/Count"},{"$ref":"#/components/parameters/Format"},{"$ref":"#/components/parameters/ZohoOrgId"},{"$ref":"#/components/parameters/StartDate"},{"$ref":"#/components/parameters/EndDate"},{"$ref":"#/components/parameters/StartLastModifiedTime"},{"$ref":"#/components/parameters/EndLastModifiedTime"},{"$ref":"#/components/parameters/OnlyFields"},{"name":"receives_ids","in":"query","schema":{"type":"string"},"description":"Comma-separated `receive_id` filter."},{"name":"purchaseorders_ids","in":"query","schema":{"type":"string"},"description":"Comma-separated `purchaseorder_id` filter."}],"responses":{"200":{"description":"Paginated purchase-receive list.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Pagination"},{"type":"object","properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/ZohoPurchaseReceive"}}}}]}}}}}}},"/api/consume/zoho/purchase_receives/{receive_id}/":{"get":{"tags":["consume:zoho"],"summary":"Get one Zoho purchase receive with its line items","description":"Returns the parent record enriched with a `line_items` array (from `zoho_purchase_receive_line_items`).","parameters":[{"name":"receive_id","in":"path","required":true,"schema":{"type":"string"},"description":"Zoho `receive_id`."},{"$ref":"#/components/parameters/ZohoOrgId"}],"responses":{"200":{"description":"The purchase receive with embedded line items.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ZohoPurchaseReceive"},{"type":"object","properties":{"line_items":{"type":"array","items":{"type":"object"}}}}]}}}},"404":{"description":"No purchase receive with that id."}}}},"/api/consume/zoho/purchase_orders/{purchaseorder_id}/":{"get":{"tags":["consume:zoho"],"summary":"Get one Zoho purchase order with its line items","description":"Returns the parent record enriched with a `line_items` array (from `zoho_purchase_order_line_items`). The list endpoint returns parent rows only.","parameters":[{"name":"purchaseorder_id","in":"path","required":true,"schema":{"type":"string"},"description":"Zoho `purchaseorder_id`."},{"$ref":"#/components/parameters/ZohoOrgId"}],"responses":{"200":{"description":"The purchase order with embedded line items.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ZohoPurchaseOrder"},{"type":"object","properties":{"line_items":{"type":"array","items":{"type":"object"}}}}]}}}},"404":{"description":"No purchase order with that id."}}}},"/api/consume/zoho/purchase_orders/":{"get":{"tags":["consume:zoho"],"summary":"List Zoho purchase orders","description":"`start_date`/`end_date` filter `date`; `start_last_modified_time`/`end_last_modified_time` filter `last_modified_time`. `purchaseorders_ids` filters the typed `purchaseorder_id`; `salesorders_ids` filters the linked sales order via `raw_data->>salesorder_id`.","parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PageSize"},{"$ref":"#/components/parameters/Count"},{"$ref":"#/components/parameters/Format"},{"$ref":"#/components/parameters/ZohoOrgId"},{"$ref":"#/components/parameters/StartDate"},{"$ref":"#/components/parameters/EndDate"},{"$ref":"#/components/parameters/StartLastModifiedTime"},{"$ref":"#/components/parameters/EndLastModifiedTime"},{"$ref":"#/components/parameters/OnlyFields"},{"name":"purchaseorders_ids","in":"query","schema":{"type":"string"},"description":"Comma-separated `purchaseorder_id` filter."},{"name":"salesorders_ids","in":"query","schema":{"type":"string"},"description":"Comma-separated linked `salesorder_id` filter."}],"responses":{"200":{"description":"Paginated purchase-order list.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Pagination"},{"type":"object","properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/ZohoPurchaseOrder"}}}}]}}}}}}},"/api/consume/zoho/full_invoices/":{"get":{"tags":["consume:zoho"],"summary":"List Zoho invoices enriched with their customer","description":"Same filters as `invoices/`, but each row carries a `customer` object joined from `zoho_customers` by `customer_id`. Mirrors `full_sales_orders/`. `only_fields` is not honored.","parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PageSize"},{"$ref":"#/components/parameters/Count"},{"$ref":"#/components/parameters/Format"},{"$ref":"#/components/parameters/ZohoOrgId"},{"$ref":"#/components/parameters/StartDate"},{"$ref":"#/components/parameters/EndDate"},{"$ref":"#/components/parameters/StartLastModifiedTime"},{"$ref":"#/components/parameters/EndLastModifiedTime"},{"name":"salesorders_ids","in":"query","schema":{"type":"string"},"description":"Comma-separated `salesorder_id` filter."}],"responses":{"200":{"description":"Paginated invoice list, each with its customer.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Pagination"},{"type":"object","properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/ZohoFullInvoice"}}}}]}}}}}}},"/api/consume/zoho/invoices/{invoice_id}/":{"get":{"tags":["consume:zoho"],"summary":"Get one Zoho invoice with its line items","description":"Returns the parent record enriched with a `line_items` array (from `zoho_invoice_line_items`). The list endpoint returns parent rows only.","parameters":[{"name":"invoice_id","in":"path","required":true,"schema":{"type":"string"},"description":"Zoho `invoice_id`."},{"$ref":"#/components/parameters/ZohoOrgId"}],"responses":{"200":{"description":"The invoice with embedded line items.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ZohoInvoice"},{"type":"object","properties":{"line_items":{"type":"array","items":{"type":"object"}}}}]}}}},"404":{"description":"No invoice with that id."}}}},"/api/consume/zoho/invoices/":{"get":{"tags":["consume:zoho"],"summary":"List Zoho invoices","parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PageSize"},{"$ref":"#/components/parameters/Count"},{"$ref":"#/components/parameters/Format"},{"$ref":"#/components/parameters/ZohoOrgId"},{"$ref":"#/components/parameters/StartDate"},{"$ref":"#/components/parameters/EndDate"},{"$ref":"#/components/parameters/StartLastModifiedTime"},{"$ref":"#/components/parameters/EndLastModifiedTime"},{"$ref":"#/components/parameters/OnlyFields"},{"name":"salesorders_ids","in":"query","schema":{"type":"string"},"description":"Comma-separated `salesorder_id` filter."}],"responses":{"200":{"description":"Paginated invoice list.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Pagination"},{"type":"object","properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/ZohoInvoice"}}}}]}}}}}}},"/api/consume/zoho/customers/{customer_id}/":{"get":{"tags":["consume:zoho"],"summary":"Get one Zoho customer by id","description":"Returns a single customer row by its natural Zoho `customer_id`. The list endpoint returns paged rows.","parameters":[{"name":"customer_id","in":"path","required":true,"schema":{"type":"string"},"description":"Zoho `customer_id` (contact id)."},{"$ref":"#/components/parameters/ZohoOrgId"}],"responses":{"200":{"description":"The customer record.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ZohoCustomer"}}}},"404":{"description":"No customer with that id."}}}},"/api/consume/zoho/items/{item_id}/":{"get":{"tags":["consume:zoho"],"summary":"Get one Zoho inventory item by id","description":"Returns a single inventory item row by its natural Zoho `item_id`. The list endpoint returns paged rows.","parameters":[{"name":"item_id","in":"path","required":true,"schema":{"type":"string"},"description":"Zoho `item_id`."},{"$ref":"#/components/parameters/ZohoOrgId"}],"responses":{"200":{"description":"The inventory item record.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ZohoInventoryItem"}}}},"404":{"description":"No item with that id."}}}},"/api/consume/zoho/shipment_orders/{shipment_id}/":{"get":{"tags":["consume:zoho"],"summary":"Get one Zoho shipment order by id","description":"Returns a single shipment order row by its natural Zoho `shipment_id`. The list endpoint returns paged rows.","parameters":[{"name":"shipment_id","in":"path","required":true,"schema":{"type":"string"},"description":"Zoho `shipment_id`."},{"$ref":"#/components/parameters/ZohoOrgId"}],"responses":{"200":{"description":"The shipment order record.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ZohoShipmentOrder"}}}},"404":{"description":"No shipment order with that id."}}}},"/api/consume/zoho/packages/{package_id}/":{"get":{"tags":["consume:zoho"],"summary":"Get one Zoho package by id","description":"Returns a single package row by its natural Zoho `package_id`. The embedded `shipment_order` and `line_items` are inside `raw_data`. The list endpoint returns paged rows.","parameters":[{"name":"package_id","in":"path","required":true,"schema":{"type":"string"},"description":"Zoho `package_id`."},{"$ref":"#/components/parameters/ZohoOrgId"}],"responses":{"200":{"description":"The package record.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ZohoPackage"}}}},"404":{"description":"No package with that id."}}}},"/api/consume/zoho/sales_orders/{salesorder_id}/":{"get":{"tags":["consume:zoho"],"summary":"Get one Zoho sales order with its line items","description":"Returns the parent record enriched with a `line_items` array (from `zoho_sales_order_line_items`). The list endpoint returns parent rows only.","parameters":[{"name":"salesorder_id","in":"path","required":true,"schema":{"type":"string"},"description":"Zoho `salesorder_id`."},{"$ref":"#/components/parameters/ZohoOrgId"}],"responses":{"200":{"description":"The sales order with embedded line items.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/ZohoSalesOrder"},{"type":"object","properties":{"line_items":{"type":"array","items":{"type":"object"}}}}]}}}},"404":{"description":"No sales order with that id."}}}},"/api/consume/zoho/sales_orders/":{"get":{"tags":["consume:zoho"],"summary":"List Zoho sales orders","description":"`installation_name` does a substring match against `zoho_sales_order_line_items.name` and returns only the parents with at least one matching line item.","parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PageSize"},{"$ref":"#/components/parameters/Count"},{"$ref":"#/components/parameters/Format"},{"$ref":"#/components/parameters/ZohoOrgId"},{"$ref":"#/components/parameters/StartDate"},{"$ref":"#/components/parameters/EndDate"},{"$ref":"#/components/parameters/StartLastModifiedTime"},{"$ref":"#/components/parameters/EndLastModifiedTime"},{"$ref":"#/components/parameters/OnlyFields"},{"name":"sales_orders_ids","in":"query","schema":{"type":"string"},"description":"Comma-separated include list (`salesorder_id`)."},{"name":"not_sales_orders_ids","in":"query","schema":{"type":"string"},"description":"Comma-separated exclude list. Ignored if `sales_orders_ids` is also present."},{"name":"installation_name","in":"query","schema":{"type":"string"},"description":"Substring match against any line item's `name`."}],"responses":{"200":{"description":"Paginated sales-order list.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Pagination"},{"type":"object","properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/ZohoSalesOrder"}}}}]}}}}}}},"/api/zoho/generate_auth_url/{zoho_org_id}/":{"get":{"tags":["zoho:integration"],"security":[],"summary":"Build the Zoho OAuth consent URL for an org","description":"Returns `{auth_url}` — the URL the admin user must visit to grant the app access. After consent, Zoho redirects back to `get_refresh_token/<org>/?code=...` which persists the token to Supabase Vault.","parameters":[{"name":"zoho_org_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Consent URL generated.","content":{"application/json":{"schema":{"type":"object","required":["auth_url"],"properties":{"auth_url":{"type":"string"}}}}}},"404":{"description":"Unknown organization."},"409":{"description":"client_id or redirect_uri not configured yet."}}}},"/api/zoho/get_refresh_token/{zoho_org_id}/":{"get":{"tags":["zoho:integration"],"security":[],"summary":"OAuth callback — exchange auth code for refresh token","description":"Zoho redirects here after consent with `?code=...`. We exchange the code for an access + refresh token pair, store the refresh token in Supabase Vault, and 302-redirect to `zoho_api_settings/<org>/`.","parameters":[{"name":"zoho_org_id","in":"path","required":true,"schema":{"type":"string"}},{"name":"code","in":"query","required":true,"schema":{"type":"string"},"description":"Authorization code returned by Zoho's consent flow."}],"responses":{"302":{"description":"Redirect to the settings endpoint on success."},"400":{"description":"Missing `code` query param."},"404":{"description":"Unknown organization."},"500":{"description":"Token exchange failed."}}}},"/api/zoho/zoho_api_settings/{zoho_org_id}/":{"get":{"tags":["zoho:integration"],"security":[],"summary":"Get the current Zoho OAuth state for an org","description":"Returns `{app_config, connected, auth_url, zoho_connection_configured}`. Initializes an empty `org_zoho_config` row if the org exists but has no row yet. **Secrets are never returned** — only Vault IDs.","parameters":[{"name":"zoho_org_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Settings payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ZohoApiSettings"}}}},"404":{"description":"Unknown organization."}}}},"/api/zoho/load_sales_orders/{zoho_org_id}/":{"post":{"tags":["zoho:integration"],"summary":"Pull sales orders from Zoho into Supabase","description":"Lists Zoho sales orders modified since `start_date` (defaults to the `last_sync_date_salesorders` watermark for the org), fetches each detail concurrently, and upserts to `zoho_sales_orders` + `zoho_sales_order_line_items`. Records a row in `integration_metrics` and updates the sync watermark on success.","parameters":[{"name":"zoho_org_id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"start_date":{"type":"string","format":"date"}}}}}},"responses":{"200":{"description":"Sync result.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["ok","partial"]},"created":{"type":"integer"},"updated":{"type":"integer"},"list_calls":{"type":"integer"},"detail_calls":{"type":"integer"},"duration_sec":{"type":"number"},"start_date":{"type":"string"},"zoho_org_id":{"type":"string"},"cutoff":{"type":"string"}}}}}},"400":{"description":"Invalid JSON body."},"500":{"description":"Upsert failed."},"502":{"description":"Zoho list endpoint failed."}}}},"/api/zoho/load_sales_orders/customer/{zoho_org_id}/":{"post":{"tags":["zoho:integration"],"summary":"Pull sales orders by customer name + upsert","parameters":[{"name":"zoho_org_id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["customer_name"],"properties":{"customer_name":{"type":"string"}}}}}},"responses":{"200":{"description":"Loaded."},"400":{"description":"Missing customer_name or invalid JSON."},"500":{"description":"Upsert failed."},"502":{"description":"Zoho list endpoint failed."}}}},"/api/zoho/load_sales_orders_to_qbwc/{zoho_org_id}/":{"post":{"tags":["zoho:integration"],"summary":"Pull sales orders for a date and return raw payload (no persistence)","description":"Used by the QuickBooks Web Connector. Returns the full Zoho detail payloads in the response — no Supabase upsert.","parameters":[{"name":"zoho_org_id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["date"],"properties":{"date":{"type":"string","format":"date"}}}}}},"responses":{"200":{"description":"Raw sales-order payloads.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"count":{"type":"integer"},"data":{"type":"array","items":{"type":"object"}}}}}}},"400":{"description":"Missing or invalid date."},"502":{"description":"Zoho list endpoint failed."}}}},"/api/zoho/load_invoices/customer/{zoho_org_id}/":{"post":{"tags":["zoho:integration"],"summary":"Pull Zoho Books invoices by customer name + upsert","parameters":[{"name":"zoho_org_id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["customer_name"],"properties":{"customer_name":{"type":"string"}}}}}},"responses":{"200":{"description":"Loaded."},"400":{"description":"Missing customer_name or invalid JSON."},"500":{"description":"Upsert failed."},"502":{"description":"Zoho list endpoint failed."}}}},"/api/zoho/metrics/":{"get":{"tags":["zoho:integration"],"security":[],"summary":"Latest integration metrics per (module, org)","description":"Returns `{module: {org_id: row}}` for every known module. Renders an HTML table when `?format=html` or `Accept: text/html`.","parameters":[{"name":"format","in":"query","schema":{"type":"string","enum":["json","html"]},"description":"Override the response format."}],"responses":{"200":{"description":"Metrics payload.","content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{"type":"object"}}}},"text/html":{"schema":{"type":"string"}}}}}}},"/api/zoho/connect/{zoho_org_id}/":{"get":{"tags":["zoho:integration"],"security":[],"summary":"Test the Zoho OAuth connection for an org","description":"Mints a short-lived access token using the stored refresh_token. Returns 200 if the round-trip succeeded. Diverges from legacy by returning real status codes on failure — the legacy view always returned 200.","parameters":[{"name":"zoho_org_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Connection healthy.","content":{"application/json":{"schema":{"type":"object","required":["message"],"properties":{"message":{"type":"string"}}}}}},"404":{"description":"Unknown organization."},"409":{"description":"OAuth not configured yet (run consent first)."},"502":{"description":"Zoho rejected the refresh request."}}}},"/api/consume/zoho/secure-endpoint/":{"get":{"tags":["consume:zoho"],"summary":"Auth probe","description":"Returns a canned message after the auth middleware accepts the request. External clients use it to verify their token works.","responses":{"200":{"description":"OK.","content":{"application/json":{"schema":{"type":"object","required":["message"],"properties":{"message":{"type":"string"}}}}}}}}},"/api/consume/zoho/delete/sales_orders/":{"get":{"tags":["consume:zoho"],"summary":"Delete sales orders by id","description":"Deletes the matching `zoho_sales_orders` rows (line items cascade). The legacy endpoint uses GET to mutate state — preserved here for client compatibility, do not use as a template for new endpoints.","parameters":[{"name":"sales_orders_ids","in":"query","required":true,"schema":{"type":"string"},"description":"Comma-separated `salesorder_id` list to delete."}],"responses":{"200":{"description":"Sales orders deleted.","content":{"application/json":{"schema":{"type":"object","required":["message"],"properties":{"message":{"type":"string"}}}}}},"400":{"description":"Missing `sales_orders_ids`."}}}},"/api/consume/zoho/invoices_to_rewards_points/":{"get":{"tags":["consume:zoho"],"summary":"Invoices feed for the rewards-points engine","description":"Returns `{count, is_email_synced, results}` (no pagination). Defaults to the main NWS org if `zoho_org_id` is omitted, matching the legacy hardcoded filter. The contact-persons email substring search from the legacy is not yet ported — see TODO in the route.","parameters":[{"$ref":"#/components/parameters/ZohoOrgId"},{"$ref":"#/components/parameters/OnlyFields"},{"name":"status","in":"query","schema":{"type":"string","enum":["paid","unpaid","overdue","sent","draft"]}},{"name":"email","in":"query","schema":{"type":"string"},"description":"Substring match against the invoice email."},{"name":"last_modified_time","in":"query","schema":{"type":"string","format":"date-time"},"description":"ISO-8601 lower bound on `last_modified_time`."},{"name":"customer_id","in":"query","schema":{"type":"string"}},{"name":"salesorders_ids","in":"query","schema":{"type":"string"},"description":"Comma-separated `salesorder_id` filter."}],"responses":{"200":{"description":"Invoice list.","content":{"application/json":{"schema":{"type":"object","required":["count","is_email_synced","results"],"properties":{"count":{"type":"integer"},"is_email_synced":{"type":"boolean"},"results":{"type":"array","items":{"$ref":"#/components/schemas/ZohoInvoice"}}}}}}},"400":{"description":"Invalid `status` or `last_modified_time`."}}}},"/api/consume/zoho/refetch_salesorder/{zoho_org_id}/{salesorder_number}/":{"get":{"tags":["consume:zoho"],"summary":"Refetch a sales order live from Zoho","description":"Live Zoho fetch by `salesorder_number`. Returns `{count, results}` with the full Zoho detail payload. No Supabase persistence.","parameters":[{"name":"zoho_org_id","in":"path","required":true,"schema":{"type":"string"}},{"name":"salesorder_number","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Sales order payload(s)."},"502":{"description":"Zoho list endpoint failed."}}}},"/api/consume/zoho/sales_orders_to_service/":{"get":{"tags":["consume:zoho"],"summary":"Multi-mode sales-order lookup with optional live Zoho calls","description":"Three modes (priority): `salesorder_number`, `date`, or free-text customer search. The first two also fetch live from Zoho (both orgs) unless `is_recent=true`. The customer mode is currently local-only — see TODO in the route.","parameters":[{"name":"salesorder_number","in":"query","schema":{"type":"string"}},{"name":"date","in":"query","schema":{"type":"string","format":"date"}},{"name":"is_recent","in":"query","schema":{"type":"boolean"}},{"name":"company_name","in":"query","schema":{"type":"string"}},{"name":"first_name","in":"query","schema":{"type":"string"}},{"name":"last_name","in":"query","schema":{"type":"string"}},{"name":"phone","in":"query","schema":{"type":"string"}},{"name":"email","in":"query","schema":{"type":"string"}},{"name":"last_modified_time","in":"query","schema":{"type":"string","format":"date-time"}},{"$ref":"#/components/parameters/ZohoOrgId"},{"$ref":"#/components/parameters/OnlyFields"}],"responses":{"200":{"description":"Merged sales-order list.","content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer"},"results":{"type":"array","items":{"type":"object"}}}}}}},"400":{"description":"Invalid date or last_modified_time."}}}},"/api/manage/auth/ajax/load-modules/":{"get":{"tags":["manage:auth"],"security":[],"description":"Cookie-auth (Supabase Auth session, is_staff). Not bearer.","summary":"List modules of a system (UI selector helper)","parameters":[{"name":"system_id","in":"query","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Modules."}}}},"/api/manage/auth/ajax/load-permissions/":{"get":{"tags":["manage:auth"],"security":[],"description":"Cookie-auth (Supabase Auth session, is_staff). Not bearer.","summary":"List permissions of a module (UI selector helper)","parameters":[{"name":"module_id","in":"query","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Permissions."}}}},"/api/manage/auth/ajax/load-user-permissions/":{"get":{"tags":["manage:auth"],"security":[],"description":"Cookie-auth (Supabase Auth session, is_staff). Not bearer.","summary":"List a user's permissions with system/module context","parameters":[{"name":"user_id","in":"query","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Sorted permissions list."}}}},"/api/manage/auth/ajax/assign-permissions/":{"post":{"tags":["manage:auth"],"security":[],"description":"Cookie-auth (Supabase Auth session, is_staff). Not bearer.","summary":"Bulk-assign permissions to a user (idempotent)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["user_id","permissions"],"properties":{"user_id":{"type":"string","format":"uuid"},"permissions":{"type":"array","items":{"type":"string","format":"uuid"}},"org_id":{"type":"string"}}}}}},"responses":{"200":{"description":"Returns {status: success|no_changes|error}."}}}},"/api/manage/auth/users/permissions/":{"get":{"tags":["manage:auth"],"security":[],"description":"Cookie-auth (Supabase Auth session, is_staff). Not bearer.","summary":"Paginated users with their assigned permissions tree","parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PageSize"},{"$ref":"#/components/parameters/Count"},{"$ref":"#/components/parameters/Format"},{"name":"username","in":"query","schema":{"type":"string"},"description":"Match the auth user's email exactly."},{"name":"org_id","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Paginated user list."}}}},"/api/manage/auth/users/manage-user-permissions/":{"post":{"tags":["manage:auth"],"security":[],"summary":"Upsert a user and replace their permissions (by name)","description":"Cookie-auth (Supabase Auth session, is_staff). Not bearer. Creates the auth user if missing (Supabase Auth admin API), upserts the profile + org membership, and **replaces** the user's permission grants for the org with the set named in `data` (system → modules → permissions, all looked up by name). `username` is treated as the email.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["username","data"],"properties":{"username":{"type":"string"},"password":{"type":"string"},"email":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"},"is_staff":{"type":"boolean"},"is_active":{"type":"boolean"},"phone_number":{"type":"string"},"country":{"type":"string"},"state":{"type":"string"},"city":{"type":"string"},"address":{"type":"string"},"zip_code":{"type":"string"},"org_id":{"type":"string"},"data":{"type":"object","properties":{"system":{"type":"string"},"modules":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"permissions":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"}}}}}}}}}}}}}},"responses":{"201":{"description":"User upserted."},"400":{"description":"Invalid body."},"404":{"description":"System / module / permission not found by name."},"500":{"description":"Auth or DB write failed."}}}},"/api/consume/zoho/full_sales_orders/":{"get":{"tags":["consume:zoho"],"summary":"List Zoho sales orders enriched with customer","description":"Identical filters to `sales_orders/`. Each result row gets a `customer` field populated from `zoho_customers` by `contact_id`.","parameters":[{"$ref":"#/components/parameters/Page"},{"$ref":"#/components/parameters/PageSize"},{"$ref":"#/components/parameters/Count"},{"$ref":"#/components/parameters/Format"},{"$ref":"#/components/parameters/ZohoOrgId"},{"$ref":"#/components/parameters/StartDate"},{"$ref":"#/components/parameters/EndDate"},{"$ref":"#/components/parameters/StartLastModifiedTime"},{"$ref":"#/components/parameters/EndLastModifiedTime"},{"$ref":"#/components/parameters/OnlyFields"},{"name":"sales_orders_ids","in":"query","schema":{"type":"string"}},{"name":"not_sales_orders_ids","in":"query","schema":{"type":"string"}},{"name":"installation_name","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Paginated, customer-enriched sales-order list.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Pagination"},{"type":"object","properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/ZohoFullSalesOrder"}}}}]}}}}}}}}}