Webhooks

Webhooks deliver generation results to your server as soon as they're ready. Essential for video generation and any async workflow where you don't want to poll for status.

Overview

When you include a webhook_url in your generation request, Fluxpool sends an HTTP POST to that URL when the generation completes (or fails). This eliminates the need to poll the /v1/generations/{id} status endpoint.

When do you need webhooks?

  • Image generation: Optional. Most image generations complete in 2–5 seconds — synchronous responses work fine.
  • Video generation: Recommended. Video generations take 30–120 seconds. Webhooks avoid long-polling and timeouts.

When to Use Webhooks

Scenario Recommended Approach
Image generation, low volume Synchronous (wait for response)
Image generation, high volume / batch Webhooks or polling
Video generation (any volume) Webhooks (strongly recommended)
Pipeline / queue-based architecture Webhooks
Serverless / edge functions Webhooks (avoids function timeout)

Setup

Add webhook_url to any generation request. That's it.

from openai import OpenAI

client = OpenAI(
    base_url="https://api.fluxpool.ai/v1",
    api_key="fp_your_api_key"
)

response = client.images.generate(
    model="wan-video-2.1",
    prompt="A koi fish transforming into a phoenix, slow motion",
    size="1280x720",
    extra_body={
        "webhook_url": "https://yourserver.com/api/webhook/fluxpool",
        "webhook_secret": "whsec_your_secret_key"
    }
)

# Response returns immediately with a generation ID
print(response.id)      # "gen_abc123..."
print(response.status)  # "processing"

Webhook Parameters

Parameter Type Required Description
webhook_url string Yes Your HTTPS endpoint. Must return 200 within 10 seconds.
webhook_secret string Recommended A secret string used to sign payloads. See Signature Verification.

Payload Format

Fluxpool sends a POST request with a JSON body to your webhook_url. The payload structure depends on the generation type and outcome.

Headers sent with every webhook request:

POST /api/webhook/fluxpool HTTP/1.1
Host: yourserver.com
Content-Type: application/json
X-Fluxpool-Signature: sha256=a1b2c3d4e5f6...
X-Fluxpool-Event: generation.completed
X-Fluxpool-Delivery: dlv_unique_id
User-Agent: Fluxpool-Webhook/1.0

Image Generation — Completed

JSON
{
  "id": "gen_abc123",
  "object": "generation",
  "event": "generation.completed",
  "created": 1719500000,
  "model": "flux-1.1-pro",
  "status": "completed",
  "type": "image",
  "prompt": "A dragon on a neon-lit Tokyo rooftop, 8K",
  "output": {
    "url": "https://cdn.fluxpool.ai/generations/gen_abc123.png",
    "width": 1024,
    "height": 1024,
    "format": "png",
    "expires_at": 1719586400
  },
  "usage": {
    "credits_used": 3,
    "credits_remaining": 147
  },
  "duration_ms": 3200,
  "metadata": {}
}

Video Generation — Completed

JSON
{
  "id": "gen_xyz789",
  "object": "generation",
  "event": "generation.completed",
  "created": 1719500000,
  "model": "wan-video-2.1",
  "status": "completed",
  "type": "video",
  "prompt": "A koi fish transforming into a phoenix, slow motion",
  "output": {
    "url": "https://cdn.fluxpool.ai/generations/gen_xyz789.mp4",
    "width": 1280,
    "height": 720,
    "format": "mp4",
    "duration_seconds": 4.0,
    "fps": 24,
    "expires_at": 1719586400
  },
  "usage": {
    "credits_used": 15,
    "credits_remaining": 132
  },
  "duration_ms": 47200,
  "metadata": {}
}

Error Payloads

If a generation fails, the webhook fires with event: generation.failed.

JSON
{
  "id": "gen_err456",
  "object": "generation",
  "event": "generation.failed",
  "created": 1719500000,
  "model": "kling-v2",
  "status": "failed",
  "type": "video",
  "prompt": "...",
  "error": {
    "code": "model_overloaded",
    "message": "Model temporarily unavailable. Try again or use a different model."
  },
  "usage": {
    "credits_used": 0,
    "credits_remaining": 147
  },
  "metadata": {}
}
No credits charged on failure. If a generation fails, credits_used is 0. You are never charged for failed generations.

Payload Field Reference

Field Type Description
id string Unique generation ID. Format: gen_*.
object string Always "generation".
event string generation.completed or generation.failed.
created integer Unix timestamp of generation request.
model string Model ID used (e.g., flux-1.1-pro).
status string completed or failed.
type string image or video.
prompt string The prompt sent in the original request.
output object | null Output details. null on failure.
output.url string CDN URL for the generated asset. Expires after 24 hours.
output.width integer Output width in pixels.
output.height integer Output height in pixels.
output.format string png, jpeg, webp (image) or mp4 (video).
output.duration_seconds float Video duration. Image payloads omit this field.
output.fps integer Video frame rate. Image payloads omit this field.
output.expires_at integer Unix timestamp when the CDN URL expires.
error object | null Error details. null on success.
error.code string Machine-readable error code.
error.message string Human-readable error description.
usage object Credit usage for this generation.
usage.credits_used integer Credits consumed. 0 on failure.
usage.credits_remaining integer Remaining credit balance after this generation.
duration_ms integer Total generation time in milliseconds.
metadata object Custom metadata passed in the original request (pass-through).

Signature Verification

Verify webhook signatures to confirm payloads are from Fluxpool, not a third party.

  1. Include webhook_secret when creating the generation (see Setup). Use a strong random string — e.g., whsec_ + 32 hex characters.
  2. Fluxpool signs each payload using HMAC-SHA256 with your secret and sends the signature in the X-Fluxpool-Signature header.
  3. On your server, compute the expected signature and compare.

Verification Example (Node.js)

JavaScript
import crypto from "crypto";

function verifyWebhook(req, secret) {
  const payload = JSON.stringify(req.body);
  const signature = req.headers["x-fluxpool-signature"];

  const expected = "sha256=" +
    crypto.createHmac("sha256", secret)
      .update(payload)
      .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

Verification Example (Python)

Python
import hmac
import hashlib

def verify_webhook(payload_bytes: bytes, signature: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(),
        payload_bytes,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)
Use timing-safe comparison. Always use crypto.timingSafeEqual (Node.js) or hmac.compare_digest (Python) to prevent timing attacks. Never use === or == for signature comparison.

Retry Logic

If your endpoint doesn't return a 2xx status within 10 seconds, Fluxpool retries the webhook delivery.

Attempt Delay After Previous
1st retry 10 seconds
2nd retry 1 minute
3rd retry 10 minutes
4th retry 1 hour
5th retry (final) 6 hours

After 5 failed retries, the webhook is marked as failed. The generation result remains accessible via the GET /v1/generations/{id} polling endpoint for 24 hours.

Best practices:

  • Return 200 immediately. Process the payload asynchronously (queue it). Don't do heavy work in the webhook handler itself.
  • Handle duplicates. In rare cases, you may receive the same webhook twice. Use the id field to deduplicate.
  • Monitor failures. Check the Webhook Logs in your dashboard to see delivery attempts and failures.

Testing Webhooks

Three ways to test webhooks during development:

1. Local Tunnel (Recommended for Development)

Use ngrok to expose your local server:

Bash
# Terminal 1: Start your local server
node server.js  # listening on port 3000

# Terminal 2: Start ngrok
ngrok http 3000
# → https://abc123.ngrok.io

# Use the ngrok URL as your webhook_url
# webhook_url: "https://abc123.ngrok.io/api/webhook/fluxpool"

2. Webhook Testing Services

Use webhook.site or RequestBin to inspect payloads without writing any code. Copy the generated URL and use it as your webhook_url.

3. Dashboard Webhook Logs

Your Fluxpool Dashboard shows a log of all webhook deliveries: payload sent, response received, status code, timing. Use it to debug failed deliveries.

Tip: Set webhook_url to a webhook.site URL, trigger a generation, and inspect the payload. Then build your handler based on the real payload structure.

Example — Express.js Webhook Handler

A minimal Express.js server that receives and verifies Fluxpool webhooks:

JavaScript — server.js
import express from "express";
import crypto from "crypto";

const app = express();
app.use(express.json());

const WEBHOOK_SECRET = process.env.FLUXPOOL_WEBHOOK_SECRET;

app.post("/api/webhook/fluxpool", (req, res) => {
  // 1. Verify signature
  const signature = req.headers["x-fluxpool-signature"];
  const expected = "sha256=" +
    crypto.createHmac("sha256", WEBHOOK_SECRET)
      .update(JSON.stringify(req.body))
      .digest("hex");

  if (!crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  )) {
    return res.status(401).send("Invalid signature");
  }

  // 2. Return 200 immediately
  res.status(200).send("OK");

  // 3. Process async
  const { id, event, type, output, error } = req.body;

  if (event === "generation.completed") {
    console.log(`✅ ${type} ready: ${output.url}`);
    // Download the asset, save to your storage, notify user, etc.
  }

  if (event === "generation.failed") {
    console.error(`❌ Generation ${id} failed: ${error.message}`);
    // Handle failure: retry, notify user, log, etc.
  }
});

app.listen(3000, () => console.log("Webhook server running on :3000"));

Example — FastAPI Webhook Handler

A minimal FastAPI (Python) server:

Python — main.py
from fastapi import FastAPI, Request, HTTPException
import hmac
import hashlib
import os

app = FastAPI()

WEBHOOK_SECRET = os.environ["FLUXPOOL_WEBHOOK_SECRET"]

@app.post("/api/webhook/fluxpool")
async def handle_webhook(request: Request):
    # 1. Read raw body for signature verification
    body = await request.body()
    signature = request.headers.get("x-fluxpool-signature", "")

    expected = "sha256=" + hmac.new(
        WEBHOOK_SECRET.encode(),
        body,
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(expected, signature):
        raise HTTPException(status_code=401, detail="Invalid signature")

    # 2. Parse payload
    payload = await request.json()

    if payload["event"] == "generation.completed":
        print(f"✅ {payload['type']} ready: {payload['output']['url']}")
        # Download, store, notify...

    if payload["event"] == "generation.failed":
        print(f"❌ Failed: {payload['error']['message']}")
        # Handle failure...

    return {"status": "ok"}

Troubleshooting

Problem Cause Solution
Webhook never received URL not reachable from public internet Use HTTPS. Check firewall rules. Test with ngrok.
401 from your server Signature mismatch Verify you're using the same webhook_secret. Check for body parsing issues (raw bytes vs. parsed JSON).
Timeout / retries Handler takes >10 seconds Return 200 immediately. Process async.
Duplicate deliveries Retry after ambiguous response Deduplicate using the id field.
output.url returns 404 URL expired (>24 hours) Download assets immediately upon webhook receipt. Store in your own S3/GCS/R2 bucket.
Payload fields missing API version mismatch Check you're using the latest API version. See Changelog.


Need help?  Join our Discord →  |  support@fluxpool.ai

Was this page helpful?