# 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_generation.complete` |
| 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:

```json
{
  "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](https://inspect.hookdeck.com) 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.

```javascript
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:

```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](/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](/docs/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](/docs/filters) and [connections](/docs/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](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### 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](/webhooks/guides/implement-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](https://hookdeck.com) checks the key, stores every callback, and retries and replays delivery to your service.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Leonardo.Ai webhooks reliably in minutes.