Guide to Leonardo.Ai Webhooks: Features and Best Practices
Leonardo.Ai webhooks save you from polling while images generate. Generating an image through the Leonardo.Ai API takes a few seconds, and instead of calling the Get a Single Generation endpoint until it's done, you can have Leonardo.Ai call your server with the finished generation and the links to its images.
This guide covers how the webhook callback is configured, what the request contains, how to authenticate it with the callback API key, and the practices that matter for a feature with very little documented delivery behaviour.
What are Leonardo.Ai webhooks?
Leonardo.Ai calls the feature a webhook callback. It belongs to a production API key: when you create the key, you can fill in a webhook callback URL and a webhook callback API key. Every generation you start with that API key then calls the URL when it completes.
Leonardo.Ai doesn't sign the request. The webhook callback API key is the credential: Leonardo.Ai sends it as authorization: Bearer <key>, and your endpoint compares it with the key you chose. Leonardo.Ai also publishes the six IP addresses the requests come from.
Leonardo.Ai webhook features
| Feature | Details |
|---|---|
| Configuration | Optional fields when creating a production API key |
| Scope | Every generation started with that API key |
| URL | HTTPS required |
| Changing settings | Create a new production API key, switch to it, then delete the old one |
| Method and format | POST, content-type: application/json |
| Authentication | authorization: Bearer <webhook callback API key> |
| Signature | None |
| Documented event | image_ |
| Envelope | type, object, timestamp, api_version, data |
| Timestamp | Milliseconds |
| Retries and timeout | Not documented |
| Source IPs | 35.173.108.170, 34.239.69.60, 52.73.75.186, 3.229.99.26, 44.218.0.197, 174.129.230.221 |
Payload
Leonardo.Ai documents one callback, for a completed image generation. Trimmed from its example:
{
"type": "image_generation.complete",
"object": "generation",
"timestamp": 1699490546932,
"api_version": "v1",
"data": {
"object": {
"id": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"status": "COMPLETE",
"prompt": "An oil painting of a cat",
"modelId": "6bef9f1b-29cb-40c7-b9df-32b51c1f67d3",
"imageHeight": 512,
"imageWidth": 512,
"images": [
{
"id": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"url": "https://cdn.leonardo.ai/users/.../Leonardo_Creative_An_oil_painting_of_a_cat_0.jpg",
"generationId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
}
]
}
}
}
data.object is the generation: its ID, status, prompt and settings, the model it used and an images array with a CDN URL for each image. Route on type, and acknowledge any other value with a 200, since Leonardo.Ai doesn't say whether other job types send callbacks.
Leonardo.Ai's full example also includes an apiKey object inside the generation, with webhookCallbackUrl and webhookCallbackApiKey. If real callbacks carry it too, the body contains the same key you authenticate with. Check your first real callback, and if the key is there, don't log raw bodies.
Inspect Leonardo.Ai webhooks as they arrive. Leonardo.Ai suggests studying its requests with a request inspector. Use a Hookdeck Inspect URL as the webhook callback URL on a test production key to see the exact headers and body, with no account or setup required.
Setting up Leonardo.Ai webhooks
Generate a long random callback API key first, for example with openssl rand -hex 32. It isn't your Leonardo.Ai API key; it's a separate value you choose so your endpoint can recognise Leonardo.Ai.
- In Leonardo.Ai, create a new production API key.
- Fill in the webhook callback URL with your HTTPS endpoint.
- Fill in the webhook callback API key with your random value.
- Start a generation with the new API key and confirm the callback arrives.
To change the URL or the key later, create another production API key with the new details, move your application to it, and delete the old key.
With Hookdeck. Leonardo.Ai is a supported source in Hookdeck. Its setup note reads: "Set a webhook callback API key on the production API key, then paste it here." Create a source with the Leonardo.Ai type and use its URL as the webhook callback URL. In Webhook Callback API Key, paste only the key, without Bearer . Leonardo.Ai sends Bearer <key>, and the source checks that header. The source accepts POST only, which is what Leonardo.Ai sends. A request with a missing or wrong key still gets a 200 from Hookdeck, but Hookdeck marks it as failed verification and doesn't deliver it to your service.
For local development, hookdeck listen 3000 leonardo-ai --path /webhooks/leonardo gives you a public URL that forwards to your machine.
Securing Leonardo.Ai webhooks
Check that the authorization header uses the Bearer scheme, then compare the key with your stored value in constant time.
const crypto = require('crypto');
const express = require('express');
const CALLBACK_KEY = process.env.LEONARDO_WEBHOOK_CALLBACK_API_KEY;
function verifyBearer(header) {
if (!CALLBACK_KEY || typeof header !== 'string') return false;
const match = /^bearer (.+)$/i.exec(header);
if (!match) return false;
const a = Buffer.from(match[1], 'utf8');
const b = Buffer.from(CALLBACK_KEY, 'utf8');
// timingSafeEqual throws on unequal lengths, so check first.
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
const app = express();
app.post('/webhooks/leonardo', express.raw({ type: '*/*' }), (req, res) => {
if (!verifyBearer(req.get('authorization'))) {
return res.sendStatus(401);
}
let callback;
try {
callback = JSON.parse(req.body.toString('utf8'));
} catch {
return res.sendStatus(400);
}
if (callback.type === 'image_generation.complete') {
const generation = callback.data?.object;
// Queue a job to download generation.images[].url and record generation.id.
}
res.sendStatus(200);
});
The same check in Python:
import hmac
import os
CALLBACK_KEY = os.environ.get("LEONARDO_WEBHOOK_CALLBACK_API_KEY", "")
def verify_leonardo_request(headers) -> bool:
if not CALLBACK_KEY:
return False # never accept when unconfigured
header = headers.get("Authorization", "")
scheme, _, token = header.partition(" ")
if scheme.lower() != "bearer" or not token:
return False
# Compare bytes: compare_digest raises TypeError on a non-ASCII str.
return hmac.compare_digest(token.encode("utf-8"), CALLBACK_KEY.encode("utf-8"))
The callback API key is static. It authenticates the sender but doesn't cover the body, so a modified payload still passes, and a captured request can be replayed. The published IP addresses add a second check: allow only those six at your firewall.
Make Leonardo.Ai webhooks production-ready. Hookdeck Event Gateway checks the callback API key, stores every completed generation, and retries delivery to your service, which Leonardo.Ai doesn't document doing itself.
Leonardo.Ai webhook limitations and pain points
No documented retries
The Problem: If your endpoint is down when a generation completes, you may never hear about it.
Why It Happens: Leonardo.Ai doesn't document retries, a timeout or what counts as success.
Workarounds:
- Record each generation ID when you start it, and poll Get a Single Generation for any that haven't called back after a reasonable time.
- Keep the handler fast and return
200as soon as the key checks out.
How Hookdeck Can Help: Hookdeck stores the callback as soon as it arrives, then delivers to your service with retries you configure. You can replay it after a fix instead of polling for it.
Rotating the key means a new API key
The Problem: Changing a leaked callback key touches your whole integration.
Why It Happens: The callback URL and key belong to the production API key and can't be edited. You create a new API key and delete the old one.
Workarounds:
- During the switch, accept both the old and the new callback key, because generations started with the old API key still call back with the old one.
- Delete the old API key once no generations are in flight.
How Hookdeck Can Help: Hookdeck logs every request, including those that fail verification, so you can see when callbacks with the old key stop.
One URL per API key
The Problem: Staging and production share a URL if they share an API key.
Why It Happens: The callback URL is a property of the API key, not of a generation request.
Workarounds:
- Use a separate production API key, with its own callback URL, for each environment.
How Hookdeck Can Help: Hookdeck filters and connections can route callbacks to different destinations, for example by a field in data.object.
Best practices
Download images in the background
The callback carries CDN URLs, not image data. Store the generation and the URLs, return 200, and download in a job. See why to process webhooks asynchronously.
Deduplicate on the generation ID
data.object.id identifies the generation. Process each one once, even if a callback arrives twice. See our guide to webhook idempotency.
Check your firewall and certificate
Leonardo.Ai's own troubleshooting list for missing callbacks is a header your system doesn't expect, traffic blocked by IP, location or headers, and an incomplete TLS certificate chain. Allow the six IPs and serve a full chain.
Conclusion
Leonardo.Ai's webhook callback sends a JSON POST with type: image_ and the finished generation when an image job made with your production API key completes. There is no signature, so compare the Bearer callback API key in constant time, allow only the published IPs, keep raw bodies out of logs, and plan for the lack of documented retries by tracking generation IDs.
Hookdeck Event Gateway checks the key, stores every callback, and retries and replays delivery to your service.
Get started with Hookdeck for free and handle Leonardo.Ai webhooks reliably in minutes.