# Tạo ảnh và video: Jobs API

> **TL;DR** — `POST /v2/jobs` với một model, một style và dữ liệu đầu vào của bạn sẽ trả về `job_id` sau khoảng một giây. Wandura tạo kết quả ở nền rồi POST job cuối cùng (có ký) tới `webhook_url` của bạn; `GET /v2/jobs/{job_id}` là đường dự phòng. Key test (`wdr_test_…`) là sandbox miễn phí. Body sai bị từ chối ngay bằng `422` kèm lý do; job `failed`, `timeout`, bị huỷ hoặc bị moderation chặn thì **không bị tính tiền**. Đây là cửa khuyên dùng cho mọi tích hợp mới.

## Cơ bản
- **Base URL**: `https://api.wandura.ai` (vùng Singapore).
- **OpenAPI 3.1**: [api.wandura.ai/v2/openapi.json](https://api.wandura.ai/v2/openapi.json), không cần key. Jobs API là các operation gắn tag `Jobs`.
- **Luồng**: submit kèm `webhook_url` và `Idempotency-Key` → `202 {job_id, status: "queued"}` → Wandura POST job cuối cùng (có ký) tới `webhook_url`, bạn trả `200` → nếu webhook không tới, poll `GET /v2/jobs/{job_id}` → tải `output[0].url`.
- Mọi request là JSON qua HTTPS.

| Endpoint | Làm gì |
|---|---|
| `POST /v2/jobs` | Gửi một job. |
| `GET /v2/jobs/{job_id}` | Trạng thái và output. Cùng body với webhook. |
| `POST /v2/jobs/{job_id}/cancel` | Huỷ job còn `queued`. |
| `GET /v2/models` | Danh sách model. |
| `GET /v2/models/{model_id}/schema` | JSON Schema của body submit cho một model. |
| `GET /v2/styles?model=` | Các style đang chạy, kèm bản xem trước. |

## Xác thực và key
Mọi lời gọi gửi `Authorization: Bearer <key>`. Tạo key ở [console](https://open.wandura.ai/keys); một product có thể có nhiều key và xoay vòng được. Giữ key ở máy chủ của bạn.

| Key | Làm gì | Chi phí |
|---|---|---|
| `wdr_test_…` | **Sandbox.** Cùng API, cùng validation, status và webhook; job trả về một file mẫu thay vì tạo thật | $0 |
| `wdr_live_…` | Tạo thật | Giá của style, chỉ khi job thành công |

Job thuộc về product của key đã gửi nó: key của product khác nhận `404`.

## Model và schema
Hai model, mỗi loại đầu ra một model.

| `model` | Đầu ra |
|---|---|
| `wandura-image-v1` | ảnh |
| `wandura-video-v1` | video |

`v1` trong model id là version của **model**, không phải version của API. Id cố định: thay đổi phá vỡ tương thích sẽ có id mới (`…-v2`), không bao giờ đổi âm thầm. Style được thêm và gỡ dưới một model mà không đổi model.

```bash
curl https://api.wandura.ai/v2/models -H "Authorization: Bearer $WANDURA_KEY"

# JSON Schema (2020-12) của body submit cho một model: kiểu, field bắt buộc, min/max,
# độ dài, danh sách style id, và với mỗi style: field nào bắt buộc, field nào không nhận
curl https://api.wandura.ai/v2/models/wandura-video-v1/schema -H "Authorization: Bearer $WANDURA_KEY"
```

Đọc schema lúc build hoặc theo lịch để dựng form nhập; nó đổi khi style được thêm hoặc gỡ.

## Style
Style là một "kiểu" đã đóng gói sẵn: prompt, các bước tạo và cài đặt của nó nằm ở phía Wandura. `GET /v2/styles?model=wandura-video-v1` liệt kê các style đang chạy mà key của bạn dùng được:

```json
{
  "id": "sty_motion_clip_test", "object": "style", "model": "wandura-video-v1", "name": "Motion Clip",
  "description": "Bring a still image to life with gentle camera motion.",
  "input_hint": "One clear image of the subject you want to bring to life.",
  "preview_url": "https://<wandura-storage-host>/samples/v1/motion-clip.mp4",
  "inputs": { "image_url": "required", "prompt": "optional" },
  "max_job_seconds": 300, "price": { "credits": 23.4, "usd": 0.234 }
}
```

| Field | Ý nghĩa |
|---|---|
| `preview_url` | Link https tới ảnh hoặc video mẫu làm bằng style này. Hiển thị trong màn chọn style của bạn. `null` nếu style chưa có mẫu. |
| `inputs` | Các field của `POST /v2/jobs` mà style này nhận, mỗi field là `required` hoặc `optional`. Field không có trong danh sách sẽ bị từ chối. |
| `max_job_seconds` | Thời gian tối đa của style. Quá mức này job kết thúc `timeout`, không tính tiền. |
| `price` | Giá của một job live **thành công** (1 credit = $0.01). |

## Gửi, poll, huỷ
```bash
curl -X POST https://api.wandura.ai/v2/jobs \
  -H "Authorization: Bearer $WANDURA_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: your-job-123" \
  -d '{
    "model": "wandura-video-v1",
    "style": "sty_motion_clip_test",
    "image_url": "https://your-bucket.example.com/u/42/photo.jpg?X-Amz-Signature=...",
    "prompt": "a girl smiling at the camera",
    "webhook_url": "https://your-backend.example.com/hooks/wandura",
    "end_user_id": "user_42"
  }'
# HTTP 202
# X-RateLimit-Limit: 60
# X-RateLimit-Remaining: 59
# X-RateLimit-Reset: 1791897360
# X-Concurrency-Limit: 20
# X-Concurrency-Running: 3
# X-Concurrency-Waiting: 0
# {"job_id":"run_QVqsXwr7VGSP","status":"queued","model":"wandura-video-v1","created_at":"2026-10-13T13:15:40.825Z"}

curl https://api.wandura.ai/v2/jobs/run_QVqsXwr7VGSP -H "Authorization: Bearer $WANDURA_KEY"

curl -X POST https://api.wandura.ai/v2/jobs/run_QVqsXwr7VGSP/cancel -H "Authorization: Bearer $WANDURA_KEY"
```

| Field | Ghi chú |
|---|---|
| `model` | Bắt buộc. `wandura-image-v1` hoặc `wandura-video-v1`. |
| `style` | Bắt buộc. Id style của model đó (`GET /v2/styles`). |
| `prompt` | Đoạn chữ mà style đưa vào kết quả. Tuỳ chọn, trừ khi style liệt kê nó là bắt buộc; `""` được coi như không gửi. |
| `image_url`, `image_urls`, `video_url` | URL https công khai (URL presigned GET dùng được, tối đa 8192 ký tự). Ảnh JPEG, PNG hoặc WEBP tới 20 MB; video MP4, MOV hoặc WEBM tới 50 MB. |
| `negative_prompt`, `aspect_ratio`, `resolution`, `duration`, `seed` | Chỉ cho style có liệt kê trong `inputs`. Xem [Validation](https://docs.wandura.ai/vi/developers/api-v2/jobs#validation). |
| `num_outputs` | Tuỳ chọn, phải là `1`: mỗi job một output. |
| `webhook_url` | Tuỳ chọn. URL https công khai nhận job cuối cùng, ký bằng Jobs API signing secret của product, hoặc nếu product chưa có thì bằng signing secret của Webhook endpoint. Cần có một trong hai trước (xem [Webhook](https://docs.wandura.ai/vi/developers/api-v2/jobs#webhooks)). |
| `end_user_id` | Tuỳ chọn. Id của người dùng trong hệ thống của bạn; khi gửi, giới hạn theo ngày cho từng người sẽ áp dụng (xem [Giới hạn](https://docs.wandura.ai/vi/developers/api-v2/jobs#limits)). |
| `lane` | **Đang triển khai.** Tuỳ chọn: `priority` (mặc định) hoặc `relax`. Xem [Lane Relax và đầu vào riêng của style](https://docs.wandura.ai/vi/developers/api-v2/jobs#lane-and-inputs). |
| `inputs` | **Đang triển khai.** Object tuỳ chọn: các đầu vào riêng của style mà không trường nào ở trên mang được. Xem [Lane Relax và đầu vào riêng của style](https://docs.wandura.ai/vi/developers/api-v2/jobs#lane-and-inputs). |

- **URL đầu vào được tải khi job bắt đầu chạy**, không phải lúc submit. URL không tải được sẽ làm job `failed`, không tính tiền. Giữ URL presigned còn hạn ít nhất bằng `max_job_seconds` của style cộng 10 phút.
- **Huỷ** được khi job còn `queued`: `200`, status `cancelled`, không tính tiền, và webhook vẫn được gửi. Khi job đã `processing` nó chạy tới cùng, và huỷ trả `409 JOB_NOT_CANCELLABLE`.

## Lane Relax và đầu vào riêng của style
**Đang triển khai.** Hai trường tuỳ chọn của `POST /v2/jobs`, để style nào key chạy được cũng chạy được thành job. Body không có hai trường này được trả lời y như trước.

**`lane`**: `priority` (mặc định) hoặc `relax`.

|  | `priority` | `relax` |
|---|---|---|
| Giá | Giá Priority của style | Giá Relax của style, không bao giờ cao hơn Priority |
| Hạn chót | `max_job_seconds`; quá hạn thì job kết thúc `timeout`, không tính tiền | Không có: job xếp hàng tới lượt thì chạy |
| Khi product đã đầy | `429 PRODUCT_BUSY` | Không bao giờ bị từ chối vì lý do đó; job chờ ở `queued` |

Body của job giống nhau ở cả hai lane (không có trường `lane`). Cả hai giá có trong catalog công khai (`GET /v2/public/styles`, `price.priority` và `price.relax`).

**`inputs`**: object chứa các đầu vào riêng của style mà không trường nào trong bảng trên mang được, đặt tên đúng như trong schema đầu vào của style. Ví dụ: công tắc `keep_audio`, trường chữ thứ hai (`prompt` điền trường đầu tiên), `mask_url`, hoặc `video_url` trên style ra ảnh.

```bash
curl -X POST https://api.wandura.ai/v2/jobs \
  -H "Authorization: Bearer $WANDURA_KEY" -H "Content-Type: application/json" \
  -d '{"model": "wandura-video-v1", "style": "<style id>", "image_urls": ["https://…/photo.jpg"], "lane": "relax", "inputs": {"keep_audio": false}}'
```

- **Tìm đầu vào của một style ở đâu**: `GET /v2/public/styles/{style}` → `inputs` (không cần key). OpenAPI của từng style, `GET /v2/public/styles/{style}/openapi.json`, có sẵn body `POST /v2/jobs` đúng cho style đó, kèm ví dụ.
- Đầu vào nào đã có trường ở trên mang thì gửi bằng trường đó, không gửi trong `inputs`: `{"field": "inputs.image_urls", "reason": "send it as image_urls"}`.
- Style có đầu vào **bắt buộc** không thuộc các trường ở trên chỉ chạy khi body gửi `inputs`. Style đó không có trong `GET /v2/styles` hay schema của model; không gửi `inputs` thì nhận đúng `422` như một style không tồn tại.
- `GET /v2/models/{model_id}/schema` chỉ liệt kê các trường trong bảng trên; `lane` và `inputs` nằm trong [OpenAPI](https://api.wandura.ai/v2/openapi.json) (`JobRequest`).
- Style ra âm thanh chưa có trên Jobs API.

Lỗi ở hai trường này trả về trong `details` của `422 INVALID_INPUT`, như mọi trường khác:

| `field` | `reason` |
|---|---|
| `lane` | `must be priority or relax` |
| `inputs` | `must be an object of the style's own inputs` |
| `inputs.<name>` | `send it as <field>` · `style <id> does not take this input` · `must be true or false` · `must be a public https URL (at most 8192 characters)` · `must be a list of 1 to 10 https URLs` · `must be a string of at most 4000 characters` · `required by style <id>` |

## Validation chặt và đồng bộ
Body sai không bao giờ trở thành job. Wandura kiểm tra cả body ngay lúc submit và liệt kê **mọi** lỗi:

```json
HTTP 422
{"error":{"code":"INVALID_INPUT","message":"Validation failed.","retryable":false,"details":[
  {"field":"num_outputs","reason":"must be 1 (one output per job)"},
  {"field":"aspect_ratio","reason":"style sty_motion_clip_test does not take this field"},
  {"field":"image_url","reason":"required by style sty_motion_clip_test"}]}}
```

- **Style tự cố định tỉ lệ khung hình, độ phân giải và thời lượng.** Các style hiện tại không nhận `aspect_ratio`, `resolution`, `duration` hay `seed`, nên gửi một trong số đó sẽ trả `422` kèm lý do. Chỉ gửi những gì `inputs` của style liệt kê, và dùng schema của model để biết đó là gì.
- Field lạ, thiếu field bắt buộc, URL sai định dạng và style không tồn tại đều là `422`.

## Idempotency
Gửi `Idempotency-Key` (1 đến 255 ký tự) ở mọi submit để retry không bao giờ chạy hai lần.

- Cùng key và cùng body: cùng một job (`200`, header `Idempotent-Replayed: true`, status hiện tại của job).
- Cùng key nhưng body khác: `409 IDEMPOTENCY_KEY_REUSED`. Dùng key mới cho job mới.
- Không gửi header: Wandura tự sinh key từ API key và body, nên cùng body trong 10 phút là cùng một job.

## Status
```text
queued → processing → succeeded | failed | cancelled | timeout
```

Bốn status cuối là status cuối cùng và không bao giờ đổi nữa.

| `status` | Cuối | Tính tiền (key live) | `error` |
|---|---|---|---|
| `queued` | không | chưa | `null` |
| `processing` | không | chưa | `null` |
| `succeeded` | có | có, giá của style | `null` |
| `failed` | có | **không** | `GENERATION_FAILED`, `INPUT_UNUSABLE` hoặc `CONTENT_MODERATION_BLOCKED` |
| `timeout` | có | **không** | `TIMEOUT` |
| `cancelled` | có | **không** | `null` |

Lỗi nằm trong một job đã kết thúc (`error.code`):

| `status` | `code` | `retryable` | Ý nghĩa |
|---|---|---|---|
| `failed` | `CONTENT_MODERATION_BLOCKED` | false | Đầu vào hoặc kết quả bị content moderation chặn. Không tính tiền. |
| `failed` | `INPUT_UNUSABLE` | false | Một file đầu vào không dùng được (không tải được, định dạng không hỗ trợ, hoặc quá dài). Không tính tiền. |
| `failed` | `GENERATION_FAILED` | true | Tạo kết quả thất bại ở phía Wandura. Không tính tiền; gửi lại thường là được. |
| `timeout` | `TIMEOUT` | true | Job không xong trong thời gian tối đa của style. Không tính tiền. |

Bị moderation chặn là một job cuối cùng bình thường, không phải lỗi HTTP:

```json
{"job_id":"run_4kT9wQx2LmPa","status":"failed","model":"wandura-video-v1","style":"sty_motion_clip_test","output":null,
 "error":{"code":"CONTENT_MODERATION_BLOCKED","message":"The input or the result was blocked by content moderation. Not charged.","retryable":false},
 "created_at":"2026-10-13T13:12:02.410Z","completed_at":"2026-10-13T13:12:03.902Z"}
```

## Output
`output` là danh sách có **một** file, và là `null` cho tới khi job `succeeded`.

```json
{"job_id":"run_…","status":"succeeded","model":"wandura-image-v1","style":"sty_…",
 "output":[{"url":"https://<wandura-storage-host>/outputs/2026-10-10/<random name>.png","type":"image/png",
   "width":48,"height":32,"duration":null,"fps":null,"size_bytes":136,"seed":null,"expires_at":"2026-10-13T00:00:00.000Z"}],
 "error":null,"created_at":"2026-10-10T00:00:00.000Z","completed_at":"2026-10-10T00:00:00.000Z"}
```

- `url` là link HTTPS trên storage của Wandura (Wandura CDN), tải thẳng bằng GET thường. Đó không phải link của bên thứ ba nào.
- **Mở được ít nhất 7 ngày.** `expires_at` cho biết link được bảo đảm tới khi nào. Cần giữ lâu hơn thì hãy chép file về storage của bạn trước đó.
- Metadata đọc từ chính file: `type`, `width`, `height`, `duration` và `fps` (video), `size_bytes`, `seed`. Giá trị không đọc được là `null`.
- File mẫu của key test có metadata `null` và không hết hạn.
- Bản ghi job được giữ lâu hơn 7 ngày nhiều: `GET /v2/jobs/{job_id}` vẫn trả lời cho job cũ.

## Webhook
Mỗi job có `webhook_url` nhận **một** POST khi đạt status cuối cùng (không có webhook trung gian). Body đúng bằng thứ `GET /v2/jobs/{job_id}` trả về tại thời điểm đó.

1. **Có một signing secret, làm một lần**: ở [console](https://open.wandura.ai/webhooks), hoặc lưu một URL Webhook endpoint (signing secret của nó ký luôn webhook của job), hoặc tạo riêng "Jobs API signing secret" (Create; chỉ hiện một lần; Roll thay ngay lập tức). Product có Jobs API signing secret thì secret đó ký webhook của job. Khi chưa có cái nào trong hai, submit kèm `webhook_url` bị từ chối bằng `422`.
2. **Verify mọi request** trên **raw body**: `X-Signature` là HMAC-SHA256 dạng hex của `X-Timestamp + "." + raw body` bằng secret của bạn, và `X-Timestamp` (unix giây) lệch hiện tại không quá 5 phút.
3. **Trả `2xx` trong 10 giây**, rồi mới xử lý việc của bạn. Dedupe theo `job_id` (hoặc header `Wandura-Event-Id`, giống nhau ở mọi lần retry): giao tin ít nhất một lần.

```http
POST /hooks/wandura HTTP/1.1
Content-Type: application/json
User-Agent: Wandura-Webhooks/1
X-Timestamp: 1791897341
X-Signature: 6f1c…(64 hex characters)
Wandura-Event-Id: evt_…
Wandura-Event-Type: job.ended

{"job_id":"run_QVqsXwr7VGSP","status":"succeeded","model":"wandura-video-v1", …}
```

### Chính sách retry
Trả non-2xx (kể cả redirect), timeout hoặc lỗi mạng sẽ được retry sau 30 giây, 2 phút, 10 phút, 1 giờ, 6 giờ và 24 giờ: **7 lần thử trong khoảng 31 giờ**, cùng body và `job_id`. URL có host trỏ về địa chỉ riêng hoặc dành riêng sẽ không bao giờ được gọi. Nếu mọi lần thử đều thất bại, job vẫn còn đó: hãy poll.

### Verify chữ ký
Node.js (Express):

```js
import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';

// raw = the body exactly as received (before JSON.parse)
export function verifyWandura(raw, headers, secret, toleranceS = 300) {
  const ts = headers['x-timestamp'], sig = String(headers['x-signature'] || '');
  if (!/^\d+$/.test(ts || '') || Math.abs(Date.now() / 1000 - Number(ts)) > toleranceS) return false;
  const want = createHmac('sha256', secret).update(`${ts}.${raw}`).digest('hex');
  return sig.length === want.length && timingSafeEqual(Buffer.from(sig), Buffer.from(want));
}

const app = express();
app.post('/hooks/wandura', express.raw({ type: 'application/json' }), (req, res) => {
  const raw = req.body.toString('utf8');
  if (!verifyWandura(raw, req.headers, process.env.WANDURA_JOBS_SECRET)) return res.sendStatus(401);
  const job = JSON.parse(raw); // { job_id, status, output, error, ... }
  // store it, dedupe on job.job_id, then:
  res.sendStatus(200);
});
```

Python (Flask):

```python
import hashlib, hmac, os, time
from flask import Flask, request

def verify_wandura(raw: bytes, headers, secret: str, tolerance_s: int = 300) -> bool:
    ts, sig = headers.get("X-Timestamp", ""), headers.get("X-Signature", "")
    if not ts.isdigit() or abs(time.time() - int(ts)) > tolerance_s:
        return False
    want = hmac.new(secret.encode(), ts.encode() + b"." + raw, hashlib.sha256).hexdigest()
    return hmac.compare_digest(want, sig)

app = Flask(__name__)

@app.post("/hooks/wandura")
def wandura_hook():
    raw = request.get_data()  # raw bytes, before any JSON parsing
    if not verify_wandura(raw, request.headers, os.environ["WANDURA_JOBS_SECRET"]):
        return "", 401
    job = request.get_json()  # dedupe on job["job_id"]
    return "", 200
```

Dùng sandbox để thử endpoint của bạn: job `wdr_test_…` cũng bắn webhook có ký y như thật.

## Lỗi
Mọi câu trả lời lỗi có dạng `{"error": {"code", "message", "retryable"}}`, thêm `details` cho `INVALID_INPUT`. Rẽ nhánh theo `code`; khi nhờ hỗ trợ, đưa kèm header `X-Request-Id`. **Đang triển khai:** mỗi lỗi có thêm `docs_url`, link tới mục của nó trên trang [Lỗi](https://docs.wandura.ai/vi/developers/api-v2/errors#jobs-errors), và các header `Wandura-Error-Code`, `Wandura-Should-Retry` (xem [Thân lỗi và header](https://docs.wandura.ai/vi/developers/api-v2/errors#error-body)).

| HTTP | `code` | `retryable` | Ý nghĩa |
|---|---|---|---|
| 422 | `INVALID_INPUT` | false | Body hoặc header sai; `details[]` liệt kê từng `{field, reason}`. |
| 401 | `UNAUTHORIZED` | false | Thiếu key, key lạ, đã thu hồi hoặc hết hạn. |
| 402 | `INSUFFICIENT_CREDITS` | false | Ví của product không đủ trả cho job. Nạp thêm rồi thử lại. |
| 403 | `FORBIDDEN` | false | Key hợp lệ nhưng không được làm việc này. Đang triển khai: key quyền Read only gửi hoặc huỷ job, hoặc key chỉ được chạy style khác (xem [Khoá API](https://docs.wandura.ai/vi/developers/api-v2/concepts#api-keys)). Trước đó: chưa trả. |
| 404 | `NOT_FOUND` | false | Không có job, model hoặc endpoint đó cho key này. |
| 409 | `IDEMPOTENCY_KEY_REUSED` | false | `Idempotency-Key` này đã dùng với body khác. |
| 409 | `JOB_NOT_CANCELLABLE` | false | Chỉ huỷ được job đang queued. |
| 429 | `RATE_LIMITED` | true | Product gửi quá nhiều job trong phút này. Thử lại sau `Retry-After`. |
| 429 | `PRODUCT_BUSY` | true | Product đã hết suất chạy và hàng chờ đầy. Thử lại sau `Retry-After` (khoảng 5 s). |
| 429 | `END_USER_LIMIT_REACHED` | true | End user này đã chạm giới hạn job trong ngày. Thử lại sau `Retry-After` (reset 00:00 UTC). |
| 500 | `INTERNAL_ERROR` | true | Phía Wandura có sự cố. Thử lại với cùng `Idempotency-Key`. |
| 503 | `SERVICE_UNAVAILABLE` | true | Style này tạm thời không chạy được trên key live (key test vẫn trả file mẫu). Hoặc hàng đợi job tạm thời không nhận: không có gì được tạo hay tính tiền; thử lại sau `Retry-After` với cùng `Idempotency-Key`. |

`429` luôn kèm header `Retry-After` (giây). Chờ đúng khoảng đó, cộng thêm một chút jitter ngẫu nhiên, rồi retry với **cùng** `Idempotency-Key`. Chỉ retry những lỗi có `retryable`. Câu trả lời submit, kể cả `429`, cũng có `X-Concurrency-Limit`, `X-Concurrency-Running` và `X-Concurrency-Waiting`: xem [Hàng chờ và nghẽn](https://docs.wandura.ai/vi/developers/api-v2/queue-and-congestion).

```http
HTTP/1.1 429
Retry-After: 12
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1791897360

{"error":{"code":"RATE_LIMITED","message":"Too many jobs submitted this minute for this product. Retry after the Retry-After header.","retryable":true}}
```

## Giới hạn
Đây là mức mặc định hiện tại, và **được nâng theo từng khách hàng khi có yêu cầu**: hãy báo cho Wandura đỉnh tải dự kiến của bạn (số job mỗi phút, tỉ lệ video) và giới hạn cho product của bạn sẽ được nâng.

| Gì | Hiện tại | Ghi chú |
|---|---|---|
| Tốc độ submit | 60 job mỗi phút mỗi product | Key live và key test tính riêng. Vượt mức: `429 RATE_LIMITED` kèm `Retry-After`. Mỗi submit trả `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`. |
| Job chạy cùng lúc | 20 mỗi product | Job thừa chờ ở `queued`. Hàng chờ chứa số job bằng số suất chạy; quá mức đó, `429 PRODUCT_BUSY` kèm `Retry-After`. Job còn chờ tới hết thời gian tối đa của nó sẽ kết thúc `timeout`, không tính tiền. Câu trả lời submit cho thấy các số này (`X-Concurrency-*`). |
| Mỗi end user | 20 job mỗi ngày | Chỉ khi bạn gửi `end_user_id`. Reset 00:00 UTC; `429 END_USER_LIMIT_REACHED`. |
| Thời gian job tối đa | ảnh 60 s, video 300 s | Theo style (`max_job_seconds`). Quá mức này job kết thúc `timeout`, không tính tiền. |
| Thời gian trả lời submit | dưới 3 giây | Submit chỉ kiểm tra, giữ chỗ giá và xếp job vào hàng. |
| File output | ít nhất 7 ngày | Xem [Output](https://docs.wandura.ai/vi/developers/api-v2/jobs#output). |

Trạng thái và thông báo bảo trì: [wandura.instatus.com](https://wandura.instatus.com). Chưa cam kết SLA uptime.

## Tính tiền thế nào
- **Tính tiền**: job `succeeded` trên key live, theo giá của style.
- **Không tính tiền**: `failed` (lỗi tạo, đầu vào không dùng được, bị moderation chặn), `timeout`, `cancelled`, và mọi thứ bị từ chối ngay lúc submit (`4xx`). Giá được giữ khi job bắt đầu và được trả lại khi job kết thúc mà không có kết quả.
- **Key test** không bao giờ tính tiền.

## Phiên bản
- Đường dẫn có version (`/v2/…`). Model id mang version riêng (`wandura-image-v1` là version của model, không phải version của API).
- **Thay đổi phá vỡ tương thích** đến dưới dạng version đường dẫn mới hoặc model id mới, báo trước ít nhất **30 ngày**. Bản cũ vẫn chạy trong thời gian đó.
- **Thay đổi cộng thêm** có thể ra bất cứ lúc nào: style mới, field tuỳ chọn mới, mã lỗi mới, field mới trong câu trả lời. Bỏ qua field bạn không biết, và coi `error.code` lạ là không retry được trừ khi `retryable` nói khác.

## Liên hệ
- Trạng thái và bảo trì: [wandura.instatus.com](https://wandura.instatus.com).
- Câu hỏi, nâng giới hạn, sự cố: _[đầu mối tích hợp của Wandura: sẽ bổ sung]_.
- Gửi kèm `job_id` và `X-Request-Id` của lời gọi.
