> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kynva.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Async render jobs

> Queue a render, then get notified via webhooks, follow live progress over SSE, or poll for status. The right tool when the user isn't staring at a spinner.

`POST /api/v1/facade/generate` can render two ways, chosen by `operation`:

| `operation` | Behavior                                                                                  | Use when                                                                     |
| ----------- | ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `"preview"` | Synchronous — the response contains the rendered outputs                                  | One image, user actively waiting                                             |
| `"render"`  | **Async** — you get a `job` back immediately (HTTP 202) and the render happens on a queue | Background pipelines, webhook-driven flows, anything the user isn't watching |

This guide covers the async path.

## Step 1 — submit the job

Submit a normal generate request with `"operation": "render"`:

```bash theme={null}
curl -X POST https://api.kynva.ai/api/v1/facade/generate \
  -H "Authorization: Bearer $KYNVA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "operation": "render",
    "generation_type": "image",
    "intent": {
      "content": { "headline": "Summer Sale", "cta": "Shop now" },
      "export_intent": {
        "primary": { "platform": "instagram", "format": "png", "profile_id": "instagram-post" }
      }
    }
  }'
```

Response — HTTP **202**, immediately, before rendering:

```json theme={null}
{
  "job": {
    "job_id": "job-9f3c2a1e-77b4-4d2c-9a1b-0c8d7e6f5a4b",
    "status": "queued",
    "created_at": "2026-07-04T18:42:11.812Z"
  }
}
```

## Step 2A — subscribe to webhooks (preferred)

Register a webhook once and Kynva delivers a
[`render.completed`](/webhooks/events) (or `render.failed`) event when any of
your jobs finishes — no polling at all:

```bash theme={null}
curl -X POST https://api.kynva.ai/api/v1/facade/webhooks \
  -H "Authorization: Bearer $KYNVA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://your.app/hooks/kynva", "events": ["render.completed", "render.failed"] }'
```

See the [webhooks overview](/webhooks/overview) for signatures, retries, and
the delivery contract.

## Step 2B — poll for status

```
GET /api/v1/facade/render-jobs/{job_id}
```

```typescript theme={null}
async function waitForJob(jobId: string) {
  let delay = 2_000;
  while (true) {
    const res = await fetch(
      `https://api.kynva.ai/api/v1/facade/render-jobs/${jobId}`,
      { headers: { Authorization: `Bearer ${process.env.KYNVA_API_KEY!}` } },
    );
    if (res.status === 404) throw new Error("unknown job");
    const { data } = await res.json();

    if (data.status === "completed") return data;
    if (data.status === "failed") throw new Error(data.error?.message ?? "render failed");

    await new Promise(r => setTimeout(r, delay));
    delay = Math.min(delay * 1.5, 15_000);
  }
}
```

While the job runs, the response also carries progress detail:

```json theme={null}
{
  "data": {
    "job_id": "job-9f3c…",
    "status": "processing",
    "progress_pct": 70,
    "stage": "UPLOADING",
    "outputs": null,
    "error": null,
    "design_id": "clx…",
    "created_at": "2026-07-04T18:42:12.001Z",
    "completed_at": null
  }
}
```

* `status` is the coarse lifecycle: `queued → processing → completed | failed`.
* `stage` is the granular pipeline step while processing (`QUEUED`,
  `ASSET_DOWNLOADING`, `ASSET_READY`, `RENDERING_SCENES`, `UPLOADING`,
  `COMPLETING`) — useful for progress UIs.

<Warning>
  Polls count against the `jobs:status` rate-limit bucket (60/min on the free
  tier). Back off as shown above — or use webhooks and don't poll at all.
</Warning>

## Step 2C — live progress over SSE

For a progress bar without polling, open the server-sent-events stream:

```
GET /api/v1/facade/render-jobs/{job_id}/stream
```

Events: `job_status` (progress updates), `job_complete`, `job_failed`. The
stream times out after 5 minutes; the poll endpoint above is the durable
fallback.

## Step 3 — fetch the results

Once `status` is `"completed"`, `outputs` holds the rendered files:

```json theme={null}
{
  "data": {
    "job_id": "job-9f3c…",
    "status": "completed",
    "progress_pct": 100,
    "outputs": [
      {
        "export_profile": "instagram-post",
        "url": "https://cdn.kynva.ai/renders/usr_…/job-9f3c…/instagram-post.png",
        "width": 1080,
        "height": 1080,
        "file_size": 184320
      }
    ],
    "completed_at": "2026-07-04T18:42:19.443Z"
  }
}
```

Output URLs are **stable, long-lived CDN URLs** (served with
`Cache-Control: immutable`) — safe to store and hot-link. Because rendering is
deterministic, the same job re-run from the same request produces the same
bytes.

## How long is job status available?

| State     | Queue retention | After that                                                                                                      |
| --------- | --------------- | --------------------------------------------------------------------------------------------------------------- |
| Completed | 24 hours        | The poll endpoint falls back to the durable design record — `status`, `design_id`, and `outputs` keep answering |
| Failed    | 7 days          | Same fallback; the detailed error message is only retained for the 7-day window                                 |

Store `design_id` if you want a permanent handle — the design (and its outputs)
outlives the job record.
