---
title: "Validate or apply a translation batch"
method: POST
path: "/projects/{projectId}/translations/{operation}"
tags: ["Translations"]
---

# Validate or apply a translation batch

`POST /projects/{projectId}/translations/{operation}`

## Request

- Base URL: `https://studio.weaverse.io/api/v1/content`
- URL: `https://studio.weaverse.io/api/v1/content/projects/{projectId}/translations/{operation}`
- Auth: HTTP bearer

Submits a batch of 1-50 translation units read from `GET /projects/{projectId}/translation-units`. `{operation}` is one of:

- `validate`: runs every check (unit membership, value shape, placeholders, source and target generations, duplicates) and writes nothing. Returns `object: "translation_validation"` with a per-unit `outcome` of `apply` or `unchanged`.
- `apply`: runs the same checks under the project lock, then writes only `Translation` rows, atomically. Returns `object: "translation_apply"` with `applied`/`unchanged` counts and an exact primary `readback` of every submitted unit. The cache is invalidated once; if that fan-out fails the write still stands and the response carries `cacheDelivery: "degraded"`.

Any other `{operation}` value returns 404.

Each unit echoes `id`, `sourceGeneration` and `targetGeneration` exactly as listed (`targetGeneration: null` when no target was stored), plus `translatedValue`:

- Text units take a string (max 20,000 characters). `{{name}}` and `{name}` placeholders in the source must appear unchanged in the translation.
- Image units take `{ url, alt?, width?, height? }`; `url` must be `https://` or root-relative.
- `null` clears the overlay so the source shows through. An empty string is rejected. Clearing an absent target, or re-clearing a cleared one, reports `unchanged`.

Concurrency and atomicity:
- Any stale source or target refuses the whole batch with 409 `STALE_TRANSLATION` (`details.unitId`, `details.side`) and writes nothing. Re-read the inventory and resubmit.
- A unit outside this project's writable content for the locale returns 404 `TRANSLATION_UNIT_NOT_FOUND` (`details.unitId`) and writes nothing.
- A unit whose target already holds the requested value is `unchanged` regardless of the generation it carries, so an identical retry converges.
- On `apply`, every submitted unit (including ones the preflight found unchanged) is re-proven against locked source and target state; a unit that moved between preflight and lock fails the whole batch.
- Under the same lock, `apply` re-proves each unit's stored-schema approval (the field must still be a declared text input, or image input for an image unit, of its component type; otherwise 409) and the target's authority (the locale must still resolve to the same configured, non-default language; otherwise 400 or 409). Nothing is written in either case.

Authentication: accepts shop API keys, scoped API keys and delegated MCP OAuth tokens that hold the scope, and project-bound translation agent tokens (`wv_agent_…`, Authorization header only). Agent tokens are accepted only on the translation-unit routes.

Required scope: `translations:write`

## Request body

- TranslationUnitsBatch
  - `targetLocale` string, required
  - `units` object[], required
    - `id` string, required
    - `sourceGeneration` string, required
    - `targetGeneration` string, nullable, required: null = no stored target when read
    - `translatedValue` union, required: null clears the overlay; empty string is rejected. Image units take an object.
      - string
      - object
        - `url` string, required: https:// or root-relative
        - `alt` string
        - `width` integer
        - `height` integer

## Response `200`

translation_validation or translation_apply result

- object

## Other responses

- `400`: Invalid batch, value or placeholders (nothing written)
- `401`: Missing or invalid credentials
- `403`: Foreign project or missing scope (INSUFFICIENT_SCOPE)
- `404`: Unknown {operation} (only validate and apply exist), or TRANSLATION_UNIT_NOT_FOUND: a unit outside this project (nothing written)
- `409`: STALE_TRANSLATION: a source or target changed since read (nothing written)

---

[API](https://skmtc.dev/weaverse/apis/weaverse-content-api.md) - [All operations](https://skmtc.dev/weaverse/apis/weaverse-content-api/llms.txt) - [OpenAPI document](https://skmtc.dev/weaverse/apis/weaverse-content-api/revisions/d189794169bb?raw)
