Webhooks

The asynchronous generation endpoint (/v1/ideogram-v4/async/generate) returns immediately with a generation_id and delivers the finished result to the webhook_url you supply. Ideogram sends a JSON POST to that URL once every image for the request has finished generating. Each delivery is signed with Ed25519 so you can confirm it genuinely came from Ideogram before acting on it.

What Ideogram delivers

The request body mirrors the synchronous generation response, so the same handler can process both. It contains:

  • generation_id: URL-safe base64 ID of the generation. Use it to correlate the webhook with your original API call, and to poll the generation.
  • created: the time the generation was created.
  • data: an array of generated images, each with url, prompt, resolution, seed, and is_image_safe.
1{
2 "generation_id": "xtdZiqPwRxqY1Y7NExFmzB",
3 "created": "2025-01-23T04:56:07Z",
4 "data": [
5 {
6 "url": "https://ideogram.ai/api/images/ephemeral/xtdZiqPwRxqY1Y7NExFmzB.png",
7 "prompt": "A photo of a cat",
8 "resolution": "2048x2048",
9 "seed": 12345,
10 "is_image_safe": true
11 }
12 ]
13}

Request headers

Every delivery includes these headers:

HeaderValue
X-Ideogram-Webhook-Generation-IdURL-safe base64 ID of the generation. Matches generation_id in the body.
X-Ideogram-Webhook-User-IdURL-safe base64 ID of the account that initiated the request.
X-Ideogram-Webhook-TimestampUnix seconds (decimal) at which the request was signed.
X-Ideogram-Webhook-Key-IdThe kid of the signing key. A hint for which public key to try first, not a requirement.
X-Ideogram-Webhook-SignatureEd25519 signature, lowercase hex.
Content-Typeapplication/json

Verifying the signature

  1. Fetch the public keys. Send a GET to https://api.ideogram.ai/v1/.well-known/jwks.json. The response lists Ed25519 public keys in JWK form. Each key’s x field is the 32-byte public key, base64url-encoded with no padding. The key set is cacheable; refresh it if a signature ever fails to verify against your cached copy, in case the keys rotated.

  2. Rebuild the signed message. Concatenate these four values in this exact order, joined with single newline (\n) separators, then encode the result as UTF-8:

    {X-Ideogram-Webhook-Generation-Id}
    {X-Ideogram-Webhook-User-Id}
    {X-Ideogram-Webhook-Timestamp}
    {sha256_hex(raw request body bytes)}

    Hash the raw body bytes exactly as received. Do not parse and re-serialize the JSON first, or the hash will not match what Ideogram signed.

  3. Verify against the published keys. Decode X-Ideogram-Webhook-Signature from hex and check it against each public key in the set. If any key verifies, the webhook is authentic. If none do, reject the request. The X-Ideogram-Webhook-Key-Id header tells you which key to try first, but fall back to the others so signatures stay verifiable across key rotations.

Python example

1import base64
2import hashlib
3
4import requests
5from cryptography.exceptions import InvalidSignature
6from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
7
8JWKS_URL = "https://api.ideogram.ai/v1/.well-known/jwks.json"
9
10
11def _b64url_decode(value: str) -> bytes:
12 return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4))
13
14
15def ideogram_public_keys() -> list[Ed25519PublicKey]:
16 jwks = requests.get(JWKS_URL, timeout=5).json()
17 return [Ed25519PublicKey.from_public_bytes(_b64url_decode(jwk["x"])) for jwk in jwks["keys"]]
18
19
20def verify_webhook(headers: dict[str, str], body: bytes) -> bool:
21 """Return True if the delivery was signed by Ideogram. `body` is the raw request bytes."""
22 body_hash = hashlib.sha256(body).hexdigest()
23 message = (
24 f"{headers['X-Ideogram-Webhook-Generation-Id']}\n"
25 f"{headers['X-Ideogram-Webhook-User-Id']}\n"
26 f"{headers['X-Ideogram-Webhook-Timestamp']}\n"
27 f"{body_hash}"
28 ).encode("utf-8")
29 signature = bytes.fromhex(headers["X-Ideogram-Webhook-Signature"])
30
31 for public_key in ideogram_public_keys():
32 try:
33 public_key.verify(signature, message)
34 return True
35 except InvalidSignature:
36 continue
37 return False

The body passed to verify_webhook must be the raw request bytes exactly as Ideogram sent them. Read them from your web framework’s raw-body accessor, not from a parsed-then-re-serialized JSON object: re-serializing can reorder keys or change whitespace, which changes the bytes and makes the SHA-256 hash (and so the signature check) fail. Examples of the raw-body accessor:

  • Flask: request.get_data()
  • FastAPI / Starlette: await request.body()
  • Django: request.body
  • Express (Node): express.raw() middleware, then req.body (a Buffer)

A complete Flask handler:

1from flask import Flask, request
2
3app = Flask(__name__)
4
5
6@app.post("/ideogram-webhook")
7def ideogram_webhook():
8 body = request.get_data() # raw bytes as received; do NOT use request.get_json() here
9 if not verify_webhook(dict(request.headers), body):
10 return {"error": "invalid signature"}, 400
11
12 payload = request.get_json() # now safe to parse for your application logic
13 # ... process payload["data"] ...
14 return {"ok": True}

Cache the public keys rather than fetching them on every delivery, and refresh them on a verification failure or on a periodic interval.

Retries, idempotency, and polling

Your endpoint should return a 2xx status to acknowledge a delivery. If it returns a non-2xx status or times out, Ideogram retries the delivery. Design your handler for this:

  • Make it idempotent. The same generation_id can arrive more than once (for example, a retry after your endpoint was briefly slow or unavailable). Key your completion state on generation_id, treat an already-processed generation_id as a no-op, and return 2xx so the retries stop.
  • Delivery is not guaranteed. Ideogram retries a delivery only a limited number of times before dropping it, so a webhook may never arrive (for example, if your endpoint is down for an extended period). Do not rely on the webhook as your only way to get a result.
  • Fall back to polling. If you do not receive a delivery, fetch the result from the polling endpoint, GET /v1/generations/{generation_id}, using the generation_id the async endpoint returned. It returns the same data payload once the generation has finished.