On this page
TL;DR — For a workflow engine that registers Wandura as an HTTP model with an API key header: it submits a run, then polls the run's
status_urluntil a final status. A signed callback is optional. Gateway keys are made by Wandura staff.
For a workflow engine that registers Wandura as an HTTP model with an API key header: it submits a run, then polls the run's status_url until a final status. A signed callback is optional. Every example below comes from the API's OpenAPI. (Engines that expect a provider-style job API can use the Jobs API instead.)
1. Get a gateway key
A gateway key is a normal API key for one product, made by Wandura staff (ask us; it cannot be made from the console). On it:
end_user_idis optional. Leave it out when your engine has no user id (the example below sends none); send it when you have one. No id means no per-user daily cap.- The limit is per product instead: a number of new runs per minute across the product's gateway keys (60 in the example below; Wandura can change it per product).
- The
Idempotency-Keyheader is optional: with theX-Job-Refheader below, retries never run or charge twice. - Its runs carry
path: "gateway", so Usage and Runs in the console can filter and compare them.
2. Submit a run
POST /v2/styles/{id} with lane and inputs (the fields of that style, see the styles reference; file inputs take https URLs, an S3 presigned GET URL works). Add X-Job-Ref with your own job id.
curl -X POST https://api.wandura.ai/v2/styles/sty_ghibli_portrait \
-H "Authorization: Bearer wdr_live_<gateway key>" \
-H "Content-Type: application/json" \
-H "X-Job-Ref: job_8f3a21" \
-d '{
"lane": "priority",
"inputs": {
"image_url": "https://example.com/photo.jpg"
}
}'The answer is 202 with the run. Keep status_url.
HTTP 202
{
"id": "run_Qk2m9XaB7Lc4",
"object": "run",
"status": "queued",
"style": "sty_ghibli_portrait",
"lane": "priority",
"end_user_id": null,
"product": "prod_lensie_app",
"livemode": true,
"price": {
"credits": 5.2,
"usd": 0.052
},
"charge": "held",
"moderation": "on",
"path": "gateway",
"created_at": "2026-10-13T08:00:00.000Z",
"request_id": "run_Qk2m9XaB7Lc4",
"status_url": "https://api.wandura.ai/v2/runs/run_Qk2m9XaB7Lc4",
"cancel_url": "https://api.wandura.ai/v2/runs/run_Qk2m9XaB7Lc4/cancel"
}3. Poll status_url
GET the status_url with the same key until the run reaches a final status. output[0].url is the result.
curl https://api.wandura.ai/v2/runs/run_Qk2m9XaB7Lc4 \
-H "Authorization: Bearer wdr_live_<gateway key>"
HTTP 200
{
"id": "run_Qk2m9XaB7Lc4",
"object": "run",
"status": "completed",
"style": "sty_ghibli_portrait",
"lane": "priority",
"end_user_id": null,
"product": "prod_lensie_app",
"livemode": true,
"price": {
"credits": 5.2,
"usd": 0.052
},
"charge": "charged",
"moderation": "on",
"path": "gateway",
"created_at": "2026-10-13T08:00:00.000Z",
"request_id": "run_Qk2m9XaB7Lc4",
"status_url": "https://api.wandura.ai/v2/runs/run_Qk2m9XaB7Lc4",
"cancel_url": "https://api.wandura.ai/v2/runs/run_Qk2m9XaB7Lc4/cancel",
"output": [
{
"type": "image",
"url": "https://files.example.com/outputs/2026-10-13/kJ3w9QzR1mXb7TcY0aLp4vNe.png",
"content_type": "image/png",
"width": 1024,
"height": 1024,
"duration": null,
"fps": null,
"size_bytes": 1482113,
"expires_at": "2026-10-16T08:00:21.000Z"
}
],
"share_url": "https://api.wandura.ai/s/run_Qk2m9XaB7Lc4"
}Priority: start at 2 s, multiply by 1.5, cap at 10 s, add 0–0.5 s of random jitter. Relax: 10 s up to 60 s. Final statuses: completed, failed, nsfw, canceled.
4. Retries and X-Job-Ref
Send the same X-Job-Ref again (your engine retried) within 10 minutes and you get the same run back, charged once:
HTTP 200
Idempotent-Replayed: true
(the same body as the first answer)The same X-Job-Ref with a different body is 409 idempotency_key_reused. With no X-Job-Ref, Wandura uses the request body instead (key order ignored). An Idempotency-Key you send is used as sent.
5. The callback (optional)
Polling is enough. If you also want a push, set the product's webhook URL in the console: when the run reaches a final status Wandura POSTs a signed event to it (run.completed, run.failed, run.nsfw, run.canceled):
POST <your webhook URL>
Content-Type: application/json
User-Agent: Wandura-Webhooks/1
Wandura-Event-Id: evt_h3Zk1uPq8Wd2
Wandura-Event-Type: run.completed
Wandura-Signature: t=1791878460,v1=<hex HMAC-SHA256(secret, t + "." + raw body)>
{
"id": "evt_h3Zk1uPq8Wd2",
"object": "event",
"type": "run.completed",
"created_at": "2026-10-13T08:00:21.000Z",
"sequence": 3,
"data": {
"id": "run_Qk2m9XaB7Lc4",
"object": "run",
"status": "completed",
"style": "sty_ghibli_portrait",
"lane": "priority",
"end_user_id": null,
"product": "prod_lensie_app",
"livemode": true,
"price": {
"credits": 5.2,
"usd": 0.052
},
"charge": "charged",
"moderation": "on",
"path": "gateway",
"created_at": "2026-10-13T08:00:00.000Z",
"request_id": "run_Qk2m9XaB7Lc4",
"status_url": "https://api.wandura.ai/v2/runs/run_Qk2m9XaB7Lc4",
"cancel_url": "https://api.wandura.ai/v2/runs/run_Qk2m9XaB7Lc4/cancel",
"output": [
{
"type": "image",
"url": "https://files.example.com/outputs/2026-10-13/kJ3w9QzR1mXb7TcY0aLp4vNe.png",
"content_type": "image/png",
"width": 1024,
"height": 1024,
"duration": null,
"fps": null,
"size_bytes": 1482113,
"expires_at": "2026-10-16T08:00:21.000Z"
}
],
"share_url": "https://api.wandura.ai/s/run_Qk2m9XaB7Lc4"
}
}Verify Wandura-Signature on the raw body, answer 2xx fast, dedupe on Wandura-Event-Id and keep the highest sequence per run. The recipe is in Concepts. data is the same run object GET returns at status_url.
6. Free runs
A run that ends failed, nsfw or canceled with a free_run.reason costs nothing:
free_run.reason |
When |
|---|---|
priority_late |
Priority did not finish within its promise. |
provider_error |
The run failed on our side. |
bad_input |
An input could not be used (for example a URL that is not http(s)). |
content_blocked |
The input or output was blocked by the content filter (status nsfw). |
7. Rate limit
Over the product's per-minute limit the call is refused with 429 gateway_rate_limited and a Retry-After header (seconds). No run is made, so retry the same call with the same X-Job-Ref after that wait:
HTTP 429
Retry-After: 40
{
"error": {
"code": "gateway_rate_limited",
"message": "This product reached its limit of 60 runs per minute on gateway keys. Retry after the Retry-After header.",
"limit": 60,
"retry_after_s": 40
}
}If you do send an end_user_id, that person's daily cap applies as on any key (end_user_cap_reached). All error codes are in Errors.
Updated 14 Oct 2026 · Change history (1)