Skip to content

Tutorial: Async generation with webhooks

The production pattern for image/video generation is: submit async → get called back → download. No polling loops, no sync timeouts, and exact error codes when something fails. This tutorial builds it end to end.

You need: a running image or video deployment, an API key, and a server that can receive HTTPS requests (for local dev, a tunnel like ngrok).

  1. Get your signing secret.

    Serverless › Webhooks → Signing secret → Reveal. Every webhook Meshive sends is HMAC-signed with this workspace secret, so your server can prove the sender.

    Terminal window
    export MESHIVE_WEBHOOK_SECRET="whsec-like-value"
  2. Stand up the receiver.

    Verify the signature on the raw body, dedupe on the event id, respond 200 fast (each delivery attempt times out after 5s — do real work in the background):

    import hashlib, hmac, os
    from fastapi import BackgroundTasks, FastAPI, Header, HTTPException, Request
    SECRET = os.environ["MESHIVE_WEBHOOK_SECRET"]
    app = FastAPI()
    seen: set[str] = set() # use a real store in production
    def verify(raw: bytes, header: str) -> None:
    try:
    parts = dict(p.split("=", 1) for p in header.split(","))
    expected = hmac.new(
    SECRET.encode(), f"{parts['t']}.".encode() + raw, hashlib.sha256
    ).hexdigest()
    ok = hmac.compare_digest(expected, parts.get("v1", ""))
    except Exception:
    ok = False
    if not ok:
    raise HTTPException(401, "bad signature")
    def handle(event: dict) -> None:
    data = event["data"]
    if event["type"] == "request.completed":
    print("done:", data["id"], "", data["output_object_key"])
    elif event["type"] == "request.failed":
    print("failed:", data["id"], data["error_code"], data["error_message"])
    @app.post("/webhooks/meshive")
    async def receiver(
    request: Request,
    background: BackgroundTasks,
    x_meshive_signature: str = Header(""),
    ):
    raw = await request.body()
    verify(raw, x_meshive_signature)
    event = await request.json()
    if event["id"] in seen: # at-least-once delivery → dedupe
    return {"ok": True}
    seen.add(event["id"])
    background.add_task(handle, event)
    return {"ok": True} # respond fast; work happens in background

    Sanity-check it before going further: Serverless › Webhooks → Test sends a sample signed event (X-Meshive-Event: webhook.test) and shows your server’s response code. Test failures never disable anything.

  3. Submit work.

    Always send an Idempotency-Key — if your submit gets retried by a flaky network, you still get exactly one job:

    import httpx, uuid
    BASE = "https://<your-deployment>.inference.meshive.ai/v1"
    KEY = {"Authorization": f"Bearer {os.environ['MESHIVE_API_KEY']}"}
    resp = httpx.post(
    f"{BASE}/images/generations",
    params={"async": "true"},
    headers={**KEY, "Idempotency-Key": str(uuid.uuid4())},
    json={
    "model": "<model-id>",
    "prompt": "A lighthouse in a storm, dramatic lighting",
    "size": "1024x1024",
    "webhook_url": "https://your-server.com/webhooks/meshive",
    },
    )
    request_id = resp.json()["id"] # 202 → req_...

    Video is the same flow — client.videos.create(...) or POST /v1/videos/generations, both always async (reference).

  4. Receive and download.

    When the job finishes, your receiver gets request.completed. Fetch the request to get fresh presigned links (the webhook deliberately carries keys, not URLs — links expire, ids don’t):

    detail = httpx.get(f"{BASE}/requests/{request_id}", headers=KEY).json()
    for i, url in enumerate(detail["output_urls"] or []):
    open(f"result_{i}.png", "wb").write(httpx.get(url).content)

    On request.failed, the event’s error_code tells you what happened — vram_exceeded means shrink the job or raise the deployment’s cap; the full table covers the rest.

  • Dedupe on event.id — delivery is at-least-once by design (retries: 3 attempts, 2s/4s/8s backoff).
  • Keep the handler under 5 seconds — acknowledge, then process. Slow handlers are recorded as failures and retried; a subscription endpoint failing ~20 times in a row is auto-disabled.
  • Have a fallback. Webhooks are a notification, not the source of truth — if your server was down past the retry window, GET /v1/requests?status=succeeded reconciles anything you missed.
  • Download or own the storage. Managed results expire after the retention window (10 days by default); set storage_credential_id (or a deployment default bucket) to keep results in your own bucket forever.
  • Watch the Deliveries log (Serverless › Webhooks) when debugging — every delivery, test included, is recorded with status code, attempts, and the exact body sent.