Back to Docs
MCP

Completion Webhooks

Pass a callback_url to any async tool and Vaybel POSTs a signed completion event when the work finishes — skip polling entirely.

Updated 2026-09-07

Vaybel's generation tools are asynchronous: they return a handle immediately and the real work runs for seconds to minutes. Instead of polling <domain>.get_generation, pass a callback_url and Vaybel will POST a signed event to it the moment the work reaches a terminal state.

When to use it

  • Long-running work you don't want to babysit — designs, mockups, videos, and imports all finish out-of-band. A webhook frees your agent from a polling loop and the 50-second long-poll cap.
  • Server-side automations — n8n, a Lambda, or your own backend can react to completion without holding an MCP connection open.

Keep polling when your caller can't accept an inbound HTTP request (a laptop behind NAT, a notebook session). The two are interchangeable — the webhook data field is byte-for-byte what a get_generation poll returns.

Tools that accept callback_url

design.generate, mockup.generate, product_video.generate, virtual_model.generate, listing.create, listing.publish, content.generate, trend.generate_concept, and optimize.run.

The parameter is always optional. Omit it and nothing changes. Supply it and it must be a public https URL (Vaybel refuses loopback, private, and .internal/.local hosts). Pass it just like any other argument:

json
{
  "product_uuid": "a1b2c3d4-…",
  "prompt": "retro sunset over palm trees",
  "callback_url": "https://hooks.your-app.com/vaybel"
}

The event

When the task finishes, Vaybel sends one POST:

http
POST /vaybel HTTP/1.1
Content-Type: application/json
User-Agent: Vaybel-Webhooks/1
X-Vaybel-Event: design.completed
X-Vaybel-Delivery: 6f5c1e2a-8b4d-4a1e-9c77-2f0b9a3d5e10
X-Vaybel-Signature: t=1757300000,v1=1d4e…c9
json
{
  "event": "design.completed",
  "handle": "3b8e0c2a-1f4d-4c9a-9b21-7e5a0d2c6f10",
  "tool": "design.generate",
  "organization_id": "8c2b…",
  "delivered_at": "2026-09-07T18:13:20.512Z",
  "data": {
    "handle": "3b8e0c2a-1f4d-4c9a-9b21-7e5a0d2c6f10",
    "resource_id": "d91a…",
    "status": "complete",
    "done": true,
    "design_id": "d91a…",
    "progress": 1.0,
    "stage": "Design ready",
    "image_url": "https://cdn.vaybel.com/…",
    "error": ""
  }
}
  • event is <category>.completed on success or <category>.failed on a failed/cancelled task. Branch on it, or on the data.status field.
  • handle matches the handle the tool returned, so you can correlate the event with the call you made.
  • tool is the exact tool name that was invoked.
  • data is the full projection — identical to <domain>.get_generation(handle).

Headers

HeaderMeaning
X-Vaybel-EventThe event name, same as the body's event.
X-Vaybel-DeliveryUnique id for this delivery attempt.
X-Vaybel-Signaturet=<unix seconds>,v1=<hex hmac-sha256> — verify before trusting the body.

Verifying the signature

The signature covers "<t>.<raw request body>" with HMAC-SHA256 keyed by your org's signing secret. Verify against the raw bytes you received — parsing and re-serializing the JSON changes the bytes and breaks the check.

python
import hmac, time
from hashlib import sha256
 
def verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    timestamp, signature = parts["t"], parts["v1"]
    if abs(time.time() - int(timestamp)) > tolerance:
        return False  # stale — reject replays
    expected = hmac.new(
        secret.encode(), f"{timestamp}.".encode() + raw_body, sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)
javascript
const crypto = require('crypto');
 
function verify(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const { t, v1 } = parts;
  if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSec) return false;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${t}.`)
    .update(rawBody)
    .digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}

Replay protection

The t value is when Vaybel signed the request. Reject anything older than your tolerance (300 seconds is a good default) so a captured delivery can't be replayed later. Deduplicate on X-Vaybel-Delivery if your handler is not idempotent — a retried delivery reuses the same event but carries a new id.

Retries

A delivery counts as accepted only on a 2xx response. Any other status, a timeout (10s), or a connection error is retried on a fixed backoff:

AttemptDelay before it
1immediate
21 minute
35 minutes
430 minutes
52 hours

After the fifth attempt Vaybel gives up. Every attempt — including the failures — is recorded under Dashboard → Settings → MCP → Completion webhooks, so you can see exactly what was sent and how your endpoint answered.

Getting and rotating your secret

Open Dashboard → Settings → MCP, scroll to Completion webhooks, and press Reveal to see the whsec_… signing secret (it is minted the first time you ask for it). Store it as an environment variable on your receiver.

Press Rotate to replace it. The old secret stops verifying immediately, so update your receiver with the new value first. The signing secret is per organization and covers every webhook that org sends.

Recipe: n8n

Step

Add a Webhook trigger

Drop a Webhook node, set the method to POST, and copy its production URL. Under the node's options set Binary Data / Raw Body on so you can verify the signature against the exact bytes.

Step

Pass the URL as callback_url

In the MCP Client node that runs design.generate (or any async tool), add callback_url to the JSON input: ={{ ({ product_uuid: $json.uuid, prompt: "retro sunset", callback_url: "https://your-n8n.app/webhook/vaybel" }) }}.

Step

Verify in a Code node

After the Webhook trigger, add a Code node that recomputes the HMAC over `${t}.${rawBody}` with your whsec_… secret and compares it to the v1 value in the X-Vaybel-Signature header. Stop the workflow when it doesn't match.

Step

Act on the event

Read body.data — it is the same shape as design.get_generation. Branch on body.event (design.completed vs design.failed) and continue the flow (generate mockups, publish, notify).