{"templateId":"markdown","sharedDataIds":{"sidebar":"sidebar-sidebars.yaml"},"props":{"metadata":{"markdoc":{"tagList":[]},"type":"markdown"},"seo":{"title":"Webhooks","siteUrl":"https://docs.bria.ai","llmstxt":{"hide":false,"title":"Bria AI API","description":"bria.ai helps you build products and workflows by delivering controllable, easy-to-integrate and safe generative visual AI capabilities.","details":{"path":"_llmstxt-details.md"},"sections":[{"title":"Integrations","description":"MCP server, Claude Skill, and Python SDK.","includeFiles":["MCP-authentication.md","integration-methods/bria-skill.md","integration-methods/python-sdk.md"],"excludeFiles":[]},{"title":"Getting Started","description":"Authentication, API overview, and platform concepts.","includeFiles":["index.md","products-overview.md","safety.md","vgl.md","best-practices-overview.md"],"excludeFiles":[]},{"title":"Image Generation","description":"Generate images using text prompts or structured JSON via FIBO models.","includeFiles":["image-generation.md","image-generation.yaml"],"excludeFiles":[]},{"title":"Image Editing","description":"Edit, transform, and enhance images with 20+ specialized endpoints.","includeFiles":["image-editing.md","image-editing.yaml"],"excludeFiles":[]},{"title":"Video Editing","description":"Remove backgrounds (REST async or real-time streaming), erase objects, upscale, and generate masks for video.","includeFiles":["video-editing.md","local-video-upload-service.md","video-editing.yaml","streaming-rmbg.md"],"excludeFiles":[]},{"title":"Product Shot Editing","description":"SKU packshots, lifestyle scenes, and automotive product imagery.","includeFiles":["product-shot-editing.md","product-shot-editing.yaml"],"excludeFiles":[]},{"title":"Optional","description":"Tailored generation, ads, image onboarding, and attribution.","includeFiles":["tailored-generation.md","campaign-generation.md","image-onboarding.md","bria-attribution-service.md","ad-generation.md","tailored-generation.yaml","campaign-generation.yaml","image-onboarding.yaml","bria-attribution-service.yaml","ad-generation.yaml"],"excludeFiles":[]}],"excludeFiles":["_llmstxt-details.md","_partials.md","CHANGELOG.md"]}},"dynamicMarkdocComponents":[],"compilationErrors":[],"ast":{"$$mdtype":"Tag","name":"article","attributes":{},"children":[{"$$mdtype":"Tag","name":"Heading","attributes":{"level":1,"id":"webhooks","__idx":0},"children":["Webhooks"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Webhooks let you receive results pushed directly to a URL you control when an asynchronous request completes — instead of polling the ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/status"},"children":["Status Service"]},". This is the recommended approach for production pipelines, batch jobs, and event-driven architectures."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"when-to-use-webhooks","__idx":1},"children":["When to Use Webhooks"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Polling works well for short, interactive tasks. Webhooks are better when:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Jobs can take more than a few seconds (e.g., Tailored Generation fine-tuning, video processing, batch image editing)."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["You're building event-driven or serverless backends (AWS Lambda, queues, etc.)."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["You want to avoid maintaining open connections or polling loops at scale."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"setting-up-a-webhook","__idx":2},"children":["Setting Up a Webhook"]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"via-rest-api","__idx":3},"children":["Via REST API"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Pass ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["webhook_url"]}," in the request body of any async endpoint:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"bash","header":{"controls":{"copy":{}}},"source":"curl -X POST https://engine.prod.bria-api.com/v2/image/generate \\\n  -H \"api_token: YOUR_API_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"prompt\": \"a serene mountain landscape at dawn\",\n    \"webhook_url\": \"https://your-app.com/api/bria/webhook\"\n  }'\n","lang":"bash"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The API immediately returns a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["request_id"]},":"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{\n  \"request_id\": \"a1b2c3d4-...\",\n  \"status_url\": \"https://engine.prod.bria-api.com/v2/status/a1b2c3d4-...\"\n}\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["When processing completes, Bria POSTs the result to your ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["webhook_url"]},"."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"via-python-sdk","__idx":4},"children":["Via Python SDK"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["webhook_url"]}," is a separate parameter on ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":[".submit()"]}," — pass it as a keyword argument, ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["not"]}," inside your ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["payload"]}," dict. The SDK merges it (along with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sync: false"]},") into the request body sent to the API; your original ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["payload"]}," dict is not modified. Works on both ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["BriaSyncClient"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["BriaAsyncClient"]},"."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"python","header":{"controls":{"copy":{}}},"source":"from bria_client import BriaAsyncClient\n\nasync with BriaAsyncClient() as client:\n    response = await client.submit(\n        endpoint=\"image/generate\",\n        payload={\"prompt\": \"a serene mountain landscape at dawn\"},\n        webhook_url=\"https://your-app.com/api/bria/webhook\",\n    )\n\nprint(f\"Submitted: {response.request_id}\")\n# Your webhook URL will receive the result — no polling needed\n","lang":"python"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"webhook-delivery","__idx":5},"children":["Webhook Delivery"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["When the job reaches a terminal state, Bria sends a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["POST"]}," to your URL with three signed headers:"]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Header"},"children":["Header"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Description"},"children":["Description"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Bria-Webhook-Id"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The job's ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["request_id"]},"."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Bria-Webhook-Timestamp"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Unix epoch (seconds) when the signature was generated."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Bria-Webhook-Signature"]}]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v1=<base64>"]},"."]}]}]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The delivered body is ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["identical to the response you'd get by polling the ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/status"},"children":["Status Service"]}]}," — same schema, same fields. If you already handle status-poll responses, the same parsing code works for webhooks."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"successful-result","__idx":6},"children":["Successful result"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{\n  \"request_id\": \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\n  \"status\": \"COMPLETED\",\n  \"result\": {\n    \"image_url\": \"https://...\",\n    \"seed\": 42,\n    \"prompt\": \"a serene mountain landscape at dawn\"\n  }\n}\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"error-result","__idx":7},"children":["Error result"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{\n  \"request_id\": \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\n  \"status\": \"ERROR\",\n  \"error\": {\n    \"message\": \"Invalid parameter: prompt is required\",\n    \"code\": \"VALIDATION_ERROR\"\n  }\n}\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"handling-webhook-deliveries","__idx":8},"children":["Handling Webhook Deliveries"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Your endpoint should:"]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Respond 2xx within 10 seconds"]}," — Bria expects an acknowledgment inside this window. Defer heavy work to a background job or queue."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Verify the signature first"]}," — reject any request that doesn't validate (see below)."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"retries--idempotency","__idx":9},"children":["Retries & Idempotency"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["If your endpoint is unreachable or returns a non-2xx response, Bria retries delivery with ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["exponential backoff — up to 5 attempts over 45 minutes"]},". A 2xx response stops retries. Because retries (and rare duplicate deliveries on success) are possible, your handler ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["must be idempotent"]},": use the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Bria-Webhook-Id"]}," header (the job's ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["request_id"]},") as a deduplication key."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"security-verifying-webhook-signatures","__idx":10},"children":["Security: Verifying Webhook Signatures"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Bria signs every webhook with HMAC-SHA256. The convention is ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Svix-inspired but Bria-specific"]}," — it borrows the signed-message format, three-header layout, and versioned signature prefix from Svix, but the signing-key derivation is Bria's own."]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Signed message format"]}," follows the Svix convention: ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["{webhook_id}.{timestamp}.{raw_body}"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Signing key"]}," is derived from your API token via HMAC-SHA256 (Bria-specific): ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["HMAC-SHA256(api_token, \"bria-webhook-signing-v1\")"]},". Your raw API token is never used directly to sign payloads."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Signature header"]}," carries a single ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v1=<base64>"]}," token."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"python-sdk-helper","__idx":11},"children":["Python (SDK helper)"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The SDK ships a verifier — no extra dependencies:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"python","header":{"controls":{"copy":{}}},"source":"from bria_client.toolkit import verify_webhook_signature\n\nok = verify_webhook_signature(\n    payload=raw_body_bytes,\n    webhook_id=request.headers[\"Bria-Webhook-Id\"],\n    timestamp=request.headers[\"Bria-Webhook-Timestamp\"],\n    signature_header=request.headers[\"Bria-Webhook-Signature\"],\n    api_token=os.environ[\"BRIA_API_TOKEN\"],\n)\nif not ok:\n    raise HTTPException(401, \"Invalid webhook signature\")\n","lang":"python"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"reference-implementation-any-language","__idx":12},"children":["Reference implementation (any language)"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["If you're not using the Python SDK, reproduce the same scheme:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"python","header":{"controls":{"copy":{}}},"source":"import base64, hashlib, hmac\n\ndef verify(payload: bytes, webhook_id: str, timestamp: str,\n           signature_header: str, api_token: str) -> bool:\n    if not signature_header.startswith(\"v1=\"):\n        return False\n    signing_key = hmac.new(api_token.encode(), b\"bria-webhook-signing-v1\", hashlib.sha256).digest()\n    message = f\"{webhook_id}.{timestamp}.{payload.decode()}\".encode()\n    expected = base64.b64encode(hmac.new(signing_key, message, hashlib.sha256).digest()).decode()\n    return hmac.compare_digest(signature_header[3:], expected)\n","lang":"python"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Always use a constant-time comparison (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["hmac.compare_digest"]}," or equivalent) when checking the signature."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"full-example-fastapi-webhook-receiver","__idx":13},"children":["Full Example: FastAPI Webhook Receiver"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["This is the pattern shipped in ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"https://github.com/Bria-AI/bria-client/blob/main/examples/webhook_handler.py"},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["examples/webhook_handler.py"]}]}," — verify, acknowledge in under 10s, process in the background."]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"python","header":{"controls":{"copy":{}}},"source":"import json, os\nfrom fastapi import BackgroundTasks, FastAPI, Header, HTTPException, Request\nfrom bria_client.toolkit import verify_webhook_signature\n\napp = FastAPI()\n\n@app.post(\"/webhook\")\nasync def receive_webhook(\n    request: Request,\n    background_tasks: BackgroundTasks,\n    bria_webhook_id: str = Header(...),\n    bria_webhook_timestamp: str = Header(...),\n    bria_webhook_signature: str = Header(...),\n):\n    body = await request.body()\n\n    if not verify_webhook_signature(\n        payload=body,\n        webhook_id=bria_webhook_id,\n        timestamp=bria_webhook_timestamp,\n        signature_header=bria_webhook_signature,\n        api_token=os.environ[\"BRIA_API_TOKEN\"],\n    ):\n        raise HTTPException(status_code=401, detail=\"Invalid webhook signature\")\n\n    data = json.loads(body)\n    background_tasks.add_task(process_result, data)\n    return {\"ok\": True}\n\nasync def process_result(data: dict):\n    if data[\"status\"] == \"COMPLETED\":\n        ...  # store result, notify user, trigger next step\n    elif data[\"status\"] == \"ERROR\":\n        ...  # log / alert\n","lang":"python"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"expressjs-receiver","__idx":14},"children":["Express.js receiver"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"javascript","header":{"controls":{"copy":{}}},"source":"import express from 'express';\nimport crypto from 'crypto';\n\nconst app = express();\n\nfunction verify(rawBody, id, ts, sigHeader, apiToken) {\n  if (!sigHeader.startsWith('v1=')) return false;\n  const signingKey = crypto.createHmac('sha256', apiToken).update('bria-webhook-signing-v1').digest();\n  const expected = crypto.createHmac('sha256', signingKey).update(`${id}.${ts}.${rawBody}`).digest('base64');\n  const got = Buffer.from(sigHeader.slice(3));\n  const exp = Buffer.from(expected);\n  return got.length === exp.length && crypto.timingSafeEqual(got, exp);\n}\n\napp.post('/api/bria/webhook', express.raw({ type: 'application/json' }), (req, res) => {\n  const ok = verify(\n    req.body.toString(),\n    req.header('Bria-Webhook-Id'),\n    req.header('Bria-Webhook-Timestamp'),\n    req.header('Bria-Webhook-Signature'),\n    process.env.BRIA_API_TOKEN,\n  );\n  if (!ok) return res.sendStatus(401);\n\n  res.sendStatus(200); // acknowledge first\n\n  const { request_id, status, result, error } = JSON.parse(req.body);\n  if (status === 'COMPLETED') console.log(`Job ${request_id} done:`, result.image_url);\n  else if (status === 'ERROR') console.error(`Job ${request_id} failed:`, error.message);\n});\n\napp.listen(3000);\n","lang":"javascript"},"children":[]}]},"headings":[{"value":"Webhooks","id":"webhooks","depth":1},{"value":"When to Use Webhooks","id":"when-to-use-webhooks","depth":2},{"value":"Setting Up a Webhook","id":"setting-up-a-webhook","depth":2},{"value":"Via REST API","id":"via-rest-api","depth":3},{"value":"Via Python SDK","id":"via-python-sdk","depth":3},{"value":"Webhook Delivery","id":"webhook-delivery","depth":2},{"value":"Successful result","id":"successful-result","depth":3},{"value":"Error result","id":"error-result","depth":3},{"value":"Handling Webhook Deliveries","id":"handling-webhook-deliveries","depth":2},{"value":"Retries & Idempotency","id":"retries--idempotency","depth":2},{"value":"Security: Verifying Webhook Signatures","id":"security-verifying-webhook-signatures","depth":2},{"value":"Python (SDK helper)","id":"python-sdk-helper","depth":3},{"value":"Reference implementation (any language)","id":"reference-implementation-any-language","depth":3},{"value":"Full Example: FastAPI Webhook Receiver","id":"full-example-fastapi-webhook-receiver","depth":2},{"value":"Express.js receiver","id":"expressjs-receiver","depth":3}],"frontmatter":{"title":"Webhooks","seo":{"title":"Webhooks"}},"lastModified":"2026-05-25T08:08:29.000Z","pagePropGetterError":{"message":"","name":""}},"slug":"/webhooks","userData":{"isAuthenticated":false,"teams":["anonymous"]},"isPublic":true}