# Integrate via a middle layer

> **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_url` until 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](https://docs.wandura.ai/developers/api-v2/jobs) 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_id` is **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-Key` header is **optional**: with the `X-Job-Ref` header 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](https://docs.wandura.ai/developers/api-v2/styles); file inputs take https URLs, an S3 presigned GET URL works). Add `X-Job-Ref` with your own job id.

```bash
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](https://open.wandura.ai/webhooks): 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](https://docs.wandura.ai/developers/api-v2/concepts#webhooks). `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](https://docs.wandura.ai/developers/api-v2/errors).
