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.completed or creation.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:

creation.completed
{
  "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.