Webhooks
Instead of polling, pass a webhook_url in your generation request and Rendergrid POSTs a signed event to it when the creation reaches a terminal status — creation.completed or creation.failed. Every delivery is signed with your account's webhook secret so you can verify it came from us.
Configuring delivery
webhook_url is set per request on POST /images/generate. It must be HTTPS and at most 2048 characters; the target is validated when the request is submitted, and a refused target fails the request with 400 before anything is charged.
Delivery headers
Each delivery is a JSON POST carrying these headers:
Content-Type—application/json.X-Webhook-Event—creation.completedorcreation.failed.X-Webhook-Delivery-ID— unique id per delivery attempt, useful for deduplication.X-Timestamp— Unix seconds when the delivery was signed.X-Webhook-Signature-256— the HMAC signature to verify.
Payload
The body contains event, id (the creation id), status, result_urls, and cost (the USD amount charged); on creation.failed it additionally carries a sanitized error message:
{
"event": "creation.completed",
"id": "c0ea4d5b-c965-4943-aaad-76fda0c53ca9",
"status": "completed",
"result_urls": [
"https://cdn.rendergrid.io/images/2026/07/02/d13e03c7-7897-48b5-8571-8cd1e78e5f45/8b5f7dcb_20260702_100059_0f9a5d99_req_c0ea4d5b.jpg"
],
"cost": 0.1
}Verifying signatures
The signature is "sha256=" + hex(HMAC_SHA256(secret, "{timestamp}.{raw_body}")) — HMAC over the X-Timestamp value, a dot, and the raw request body, keyed with your signing secret. Always verify against the raw body bytes (before JSON parsing), compare with a constant-time comparison, and reject deliveries whose timestamp falls outside your own freshness window to protect against replays:
import hashlib
import hmac
import time
TOLERANCE_SECONDS = 300 # your own freshness window — tune to taste
def verify_webhook(
secret: str, timestamp: str, raw_body: bytes, signature: str
) -> bool:
"""Verify X-Webhook-Signature-256 for a delivery.
timestamp = the X-Timestamp header, raw_body = the exact request
bytes (before any JSON parsing), signature = X-Webhook-Signature-256.
"""
if abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:
return False # too old / too far in the future — possible replay
expected = "sha256=" + hmac.new(
secret.encode(),
timestamp.encode() + b"." + raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature)Retries & idempotent handling
Any 2xx response marks the delivery as successful. Otherwise Rendergrid retries up to 5 attempts with exponential backoff (60s, 120s, 240s, …), with a 10-second timeout per attempt.
Because of retries your endpoint may receive the same event more than once — process deliveries idempotently. Deduplicate on X-Webhook-Delivery-ID (or on the creation id + event pair), respond 2xx quickly, and do any heavy processing asynchronously so slow handlers don't trigger the 10-second timeout.
Your signing secret
Deliveries are signed with a per-account secret, available in the client portal (it is issued the first time you open it there). Webhooks only fire once your account has a secret issued.
- Rotation: you can rotate the secret at any time from the portal — after rotation the previous secret remains valid for 24 hours so in-flight verifiers don't break. Accept signatures from either secret during that window.
- Test deliveries: the portal can send a one-shot signed test delivery to any HTTPS URL so you can verify your handler end-to-end before going live (rate-limited to one test every 30 seconds).
- Delivery log: recent deliveries and their outcomes are visible in the portal, newest first.