API reference
Webhooks
When an intelligence product is delivered (or fails), EU Optikos POSTs a signed JSON payload to your configured endpoints. Manage them at /account/webhooks (Institutional).
Event catalogue (V1)
| Event | Fired when |
|---|---|
| product.delivered | A scheduled product was generated and delivered. |
| product.failed | A scheduled product run failed. |
Payload envelope
Every delivery shares this envelope; data is event-specific.
{
"event": "product.delivered",
"delivery_id": "5b1c…-uuid",
"timestamp": "2026-05-20T08:30:00+00:00",
"organization_id": 123,
"api_version": "v1",
"data": {
"product_id": "…",
"product_type": "risk_assessment",
"region_nuts_code": "DE300",
"title": "…",
"notification_id": 1000,
"action_url": "/inbox"
}
}Headers
| Header | Value |
|---|---|
| X-Optikos-Signature | sha256=<hex> · HMAC-SHA256(secret, raw body) |
| X-Optikos-Event | Event name (route on this without parsing the body) |
| X-Optikos-Delivery | UUID: idempotency key; identical on retries of the same delivery |
| X-Optikos-Timestamp | ISO-8601 UTC: reject if it skews > 5 min (anti-replay) |
Verifying a delivery
Compute sha256=HMAC-SHA256(secret, raw_body) over the raw request body bytes exactly as received (don't re-serialise the JSON), compare it constant-time against X-Optikos-Signature, and reject payloads whose X-Optikos-Timestamp is more than 5 minutes old.
Python
import hmac, hashlib, time
from datetime import datetime
SECRET = b"whsec_…" # the secret shown once at creation
def verify(raw_body: bytes, sig_header: str, ts_header: str) -> bool:
# 1. Anti-replay: reject if the timestamp skews > 5 minutes.
ts = datetime.fromisoformat(ts_header).timestamp()
if abs(time.time() - ts) > 300:
return False
# 2. Recompute over the RAW body bytes exactly as received.
expected = "sha256=" + hmac.new(SECRET, raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig_header)Node.js
const crypto = require("crypto");
const SECRET = "whsec_…";
function verify(rawBody, sigHeader, tsHeader) {
const ts = Date.parse(tsHeader) / 1000; // anti-replay
if (Math.abs(Date.now() / 1000 - ts) > 300) return false;
const expected =
"sha256=" + crypto.createHmac("sha256", SECRET).update(rawBody).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sigHeader));
}Shell (openssl)
# Recompute the expected signature for a captured payload:
printf '%s' "$RAW_BODY" \
| openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -r \
| sed 's/ .*/ /; s/^/sha256=/'
# Compare (constant-time) against the X-Optikos-Signature header.Retries & circuit-breaker
- Respond
2xxto acknowledge.5xx, timeouts, and429are retried with exponential backoff up to the webhook'smax_retries. 4xx(other than 429) is treated as a permanent rejection, not retried.- After several consecutive failures the webhook is auto-paused (circuit-breaker). Fix your receiver, then unpause it from /account/webhooks.
- Deliveries are at-least-once · dedupe on
X-Optikos-Delivery.