# Async requests, webhooks and polling

All Bria v2 endpoints process requests asynchronously by default. This keeps long generations from tying up connections and lets you fan out hundreds of jobs at once. Every request follows the same lifecycle:

1. **Submit.** `POST` the request. The API validates it and responds with `202 Accepted`, a `request_id` and a `status_url`.
2. **Receive the result.** Either pass a `webhook_url` and Bria POSTs the result to you when the job finishes (recommended for production), or poll the `status_url` until it does.
3. **Collect.** When `status` is `COMPLETED`, the `result` object holds the output: `image_url` for images, `video_url` for videos, `structured_prompt` for structured prompt generation, plus endpoint-specific fields such as `seed`.


Webhook deliveries and status polls carry the same body, so one parser handles both:

```json
{
  "request_id": "9d4a1a2f5c7e4b6f8a0c2d3e4f5a6b7c",
  "status": "COMPLETED",
  "result": {
    "image_url": "https://.../result.png",
    "seed": 314159
  }
}
```

## Webhooks

Pass `webhook_url` with any asynchronous request. Use webhooks for pipelines, queues and serverless backends: no polling loops, no open connections, and results arrive the moment a job finishes.

Python SDK
`webhook_url` is a keyword argument of `.submit()`, not part of the payload.

```python
from bria_client import BriaSyncClient

client = BriaSyncClient()  # reads BRIA_API_TOKEN

job = client.submit(
    endpoint="image/generate",
    payload={"prompt": "a serene mountain landscape at dawn"},
    webhook_url="https://your-app.com/api/bria/webhook",
)
print("submitted", job.request_id)  # the result will be POSTed to your webhook
```

TypeScript SDK
`webhookUrl` is an option of `.submit()`, not part of the payload.

```typescript
import { BriaClient } from "@bria-ai/client";

const client = new BriaClient(); // reads BRIA_API_TOKEN

const job = await client.submit(
  "image/generate",
  { prompt: "a serene mountain landscape at dawn" },
  { webhookUrl: "https://your-app.com/api/bria/webhook" },
);
console.log("submitted", job.requestId); // the result will be POSTed to your webhook
```

cURL
```bash
curl -X POST https://engine.prod.bria-api.com/v2/image/generate \
  -H "api_token: $BRIA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"prompt": "a serene mountain landscape at dawn", "webhook_url": "https://your-app.com/api/bria/webhook"}'
```

### What Bria sends

When the job reaches a terminal state, Bria sends a `POST` with the body shown above and three signed headers:

| Header | Value |
|  --- | --- |
| `Bria-Webhook-Id` | The job's `request_id`. Use it as your deduplication key. |
| `Bria-Webhook-Timestamp` | Unix epoch seconds when the signature was generated. |
| `Bria-Webhook-Signature` | `v1=<base64>`, an HMAC-SHA256 signature (see below). |


Your endpoint must answer with any `2xx` within 10 seconds; do heavy work afterwards, in a queue or background task. If the endpoint is unreachable or returns a non-2xx response, Bria retries with exponential backoff, up to 5 attempts over 45 minutes. Because retries and rare duplicates are possible, treat deliveries as idempotent and key them on `Bria-Webhook-Id`.

### Verify the signature

The signing key is derived from your API token (`HMAC-SHA256(api_token, "bria-webhook-signing-v1")`), and the signed message is `{webhook_id}.{timestamp}.{raw_body}`. Verify with a constant-time comparison before trusting a delivery.

Python SDK
```python
import os
from bria_client.toolkit import verify_webhook_signature

def is_valid(raw_body: bytes, headers) -> bool:
    return verify_webhook_signature(
        payload=raw_body,
        webhook_id=headers["Bria-Webhook-Id"],
        timestamp=headers["Bria-Webhook-Timestamp"],
        signature_header=headers["Bria-Webhook-Signature"],
        api_token=os.environ["BRIA_API_TOKEN"],
    )
```

TypeScript SDK
```typescript
import { verifyWebhookSignature } from "@bria-ai/client";

function isValid(rawBody: string, headers: Record<string, string | undefined>): boolean {
  return verifyWebhookSignature({
    payload: rawBody,
    webhookId: headers["bria-webhook-id"] ?? "",
    timestamp: headers["bria-webhook-timestamp"] ?? "",
    signatureHeader: headers["bria-webhook-signature"] ?? "",
    apiToken: process.env.BRIA_API_TOKEN ?? "",
  });
}
```

Any language
```text
signing_key = HMAC_SHA256(key = api_token, message = "bria-webhook-signing-v1")
expected    = BASE64(HMAC_SHA256(key = signing_key, message = webhook_id + "." + timestamp + "." + raw_body))
valid       = constant_time_equal(expected, signature_header without the "v1=" prefix)
```

### A complete receiver

Verify, acknowledge, then process in the background.

Python (FastAPI)
```python
import json
import os

from bria_client.toolkit import verify_webhook_signature
from fastapi import BackgroundTasks, FastAPI, Header, HTTPException, Request

app = FastAPI()


@app.post("/api/bria/webhook")
async def receive(
    request: Request,
    background: BackgroundTasks,
    bria_webhook_id: str = Header(...),
    bria_webhook_timestamp: str = Header(...),
    bria_webhook_signature: str = Header(...),
):
    body = await request.body()
    if not verify_webhook_signature(
        payload=body,
        webhook_id=bria_webhook_id,
        timestamp=bria_webhook_timestamp,
        signature_header=bria_webhook_signature,
        api_token=os.environ["BRIA_API_TOKEN"],
    ):
        raise HTTPException(status_code=401, detail="Invalid webhook signature")
    background.add_task(process, json.loads(body))
    return {"ok": True}


async def process(data: dict):
    if data["status"] == "COMPLETED":
        ...  # store data["result"]["image_url"], notify, trigger the next step
    elif data["status"] == "ERROR":
        ...  # log data["error"]
```

TypeScript (Express)
```typescript
import express from "express";
import { verifyWebhookSignature } from "@bria-ai/client";

const app = express();

app.post("/api/bria/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const body = req.body.toString();
  const header = (name: string) => String(req.headers[name] ?? "");
  const valid = verifyWebhookSignature({
    payload: body,
    webhookId: header("bria-webhook-id"),
    timestamp: header("bria-webhook-timestamp"),
    signatureHeader: header("bria-webhook-signature"),
    apiToken: process.env.BRIA_API_TOKEN ?? "",
  });
  if (!valid) return res.sendStatus(401);
  res.sendStatus(200); // acknowledge first

  const { request_id, status, result, error } = JSON.parse(body);
  if (status === "COMPLETED") console.log(`job ${request_id} done`, result.image_url);
  else if (status === "ERROR") console.error(`job ${request_id} failed`, error.message);
});

app.listen(3000);
```

## Polling the status endpoint

If you would rather pull than push, call `GET /v2/status/{request_id}` (the `status_url` from the submit response) until the job reaches a terminal state. The [status endpoint reference](/status) has the full response schema.

Python SDK
```python
job = client.submit(endpoint="image/generate", payload={"prompt": "a serene mountain landscape at dawn"})
result = client.poll(job, interval=2, timeout=120)  # raises TimeoutError after 120 s
print(result.result.image_url)

# client.run(...) does submit and poll in one call
```

TypeScript SDK
```typescript
const job = await client.submit("image/generate", { prompt: "a serene mountain landscape at dawn" });
const result = await client.poll(job, { interval: 2, timeout: 120 }); // throws after 120 s
console.log(result.result?.image_url);

// client.run(...) does submit and poll in one call
```

cURL
```bash
curl "https://engine.prod.bria-api.com/v2/status/<request_id>" -H "api_token: $BRIA_API_TOKEN"
```

| `status` | Meaning | What to do |
|  --- | --- | --- |
| `IN_PROGRESS` | Accepted and processing. | Keep polling. |
| `COMPLETED` | Finished successfully. | Read `result`. |
| `ERROR` | Processing failed; `error` holds `code`, `message` and `details`. | Stop polling. Fix the input if it is a validation or moderation error, otherwise retry the submission. |
| `UNKNOWN` | Unexpected failure, or an unknown or expired `request_id`. | Stop polling. Contact [support@bria.ai](mailto:support@bria.ai) with the `request_id`. |


The status endpoint returns HTTP `200` for all of these; the job state is in the body. A `404` means the `request_id` does not exist or has expired.

Polling guidance:

- Poll every 1 to 2 seconds for image endpoints and every 5 seconds for video, with a timeout that fits the workload (a minute for images, ten minutes or more for long videos).
- Status calls count toward your [rate limit](/getting-started/rate-limits-and-errors), so do not poll in a tight loop.
- Submissions are not idempotent: retrying a `POST` creates a new job. Retry only when the submission itself failed (network error or `5xx`), never while a job is `IN_PROGRESS`. The SDKs never retry `run` or `submit` for this reason.
- Download results as soon as they are ready and store them yourself. Result URLs are temporary.


## Sync mode

Most endpoints accept `"sync": true`, which holds the connection open and returns the result in the response body with HTTP `200`. It is convenient for quick experiments and low-volume interactive use. Prefer webhooks or polling for production: sync calls occupy a connection for the whole generation, and some options are async-only, for example `resolution: "4MP"` on `/v2/image/generate` or `placement_type: "automatic"` on lifestyle shots. Combining an async-only option with `sync: true` returns `422`.

Some product-shot endpoints are synchronous by default and document their own `sync` behavior; the endpoint pages in the [Product Shot Editing reference](/product-shot-editing) state it.