REST API v1
B2B integration surface for PLM systems (Centric C8, SAP, …): submit an ingredient line, poll the job, receive structured JSON with translations, detected allergens and per-region label HTML with character-offset highlight spans.
Base path: /api/v1 — all requests and responses are JSON (UTF-8).
Authentication
Every endpoint except GET /api/v1/health requires a Bearer token:
Authorization: Bearer ing_live_4f9c2e7a1b8d3c6f5e0a9b2d4c7f1e3a
Org admins create and administer org tokens on /team.
Reviewers can create and revoke only their own credentials on
/profile/tokens, without team, billing or other-user token
access. The plain token is shown exactly once at creation
— only its SHA-256 hash is stored. Lost tokens cannot be recovered;
revoke and create a new one.
Session cookies are not accepted on /api/v1/*.
Quickstart
Submit a job (a comma-separated label line, German source):
curl -s -X POST https://app.example.com/api/v1/translations \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"ingredient": "Haferdrink, Wasser, Milch, Sojalecithin",
"source_language": "de",
"client_request_id": "plm-item-88213-rev4"
}'
{
"job_id": "9f0c2b7e4a5d4e8f9a1b2c3d4e5f6a7b",
"status": "pending",
"status_url": "/api/v1/translations/9f0c2b7e4a5d4e8f9a1b2c3d4e5f6a7b",
"estimated_seconds": 15
}
Poll until status is done (or failed):
curl -s https://app.example.com/api/v1/translations/9f0c2b7e4a5d4e8f9a1b2c3d4e5f6a7b \
-H "Authorization: Bearer $API_TOKEN"
{
"job_id": "9f0c2b7e4a5d4e8f9a1b2c3d4e5f6a7b",
"status": "done",
"result": {
"schema_version": "1.0",
"detected_allergens": [
{"code": "milk", "label_en": "milk"},
{"code": "oats", "label_en": "oats"},
{"code": "soy", "label_en": "soy"}
],
"translations": {
"de": "Haferdrink, Wasser, Milch, Sojalecithin",
"en": "Oat drink, water, milk, soy lecithin",
"fr": "Boisson à l'avoine, eau, lait, lécithine de soja"
},
"region_outputs": {
"en": {
"eu": {
"ingredient_line": "Oat drink, water, milk, soy lecithin",
"highlights": [
{"start": 0, "end": 3, "allergen": "oats", "surface": "Oat"},
{"start": 17, "end": 21, "allergen": "milk", "surface": "milk"}
],
"contains_statement": "milk, oats, soy"
}
}
},
"audit": {
"from_cache": false,
"escalated_to_judge": false,
"duration_ms": 12840
}
}
}
(Response abbreviated — real responses contain all configured
target languages and all five regions: eu, uk,
ch, usa, aus.)
Polling cadence: poll every 2 seconds;
give up after 60 seconds and treat the job as delayed
(the job keeps running server-side — the same
status_url stays valid). Typical completion is
10–20 s; cache hits return in under a second.
POST /api/v1/translations
Request body:
| field | type | required | notes |
|---|---|---|---|
ingredient | string | yes | 1–2000 chars. Single ingredient or comma-separated label string — the pipeline handles both. |
source_language | string | no | One of en de fr it hu sl es. Default en. |
target_languages | string[] | no | Subset of the same codes, min 1. Default: token owner's saved preferences, else all seven. |
domain | string | no | Only "food" in v1. |
callback_url | string | no | Reserved. Setting it returns 501
— webhooks arrive in a later release. Poll
status_url instead. |
client_request_id | string | no | ≤128 chars. Idempotency key, see below. |
Success → 202 Accepted:
{"job_id": "…", "status": "pending", "status_url": "/api/v1/translations/…", "estimated_seconds": 15}
Idempotency
If client_request_id is set and the same value was already
accepted by your org within the last 24 hours, no new
job is created — you get the existing job's current status
response with HTTP 200 (not 202). Keys are scoped per org;
different orgs can reuse the same value independently. Use a stable
per-item identifier from your PLM (e.g. item id + revision) so network
retries never double-submit or double-charge.
If durable idempotency metadata exists but its job record cannot be read,
the API fails closed with 409 instead of creating another
paid job. Reusing the same client_request_id with different
input, source/target languages or domain also returns 409.
Pricing
Each accepted job costs €0.20 × number of target languages,
charged against the org's prepaid credit balance at job completion.
Submission is refused with 402 when the balance can't cover
the estimated cost. See Pricing.
GET /api/v1/translations/{job_id}
| job state | HTTP | body |
|---|---|---|
| pending / running | 200 | {"job_id", "status", "status_url"} |
| done | 200 | {"job_id", "status": "done", "result": {…}} (schema above) |
| failed | 200 | {"job_id", "status": "failed", "error": "<safe message>"} |
| unknown / other org | 404 | {"detail": "unknown job_id"} |
Jobs are only visible to tokens of the org that submitted them; foreign
job ids return the same 404 as unknown ids. Internal
failures are reported with a generic error message —
input-validation failures (e.g. an ingredient that is empty after
normalization) carry a specific one.
Rollout note: the default registry remains in-memory. Allowlisted organisations retain durable pending/done/failed status URLs and results across restarts. A running job without a canonical audit is never automatically repeated, because that could duplicate model work or billing. Non-allowlisted status URLs retain the previous restart limit.
Package review & corrections pilot
Exact organisations in the default-off PostgreSQL pilot can review complete
translation packages through the same API workflow. Completed jobs include
additive review metadata with revision number, package SHA-256,
confirmation state, active correction state and any verification blocker.
The existing result schema and canonical job_id do not change.
| endpoint | purpose |
|---|---|
POST /api/v1/translations/{job_id}/feedback |
Submit bounded plain-text feedback against an expected current revision. |
GET /api/v1/translations/{job_id}/feedback/{request_id} |
Poll the isolated correction and independent quality reviews. |
GET /api/v1/translations/{job_id}/revisions |
List immutable revision metadata; append a revision number to fetch its complete historical PLM payload plus revision creation time, kind and provenance. |
POST /api/v1/translations/{job_id}/confirm |
Reviewer-only confirmation of the exact current whole-package revision and hash. |
A correction stays within the same job and carries no second customer charge. It does not write another audit, billing quote, cache or translation memory entry. Ambiguous, low-confidence or allergen-changing feedback fails closed without publishing a new revision. A later correction preserves an older confirmation as history but requires the new package to be confirmed again. Safety-relevant failed feedback pauses effective verification until a later successful correction creates a new head. Confirmation records eligibility for a later translation-memory promotion flow; it does not hide the current result or block PLM export in this pilot.
A historical revision payload keeps the original translation job's
result.request.submitted_at. The later revision timestamp is
exposed separately as revision.created_at.
Full request/response examples and role semantics are in the detailed integration reference supplied to pilot customers.
GET /api/v1/health
Unauthenticated liveness probe → 200 {"status": "ok"}.
Errors
All errors are JSON. Two shapes exist:
- Auth/rate-limit errors (from middleware):
{"error": "<code>", …} - Route errors (FastAPI):
{"detail": "<message>"}— validation errors (422) carry FastAPI's structureddetaillist.
| HTTP | when | body sketch |
|---|---|---|
| 400 | malformed request (non-JSON body, …) | {"detail": …} |
| 401 | missing / invalid / revoked token | {"error": "unauthorized"} |
| 402 | insufficient prepaid credits, or org monthly cap reached | {"detail": "Insufficient credits: …"} |
| 404 | unknown job id (incl. jobs of other orgs) | {"detail": "unknown job_id"} |
| 409 | idempotency record unreadable or key reused for another request | {"detail": "client_request_id conflicts …"} |
| 422 | schema violation (bad language code, empty/oversize input) | {"detail": [ … ]} |
| 429 | rate limit exceeded | {"error": "rate_limit_exceeded", "retry_after_seconds": n}
+ Retry-After header |
| 501 | callback_url set (webhooks not shipped yet) |
{"detail": "callback_url … not implemented …"} |
| 503 | package review or feedback revisions not enabled | {"detail": "… not enabled"} |
Rate limits
Default 60 requests/minute per token (sliding 60-second
window, counts every authenticated /api/v1/* request). On
429, wait retry_after_seconds (also sent as the
Retry-After header) before retrying.
Contact support to raise a token's limit.
Webhooks
callback_url is reserved in the request schema and returns
501 until webhook delivery ships in a later release. Until
then, poll status_url (2 s cadence, 60 s timeout
— see above).