Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.runflow.io/llms.txt

Use this file to discover all available pages before exploring further.

What you’ll do

Stand up a webhook handler that takes a Runflow callback, persists the result, and returns 200 fast.

Prerequisites

  • A Runflow API key. Create one.
  • A public URL for the handler. Use ngrok for local dev.
  • A database or queue to persist results.

Steps

1

Stand up the handler

The handler should: parse the body, persist the relevant fields, return 200 within a few seconds.These examples parse JSON directly for a local prototype. In production, verify Runflow-Signature against the raw request body before parsing JSON; see Verify callback signatures.
import express from "express";
const app = express();
app.use(express.json({ limit: "5mb" }));

app.post("/webhook/runflow", async (req, res) => {
  const { event, run_id, status, output } = req.body;

  // Persist immediately. Output URLs are presigned; copy them to your storage.
  await db.runs.upsert({ run_id, event, status, output });

  res.sendStatus(200);
  // Heavy work (downloading outputs) goes in a background job.
});

app.listen(3000);
2

Pass callback_url on every run

curl -X POST https://api.runflow.io/v1/models/runflow/background-replace/runs \
  -H "Authorization: Bearer $RUNFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "input": { "image_url": "https://...", "prompt": "..." },
    "callback_url": "https://your-server.com/webhook/runflow"
  }'
3

Be idempotent

Runflow retries failed callbacks. Match on run_id for run callbacks or batch_id for batch callbacks, and skip if you have already processed it.
const existing = await db.runs.findByRunId(run_id);
if (existing?.status === "succeeded") return res.sendStatus(200);
4

Copy outputs to your storage

Output URLs are presigned and time-limited. Download or re-upload them to your own bucket on receipt. Do not store the raw URLs as your source of truth.

Verify it worked

Trigger a run, watch your handler logs:
POST /webhook/runflow 200 1234ms
{ event: 'run.completed', run_id: '01J0...', status: 'succeeded' }
You have a row in your DB with status: succeeded. You’re done.

Troubleshooting

SymptomLikely causeFix
Callback arrives but body is emptyMissing body parserFor unsigned prototypes, use express.json() or framework equivalent. For signed handlers, read the raw body first.
Handler times outHeavy work in the requestReturn 200 first, defer heavy work.
Same payload arrives twiceRetry on a 5xx your handler returnedBe idempotent on run_id.
Nothing arrivesURL not publicUse ngrok or Cloudflare Tunnel for local dev.

Production hardening

  • Verify the signature so spoofed POSTs cannot reach your DB.
  • Wrap your handler in a queue (SQS, Pub/Sub, BullMQ) so a slow downstream cannot drop callbacks.
  • Monitor 4xx and 5xx from your endpoint. Runflow stops retrying after a few attempts.

Verify signatures

HMAC verify in code.

Callbacks concept

Pattern, retries, redelivery.