Webhooks and Events

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

Register an Endpoint

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

EventTriggered by
agent.createdA new agent deployment was saved
agent.updatedMaster data, chargeback rate, or artifacts changed
agent.archivedAgent deployment was retired
catalog_entry.completedGeneration finished, record is ready
catalog_entry.publishedCatalog entry is live in the internal catalog
request.receivedA team submitted an access request for an agent
request.scoredMatching scored an access request
session.confirmedAn evaluation session was confirmed
session.cancelledA session was cancelled by either side
settlement.createdA usage chargeback settlement was generated

Anatomy of a Delivery

{
  "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:

HeaderContent
X-Nexora-EventEvent type, e.g. catalog_entry.completed
X-Nexora-DeliveryUnique ID of this delivery
X-Nexora-Signaturet=<unix_time>,v1=<hex>

Verifying the Signature

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.

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.