{"openapi":"3.1.0","info":{"title":"Trackable ECOM API","version":"0.1.0","description":"Multi-tenant e-commerce command centre: unified orders, sales analytics, ad-platform reporting (Meta, Google Ads), and self-serve channel integrations. `source: live` means tenant-scoped Supabase rows; `source: mock` means typed demo fallback. Session routes require a signed-in user (same-origin cookies work in Try-it-out when logged in). Full narrative reference: docs/api.md."},"servers":[{"url":"/","description":"Same origin (local dev or deployed app)"}],"tags":[{"name":"System","description":"Liveness and machine-readable spec"},{"name":"Workspace","description":"Organization provisioning, read, rename"},{"name":"Dashboards","description":"KPIs, trends, splits, catalogue reads"},{"name":"Orders","description":"Unified cross-channel orders + CSV audit export"},{"name":"Sync","description":"Tenant-scoped sync triggers and reconciliation"},{"name":"Integrations","description":"Per-channel self-serve connect / test / serviceability"},{"name":"OAuth","description":"Browser provider-consent flows (302 redirects, not JSON)"},{"name":"Webhooks","description":"Inbound provider events + registration ops"}],"security":[],"components":{"securitySchemes":{"sessionCookie":{"type":"apiKey","in":"cookie","name":"sb-<project-ref>-auth-token","description":"Supabase auth session cookie (set by /login). In Try-it-out, sign in to the app in the same browser first — the cookie is sent automatically."},"registerSecret":{"type":"apiKey","in":"header","name":"x-register-secret","description":"Ops secret, value = SHOPIFY_API_SECRET."}},"schemas":{"ErrorEnvelope":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","example":false},"error":{"type":"string","example":"Not authenticated or no organization."}}},"DashboardMetrics":{"type":"object","properties":{"totalSales":{"type":"number"},"totalOrders":{"type":"integer"},"aov":{"type":"number"},"metaSpend":{"type":"number"},"metaRoas":{"type":["number","null"]},"googleSpend":{"type":"number"},"googleRoas":{"type":["number","null"]},"totalAdSpend":{"type":"number"},"blendedRoas":{"type":["number","null"]},"currency":{"type":"string","example":"INR"},"range":{"type":"object","properties":{"from":{"type":"string"},"to":{"type":"string"}}},"dataAsOf":{"type":"string","format":"date-time"},"source":{"type":"string","enum":["live","mock"]}}},"TrendPoint":{"type":"object","properties":{"date":{"type":"string","example":"2026-09-30"},"revenue":{"type":"number"},"orders":{"type":"integer"}}},"ChannelSales":{"type":"object","properties":{"channel":{"type":"string","enum":["shopify","amazon","flipkart"]},"orders":{"type":"integer"},"revenue":{"type":"number"},"share":{"type":"number"}}},"OrderRow":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organization_id":{"type":"string","format":"uuid"},"channel":{"type":"string"},"external_order_id":{"type":"string"},"order_number":{"type":["string","null"]},"order_date":{"type":"string","format":"date-time"},"currency":{"type":"string"},"gross_amount":{"type":"number"},"discount_amount":{"type":"number"},"refund_amount":{"type":"number"},"net_amount":{"type":"number"},"payment_mode":{"type":"string","enum":["cod","prepaid","unknown"]},"financial_status":{"type":"string"},"fulfillment_status":{"type":"string"},"customer_name":{"type":["string","null"]},"customer_email":{"type":["string","null"]},"shipping_pincode":{"type":["string","null"]}}},"ChannelStatus":{"type":"object","properties":{"connected":{"type":"boolean"},"source":{"type":"string","enum":["database","environment","none"]},"accountName":{"type":"string"},"authType":{"type":"string","enum":["oauth","api_key","login","unknown"]},"maskedDetails":{"type":"object","additionalProperties":{"type":"string"}},"lastSyncAt":{"type":["string","null"],"format":"date-time"}}},"SyncOutcome":{"type":"object","properties":{"channel":{"type":"string"},"status":{"type":"string","enum":["success","partial","failed"]},"recordsRead":{"type":"integer"},"recordsWritten":{"type":"integer"},"campaigns":{"type":"integer"},"dateRange":{"type":"object","properties":{"since":{"type":"string"},"until":{"type":"string"}}},"cursor":{"type":["string","null"]},"error":{"type":"string"},"live":{"type":"boolean"},"persistence":{"type":"string"}}}}},"paths":{"/api/health":{"get":{"tags":["System"],"summary":"Liveness probe","description":"No auth. DB connectivity, per-provider env flags, memory. Never exposes secrets.","responses":{"200":{"description":"Always 200; check `status` (`healthy` vs `operational-degraded`).","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/openapi.json":{"get":{"tags":["System"],"summary":"This OpenAPI document","responses":{"200":{"description":"The JSON rendered by /docs.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/org":{"get":{"tags":["Workspace"],"summary":"Current workspace","security":[{"sessionCookie":[]}],"responses":{"200":{"description":"orgId, organization {id,name,slug}, profile {id,role,email}.","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"No organization — complete onboarding (POST below).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"post":{"tags":["Workspace"],"summary":"Create / recover workspace","description":"Idempotent — returns the existing org when the profile already has one.","security":[{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":2,"example":"Acme Wellness"}}}}}},"responses":{"200":{"description":"Workspace context (same shape as GET).","content":{"application/json":{"schema":{"type":"object"}}}},"500":{"description":"Includes the actionable RLS/migration hint when policy-blocked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"patch":{"tags":["Workspace"],"summary":"Rename workspace (slug stays stable)","security":[{"sessionCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","minLength":2,"maxLength":80}}}}}},"responses":{"200":{"description":"Updated workspace context.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Name must be 2–80 characters.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Not authenticated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"No organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/dashboard":{"get":{"tags":["Dashboards"],"summary":"KPI metrics (public, RLS-scoped)","parameters":[{"name":"preset","in":"query","schema":{"type":"string","enum":["today","yesterday","last7","last30","mtd","custom"],"default":"last30"}},{"name":"from","in":"query","schema":{"type":"string","example":"2026-09-01"}},{"name":"to","in":"query","schema":{"type":"string","example":"2026-09-30"}}],"responses":{"200":{"description":"{ ok, data: DashboardMetrics, source, dataAsOf }.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"data":{"$ref":"#/components/schemas/DashboardMetrics"},"source":{"type":"string"},"dataAsOf":{"type":"string"}}}}}}}}},"/api/analytics":{"get":{"tags":["Dashboards"],"summary":"Trend + channel/status/payment splits","description":"Known limitation: always resolves the last 30 days; from/to are accepted but ineffective.","parameters":[{"name":"from","in":"query","schema":{"type":"string"}},{"name":"to","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"{ ok, data: { trend, channels, statuses, payments } }.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/orders":{"get":{"tags":["Orders"],"summary":"Unified orders (paginated, filterable)","parameters":[{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"default":1}},{"name":"pageSize","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"channel","in":"query","schema":{"type":"string","enum":["shopify","amazon","flipkart"]}},{"name":"payment","in":"query","schema":{"type":"string","enum":["cod","prepaid"]}},{"name":"status","in":"query","schema":{"type":"string"}},{"name":"q","in":"query","schema":{"type":"string","maxLength":100}},{"name":"from","in":"query","schema":{"type":"string"}},{"name":"to","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"{ ok, data: { rows, page, pageSize, total }, source, dataAsOf }.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"data":{"type":"object","properties":{"rows":{"type":"array","items":{"$ref":"#/components/schemas/OrderRow"}},"page":{"type":"integer"},"pageSize":{"type":"integer"},"total":{"type":"integer"}}}}}}}},"400":{"description":"Invalid query (see details).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/products":{"get":{"tags":["Dashboards"],"summary":"Catalogue (up to 100, live-first)","responses":{"200":{"description":"{ ok, data: Product[], source, dataAsOf }.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/reports/reconciliation":{"get":{"tags":["Orders"],"summary":"Reconciliation CSV download","parameters":[{"name":"from","in":"query","schema":{"type":"string"}},{"name":"to","in":"query","schema":{"type":"string"}},{"name":"channel","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"text/csv attachment (first 1000 rows, 15 columns).","content":{"text/csv":{"schema":{"type":"string"}}}}}}},"/api/sync/{channel}":{"post":{"tags":["Sync"],"summary":"Trigger a channel sync (tenant-scoped, logged)","security":[{"sessionCookie":[]}],"parameters":[{"name":"channel","in":"path","required":true,"schema":{"type":"string","enum":["shopify","meta","googleads","amazon","flipkart"]}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"mode":{"type":"string","enum":["incremental","full"],"default":"incremental"},"since":{"type":"string","format":"date-time"}}}}}},"responses":{"200":{"description":"{ ok, data: SyncOutcome + persistence, source, dataAsOf }. Unknown channels return not-implemented/mock.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"data":{"$ref":"#/components/schemas/SyncOutcome"},"source":{"type":"string"},"dataAsOf":{"type":"string"}}}}}},"401":{"description":"No session / organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"502":{"description":"Provider failure (also logged to sync_errors).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/cron/sync":{"get":{"tags":["Sync"],"summary":"Scheduled sync fan-out (Vercel Cron, every 6h)","security":[{"cronSecret":[]}],"responses":{"200":{"description":"{ ok, data: { targets, succeeded, failed, results[] }, dataAsOf }. Runs every connected (org, channel) sequentially; quota bypassed.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"Missing/invalid cron secret or admin key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"503":{"description":"Database service not configured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/reconcile/shopify":{"post":{"tags":["Sync"],"summary":"Diff Shopify vs Trackable, repair drift","security":[{"sessionCookie":[]}],"parameters":[{"name":"since","in":"query","schema":{"type":"string","format":"date-time","description":"Defaults to 7 days ago"}}],"responses":{"200":{"description":"{ ok, data: { checked, upserted, …, sinceIso } }.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"No session / organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"502":{"description":"Reconciliation failed (logged).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"503":{"description":"Service role not configured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/integrations":{"get":{"tags":["Integrations"],"summary":"Server-env connection flags (public)","description":"Env-level only — per-workspace truth is GET …/connect (session).","responses":{"200":{"description":"{ ok, data: [{ channel, connected, status, lastSyncAt }], source, dataAsOf }.","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/integrations/{channel}/connect":{"get":{"tags":["Integrations"],"summary":"Masked per-workspace connection info","security":[{"sessionCookie":[]}],"parameters":[{"name":"channel","in":"path","required":true,"schema":{"type":"string","enum":["shopify","meta","googleads","amazon","flipkart","shipping"]}}],"responses":{"200":{"description":"{ ok, data: ChannelStatus }. Secrets masked, never returned.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"data":{"$ref":"#/components/schemas/ChannelStatus"}}}}}},"401":{"description":"No session / organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"post":{"tags":["Integrations"],"summary":"Validate live against the provider, then save (tenant-scoped)","description":"Fields per channel — shopify: storeDomain, adminToken; meta: accessToken, adAccountId; googleads: customerId, refreshToken, managerCustomerId?; amazon: refreshToken, clientId, clientSecret; flipkart: appId, appSecret; shipping: email, password.","security":[{"sessionCookie":[]}],"parameters":[{"name":"channel","in":"path","required":true,"schema":{"type":"string","enum":["shopify","meta","googleads","amazon","flipkart","shipping"]}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"Channel fields (see description)"}}}},"responses":{"200":{"description":"{ ok:true, message }.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Missing fields.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Provider rejected the credentials (nothing stored).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"delete":{"tags":["Integrations"],"summary":"Disconnect channel (wipes secrets)","security":[{"sessionCookie":[]}],"parameters":[{"name":"channel","in":"path","required":true,"schema":{"type":"string","enum":["shopify","meta","googleads","amazon","flipkart","shipping"]}}],"responses":{"200":{"description":"{ ok:true, message }.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"No session / organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/integrations/{channel}/auto-connect":{"get":{"tags":["Integrations"],"summary":"Auto-discovery capability + current status","security":[{"sessionCookie":[]}],"parameters":[{"name":"channel","in":"path","required":true,"schema":{"type":"string","enum":["shopify","meta","googleads","amazon","flipkart","shipping"]}}],"responses":{"200":{"description":"{ ok, channel, autoFetchSupported, currentStatus }.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Invalid channel.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"post":{"tags":["Integrations"],"summary":"Zero-input provider discovery + bind","security":[{"sessionCookie":[]}],"parameters":[{"name":"channel","in":"path","required":true,"schema":{"type":"string","enum":["shopify","meta","googleads","amazon","flipkart","shipping"]}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"storeDomain":{"type":"string"},"shop":{"type":"string"},"code":{"type":"string"}}}}}},"responses":{"200":{"description":"{ ok, message, data: { accountName, externalAccountId, metadata, status } }.","content":{"application/json":{"schema":{"type":"object"}}}},"422":{"description":"Discovery failed (see message/error).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/integrations/{channel}/test":{"get":{"tags":["Integrations"],"summary":"Live connectivity probe (public)","description":"meta/shopify/googleads return { ok, data, source, dataAsOf } (200/503); amazon/flipkart/shipping return { ok, configured, tokenObtained?, message } (200/502).","parameters":[{"name":"channel","in":"path","required":true,"schema":{"type":"string","enum":["shopify","meta","googleads","amazon","flipkart","shipping"]}}],"responses":{"200":{"description":"Probe result (check `ok` in body).","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/integrations/{channel}/oauth/start":{"get":{"tags":["OAuth"],"summary":"Begin provider consent (302 redirect)","description":"Open in a browser while signed in. shopify requires ?shop=. googleads accepts optional ?customer_id=. Missing platform credentials redirect back to the channel page with ?error= (never a bogus provider page).","parameters":[{"name":"channel","in":"path","required":true,"schema":{"type":"string","enum":["shopify","meta","googleads","amazon","flipkart"]}},{"name":"shop","in":"query","schema":{"type":"string","description":"shopify only, required"}},{"name":"customer_id","in":"query","schema":{"type":"string","description":"googleads only, optional"}},{"name":"client_id","in":"query","schema":{"type":"string","description":"override platform client (rare)"}}],"responses":{"302":{"description":"Redirect to provider consent (sets state cookies)."},"400":{"description":"shopify without ?shop= (JSON).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/integrations/{channel}/oauth/callback":{"get":{"tags":["OAuth"],"summary":"Provider redirect target (302 back to app)","description":"Called by the provider with ?code=&state=. Verifies state, exchanges the code, auto-discovers the account, saves tenant-scoped credentials. Never saves placeholder tokens — missing tokens are error redirects.","parameters":[{"name":"channel","in":"path","required":true,"schema":{"type":"string","enum":["shopify","meta","googleads","amazon","flipkart"]}},{"name":"code","in":"query","schema":{"type":"string"}},{"name":"state","in":"query","schema":{"type":"string"}},{"name":"shop","in":"query","schema":{"type":"string","description":"shopify"}},{"name":"selling_partner_id","in":"query","schema":{"type":"string","description":"amazon"}}],"responses":{"302":{"description":"→ /integrations/<channel>?connected=true&auth=oauth, or ?error=…, or /login when signed out."}}}},"/api/integrations/shopify/auth":{"get":{"tags":["OAuth"],"summary":"Legacy Shopify OAuth entry (superseded by oauth/start)","parameters":[{"name":"shop","in":"query","schema":{"type":"string"}}],"responses":{"302":{"description":"Redirect to Shopify consent (sets shopify_oauth_* cookies)."},"503":{"description":"Platform OAuth not configured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/integrations/shopify/callback":{"get":{"tags":["OAuth"],"summary":"Legacy Shopify callback — token shown once (JSON)","parameters":[{"name":"code","in":"query","schema":{"type":"string"}},{"name":"shop","in":"query","schema":{"type":"string"}},{"name":"state","in":"query","schema":{"type":"string"}},{"name":"hmac","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"{ ok, data: { shop, scope, access_token, instruction } } — copy the token to server env.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Invalid OAuth state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Invalid callback HMAC.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/integrations/shopify/webhooks":{"get":{"tags":["Webhooks"],"summary":"List webhooks registered on the Shopify store","security":[{"registerSecret":[]}],"responses":{"200":{"description":"{ ok, data: [{ id, topic, address }] }.","content":{"application/json":{"schema":{"type":"object"}}}},"403":{"description":"Missing/unconfigured secret.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"post":{"tags":["Webhooks"],"summary":"Register missing order-lifecycle webhooks (idempotent)","description":"Refuses localhost addresses with 400 (Shopify can't deliver there).","security":[{"registerSecret":[]}],"responses":{"200":{"description":"{ ok, data: { address, registered, alreadyPresent, failed } }.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Localhost address.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Missing/unconfigured secret.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"502":{"description":"Some topics failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/webhooks/shopify":{"post":{"tags":["Webhooks"],"summary":"Inbound Shopify events (HMAC-verified, idempotent)","description":"Verify HMAC-SHA256 over the raw body (secret = SHOPIFY_WEBHOOK_SECRET). Unknown shops → 404. Unknown topics stored + skipped, never mis-normalized.","parameters":[{"name":"x-shopify-topic","in":"header","required":true,"schema":{"type":"string"}},{"name":"x-shopify-hmac-sha256","in":"header","required":true,"schema":{"type":"string"}},{"name":"x-shopify-shop-domain","in":"header","required":true,"schema":{"type":"string"}},{"name":"x-shopify-webhook-id","in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"Order payload (must contain numeric id for order topics)"}}}},"responses":{"200":{"description":"{ ok, data: { topic, externalId, orderId?, stored, action } }.","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Invalid JSON payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Invalid webhook signature.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Unknown shop — connect it in Integrations first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"500":{"description":"Processing failed (logged to sync_errors).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/api/integrations/shipping/serviceability":{"post":{"tags":["Integrations"],"summary":"Courier serviceability lookup","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["pickupPincode","deliveryPincode"],"properties":{"pickupPincode":{"type":"string","pattern":"^\\d{6}$","example":"110001"},"deliveryPincode":{"type":"string","pattern":"^\\d{6}$","example":"400001"},"cod":{"type":"boolean","default":false},"weightKg":{"type":"number","minimum":0.01,"maximum":50,"default":0.5}}}}}},"responses":{"200":{"description":"{ ok, data: courier options, live, dataAsOf } (live:false = deterministic fallback).","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Invalid pincodes/weight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}}}}