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'saccessToken. E-mail = identity; no client account. - Translator studio — header
x-thide-studiomust equalSTUDIO_DEV_KEY(defaultbeatrijs-dev). A dev key, not security. - Integrator / automator — header
x-thide-keymust be one ofTHIDE_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 are409 illegal_transition, a post-escrow rate/count/legalisation change is409 price_locked, missing or stale terms acceptance is400 terms_not_accepted, and contracts that failed to ship with the build are503 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_ACTORSindomain.ts) and appends to the immutable status log.