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).
-
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" -
Stand up the receiver.
Verify the signature on the raw body, dedupe on the event id, respond
200fast (each delivery attempt times out after 5s — do real work in the background):import hashlib, hmac, osfrom fastapi import BackgroundTasks, FastAPI, Header, HTTPException, RequestSECRET = os.environ["MESHIVE_WEBHOOK_SECRET"]app = FastAPI()seen: set[str] = set() # use a real store in productiondef 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 = Falseif 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 → dedupereturn {"ok": True}seen.add(event["id"])background.add_task(handle, event)return {"ok": True} # respond fast; work happens in backgroundimport crypto from "crypto";import express from "express";const SECRET = process.env.MESHIVE_WEBHOOK_SECRET;const app = express();const seen = new Set(); // use a real store in productionapp.post("/webhooks/meshive", express.raw({ type: "application/json" }), (req, res) => {const parts = Object.fromEntries((req.header("X-Meshive-Signature") || "").split(",").map((p) => p.split("=")),);const expected = crypto.createHmac("sha256", SECRET).update(parts.t + "." + req.body).digest("hex");let ok = false;try { ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 || "")); } catch {}if (!ok) return res.status(401).send("bad signature");const event = JSON.parse(req.body.toString());if (!seen.has(event.id)) { // at-least-once delivery → dedupeseen.add(event.id);setImmediate(() => { // respond fast; work happens afterconst d = event.data;if (event.type === "request.completed") console.log("done:", d.id, "→", d.output_object_key);else if (event.type === "request.failed") console.log("failed:", d.id, d.error_code);});}res.json({ ok: true });});app.listen(3000);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. -
Submit work.
Always send an
Idempotency-Key— if your submit gets retried by a flaky network, you still get exactly one job:import httpx, uuidBASE = "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(...)orPOST /v1/videos/generations, both always async (reference). -
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’serror_codetells you what happened —vram_exceededmeans shrink the job or raise the deployment’s cap; the full table covers the rest.
Production checklist
Section titled “Production checklist”- 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=succeededreconciles 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.