Trên trang này
TL;DR —
POST /v2/jobsvới một model, một style và dữ liệu đầu vào của bạn sẽ trả vềjob_idsau 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ớiwebhook_urlcủ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ằng422kèm lý do; jobfailed,timeout, bị huỷ hoặc bị moderation chặn thì không bị tính tiền.
Cơ bản
- Base URL:
https://api.wandura.ai(vùng Singapore). - OpenAPI 3.1: 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_urlvàIdempotency-Key→202 {job_id, status: "queued"}→ Wandura POST job cuối cùng (có ký) tớiwebhook_url, bạn trả200→ nếu webhook không tới, pollGET /v2/jobs/{job_id}→ tảioutput[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; 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.
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:
{
"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ỷ
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
# {"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. |
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, có ký. Cần có Jobs API signing secret của product trước (xem Webhook). |
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). |
- 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ằngmax_job_secondscủa style cộng 10 phút. - Huỷ được khi job còn
queued:200, statuscancelled, không tính tiền, và webhook vẫn được gửi. Khi job đãprocessingnó chạy tới cùng, và huỷ trả409 JOB_NOT_CANCELLABLE.
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:
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,durationhayseed, nên gửi một trong số đó sẽ trả422kèm lý do. Chỉ gửi những gìinputscủ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, headerIdempotent-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
queued → processing → succeeded | failed | cancelled | timeoutBố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:
{"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.
{"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"}urllà 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.- Lưu 72 giờ.
expires_atcho biết chính xác khi nào hết hạn. Hãy chép file về storage của bạn trước đó. - Metadata đọc từ chính file:
type,width,height,durationvàfps(video),size_bytes,seed. Giá trị không đọc được lànull. - File mẫu của key test có metadata
nullvà không hết hạn. - Bản ghi job được giữ lâu hơn 72 giờ 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 đó.
- Tạo Jobs API signing secret của product một lần: ở console, mục "Jobs API signing secret" → Create. Secret chỉ hiện một lần; Roll thay ngay lập tức. Khi chưa có secret, submit kèm
webhook_urlbị từ chối bằng422. - Verify mọi request trên raw body:
X-Signaturelà HMAC-SHA256 dạng hex củaX-Timestamp + "." + raw bodybằng secret của bạn, vàX-Timestamp(unix giây) lệch hiện tại không quá 5 phút. - Trả
2xxtrong 10 giây, rồi mới xử lý việc của bạn. Dedupe theojob_id(hoặc headerWandura-Event-Id, giống nhau ở mọi lần retry): giao tin ít nhất một lần.
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):
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):
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 "", 200Dù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.
| 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. Dự phòng: hiện 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 | 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. |
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.
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. 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. |
| 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 | 72 giờ | Xem Output. |
Trạng thái và thông báo bảo trì: wandura.instatus.com. Chưa cam kết SLA uptime.
Tính tiền thế nào
- Tính tiền: job
succeededtrê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-v1là 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.codelạ là không retry được trừ khiretryablenói khác.
Liên hệ
- Trạng thái và bảo trì: 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_idvàX-Request-Idcủa lời gọi.
Cập nhật 14/10/2026 · Lịch sử thay đổi (1)