Skip to content

TerpIQ API

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.

Machine-readable spec: /openapi.json · version 0.4.0

imports

CSV and spreadsheet inventory ingestion.

post/api/importscreateImport

Open an import job

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.

get/api/importslistImports

Recent imports

get/api/imports/{id}getImport

Import status and report

post/api/imports/{id}/commitcommitImportChunk

Apply one chunk of rows

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.

products

The shop’s catalog.

get/api/productslistProducts

List catalog products

post/api/productscreateProduct

Add a product by hand

patch/api/products/{id}updateProduct

Edit a product

delete/api/products/{id}deleteProduct

Remove a product

get/api/products/{id}/straingetStrainSuggestions

Ranked strain candidates

post/api/products/{id}/strainlinkProductStrain

Confirm or reject a strain link

coas

Lab reports and the batches they produce.

post/api/batchescreateBatch

Upload a COA and open a batch for review

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.

get/api/batcheslistBatches

Review queue and batch history

get/api/batches/{id}getBatch

One batch, with a signed link to its document

delete/api/batches/{id}discardBatch

Discard a pending review

post/api/batches/{id}/commitcommitBatch

Commit the reviewed values

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.

finder

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.

post/api/finder/sessionstartFinderSession

Open a finder session

Called when the shopper starts the quiz, so completion rate can be measured. Records no identity of any kind. Unauthenticated.

post/api/matchmatchShopper

Match a shopper against the shop’s shelf

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.

post/api/finder/codecreateCounterCode

Issue a counter code for a finished session

post/api/kiosk/exitverifyKioskPin

Check the kiosk exit PIN

Yes or no. The hash is not readable by tenant roles at all, so the comparison happens server-side under the service role.

counter

Counter codes, looked up by staff.

get/api/counter-codes/{code}lookupCounterCode

Look up a code at the counter

post/api/counter-codes/{code}/redeemredeemCounterCode

Mark a code redeemed

shop

Shop-level settings.

patch/api/shopupdateShopSettings

Publish state, brand color, kiosk PIN

Entitlement columns (plan_status, trial_ends_at) are outside the tenant column grant entirely, so no request shaped like this one can reach them.

connectors

Inventory sources, and the machine endpoints that drive them.

post/api/connectors/{id}/syncsyncConnectorNow

Run one connection immediately

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.

post/api/cron/syncrunScheduledSync

Machine endpoint for the nightly scheduled function

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.

post/api/inboundreceiveInboundInventoryEmail

Inventory export received by email

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.

billing

Subscription lifecycle. TerpIQ sells software, never hemp products.

post/api/cron/notifyrunScheduledNotices

Machine endpoint for the daily trial-ending sweep

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.

post/api/stripe/checkoutstartCheckout

Begin a $149/mo subscription

Returns a Stripe Checkout URL. No card data touches this app. Owner only.

post/api/stripe/portalopenBillingPortal

Open the Stripe billing portal

post/api/stripe/webhookstripeWebhook

Subscription lifecycle from Stripe

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.

insights

What the finder saw, and what the shelf could answer.

get/api/analytics/gapsgetAssortmentGaps

Requests the shelf could not answer well

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.

post/api/tagsrenderShelfTags

Printable shelf tags for the chosen products

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%.

alerts

Batch drift, sync failures, and COA reminders for the shop.

patch/api/alerts/{id}updateAlert

Mark an alert read or unread

Alerts are never deleted — the record that a batch drifted or a connector broke outlives the owner clearing their feed.

marketing

Public pages. TerpIQ sells software to retailers, never products.

post/api/contactsubmitContactForm

Send an enquiry from the marketing site

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.