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:

fieldtyperequirednotes
ingredientstringyes 1–2000 chars. Single ingredient or comma-separated label string — the pipeline handles both.
source_languagestringno One of en de fr it hu sl es. Default en.
target_languagesstring[]no Subset of the same codes, min 1. Default: token owner's saved preferences, else all seven.
domainstringno Only "food" in v1.
callback_urlstringno Reserved. Setting it returns 501 — webhooks arrive in a later release. Poll status_url instead.
client_request_idstringno ≤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 stateHTTPbody
pending / running200 {"job_id", "status", "status_url"}
done200 {"job_id", "status": "done", "result": {…}} (schema above)
failed200 {"job_id", "status": "failed", "error": "<safe message>"}
unknown / other org404 {"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.

endpointpurpose
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:

HTTPwhenbody sketch
400malformed request (non-JSON body, …) {"detail": …}
401missing / invalid / revoked token {"error": "unauthorized"}
402insufficient prepaid credits, or org monthly cap reached {"detail": "Insufficient credits: …"}
404unknown job id (incl. jobs of other orgs) {"detail": "unknown job_id"}
409idempotency record unreadable or key reused for another request {"detail": "client_request_id conflicts …"}
422schema violation (bad language code, empty/oversize input) {"detail": [ … ]}
429rate limit exceeded {"error": "rate_limit_exceeded", "retry_after_seconds": n} + Retry-After header
501callback_url set (webhooks not shipped yet) {"detail": "callback_url … not implemented …"}
503package 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).