Webhooks and Events
Instead of polling every minute to see if something happened: register a URL and get notified.
Instead of polling every minute to see if something happened: register a URL and get notified.
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.
| 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 |
{
"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> |
The signed payload is "<unix_time>.<raw_body>", via HMAC-SHA256 with your signing_secret.
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.
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.