Gareth Wilson Gareth Wilson

Guide to Leonardo.Ai Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationOptional fields when creating a production API key
ScopeEvery generation started with that API key
URLHTTPS required
Changing settingsCreate a new production API key, switch to it, then delete the old one
Method and formatPOST, content-type: application/json
Authenticationauthorization: Bearer <webhook callback API key>
SignatureNone
Documented eventimage_generation.complete
Envelopetype, object, timestamp, api_version, data
TimestampMilliseconds
Retries and timeoutNot documented
Source IPs35.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.

  1. In Leonardo.Ai, create a new production API key.
  2. Fill in the webhook callback URL with your HTTPS endpoint.
  3. Fill in the webhook callback API key with your random value.
  4. 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 200 as 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_generation.complete 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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.