{"openapi":"3.1.0","info":{"title":"TerpIQ API","version":"0.4.0","description":"Multi-tenant API for hemp retailers. Every request is scoped to the caller’s shop by Postgres row level security; there is no cross-tenant access path. Shopper-facing surfaces are unauthenticated and resolve the tenant from the shop slug."},"servers":[{"url":"https://terpiq.app"}],"tags":[{"name":"imports","description":"CSV and spreadsheet inventory ingestion."},{"name":"products","description":"The shop’s catalog."},{"name":"coas","description":"Lab reports and the batches they produce."},{"name":"finder","description":"The shopper surfaces. These routes are UNAUTHENTICATED: the shop is resolved from its slug and an unpublished finder answers 404 to everyone but its own team."},{"name":"counter","description":"Counter codes, looked up by staff."},{"name":"shop","description":"Shop-level settings."},{"name":"connectors","description":"Inventory sources, and the machine endpoints that drive them."},{"name":"billing","description":"Subscription lifecycle. TerpIQ sells software, never hemp products."},{"name":"insights","description":"What the finder saw, and what the shelf could answer."},{"name":"alerts","description":"Batch drift, sync failures, and COA reminders for the shop."},{"name":"marketing","description":"Public pages. TerpIQ sells software to retailers, never products."}],"components":{"securitySchemes":{"clerkSession":{"type":"apiKey","in":"cookie","name":"__session","description":"Clerk session cookie. The active organization selects the shop."}}},"security":[{"clerkSession":[]}],"paths":{"/api/imports":{"post":{"operationId":"createImport","tags":["imports"],"summary":"Open an import job","description":"The spreadsheet is parsed in the browser, so this takes the headers and row count rather than a file. Returns a guessed column mapping and any saved template whose headers match.","requestBody":{"description":"","required":true,"content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"filename":{"type":"string","minLength":1,"maxLength":255},"kind":{"type":"string","enum":["csv","xlsx"]},"headers":{"minItems":1,"maxItems":200,"type":"array","items":{"type":"string"}},"rowsTotal":{"type":"integer","minimum":0,"maximum":200000}},"required":["filename","kind","headers","rowsTotal"],"additionalProperties":false}}}},"responses":{"200":{"description":"Job opened."},"400":{"description":"The request was not valid.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"No shop selected, or the role cannot do this.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Not found in this shop.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}}}},"get":{"operationId":"listImports","tags":["imports"],"summary":"Recent imports","responses":{"200":{"description":"Recent jobs."},"400":{"description":"The request was not valid.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"No shop selected, or the role cannot do this.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Not found in this shop.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}}}}},"/api/imports/{id}":{"get":{"operationId":"getImport","tags":["imports"],"summary":"Import status and report","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The job.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"kind":{"type":"string","enum":["csv","xlsx","shopify","square"]},"status":{"type":"string","enum":["pending","processing","complete","failed"]},"filename":{"anyOf":[{"type":"string"},{"type":"null"}]},"rowsTotal":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"rowsProcessed":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"report":{"type":"object","properties":{"rowsOk":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"rowsFailed":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"created":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"updated":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"autoLinked":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"suggested":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"unmatched":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"absentFlagged":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"errors":{"type":"array","items":{"type":"object","properties":{"rowNumber":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"label":{"type":"string"},"message":{"type":"string"}},"required":["rowNumber","label","message"],"additionalProperties":false}},"warnings":{"type":"array","items":{"type":"object","properties":{"rowNumber":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"message":{"type":"string"}},"required":["rowNumber","message"],"additionalProperties":false}}},"additionalProperties":false},"createdAt":{"type":"string"}},"required":["id","kind","status","filename","rowsTotal","rowsProcessed","report","createdAt"],"additionalProperties":false}}}},"400":{"description":"The request was not valid.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"No shop selected, or the role cannot do this.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Not found in this shop.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}}}}},"/api/imports/{id}/commit":{"post":{"operationId":"commitImportChunk","tags":["imports"],"summary":"Apply one chunk of rows","description":"Called repeatedly so large files import with progress. Partial success is the contract: invalid rows are reported and every valid row still lands. Set final=true on the last chunk.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"description":"","required":true,"content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"mapping":{"type":"object","properties":{"name":{"type":"string"},"strainName":{"type":"string"},"format":{"type":"string"},"price":{"type":"string"},"stock":{"type":"string"},"sku":{"type":"string"}},"additionalProperties":false},"rows":{"maxItems":1000,"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"startRow":{"type":"integer","minimum":1,"maximum":9007199254740991},"final":{"default":false,"type":"boolean"},"saveTemplateAs":{"anyOf":[{"type":"string","minLength":1,"maxLength":80},{"type":"null"}]},"flagAbsent":{"default":true,"type":"boolean"}},"required":["mapping","rows","startRow","final","flagAbsent"],"additionalProperties":false}}}},"responses":{"200":{"description":"Chunk applied; running report."},"400":{"description":"The request was not valid.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"No shop selected, or the role cannot do this.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Not found in this shop.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"409":{"description":"That import already finished.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}}}}},"/api/products":{"get":{"operationId":"listProducts","tags":["products"],"summary":"List catalog products","parameters":[{"name":"search","in":"query","required":false,"schema":{"type":"string","maxLength":200}},{"name":"format","in":"query","required":false,"schema":{"type":"string","enum":["flower","preroll","vape","gummy","tincture","other"]}},{"name":"stock","in":"query","required":false,"schema":{"type":"string","enum":["in","out"]}},{"name":"profiled","in":"query","required":false,"schema":{"type":"string","enum":["yes","no"]}},{"name":"matchStatus","in":"query","required":false,"schema":{"type":"string","enum":["unmatched","suggested","auto","confirmed","rejected"]}},{"name":"limit","in":"query","required":false,"schema":{"default":50,"type":"integer","minimum":1,"maximum":200}},{"name":"offset","in":"query","required":false,"schema":{"default":0,"type":"integer","minimum":0,"maximum":1000000}}],"responses":{"200":{"description":"Matching products."},"400":{"description":"The request was not valid.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"No shop selected, or the role cannot do this.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Not found in this shop.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}}}},"post":{"operationId":"createProduct","tags":["products"],"summary":"Add a product by hand","requestBody":{"description":"","required":true,"content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"format":{"default":"other","type":"string","enum":["flower","preroll","vape","gummy","tincture","other"]},"priceCents":{"anyOf":[{"type":"integer","minimum":0,"maximum":10000000},{"type":"null"}]},"inStock":{"default":true,"type":"boolean"},"sku":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}]},"productUrl":{"anyOf":[{"type":"string","maxLength":500,"format":"uri"},{"type":"null"}]},"strainId":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}]}},"required":["name","format","inStock"],"additionalProperties":false}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"name":{"type":"string"},"format":{"type":"string","enum":["flower","preroll","vape","gummy","tincture","other"]},"priceCents":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"inStock":{"type":"boolean"},"sku":{"anyOf":[{"type":"string"},{"type":"null"}]},"strainId":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}]},"strainName":{"anyOf":[{"type":"string"},{"type":"null"}]},"strainMatchStatus":{"type":"string","enum":["unmatched","suggested","auto","confirmed","rejected"]},"strainMatchScore":{"anyOf":[{"type":"number"},{"type":"null"}]},"sourceStrainName":{"anyOf":[{"type":"string"},{"type":"null"}]},"absentFromLastImport":{"type":"boolean"},"hasActiveBatch":{"type":"boolean"}},"required":["id","name","format","priceCents","inStock","sku","strainId","strainName","strainMatchStatus","strainMatchScore","sourceStrainName","absentFromLastImport","hasActiveBatch"],"additionalProperties":false}}}},"400":{"description":"The request was not valid.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"No shop selected, or the role cannot do this.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Not found in this shop.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}}}}},"/api/products/{id}":{"patch":{"operationId":"updateProduct","tags":["products"],"summary":"Edit a product","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"description":"","required":true,"content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"format":{"default":"other","type":"string","enum":["flower","preroll","vape","gummy","tincture","other"]},"priceCents":{"anyOf":[{"type":"integer","minimum":0,"maximum":10000000},{"type":"null"}]},"inStock":{"default":true,"type":"boolean"},"sku":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}]},"productUrl":{"anyOf":[{"type":"string","maxLength":500,"format":"uri"},{"type":"null"}]},"strainId":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}]}},"additionalProperties":false}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"name":{"type":"string"},"format":{"type":"string","enum":["flower","preroll","vape","gummy","tincture","other"]},"priceCents":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"inStock":{"type":"boolean"},"sku":{"anyOf":[{"type":"string"},{"type":"null"}]},"strainId":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}]},"strainName":{"anyOf":[{"type":"string"},{"type":"null"}]},"strainMatchStatus":{"type":"string","enum":["unmatched","suggested","auto","confirmed","rejected"]},"strainMatchScore":{"anyOf":[{"type":"number"},{"type":"null"}]},"sourceStrainName":{"anyOf":[{"type":"string"},{"type":"null"}]},"absentFromLastImport":{"type":"boolean"},"hasActiveBatch":{"type":"boolean"}},"required":["id","name","format","priceCents","inStock","sku","strainId","strainName","strainMatchStatus","strainMatchScore","sourceStrainName","absentFromLastImport","hasActiveBatch"],"additionalProperties":false}}}},"400":{"description":"The request was not valid.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"No shop selected, or the role cannot do this.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Not found in this shop.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}}}},"delete":{"operationId":"deleteProduct","tags":["products"],"summary":"Remove a product","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted."},"400":{"description":"The request was not valid.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"No shop selected, or the role cannot do this.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Not found in this shop.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}}}}},"/api/alerts/{id}":{"patch":{"operationId":"updateAlert","tags":["alerts"],"summary":"Mark an alert read or unread","description":"Alerts are never deleted — the record that a batch drifted or a connector broke outlives the owner clearing their feed.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"description":"","required":true,"content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"read":{"type":"boolean"}},"required":["read"],"additionalProperties":false}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"read":{"type":"boolean"}},"required":["id","read"],"additionalProperties":false}}}},"400":{"description":"The request was not valid.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"No shop selected, or the role cannot do this.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Not found in this shop.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}}}}},"/api/batches":{"post":{"operationId":"createBatch","tags":["coas"],"summary":"Upload a COA and open a batch for review","description":"Stores the document privately and opens a batch in pending_review with whatever extraction could work out. Never fails because extraction did: a scan, an exhausted monthly budget, or a model error all land on the same review screen with fields left empty.","requestBody":{"description":"","required":true,"content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"productId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"filename":{"type":"string","minLength":1,"maxLength":255},"fileBase64":{"type":"string","maxLength":28000000},"contentType":{"type":"string","enum":["application/pdf","image/png","image/jpeg"]}},"required":["productId","filename","fileBase64","contentType"],"additionalProperties":false}}}},"responses":{"201":{"description":"Batch opened for review."},"400":{"description":"The request was not valid.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"No shop selected, or the role cannot do this.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Not found in this shop.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"413":{"description":"File larger than 20MB.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}}}},"get":{"operationId":"listBatches","tags":["coas"],"summary":"Review queue and batch history","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["pending_review","active","superseded"]}}],"responses":{"200":{"description":"Batches, newest first."},"400":{"description":"The request was not valid.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"No shop selected, or the role cannot do this.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Not found in this shop.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}}}}},"/api/batches/{id}":{"get":{"operationId":"getBatch","tags":["coas"],"summary":"One batch, with a signed link to its document","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The batch.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"productId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"productName":{"type":"string"},"status":{"type":"string","enum":["pending_review","active","superseded"]},"labName":{"anyOf":[{"type":"string"},{"type":"null"}]},"batchCode":{"anyOf":[{"type":"string"},{"type":"null"}]},"testedAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"cannabinoids":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"anyOf":[{"type":"number"},{"type":"null"}]}},"terpenes":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","enum":["myrcene","linalool","limonene","caryophyllene","pinene","terpinolene","humulene","ocimene"]},"pct":{"type":"number"}},"required":["id","pct"],"additionalProperties":false}},"otherTerpenes":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"pct":{"type":"number"}},"required":["name","pct"],"additionalProperties":false}},"extractionConfidence":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"number"}},"extractionOutcome":{"anyOf":[{"type":"string"},{"type":"null"}]},"extractionNote":{"anyOf":[{"type":"string"},{"type":"null"}]},"unitsNote":{"anyOf":[{"type":"string"},{"type":"null"}]},"productMismatch":{"type":"boolean"},"coaUrl":{"anyOf":[{"type":"string"},{"type":"null"}]},"committedAt":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","productId","productName","status","labName","batchCode","testedAt","cannabinoids","terpenes","otherTerpenes","extractionConfidence","extractionOutcome","extractionNote","unitsNote","productMismatch","coaUrl","committedAt"],"additionalProperties":false}}}},"400":{"description":"The request was not valid.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"No shop selected, or the role cannot do this.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Not found in this shop.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}}}},"delete":{"operationId":"discardBatch","tags":["coas"],"summary":"Discard a pending review","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Discarded."},"400":{"description":"The request was not valid.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"No shop selected, or the role cannot do this.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Not found in this shop.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}}}}},"/api/batches/{id}/commit":{"post":{"operationId":"commitBatch","tags":["coas"],"summary":"Commit the reviewed values","description":"The human review gate. These values — not the extraction’s proposal — become the active batch, supersede the previous one, and drive matching. Returns a drift note when the new numbers differ from the old ones past the alert thresholds.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"description":"","required":true,"content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"labName":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}]},"batchCode":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}]},"testedAt":{"anyOf":[{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},{"type":"null"}]},"cannabinoids":{"type":"object","properties":{"thcaPct":{"type":"number","minimum":0,"maximum":100},"d9Pct":{"type":"number","minimum":0,"maximum":100},"cbdPct":{"type":"number","minimum":0,"maximum":100},"cbgPct":{"type":"number","minimum":0,"maximum":100},"totalPct":{"anyOf":[{"type":"number","minimum":0,"maximum":100},{"type":"null"}]}},"required":["thcaPct","d9Pct","cbdPct","cbgPct","totalPct"],"additionalProperties":false},"terpenes":{"maxItems":8,"type":"array","items":{"type":"object","properties":{"id":{"type":"string","enum":["myrcene","linalool","limonene","caryophyllene","pinene","terpinolene","humulene","ocimene"]},"pct":{"type":"number","minimum":0,"maximum":100}},"required":["id","pct"],"additionalProperties":false}},"otherTerpenes":{"default":[],"maxItems":40,"type":"array","items":{"type":"object","properties":{"name":{"type":"string","maxLength":120},"pct":{"type":"number","minimum":0,"maximum":100}},"required":["name","pct"],"additionalProperties":false}},"unitsNote":{"anyOf":[{"type":"string","maxLength":200},{"type":"null"}]}},"required":["cannabinoids","terpenes","otherTerpenes"],"additionalProperties":false}}}},"responses":{"200":{"description":"Committed; drift result included."},"400":{"description":"The request was not valid.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"No shop selected, or the role cannot do this.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Not found in this shop.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"409":{"description":"That batch was already superseded.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}}}}},"/api/finder/session":{"post":{"operationId":"startFinderSession","tags":["finder"],"summary":"Open a finder session","description":"Called when the shopper starts the quiz, so completion rate can be measured. Records no identity of any kind. Unauthenticated.","security":[],"requestBody":{"description":"","required":true,"content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"slug":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[a-z0-9-]+$"},"surface":{"default":"web","type":"string","enum":["web","kiosk","embed"]}},"required":["slug","surface"],"additionalProperties":false}}}},"responses":{"200":{"description":"Session opened."},"404":{"description":"Not found in this shop.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}}}}},"/api/match":{"post":{"operationId":"matchShopper","tags":["finder"],"summary":"Match a shopper against the shop’s shelf","description":"The deterministic engine, scoped to in-stock profiled products of this shop. Exclusion rules REMOVE products and the removal survives every fallback, so a shopper who declared a sensitivity cannot be shown an excluded product by any route. Unauthenticated.","security":[],"requestBody":{"description":"","required":true,"content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"slug":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[a-z0-9-]+$"},"surface":{"default":"web","type":"string","enum":["web","kiosk","embed"]},"sessionId":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}]},"answers":{"type":"object","properties":{"feeling":{"type":"string","enum":["calm-ease","rest-wind-down","comfort-unwind","bright-social","clear-creative","balanced-easy"]},"experience":{"type":"string","enum":["new","some","seasoned"]},"sensitivities":{"maxItems":3,"type":"array","items":{"type":"string","enum":["strong-effects","grogginess","none"]}},"format":{"type":"string","enum":["flower","preroll","vape","gummy","tincture","other"]},"timeOfDay":{"type":"string","enum":["morning","afternoon","evening","late"]}},"required":["feeling","experience","sensitivities","format","timeOfDay"],"additionalProperties":false}},"required":["slug","surface","answers"],"additionalProperties":false}}}},"responses":{"200":{"description":"Ranked matches.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"sessionId":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}]},"results":{"type":"array","items":{"type":"object","properties":{"productId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"name":{"type":"string"},"format":{"type":"string","enum":["flower","preroll","vape","gummy","tincture","other"]},"priceCents":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"productUrl":{"anyOf":[{"type":"string"},{"type":"null"}]},"why":{"type":"string"},"chemistrySource":{"type":"string","enum":["batch","strain-default"]},"batchId":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}]},"thcaPct":{"type":"number"},"cbdPct":{"type":"number"},"terpenes":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","enum":["myrcene","linalool","limonene","caryophyllene","pinene","terpinolene","humulene","ocimene"]},"pct":{"type":"number"}},"required":["id","pct"],"additionalProperties":false}}},"required":["productId","name","format","priceCents","productUrl","why","chemistrySource","batchId","thcaPct","cbdPct","terpenes"],"additionalProperties":false}},"ruledOut":{"type":"object","properties":{"count":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"reasons":{"type":"array","items":{"type":"string"}}},"required":["count","reasons"],"additionalProperties":false},"fallbackNote":{"anyOf":[{"type":"string"},{"type":"null"}]},"emptyNote":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["sessionId","results","ruledOut","fallbackNote","emptyNote"],"additionalProperties":false}}}},"400":{"description":"The request was not valid.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Not found in this shop.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}}}}},"/api/finder/code":{"post":{"operationId":"createCounterCode","tags":["finder"],"summary":"Issue a counter code for a finished session","security":[],"requestBody":{"description":"","required":true,"content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"slug":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[a-z0-9-]+$"},"sessionId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},"required":["slug","sessionId"],"additionalProperties":false}}}},"responses":{"200":{"description":"The code."},"404":{"description":"Not found in this shop.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"409":{"description":"Could not issue a code.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}}}}},"/api/kiosk/exit":{"post":{"operationId":"verifyKioskPin","tags":["finder"],"summary":"Check the kiosk exit PIN","description":"Yes or no. The hash is not readable by tenant roles at all, so the comparison happens server-side under the service role.","security":[],"requestBody":{"description":"","required":true,"content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"slug":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[a-z0-9-]+$"},"pin":{"type":"string","pattern":"^\\d{4}$"}},"required":["slug","pin"],"additionalProperties":false}}}},"responses":{"200":{"description":"PIN accepted."},"403":{"description":"PIN rejected.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Not found in this shop.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}}}}},"/api/counter-codes/{code}":{"get":{"operationId":"lookupCounterCode","tags":["counter"],"summary":"Look up a code at the counter","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","minLength":6,"maxLength":6}}],"responses":{"200":{"description":"The session.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"code":{"type":"string"},"createdAt":{"type":"string"},"redeemedAt":{"anyOf":[{"type":"string"},{"type":"null"}]},"answers":{"type":"object","properties":{"feeling":{"type":"string","enum":["calm-ease","rest-wind-down","comfort-unwind","bright-social","clear-creative","balanced-easy"]},"experience":{"type":"string","enum":["new","some","seasoned"]},"sensitivities":{"maxItems":3,"type":"array","items":{"type":"string","enum":["strong-effects","grogginess","none"]}},"format":{"type":"string","enum":["flower","preroll","vape","gummy","tincture","other"]},"timeOfDay":{"type":"string","enum":["morning","afternoon","evening","late"]}},"additionalProperties":false},"products":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"name":{"type":"string"},"format":{"type":"string","enum":["flower","preroll","vape","gummy","tincture","other"]}},"required":["id","name","format"],"additionalProperties":false}}},"required":["code","createdAt","redeemedAt","answers","products"],"additionalProperties":false}}}},"400":{"description":"The request was not valid.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"No shop selected, or the role cannot do this.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Not found in this shop.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}}}}},"/api/counter-codes/{code}/redeem":{"post":{"operationId":"redeemCounterCode","tags":["counter"],"summary":"Mark a code redeemed","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","minLength":6,"maxLength":6}}],"responses":{"200":{"description":"Redeemed."},"400":{"description":"The request was not valid.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"No shop selected, or the role cannot do this.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Not found in this shop.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}}}}},"/api/analytics/gaps":{"get":{"operationId":"getAssortmentGaps","tags":["insights"],"summary":"Requests the shelf could not answer well","description":"Ranked by how many people asked, measured against the shelf as it is now rather than as it was when the session ran. `format=csv` returns the buying list as a download.","parameters":[{"name":"format","in":"query","required":false,"schema":{"type":"string","enum":["json","csv"]}}],"responses":{"200":{"description":"Findings, most-asked first."},"400":{"description":"The request was not valid.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"No shop selected, or the role cannot do this.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Not found in this shop.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}}}}},"/api/tags":{"post":{"operationId":"renderShelfTags","tags":["insights"],"summary":"Printable shelf tags for the chosen products","description":"Returns a PDF. The QR points at the public batch page when a committed batch backs the numbers, and at the shop finder when it does not — a tag never sends a phone to a missing page. Products with no chemistry are skipped rather than printed as 0.0%.","requestBody":{"description":"","required":true,"content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"productIds":{"minItems":1,"maxItems":200,"type":"array","items":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},"sheet":{"default":"avery-5160","type":"string","enum":["avery-5160","two-by-one"]},"cutLines":{"default":false,"type":"boolean"}},"required":["productIds","sheet","cutLines"],"additionalProperties":false}}}},"responses":{"200":{"description":"The PDF."},"400":{"description":"The request was not valid.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"No shop selected, or the role cannot do this.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Not found in this shop.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}}}}},"/api/connectors/{id}/sync":{"post":{"operationId":"syncConnectorNow","tags":["connectors"],"summary":"Run one connection immediately","description":"The connector is found through the caller’s own session, so RLS decides ownership; only then does the sync run as the service role, which it must, because credentials are outside the tenant column grant.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Sync finished."},"400":{"description":"The request was not valid.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"No shop selected, or the role cannot do this.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Not found in this shop.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"502":{"description":"The source could not be read; last good inventory still serves.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}}}}},"/api/cron/sync":{"post":{"operationId":"runScheduledSync","tags":["connectors"],"summary":"Machine endpoint for the nightly scheduled function","description":"Authenticated by a shared secret compared in constant time, not by a session — a cron job has no user. One connector per call, because a serverless function has a wall clock.","security":[],"requestBody":{"description":"","required":true,"content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"connectorId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},"required":["connectorId"],"additionalProperties":false}}}},"responses":{"200":{"description":"Sync finished."},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}}}}},"/api/contact":{"post":{"operationId":"submitContactForm","tags":["marketing"],"summary":"Send an enquiry from the marketing site","description":"Unauthenticated: the sender does not have an account yet. Guarded by the shopper rate limiter, a honeypot field, and schema length caps. The sender becomes reply-to, never from.","security":[],"requestBody":{"description":"","required":true,"content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"email":{"type":"string","maxLength":200,"format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"shop":{"type":"string","maxLength":120},"message":{"type":"string","minLength":10,"maxLength":4000},"website":{"type":"string","maxLength":200}},"required":["name","email","message"],"additionalProperties":false}}}},"responses":{"200":{"description":"Message sent."},"400":{"description":"The request was not valid.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"429":{"description":"Too many messages from this connection.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"502":{"description":"The mail provider refused it.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"No destination address is configured.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}}}}},"/api/cron/notify":{"post":{"operationId":"runScheduledNotices","tags":["billing"],"summary":"Machine endpoint for the daily trial-ending sweep","description":"TerpIQ trials are its own, not Stripe’s, so no webhook can see one ending. Sent once per trial end date. Shared-secret authenticated, compared in constant time.","security":[],"responses":{"200":{"description":"Sweep finished."},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}}}}},"/api/inbound":{"post":{"operationId":"receiveInboundInventoryEmail","tags":["connectors"],"summary":"Inventory export received by email","description":"Posted by the email provider when a shop mails its export to inv-<token>@in.terpiq.app. The token in the address is the tenant boundary; a shared secret authenticates the provider.","security":[],"responses":{"200":{"description":"Attachment imported."},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Unknown address.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"413":{"description":"Attachment too large.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}}}}},"/api/stripe/checkout":{"post":{"operationId":"startCheckout","tags":["billing"],"summary":"Begin a $149/mo subscription","description":"Returns a Stripe Checkout URL. No card data touches this app. Owner only.","responses":{"200":{"description":"Checkout URL."},"400":{"description":"The request was not valid.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"No shop selected, or the role cannot do this.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Not found in this shop.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}}}}},"/api/stripe/portal":{"post":{"operationId":"openBillingPortal","tags":["billing"],"summary":"Open the Stripe billing portal","responses":{"200":{"description":"Portal URL."},"400":{"description":"The request was not valid.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"No shop selected, or the role cannot do this.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Not found in this shop.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}}}}},"/api/stripe/webhook":{"post":{"operationId":"stripeWebhook","tags":["billing"],"summary":"Subscription lifecycle from Stripe","description":"The only writer of entitlement. Signature is verified before anything else, the event is recorded before it is applied so a retry cannot double-apply, and a recorded event always answers 200 so a poison event is not retried forever.","security":[],"responses":{"200":{"description":"Recorded."},"400":{"description":"Missing or invalid signature.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Webhook secret not configured.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}}}}},"/api/shop":{"patch":{"operationId":"updateShopSettings","tags":["shop"],"summary":"Publish state, brand color, kiosk PIN","description":"Entitlement columns (plan_status, trial_ends_at) are outside the tenant column grant entirely, so no request shaped like this one can reach them.","requestBody":{"description":"","required":true,"content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"brandColor":{"type":"string","pattern":"^#?[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$"},"finderPublished":{"type":"boolean"},"kioskPin":{"anyOf":[{"type":"string","pattern":"^\\d{4}$"},{"type":"null"}]},"alertEmail":{"anyOf":[{"type":"string","maxLength":200,"format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}]}},"additionalProperties":false}}}},"responses":{"200":{"description":"Updated."},"400":{"description":"The request was not valid.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"No shop selected, or the role cannot do this.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Not found in this shop.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}}}}},"/api/products/{id}/strain":{"get":{"operationId":"getStrainSuggestions","tags":["products"],"summary":"Ranked strain candidates","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Candidates, best first."},"400":{"description":"The request was not valid.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"No shop selected, or the role cannot do this.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Not found in this shop.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}}}},"post":{"operationId":"linkProductStrain","tags":["products"],"summary":"Confirm or reject a strain link","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"description":"","required":true,"content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"strainId":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"type":"null"}]},"status":{"default":"confirmed","type":"string","enum":["confirmed","rejected"]}},"required":["strainId","status"],"additionalProperties":false}}}},"responses":{"200":{"description":"Recorded."},"400":{"description":"The request was not valid.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Not signed in.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"No shop selected, or the role cannot do this.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Not found in this shop.","content":{"application/json":{"schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"error":{"type":"string"},"detail":{"type":"string"},"fields":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"message":{"type":"string"}},"required":["path","message"],"additionalProperties":false}}},"required":["error"],"additionalProperties":false}}}}}}}}}