JIUFENG API

Webhook callbacks

Terminal-state callbacks — triggers, payload, signature verification, and retries.

This page describes the gateway's real webhook contract (as opposed to the "illustrative examples" in earlier drafts). Fields and behavior here are authoritative.

Webhooks free you from polling: when a task reaches a terminal state, the platform pushes the result to your endpoint.

Enabling it

Pass a webhook field (a base URL) when you submit on Generate images:

{
  "model": "gpt-image-2",
  "prompt": "A Shiba Inu running through a neon cyberpunk city",
  "size": "16:9",
  "resolution": "1K",
  "webhook": "https://your-app.example.com/hooks/jiufeng"
}

On a terminal state the platform POSTs to <webhook>/callback (the /callback suffix is appended automatically). The example above calls back to https://your-app.example.com/hooks/jiufeng/callback.

Trigger rules

SituationCallback?
Task succeeds (completed)Once
Task fails (failed)Once
Intermediate states (pending / processing)No
Task refunded on timeoutNo (poll these via Query tasks)

A callback is sent only on terminal states, once each. Your endpoint should be idempotent (the same id may arrive more than once due to retries), and you shouldn't rely on the callback alone — keep a polling fallback for success.

Payload

The callback body is the Query tasks data object itself, without the { code, data } envelope:

{
  "id": "task_01M0CGM9MRSMD6R6PPNQWXQZ77",
  "status": "completed",
  "progress": "100%",
  "created": 1785076811,
  "completed": 1785076816,
  "actual_time": 5,
  "credits_cost": 12,
  "result": {
    "url": ["https://token-img.jiufeng.ai/f/image/xxx_0.png"],
    "expires_at": 1785163216,
    "image_ids": ["img_01M0..."]
  }
}

On failure, status is failed with error: { message, type, param, code }.

Signature verification

Each callback carries a request header:

X-Webhook-Signature: <hex(HMAC-SHA256(webhook_secret, raw body))>
  • webhook_secret is an account-level setting (configured in the console) — not a request parameter.
  • When the secret is empty, the platform sends no signature header.
  • Compute HMAC-SHA256 over the raw body bytes (not a re-serialized parse of the JSON), then compare against the header value in constant time.

Minimal receiver

Python (Flask)
import hmac
import hashlib
from flask import Flask, request, abort
 
WEBHOOK_SECRET = b"your-account-webhook-secret"
app = Flask(__name__)
 
@app.post("/hooks/jiufeng/callback")
def callback():
    raw = request.get_data()  # raw body; do NOT use the parsed object
    sig = request.headers.get("X-Webhook-Signature", "")
    expected = hmac.new(WEBHOOK_SECRET, raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(sig, expected):
        abort(401)
 
    event = request.get_json()
    if event["status"] == "completed":
        urls = event["result"]["url"]
        # TODO: download / re-host urls promptly (they expire after 24h)
    return "", 200  # any 2xx acks; the platform won't retry
Node (Express)
import crypto from "node:crypto";
import express from "express";
 
const WEBHOOK_SECRET = "your-account-webhook-secret";
const app = express();
 
// verify against the raw body; do NOT JSON-parse first
app.post(
  "/hooks/jiufeng/callback",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const sig = req.get("X-Webhook-Signature") || "";
    const expected = crypto
      .createHmac("sha256", WEBHOOK_SECRET)
      .update(req.body)
      .digest("hex");
    const ok =
      sig.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
    if (!ok) return res.sendStatus(401);
 
    const event = JSON.parse(req.body.toString());
    if (event.status === "completed") {
      // event.result.url ...
    }
    res.sendStatus(200); // any 2xx acks
  }
);

Retry policy

Your endpoint returnsPlatform behavior
2xxAck; done, no further pushes
4xx (except 429)Treated as a permanent failure; no retry
5xx / timeout / network error / 429Backoff retry: 10s → 30s → 60s, up to 3 times; then dead-lettered
  • Each callback request times out at 10s — return quickly (do heavy work asynchronously; reply 200 first).
  • For reliable delivery, ack fast and move downloading / re-hosting into a background job.

On this page

Webhook callbacks · Jiufeng Open API