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. |
HTTPS required. Webhook URLs must use https://. HTTP endpoints are rejected. For local development, use a tunneling tool like ngrok or Cloudflare Tunnel .
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
{
"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
{
"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.
{
"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": {}
}
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). |
output.url as a permanent link.
Signature Verification
Verify webhook signatures to confirm payloads are from Fluxpool, not a third party.
- Include
webhook_secretwhen creating the generation (see Setup). Use a strong random string — e.g.,whsec_+ 32 hex characters. - Fluxpool signs each payload using HMAC-SHA256 with your secret and sends the signature in the
X-Fluxpool-Signatureheader. - On your server, compute the expected signature and compare.
Verification Example (Node.js)
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)
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)
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
200immediately. 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
idfield 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:
# 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.
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:
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:
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