---
title: "Webhooks and Events"
description: "Subscribe to events, verify signatures, handle retries correctly."
url: "https://nexora.apim.eu/guides/webhooks"
image: "https://nexora.apim.eu/_og/d/c_Ocean.takumi,title_Webhooks+and+Events,description_~U3Vic2NyaWJlIHRvIGV2ZW50cywgdmVyaWZ5IHNpZ25hdHVyZXMsIGhhbmRsZSByZXRyaWVzIGNvcnJlY3RseS4,props_eyJ0aGVtZSI6eyJtb2RlIjoiZGFyayIsImNvbG9ycyI6eyJwcmltYXJ5IjoiIzM5RkYxNCJ9fX0,p_Ii9ndWlkZXMvd2ViaG9va3Mi,s_NWRJHAm-_xNtv7cH.png"
---

## Webhooks and Events

Instead of polling every minute to see if something happened: register a URL and get notified.

## [Register an Endpoint](#register-an-endpoint)

```bash
curl -X POST https://api.nexora.example/v1/webhooks \
  -H "apikey: $NEXORA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "url": "https://ops.your-company.com/hooks/nexora",
        "events": ["catalog_entry.completed", "request.received", "session.confirmed"],
        "description": "Production ops integration"
      }'
```

The response contains a `signing_secret`. It's only shown once and is used to verify incoming deliveries.

## [Available Events](#available-events)

| Event                   | Triggered by                                       |
| :---------------------- | :------------------------------------------------- |
| agent.created           | A new agent deployment was saved                   |
| agent.updated           | Master data, chargeback rate, or artifacts changed |
| agent.archived          | Agent deployment was retired                       |
| catalog_entry.completed | Generation finished, record is ready               |
| catalog_entry.published | Catalog entry is live in the internal catalog      |
| request.received        | A team submitted an access request for an agent    |
| request.scored          | Matching scored an access request                  |
| session.confirmed       | An evaluation session was confirmed                |
| session.cancelled       | A session was cancelled by either side             |
| settlement.created      | A usage chargeback settlement was generated        |

## [Anatomy of a Delivery](#anatomy-of-a-delivery)

```json
{
  "id": "evt_0d41c8",
  "type": "catalog_entry.completed",
  "created_at": "2026-08-20T09:41:20Z",
  "data": {
    "catalog_entry_id": "cat_4d9b2e",
    "agent_id": "agt_8f2c1a",
    "status": "completed",
    "record_url": "https://api.nexora.example/v1/catalog-entries/cat_4d9b2e/record"
  }
}
```

Headers on every delivery:

| Header             | Content                                  |
| :----------------- | :--------------------------------------- |
| X-Nexora-Event     | Event type, e.g. catalog_entry.completed |
| X-Nexora-Delivery  | Unique ID of this delivery               |
| X-Nexora-Signature | t=<unix_time>,v1=<hex>                   |

## [Verifying the Signature](#verifying-the-signature)

The signed payload is `"<unix_time>.<raw_body>"`, via HMAC-SHA256 with your `signing_secret`.

```python
import hashlib, hmac, time

def is_signature_valid(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    timestamp, signature = parts["t"], parts["v1"]

    if abs(time.time() - int(timestamp)) > tolerance:
        return False  # too old - protects against replay

    expected = hmac.new(
        secret.encode(),
        f"{timestamp}.".encode() + raw_body,
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(expected, signature)
```

Verify against the **raw** request body. Parsing to JSON and re-serializing first changes whitespace and key order - the signature then no longer matches.

## [Retries and Idempotency](#retries-and-idempotency)

We expect a `2xx` status within five seconds. If none arrives, we retry with increasing backoff: after 1 min, 5 min, 30 min, 2 h and 6 h. After that, the delivery is considered failed, and the endpoint is automatically paused after 24 hours without success.

Deliveries can arrive more than once. Keep the processed `id` values around for at least seven days and discard repeats. Respond immediately with `202` and keep working asynchronously - slow processing in the request handler is the most common cause of unnecessary retries.