> ## Documentation Index
> Fetch the complete documentation index at: https://docs.blon.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Get a request on your server when a job ends.

## Set up

Pass `webhook_url`, a public `https` URL, when you submit a job:

```bash theme={null}
curl https://api.blon.ai/v1/jobs \
  -H "Authorization: Bearer $BLON_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"model":"gpt-image-2","input":{"prompt":"A ceramic teapot on a linen table, soft morning light"},"webhook_url":"https://example.com/hooks/blon","metadata":{"order":"A-17"}}'
```

When the job ends, Blon sends one event to that URL:

| Event | Sent when |
| - | - |
| `job.completed` | The job completed |
| `job.failed` | The job failed |
| `job.canceled` | The job was canceled |

Each job sends each event at most once.

## Payload

The body is the event with the whole job in `data`. Artifact URLs are signed fresh for each delivery.

```json theme={null}
{
  "id": "whd_4f1c2e9b7a6d4c3b",
  "event": "job.completed",
  "created_at": "2026-10-30T08:00:09.000Z",
  "data": {
    "id": "cmh2x0f6k0001qv8z9d4c1a7e",
    "object": "job",
    "model": "gpt-image-2",
    "status": "completed",
    "provider_status": "succeeded",
    "queue_position": null,
    "cancelable": false,
    "next_poll_after_seconds": null,
    "urls": {
      "get": "https://api.blon.ai/v1/jobs/cmh2x0f6k0001qv8z9d4c1a7e",
      "cancel": null
    },
    "input": {
      "prompt": "A ceramic teapot on a linen table, soft morning light"
    },
    "metadata": {
      "order": "A-17"
    },
    "cost": {
      "currency": "CREDITS",
      "estimated": "8",
      "charged": "8",
      "refunded": "0"
    },
    "usage": null,
    "artifacts": [
      {
        "id": "cmh2x0g2p0002qv8zb3n5k8wd",
        "name": "default",
        "type": "image",
        "url": "https://blon.ai/api/assets/cmh2x0g2p0002qv8zb3n5k8wd?ticket=…",
        "url_expires_at": "2026-10-30T08:15:00.000Z",
        "content_type": "image/png",
        "size_bytes": 1482113,
        "width": 1024,
        "height": 1024,
        "duration_ms": null
      }
    ],
    "expires_at": "2026-11-06T08:00:00.000Z",
    "error": null,
    "webhook": {
      "url": "https://example.com/hooks/blon",
      "status": "delivered",
      "attempts": 1
    },
    "idempotency_hit": false,
    "request_id": "cmh2x0f6k0001qv8z9d4c1a7e",
    "created_at": "2026-10-30T08:00:00.000Z",
    "started_at": "2026-10-30T08:00:01.000Z",
    "completed_at": "2026-10-30T08:00:09.000Z",
    "updated_at": "2026-10-30T08:00:09.000Z"
  }
}
```

The job's `webhook` field shows the delivery: `status` (`pending`, `delivered` or `failed`) and `attempts`.

## Verify the signature

Signatures follow [Standard Webhooks](https://www.standardwebhooks.com). Each request carries `webhook-id` (the event `id`), `webhook-timestamp` and `webhook-signature`. Use an official Standard Webhooks library with your signing secret and the raw request body.

```javascript Node.js theme={null}
import { Webhook } from "standardwebhooks";

const wh = new Webhook(process.env.BLON_WEBHOOK_SECRET);
// Pass the raw body string and the request headers; throws when the signature is invalid.
const event = wh.verify(rawBody, {
  "webhook-id": req.headers["webhook-id"],
  "webhook-timestamp": req.headers["webhook-timestamp"],
  "webhook-signature": req.headers["webhook-signature"],
});
```

```python Python theme={null}
from standardwebhooks.webhooks import Webhook

wh = Webhook(os.environ["BLON_WEBHOOK_SECRET"])
# raises when the signature is invalid
event = wh.verify(raw_body, dict(request.headers))
```

Find and rotate the signing secret in the [developer console](https://blon.ai/console/webhooks). After a rotation the old and new secrets are both valid for 24 hours.

## Delivery and retries

* Each attempt waits 10 seconds for your response.
* A `2xx` response is a success.
* A `4xx` response is a permanent failure and is not retried.
* Anything else is retried up to 10 times, starting at 30 seconds and doubling, at most 1 hour apart.

There is no redelivery endpoint. If you miss an event, read the job with `GET /jobs/{id}`.

Respond quickly with `2xx` and do slow work afterwards. Use the event `id` to ignore duplicates.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.