---
title: "Image Translate"
description: "Replace text inside product images with a translation that preserves the original visual style — font weight, colour, layout, and badge backdrops."
---

## Endpoint

```
POST /v1/image-translate/translate
```

Send a product image as raw bytes; receive the same image with the source text removed and replaced with a translation. Font, weight, and render mode are caller-controlled via query string.

- **Input languages** — Chinese (Simplified or Traditional), Japanese, English. Pass `auto` to skip the script gate.
- **Output languages** — any ISO-639-style code. Visual fidelity depends on whether the chosen `font` covers the script. Korean, Simplified Chinese, Japanese, and Latin targets are first-class. Call [`/supported-fonts`](./supported-fonts) for the live list of fonts and the source-language codes accepted.

## Request Format

The endpoint accepts **raw image bytes** in the HTTP body. JSON-with-base64 is not accepted.

| Header | Required | Notes |
|---|---|---|
| `Content-Type` | Yes | `image/jpeg`, `image/png`, `image/webp`, or `application/octet-stream` |
| `x-api-key` | Yes | Your API key |

All other parameters travel in the **query string**:

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `source_language` | string | Yes | — | One of `ch`, `zh`, `zh-CN`, `zh-TW`, `chinese_cht`, `ja`, `japan`, `en`, or `auto`. `auto` skips the script gate — every region with a translation is rendered. |
| `target_language` | string (2–8) | Yes | — | ISO-639-style target code. Free-form. |
| `font` | string | No | `Pretendard` | One of the values returned by [`/supported-fonts`](./supported-fonts) under `allFonts`. URL-encode if it contains non-ASCII characters. **`Pretendard` is the default** — modern Korean variable sans with full Hangul, CJK punctuation, and Latin Extended-A coverage. The renderer also runs a built-in fallback chain so any glyph the chosen font is missing (e.g. CJK Han ideographs) is filled from `Pretendard` and `NotoSansSC` automatically. |
| `font_weight` | string | No | `ExtraBold` | One of `auto`, `Thin`, `Light`, `Regular`, `Medium`, `SemiBold`, `Bold`, `ExtraBold`, `Black`. `auto` matches the original per-region weight where possible; any explicit weight overrides every region uniformly. |
| `render` | boolean | No | `true` | `true` returns a final translated image (binary). `false` returns the inpainted image plus per-region metadata as JSON so you can draw the overlay yourself. |

## Response

### `render=true` (default) — binary image

When `render=true` the response body is the **rendered JPEG bytes** with `Content-Type: image/jpeg`. Metadata flows through response headers:

| Header | Description |
|---|---|
| `X-Apinoa-Request-Id` | Request id (echo this when filing a ticket) |
| `X-Apinoa-Regions-Count` | Number of text regions detected and replaced |

If no text is detected, the response is the original image bytes with `X-Apinoa-Regions-Count: 0`.

### `render=false` — JSON with regions

When `render=false` the response is JSON:

```json
{
  "success": true,
  "requestId": "550e8400e29b41d4a716446655440000",
  "data": {
    "inpainted_base64": "<base64 JPEG with text removed>",
    "regions": [
      {
        "polygon": [[10, 20], [210, 20], [210, 60], [10, 60]],
        "bbox": { "x": 10, "y": 20, "w": 200, "h": 40 },
        "text": "免费送货",
        "text_kr": "무료 배송",
        "confidence": 0.98,
        "text_color": { "r": 255, "g": 255, "b": 255 },
        "bg_color": { "r": 200, "g": 50, "b": 50 },
        "font_weight": "Bold",
        "is_badge": true
      }
    ],
    "regions_count": 5
  }
}
```

| Field | Type | Description |
|-------|------|-------------|
| `inpainted_base64` | string | Base64-encoded JPEG with the source text removed. |
| `regions[].polygon` | number[][] | 4-point polygon in original image coordinates. |
| `regions[].bbox` | object | Axis-aligned bounding box in original coordinates. |
| `regions[].text` | string | Detected source text. |
| `regions[].text_kr` | string | The translation, in the requested `target_language`. |
| `regions[].confidence` | number | Recognition confidence, 0–1. |
| `regions[].text_color` / `bg_color` | object | RGB colour estimates. |
| `regions[].font_weight` | string | Detected weight: `Thin`, `Regular`, `Bold`. |
| `regions[].is_badge` | boolean | `true` for filled-rectangle text labels (price tags, sale badges). |

## Examples

### Basic translation (cURL)

```bash
curl -X POST "https://gateway.apinoa.com/v1/image-translate/translate?source_language=zh-CN&target_language=ko" \
  -H "x-api-key: $APINOA_API_KEY" \
  -H "Content-Type: image/jpeg" \
  --data-binary @product.jpg \
  -o translated.jpg
```

### Pick a font (Python)

```python
import requests

with open("product.jpg", "rb") as f:
    img_bytes = f.read()

r = requests.post(
    "https://gateway.apinoa.com/v1/image-translate/translate",
    headers={
        "x-api-key": "YOUR_API_KEY",
        "Content-Type": "image/jpeg",
    },
    params={
        "source_language": "zh-CN",
        "target_language": "ko",
        "font": "Pretendard",
        "font_weight": "Bold",
    },
    data=img_bytes,
    timeout=300,  # the server timeout is 5 minutes — see "Timeouts" below
)
r.raise_for_status()
with open("translated.jpg", "wb") as f:
    f.write(r.content)
print("regions:", r.headers.get("X-Apinoa-Regions-Count"))
```

### Inpaint-only, render client-side (Node.js / TypeScript)

```typescript
import { readFileSync, writeFileSync } from "node:fs";

const image = readFileSync("product.jpg");

const url = new URL("https://gateway.apinoa.com/v1/image-translate/translate");
url.searchParams.set("source_language", "zh-CN");
url.searchParams.set("target_language", "ko");
url.searchParams.set("render", "false");

const res = await fetch(url, {
  method: "POST",
  headers: {
    "x-api-key": process.env.APINOA_API_KEY!,
    "Content-Type": "image/jpeg",
  },
  body: image,
});

const { data } = (await res.json()) as {
  data: { inpainted_base64: string; regions: any[] };
};
writeFileSync("inpainted.jpg", Buffer.from(data.inpainted_base64, "base64"));
```

## Limits

| Limit | Value |
|-------|-------|
| Max image dimensions | 8192 × 8192 px |
| Max request body | 20 MB |
| Per-API-key QPS | matches your plan's per-second limit |
| Image format | JPEG, PNG, WebP |

Oversize images return `413 Payload Too Large`. Excessive QPS returns `429 Too Many Requests` with `retry-after` set.

## Errors

| Code | HTTP | When | What to do |
|------|------|------|------------|
| `BAD_REQUEST` | 400 | Empty body or invalid query params | Check the request shape |
| `UNAUTHORIZED` | 401 | Missing or invalid API key | Check the `x-api-key` header |
| `UNSUPPORTED_MEDIA_TYPE` | 415 | `Content-Type` is not a binary image type | Set `Content-Type: image/jpeg` (or png/webp) and POST raw bytes |
| `PAYLOAD_TOO_LARGE` | 413 | Body > 20 MB | Downscale client-side |
| `INSUFFICIENT_BALANCE` | 402 | Your balance does not cover the call | Top up under **Dashboard > Balance**; retrying will not clear it |
| `UPSTREAM_ERROR` | 502 | Image processing temporarily unavailable | Retry; usually recovers in &lt; 60 s |
| `SERVICE_UNAVAILABLE` | 503 | Temporarily over capacity | Retry with backoff |

## Timeouts

A request can take **up to 5 minutes** (300 s) under heavy load. Set your HTTP client's timeout to **at least 300 seconds** so a slow response is not aborted on your side.

| Latency | Typical | Worst case |
|---|---|---|
| Normal load | 1.5 – 3 s | < 10 s |
| Heavy load | 5 – 30 s | 5 min |

Most requests finish in a few seconds; the 5-minute window only matters under sustained burst traffic. If you reliably approach it, lower your concurrency or contact support.

## Pricing

Each call is charged against your balance at the image-translate rate on the [pricing page](/pricing).

## Support

If you hit repeated 5xx errors or see obvious quality regressions, include the `requestId` from the `X-Apinoa-Request-Id` response header when you open a ticket at [apinoa.com/contact](/contact).
