Webhook

生成完成时, 第一时间通知你

不用再轮询。无论请求是同步的(图像模型)还是异步的(视频模型、排队较久的任务),只要生成一落地,Flixly就会向你的URL POST一个带签名的事件。

HMAC-SHA256签名

每次投递都带有对`${timestamp}.${body}`计算出的签名,你可以据此验证它确实来自我们,并且没有被篡改。

防重放

X-Flixly-Timestamp请求头让你可以拒绝超过约5分钟的签名,用来防范被截获后的重放攻击。

自动重试

共3次尝试,采用指数退避(立即、+2秒、+6秒)。每次尝试的超时时间为5秒。轮询/generations/{id}是你的兜底方案。

每个密钥独立的签名密钥

每个API密钥都有自己的webhook签名密钥。你可以随时在工作台里轮换,旧签名会立即校验失败。

启用webhook

有两种方式告诉Flixly该把事件POST到哪里:

方式一 — 密钥级默认值

在API密钥上设置

为每个API密钥设置默认的webhook URL并生成签名密钥。用该密钥提交的每一次生成,都会投递到这个URL。

方式二 — 单次请求

在生成请求体中传入

在POST请求体中设置webhook_url,即可覆盖密钥上的默认值。适合一次性的接收地址,或者按环境分流。

POST /api/v1/generate
{
  "model": "veo-3-fast",
  "prompt": "...",
  "webhook_url": "https://you/hook"
}

你会收到的事件

每完成(或失败)一次生成就POST一次。JSON请求体,附带签名请求头。

请求头

POST https://your-server.example.com/flixly-webhook
Content-Type: application/json
User-Agent: Flixly-Webhook/1.0
X-Flixly-Event: generation.completed
X-Flixly-Timestamp: 1781085600
X-Flixly-Signature: sha256=abc123...
X-Flixly-Delivery-Id: wh_a1b2c3d4e5f6g7h8

JSON请求体

{
  "event": "generation.completed",
  "id": "j5h2k9...",
  "status": "completed",
  "type": "TEXT_TO_IMAGE",
  "model": "flux-dev",
  "output_url": "https://cdn.flixly.ai/outputs/...",
  "credits_charged": 1,
  "error": null,
  "created_at": "2026-06-06T12:00:00Z",
  "completed_at": "2026-06-06T12:00:05Z"
}

字段说明

event
generation.completedgeneration.failed
id
任务id——就是POST /api/v1/generate返回给你、并用于GET /api/v1/generations/{id}的那一个。
status
投递时一定是completedfailed
output_url
成功时是一个cdn.flixly.ai的URL,失败时为null
credits_charged
已扣除的积分,整数。失败时为0(会自动退款)。
error
失败时是一条面向用户的错误信息,成功时为null。已做过脱敏处理,可以安全地展示给你的用户。

校验签名

我们用HMAC-SHA256对`${timestamp}.${body}`为每次投递签名。你在自己的服务器上,用生成密钥时展示给你的那串值重新计算HMAC,然后用常数时间比较。

请使用原始请求体。

JSON.parse之后再JSON.stringify会改变空白字符和字段顺序,签名因此对不上。在Express中请使用raw-body中间件;在Next.js App Router中,请先调用await req.text(),再调用await req.json()

import { Flixly } from "@flixly/sdk";

// In your webhook handler — Express, Hono, Next.js, etc.
export async function POST(req) {
  const rawBody = await req.text();   // MUST be raw — re-stringifying breaks the signature
  const signature = req.headers.get("x-flixly-signature");
  const timestamp = req.headers.get("x-flixly-timestamp");

  const valid = await Flixly.verifyWebhookSignature({
    secret:    process.env.FLIXLY_WEBHOOK_SECRET,
    timestamp,
    signature,
    body:      rawBody,
    tolerance: 300,   // optional — defaults to 300s replay window
  });

  if (!valid) {
    return new Response("invalid signature", { status: 401 });
  }

  const event = JSON.parse(rawBody);
  // event.event === "generation.completed" | "generation.failed"
  // event.id, event.status, event.output_url, event.credits_charged, ...
  await handleGenerationEvent(event);
  return new Response("ok");
}

投递与重试

无论同步还是异步,每次投递都遵循同样的流程:

  1. 1
    第1次尝试 — 立即
    生成一落地就POST。超时5秒,任何2xx响应都算成功。
  2. 2
    第2次尝试 — 2秒后
    超时时间相同。只有第1次尝试返回非2xx响应或超时时才会触发。
  3. 3
    第3次尝试 — 6秒后(累计8秒)
    最后一次尝试。之后我们会记录日志并放弃。任务本身在我们的数据库中是正常的——你随时可以轮询/generations/{id}作为兜底。

什么算成功

  • 任何HTTP 2xx响应。如果没什么要返回的,回一个空响应体的200 OK就行。
  • 我们不会跟随重定向,请确保你提供的URL就是最终地址。
  • 我们把301302等3xx响应视为失败——它们会在下一跳中悄悄丢掉X-Flixly-Signature请求头。

幂等处理

每次投递都带有唯一的X-Flixly-Delivery-Id请求头。如果重试导致你收到同一个事件两次(比如我们的重试已经发出,而你之前那次响应太慢才到达),可以用这个请求头在自己这边去重。

事件中的id字段是生成任务id——同一次投递的多次重试中它保持不变。只有当我们重新构建载荷时,X-Flixly-Delivery-Id才会逐次变化(目前我们不会这么做)。

准备好接上了吗?

在你的API密钥上设置webhook URL并生成签名密钥——两步加起来大约30秒。