---
title: Webhooks
description: Get a signed POST to your server when a workflow run finishes, instead of polling.
updated: 2026-09-28
---

Wireflow sends an outbound **completion webhook**: start a run with `POST /api/v1/workflows/{id}/run`, include a `webhook` block, and Wireflow POSTs the result to your URL when the run reaches a terminal state. Polling keeps working either way, so you can use both.

## Request a Completion Webhook

Add `webhook` (and optionally `metadata`) to the [run](/docs/api/run) body:

```bash
curl -X POST https://www.wireflow.ai/api/v1/workflows/cm1abc123/run \
  -H "Authorization: Bearer sk-your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "inputs": { "node-prompt": "A cat wearing a top hat" },
    "metadata": { "jobId": "job_42" },
    "webhook": {
      "url": "https://yourapp.com/api/webhooks/wireflow",
      "secret": "whsec_your_signing_secret"
    }
  }'
```

| Field            | Type     | Required | Description                                                                                                                                    |
| ---------------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `webhook.url`    | `string` | Yes      | Where to POST the result. Must be `https` (plain `http` is accepted only for `localhost`, `127.0.0.1` and `*.localhost`). Max 2048 characters. |
| `webhook.secret` | `string` | No       | Signing secret. When set, every delivery carries an HMAC signature. Recommended.                                                               |
| `metadata`       | `object` | No       | Any JSON object. Echoed back verbatim in the delivery so you can match it to your own job.                                                     |

An invalid `webhook` block is rejected up front with `400` and code `invalid_webhook`, before anything runs.

## The Delivery

On a terminal state Wireflow sends `POST` to your URL with a JSON body:

```json
{
  "event": "execution.completed",
  "eventId": "550e8400-e29b-41d4-a716-446655440000",
  "executionId": "exec_789",
  "workflowId": "cm1abc123",
  "status": "COMPLETED",
  "nodeStates": [
    {
      "nodeId": "node-video",
      "label": "Final video",
      "status": "COMPLETED",
      "error": null,
      "outputUrl": "https://cdn.wireflow.ai/..."
    }
  ],
  "metadata": { "jobId": "job_42" },
  "createdAt": "2026-09-28T12:01:15.000Z"
}
```

- `event` is `execution.completed` when `status` is `COMPLETED`, and `execution.failed` for `FAILED`, `TIMEOUT` and `CANCELLED`. Read `status` for the precise reason.
- `nodeStates` has one entry per node that ran, with `outputUrl` / `outputUrls` / `text` / `error` as applicable.
- `metadata` appears only if you sent it.

Headers:

```
Content-Type: application/json
User-Agent: Wireflow-Webhook/1.0
X-Wireflow-Event: execution.completed
X-Wireflow-Delivery: <delivery id>
X-Wireflow-Signature: sha256=<hex>
```

`X-Wireflow-Signature` is present only when you set `webhook.secret`.

## Verify the Signature

The signature is an HMAC-SHA256 of the **raw** request body with your secret. Verify it before trusting the payload:

```ts
import { createHmac, timingSafeEqual } from 'crypto';

function verify(rawBody: string, header: string, secret: string): boolean {
  const expected = `sha256=${createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex')}`;
  const a = Buffer.from(header);
  const b = Buffer.from(expected);
  return a.length === b.length && timingSafeEqual(a, b);
}
```

## Delivery Guarantees

- **At least once.** Any `2xx` response counts as delivered. Anything else, or no answer within 10 seconds, is retried with exponential backoff (starting at 30 seconds, capped at 6 hours) for up to 12 attempts.
- **One event per run.** Each execution produces exactly one delivery event, whichever way the run finished. Retries of that event reuse the same `eventId`, so dedupe on it.
- Respond quickly and do heavy work after you return `200`.

## Triggering Runs From Other Systems

To start a run from Zapier, Make, n8n, a cron job or your own backend, call [`POST /api/v1/workflows/{id}/run`](/docs/api/run) with an API key. Add a `webhook` block if you want the result pushed back.

> **Inbound trigger URLs are not available.** The API has a keyless trigger path (`/api/v1/workflow/{webhookId}/trigger`). It needs a `webhookId`, which you can set through `POST` or `PUT /api/v1/workflows`, plus a matching `X-Webhook-Secret` header, and there is currently no way to obtain that secret. So every call fails: `429` once an IP passes 100 requests an hour (checked before auth), otherwise `401`. A workflow that did authenticate would also be capped at 20 triggered runs an hour. Use `POST /run` with an API key instead.

---

Documentation index: fetch https://www.wireflow.ai/llms.txt for the full list of Wireflow docs.
