10API 基础
Webhook
任务变为 completed 或 failed 时,我们会向你的地址发送一个带签名的 POST 请求。无需持续轮询状态。
这个模型并不支持所有分辨率和宽高比:示例中使用的是它支持的那些。
准备工作
获取签名密钥
前往控制台:在“Webhook”页面中复制账号的签名密钥。它以
whsec_开头,你所有的 API 密钥共用这一个。任务完成时,这个密钥需要已经存在。它不是 API 密钥:两者的值不同。准备地址
需要一个可从互联网访问、证书有效的 HTTPS 地址。Localhost、内部网络的地址和其他被禁止的目标不可用。地址长度最多 2,048 个字符。投递时不会跟随重定向,因此请填写最终地址。
在请求中添加
webhook_url在每个需要通知的生成请求中都指定该地址。无效或被禁止的地址会在扣费前返回
400。{ "model": "nano-banana-pro", "prompt": "大理石上的陶瓷杯", "webhook_url": "https://example.com/ webhooks/ rendergrid" }
你的地址会收到什么
| 请求头 | 值 |
|---|---|
Content-Type | application/json |
X-Webhook-Event | creation.completed 或 creation.failed。业务逻辑请以带签名请求体中的 event 字段为准 |
X-Webhook-Delivery-ID | 投递 id。同一次投递的重试中保持不变 |
X-Timestamp | 签名时间,Unix 秒。每次尝试都会重新计算 |
X-Webhook-Signature-256 | sha256= 加上 HMAC-SHA256 签名,使用小写十六进制字符 |
事件
事件体是 JSON,包含字段 event、id、status、result_urls 和 cost。失败事件会额外包含 error 和 failure。
{
"event": "creation.completed",
"id": "5b0214ae-9e39-460a-820a-f6ae3e24400b",
"status": "completed",
"result_urls": [
"https:// cdn.rendergrid.io/ images/ example/ result.jpg"
],
"cost": 0.04
}{
"event": "creation.failed",
"id": "5b0214ae-9e39-460a-820a-f6ae3e24400b",
"status": "failed",
"result_urls": [],
"cost": 0.04,
"error": "Generation failed.",
"failure": {
"type": "error",
"code": "internal",
"retryable": true
}
}{
"event": "creation.completed",
"id": "test",
"test": true
}- cost
- 最初的扣费金额,单位为 USD。它既不是退款确认,也不是余额。如果没有金额,则为
null。 - error
- 用于展示的文本,或
null。判断成功还是失败请依据status,不要依据文本。 - failure
- 供程序使用的失败原因:
type、code、retryable。它们的含义。 - result_urls
- 文件链接。失败事件中该数组为空。文件保存一天:请立即下载。
以后可能会出现新字段。如果你按严格的模式校验请求体,请允许未知字段,否则投递会开始被拒绝。
验证签名
签名用来确认:这个请求确实来自我们,且没有被改动。计算方法如下:
- 取请求体的原始字节——在框架解析 JSON 之前。
- 依次拼接
X-Timestamp的值、一个点(.)和请求体。 - 对这个字符串计算 HMAC-SHA256。计算时使用的密钥是完整的签名密钥,包括
whsec_,以 UTF-8 字节表示。 - 将
sha256=加上计算结果,与请求头X-Webhook-Signature-256做常量时间对比。 - 如果时间戳不在时间窗口内,则拒绝该请求。示例中的时间窗口为 300 秒;这是接收方的规则,而不是重新投递的期限。
import hashlib import hmac import re import time TOLERANCE_SECONDS = 300 def verify_webhook( secret: str, timestamp: str, raw_body: bytes, signature: str ) -> bool: if not isinstance(secret, str) or not secret: return False if not isinstance(raw_body, bytes): return False if not re.fullmatch(r"[0-9]{1,12}", timestamp or ""): return False if not re.fullmatch(r"sha256=[0-9a-f]{64}", signature or ""): return False if abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS: return False expected = "sha256=" + hmac.new( secret.encode("utf-8"), timestamp.encode("ascii") + b"." + raw_body, hashlib.sha256, ).hexdigest() return hmac.compare_digest(expected, signature)
import { createHmac, timingSafeEqual } from "node:crypto"; const TOLERANCE_SECONDS = 300; const TS_RE = /^[0-9]{1,12}$/; const SIG_RE = /^sha256=[0-9a-f]{64}$/; export function verifyWebhook(secret, timestamp, rawBody, signature) { if (typeof secret !== "string" || !secret) return false; if (!Buffer.isBuffer(rawBody)) return false; if (typeof timestamp !== "string" || !TS_RE.test(timestamp)) { return false; } if (typeof signature !== "string" || !SIG_RE.test(signature)) { return false; } const age = Math.abs(Date.now() / 1000 - Number(timestamp)); if (age > TOLERANCE_SECONDS) return false; const expected = "sha256=" + createHmac("sha256", Buffer.from(secret, "utf8")) .update(timestamp + ".", "ascii") .update(rawBody) .digest("hex"); return timingSafeEqual(Buffer.from(expected, "ascii"), Buffer.from(signature, "ascii")); }
验证之前不要解析 JSON 再重新序列化:空格、转义和键的顺序都会变化,签名就会对不上。不要对签名密钥做解码,也不要去掉其中的 whsec_。请同步服务器的时钟。
只有验证成功之后,才能解析 JSON 并执行操作。
用示例验证
三个现成的请求,签名都是真实的。选择其中一个,然后点击按钮。
确认与重试
任何 2xx 响应都表示“已收到”。其他响应和网络错误我们都视为失败,并重新投递:默认总共最多尝试五次。
| 尝试 | 时间 |
|---|---|
| 1 | 任务完成后立即 |
| 2 | 第一次之后 60 秒 |
| 3 | 第二次之后 120 秒 |
| 4 | 第三次之后 240 秒 |
| 5 | 第四次之后 480 秒 |
- 我们最多等待 10 秒的响应。请尽快确认接收,下载图片和耗时的处理放到之后进行。
- 请先可靠地保存事件或将其放入队列,然后再返回
2xx。如果无法接收,请返回其他状态。 - 对每一对
(id, event)只执行一次操作。X-Webhook-Delivery-ID可以帮助去重。即使你的确认丢失了,重复投递也要是安全的。 - 投递失败不会让任务变为
failed。任务状态始终可以通过GET /creations/查询,投递历史则可以在控制台中查看。{id}
签名密钥及其更换
账号的所有 API 密钥使用同一个签名密钥对 Webhook 签名。请把它保存在服务器上的密钥存储库中。
在控制台更换后,新的签名密钥会立即成为当前密钥:之后的所有尝试,包括重试,都用它签名。请立即更新你的处理程序,并在此后的 24 小时内继续接受旧的签名密钥,同时仍要校验时间戳的时效性。系统只保存一个旧的签名密钥:下一次更换会将其替换掉。
测试发送
前往控制台:可以向允许的 HTTPS 地址发送一条带签名的测试。每个账号每 30 秒最多发送一次;测试只发送一次,不会重试。请求体比真实事件短:
{
"event": "creation.completed",
"id": "test",
"test": true
}请验证签名,确认其中有 test: true,返回 2xx,并且不要处理其中的结果。测试中没有 status、result_urls 和 cost:不要要求它们存在。最近投递及其结果的日志也在控制台中,最新的在最上面。
常见问题
每个请求都需要指定 webhook_url 吗?
如果没有收到 Webhook 怎么办?
GET /creations/ {id} 查询任务状态。投递不能百分之百保证,因此轮询是备用方案。为什么签名对不上?
whsec_,或者对错误的字节计算了签名。请原样使用原始请求体和 X-Timestamp 的值。