How do I know when an AI image or video job is done: polling or webhooks?

Know when an AI image or video job is done: poll the run until a final status, or add a webhook to /run and get a signed POST when it ends.

Last updated View as Markdown

To know when a Wireflow image or video job is finished, poll GET /api/v1/workflows/executions/{executionId}/poll until status is COMPLETED, FAILED, CANCELLED or TIMEOUT, or add a webhook block to POST /api/v1/workflows/{id}/run so Wireflow POSTs the result to your server when the run ends. Polling keeps working when you use a webhook, so you can use both.

How does an asynchronous AI video job work?

The call that starts a run returns straight away with an executionId, and the work carries on in the background until the run reaches a final status.

  1. POST /api/v1/workflows/{id}/run returns 202 Accepted with the executionId and a poll path. The Location header points to the same poll URL.
  2. Wireflow runs the workflow's nodes. Video generation can take 5+ minutes, and no HTTP connection has to stay open while it works.
  3. The run ends as COMPLETED, FAILED, CANCELLED or TIMEOUT. You read the result by polling, or from the webhook delivery.

Should I poll or use a webhook?

Poll when a script can wait for its own result, and use a webhook when a server should hear about the result without asking. The two compare like this:

Polling Webhook
How it works You GET the poll URL every few seconds Wireflow sends one POST to your URL when the run ends
How you ask for it Nothing extra: every run has a poll URL Add webhook: { url, secret } to the POST /run body
What you need Your API key An https URL that your server answers
Proof it is genuine Your own authenticated request An HMAC-SHA256 signature, when you set a secret
If something goes wrong Poll again Wireflow retries, up to 12 attempts
Documented for Workflow runs, app runs, and agent runs through get_execution Runs started with POST /api/v1/workflows/{id}/run

How do I poll until the job is done?

Request the poll URL every few seconds with an API key that has the executions:read scope, let the wait grow to about 10 seconds, and stop as soon as status is COMPLETED, FAILED, CANCELLED or TIMEOUT. If you get 429 Too Many Requests, wait the number of seconds in the Retry-After header before the next request.

POLL="https://www.wireflow.ai/api/v1/workflows/executions/exec_789/poll"
for i in $(seq 1 120); do
  CODE=$(curl -s -D headers.txt -o run.json -w '%{http_code}' "$POLL" \
    -H "Authorization: Bearer $WIREFLOW_API_KEY")
  if [ "$CODE" = "429" ]; then
    WAIT=$(grep -i '^retry-after:' headers.txt | tr -dc '0-9')
    sleep "${WAIT:-5}"; continue
  fi
  if [ "$CODE" != "200" ]; then echo "HTTP $CODE: $(cat run.json)"; exit 1; fi
  STATUS=$(jq -r '.status' run.json)
  echo "$STATUS"
  case "$STATUS" in
    COMPLETED|FAILED|CANCELLED|TIMEOUT) break ;;
  esac
  sleep 5
done
const FINAL = ['COMPLETED', 'FAILED', 'CANCELLED', 'TIMEOUT'];
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function waitUntilDone(executionId) {
  const url = `https://www.wireflow.ai/api/v1/workflows/executions/${executionId}/poll`;
  const headers = { Authorization: `Bearer ${process.env.WIREFLOW_API_KEY}` };
  let delay = 1000;
  const deadline = Date.now() + 20 * 60 * 1000;
  while (Date.now() < deadline) {
    const res = await fetch(url, { headers });
    if (res.status === 429) {
      await sleep(Number(res.headers.get('Retry-After') || 5) * 1000);
      continue;
    }
    if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
    const run = await res.json();
    if (FINAL.includes(run.status)) return run;
    await sleep(delay);
    delay = Math.min(delay * 1.5, 10000);
  }
  throw new Error('Polling deadline exceeded');
}

const run = await waitUntilDone('exec_789');
console.log(run.status, run.error ?? '');
import os
import time
import requests

FINAL = {"COMPLETED", "FAILED", "CANCELLED", "TIMEOUT"}

def wait_until_done(execution_id):
    url = f"https://www.wireflow.ai/api/v1/workflows/executions/{execution_id}/poll"
    headers = {"Authorization": f"Bearer {os.environ['WIREFLOW_API_KEY']}"}
    delay = 1
    deadline = time.time() + 20 * 60
    while time.time() < deadline:
        res = requests.get(url, headers=headers)
        if res.status_code == 429:
            time.sleep(int(res.headers.get("Retry-After", "5")))
            continue
        res.raise_for_status()
        run = res.json()
        if run["status"] in FINAL:
            return run
        time.sleep(delay)
        delay = min(delay * 1.5, 10)
    raise TimeoutError("Polling deadline exceeded")

run = wait_until_done("exec_789")
print(run["status"], run.get("error", ""))

The 1-second start, the 1.5 times growth and the 10-second cap follow the polling strategy in Executions. Each example stops after a client deadline, so raise it for long videos.

Which status means the job is finished?

COMPLETED and FAILED are the normal endings, and CANCELLED and TIMEOUT can also appear. RUNNING and PENDING mean the job is still going.

status Finished? What to do
RUNNING No Keep polling
PENDING No Keep polling
COMPLETED Yes Read the outputs
FAILED Yes Show the error message to a person
CANCELLED Yes The run stopped before it finished
TIMEOUT Yes The run stopped before it finished

The 202 reply from /run says running in lowercase. The poll and the webhook use the uppercase values above, so branch on those. error is a message for a person to read and its wording can change, so branch on status, never on the message. Files that finished generating before a run stopped are still charged.

How do I get a webhook when the job is done?

Add a webhook block with your https URL, and a secret to sign it, to the POST /run body. When the run reaches a final status, Wireflow sends one POST to that URL with the result.

curl -X POST https://www.wireflow.ai/api/v1/workflows/YOUR_WORKFLOW_ID/run \
  -H "Authorization: Bearer $WIREFLOW_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": "YOUR_WEBHOOK_SECRET"
    }
  }'
const res = await fetch('https://www.wireflow.ai/api/v1/workflows/YOUR_WORKFLOW_ID/run', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.WIREFLOW_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    inputs: { 'node-prompt': 'A cat wearing a top hat' },
    metadata: { jobId: 'job_42' },
    webhook: {
      url: 'https://yourapp.com/api/webhooks/wireflow',
      secret: process.env.WIREFLOW_WEBHOOK_SECRET,
    },
  }),
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
import os
import requests

res = requests.post(
    "https://www.wireflow.ai/api/v1/workflows/YOUR_WORKFLOW_ID/run",
    headers={"Authorization": f"Bearer {os.environ['WIREFLOW_API_KEY']}"},
    json={
        "inputs": {"node-prompt": "A cat wearing a top hat"},
        "metadata": {"jobId": "job_42"},
        "webhook": {
            "url": "https://yourapp.com/api/webhooks/wireflow",
            "secret": os.environ["WIREFLOW_WEBHOOK_SECRET"],
        },
    },
)
res.raise_for_status()

The delivery is a JSON body like this:

{
  "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 exact reason.
  • nodeStates has one entry per node that ran, with outputUrl, outputUrls, text or error as they apply.
  • metadata comes back only if you sent it, so use it to match the result to your own job.
  • The request carries the headers X-Wireflow-Event, X-Wireflow-Delivery and, when you set a secret, X-Wireflow-Signature.

webhook.url must be https, with plain http accepted only for localhost, 127.0.0.1 and *.localhost, and it can be at most 2048 characters. An invalid block is refused with 400 and code invalid_webhook before anything runs.

How do I check that a webhook really came from Wireflow?

Compute an HMAC-SHA256 of the raw request body with your secret and compare it with the X-Wireflow-Signature header, which reads sha256= followed by the hex digest. Check it before you trust the payload, and use the body exactly as it arrived, before any JSON parsing.

import { createHmac, timingSafeEqual } from 'crypto';

function verify(rawBody, header, secret) {
  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);
}
import hashlib
import hmac

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest((header or "").encode(), expected.encode())

The signature header is only there when you set webhook.secret on the run.

What happens if my server misses the webhook?

Wireflow retries it. Any 2xx answer counts as delivered, and anything else, or no answer within 10 seconds, is retried with exponential backoff, starting at 30 seconds and capped at 6 hours, for up to 12 attempts.

  • Deliveries are at least once. Each run produces one event, and its retries reuse the same eventId, so skip an eventId you have already handled.
  • Answer fast. Return 200 first and do the heavy work afterwards.
  • Polling still works. If every attempt fails, poll the executionId, or list the workflow's recent runs with GET /api/v1/workflows/{id}/executions.

Can a webhook start a run?

No. The webhook is outbound only: Wireflow calls you when a run ends. To start a run from another system, a cron job or your own backend, call POST /api/v1/workflows/{id}/run with an API key, and add a webhook block if you want the result pushed back. The Webhooks reference states that inbound trigger URLs are not available.

How do I check a run from a published app or an AI agent?

An app run gives you its own pollUrl, and an agent connected over MCP asks with the get_execution tool.

  • Apps. POST /api/v1/apps/{slug}/execute returns an executionId, a pollUrl and, when the server could sign one, a pollToken. Put https://www.wireflow.ai in front of pollUrl and GET it every few seconds. Authenticate with an apps:execute key from the same account that started the run, or with the token in the x-wireflow-run-token header or the ?t= query parameter. The token lasts 24 hours.
  • Agents. Over MCP, run_workflow and run_app return an executionId, and get_execution returns the status and the output URLs.
curl "https://www.wireflow.ai/api/v1/apps/YOUR_APP_SLUG/executions/EXECUTION_ID/poll" \
  -H "x-wireflow-run-token: YOUR_POLL_TOKEN"

Each item in an app's outputs has nodeId and type, plus urls for media, text, or both. See Apps API for the full shape.

How do I see how long a job took and what it cost?

Call GET /api/v1/workflows/{id}/executions with your API key once the run has finished. Each run in the list includes executionTime, creditsUsed, startedAt and completedAt. The full record at GET /api/v1/workflows/executions/{executionId} takes a browser session only and does not accept API keys.

curl "https://www.wireflow.ai/api/v1/workflows/YOUR_WORKFLOW_ID/executions?limit=5" \
  -H "Authorization: Bearer $WIREFLOW_API_KEY"

Response 200 OK (trimmed):

{
  "data": [
    {
      "id": "exec_456",
      "workflowId": "cm1abc123",
      "status": "COMPLETED",
      "executionTime": 15000,
      "creditsUsed": 10,
      "startedAt": "2025-03-20T12:01:00.000Z",
      "completedAt": "2025-03-20T12:01:15.000Z"
    }
  ]
}

To list a workflow's recent runs, newest first, call GET /api/v1/workflows/{id}/executions with limit (default 10) and offset.

For AI agents: the full documentation index is at /llms.txt, and most docs pages are available as Markdown by adding .md to the URL.

© 2026 Wireflow. All rights reserved.