10API basics
Webhooks
We send a signed POST to your address when a task becomes completed or failed. You do not need to keep polling the status.
This model doesn’t produce every resolution and aspect ratio: the examples use the ones available to it.
Setup
Get the signing secret
In the portal, in the “Webhooks” section, copy the account’s signing secret. It starts with
whsec_and is shared by all your API keys. The secret must exist by the time the task finishes. It is not an API key: the values are different.Prepare an address
You need an HTTPS address reachable from the internet, with a valid certificate. Localhost, internal network addresses, and other prohibited destinations will not work. The address can be up to 2048 characters long. Redirects are not followed on delivery, so give the final address.
Add
webhook_urlto the requestGive the address in every generation request that needs a notification. An invalid or prohibited address returns
400before the charge.{ "model": "nano-banana-pro", "prompt": "A ceramic mug on marble", "webhook_url": "https://example.com/ webhooks/ rendergrid" }
What arrives at your address
| Header | Value |
|---|---|
Content-Type | application/json |
X-Webhook-Event | creation.completed or creation.failed. In your logic, rely on the event field of the signed body |
X-Webhook-Delivery-ID | The delivery identifier. It does not change when the same delivery is retried |
X-Timestamp | The signing time in Unix seconds. It is recomputed for every attempt |
X-Webhook-Signature-256 | sha256= and the HMAC-SHA256 signature in lowercase hexadecimal characters |
Events
The event body is JSON with the fields event, id, status, result_urls, and cost. A failure event adds error and 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
- The original charge in USD. It is not a confirmation of a refund and not the balance. If there is no amount, you get
null. - error
- Text to display, or
null. Decide success or failure bystatus, not by the text. - failure
- The reason for the failure, for programs:
type,code,retryable. What they mean. - result_urls
- Links to the files. For a failed event the array is empty. Files live for a day: download them right away.
New fields may appear later. If you validate the body against a strict schema, allow unknown fields, or deliveries will start to fail.
Verifying the signature
The signature is there to confirm that the request came from us and was not changed. Compute it like this:
- Take the raw bytes of the body—before the framework parses the JSON.
- Concatenate the
X-Timestampvalue, a dot, and the body. - Compute HMAC-SHA256 of that string. The key is the whole secret,
whsec_included, as UTF-8 bytes. - Compare
sha256=plus the result with theX-Webhook-Signature-256header in constant time. - Reject the request if the timestamp is outside the window. In the examples the window is 300 seconds; this is the receiver’s rule, not the redelivery period.
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")); }
Do not parse or rebuild the JSON before verifying: whitespace, escaping, and key order change, and the signature stops matching. Do not decode the secret and do not strip whsec_ from it. Keep your server’s clock synchronized.
Parse the JSON and take any action only after verification succeeds.
Check it on an example
Three ready-made requests, signed for real. Choose one and click the button.
Acknowledgment and retries
Any 2xx response means “received.” We treat other responses and network errors as a failure and retry the delivery: by default, up to five attempts in total.
| Attempt | When |
|---|---|
| 1 | immediately, as soon as the task finished |
| 2 | 60 seconds after the first |
| 3 | 120 seconds after the second |
| 4 | 240 seconds after the third |
| 5 | 480 seconds after the fourth |
- We wait 10 seconds for a response. Acknowledge receipt quickly, and do image downloads and long work afterward.
- First save the event reliably or put it in a queue, then answer
2xx. If you could not accept it, answer with another status. - Perform the action once per
(id, event)pair.X-Webhook-Delivery-IDhelps remove duplicates. A retry must be safe even if your acknowledgment was lost. - A delivery failure does not move the task to
failed. You can always get the state fromGET /creations/, and see the delivery history in the portal.{id}
The signing secret and replacing it
All of an account’s API keys sign webhooks with one secret. Keep it on your server, in a secret store.
After you replace it in the portal, the new secret becomes the current one at once: all following attempts, retries included, are signed with it. Update your handler right away and keep accepting the previous secret for another 24 hours, while still checking the freshness of the timestamp. Only one previous secret is kept: the next replacement pushes it out.
Test delivery
In the portal you can send a signed test to an allowed HTTPS address. It can be sent no more often than once every 30 seconds per account; a test is sent once, with no retries. The body is shorter than a real event:
{
"event": "creation.completed",
"id": "test",
"test": true
}Check the signature, see test: true, answer 2xx, and do not process the result. A test has no status, result_urls, or cost: do not require them. The log of recent deliveries and their outcomes is in the same place, in the portal, with the newest on top.
Frequently asked questions
Do I need to set webhook_url in every request?
What should I do if a webhook did not arrive?
GET /creations/ {id}. Delivery is not guaranteed one hundred percent, so polling is a fallback.Why does the signature not match?
whsec_ was stripped from the secret, or the signature is computed over the wrong bytes. Take the raw body and the X-Timestamp value as they are.