Skip to content

API

The versioned REST endpoints under /api/v1 are the product's public surface: the web app consumes the same routes an automator or AI agent will use (demo-spec §1, "API-first"). The machine-readable endpoint map lives at docs/api/openapi.yaml — keep it in the definition of done for every endpoint change.

Authentication (dev placeholder)

Replaced by Supabase Auth at provisioning (task 1.2):

  • Client — tokenized link: ?token= must equal the mandate's accessToken. E-mail = identity; no client account.
  • Translator studio — header x-thide-studio must equal STUDIO_DEV_KEY (default beatrijs-dev). A dev key, not security.
  • Integrator / automator — header x-thide-key must be one of THIDE_API_KEYS (comma-separated, so keys rotate per integrator). This is the identity for an agent that has no mandate yet because it is about to create one.

Nothing is browsable before launch (ThD 30 Jul): the non-mandate reads — catalog, terms, the OpenAPI map — require an integrator key, the studio key, or a ?token= from an existing mandate. requireReadAccess in apps/web/src/app/api/v1/_lib/api.ts is the single function to relax at launch.

Baked-in dev credentials are honoured only on a non-exposed deployment (isExposedDeployment() in lib/repo.ts): the Supabase backend, any named NEXT_PUBLIC_THIDE_ENV other than local, or NODE_ENV=production all deny them. THIDE_ALLOW_DEV_KEYS=1 is the one escape hatch, for a local production build — never set it on a host that answers to the internet.

Endpoints

Method Path Auth Purpose
GET /api/v1/catalog key, studio or token Everything the intake offers as data: pairs, kinds, deadlines + modifiers, countries + legalisation route, document categories, upload limits, terms
GET /api/v1/terms key, studio or token The contracts a client must accept, with versions and content hashes
GET /api/v1/terms/{id} key, studio or token Full text of one contract (?format=markdown for terminals)
GET /api/v1/openapi.yaml key, studio or token The machine-readable endpoint map
POST /api/v1/mandates none — deliberate Create a mandate: count, store source docs, auto-quote, notify both sides. Intake stays open because a first-time client has no credential; it is protected by what it returns (the mandate is only readable with the access token handed back). Requires acceptedTerms; documentCategory decides the intra-EU legalisation answer
GET /api/v1/mandates studio Work-queue list (?status= filter)
GET /api/v1/mandates/{id} token or studio One mandate (envelope)
GET /api/v1/mandates/{id}/documents/{docId} token or studio Tokenized document download (the link delivery e-mails embed)
POST /api/v1/mandates/{id}/decision studio Accept / decline / counter a paid mandate
POST /api/v1/mandates/{id}/progress studio in_progress / translated
POST /api/v1/mandates/{id}/deliver studio Upload deliverables, mark delivered, e-mail the client
POST /api/v1/mandates/{id}/pay token Pay into escrow — or confirm a counter-offer for free (no second charge)
POST /api/v1/mandates/{id}/cancel token Client walks away: pre-payment (submitted/quoted) or a countered offer (escrow refunded in full)
GET /api/v1/mandates/{id}/questions token or studio Read the question thread (the relay is persisted, so an agent can read the answer)
POST /api/v1/mandates/{id}/questions token or studio Relay a question between client and translator (e-mail + thread)
GET /api/v1/mandates/{id}/terms token or studio The contract versions this mandate was formed under, with their text
GET /api/v1/mandates/{id}/legalisation token or studio What THIDE recommends vs what applies (recommended, effective, override, deviates)
POST /api/v1/mandates/{id}/legalisation token or studio Override the recommendation — either side may; refused 409 price_locked after escrow
GET /api/v1/mandates/{id}/self-bill studio The mandate's self-bill (draft or issued)
POST /api/v1/mandates/{id}/self-bill studio Accept & issue, contest, or dismiss the notice
GET /api/v1/pricing studio The professional's persisted rate matrix
POST /api/v1/webhooks/stripe Stripe signature checkout.session.completed → quoted→paid

Conventions

  • Errors always use the envelope {"error": {"code", "message"}}; illegal state-machine moves are 409 illegal_transition, a post-escrow rate/count/legalisation change is 409 price_locked, missing or stale terms acceptance is 400 terms_not_accepted, and contracts that failed to ship with the build are 503 terms_unavailable.
  • Money is integer cents (EUR).
  • State changes go exclusively through the repository's transition(), which enforces both the legal edge and the allowed actor (TRANSITION_ACTORS in domain.ts) and appends to the immutable status log.