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.
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.
POST /api/v1/workflows/{id}/runreturns202 Acceptedwith theexecutionIdand apollpath. TheLocationheader points to the same poll URL.- Wireflow runs the workflow's nodes. Video generation can take 5+ minutes, and no HTTP connection has to stay open while it works.
- The run ends as
COMPLETED,FAILED,CANCELLEDorTIMEOUT. 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"
}
eventisexecution.completedwhenstatusisCOMPLETED, andexecution.failedforFAILED,TIMEOUTandCANCELLED. Readstatusfor the exact reason.nodeStateshas one entry per node that ran, withoutputUrl,outputUrls,textorerroras they apply.metadatacomes 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-Deliveryand, 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 aneventIdyou have already handled. - Answer fast. Return
200first and do the heavy work afterwards. - Polling still works. If every attempt fails, poll the
executionId, or list the workflow's recent runs withGET /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}/executereturns anexecutionId, apollUrland, when the server could sign one, apollToken. Puthttps://www.wireflow.aiin front ofpollUrlandGETit every few seconds. Authenticate with anapps:executekey from the same account that started the run, or with the token in thex-wireflow-run-tokenheader or the?t=query parameter. The token lasts 24 hours. - Agents. Over MCP,
run_workflowandrun_appreturn anexecutionId, andget_executionreturns 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.