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":
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:
{
"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 (or render.failed) event when any of
your jobs finishes — no polling at all:
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 for signatures, retries, and
the delivery contract.
Step 2B — poll for status
GET /api/v1/facade/render-jobs/{job_id}
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:
{
"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.
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.
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:
{
"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.