Use webhooks when you want Nexio to push terminal run updates to your backend instead of waiting for
your next poll. Polling GET /api/v1/jobs/{run_id} is still the canonical reconciliation read.
Supported Events
| Event | Fired when |
|---|
run.completed | A run finishes successfully with results |
run.failed | A run ends with an error |
Both quote matching and coverage analysis runs fire the same events. A coverage analysis completion
delivers run.completed with the gap analysis output in data.run.output instead of ranked
solutions.
Security
Every webhook delivery is cryptographically signed. You can also add an optional bearer token for
simpler setups. The two mechanisms serve different purposes:
-
Signing secret (recommended) — Nexio generates an HMAC-SHA256 signing secret (
whsec_...)
when you create an endpoint. Every delivery includes an X-Nexio-Signature header computed from
the payload. Your server recomputes the signature and compares — proving the request is from Nexio,
the payload was not tampered with, and the request is not a replay.
-
Auth token (optional, simpler) — A static bearer token you provide when creating or updating
an endpoint. Nexio includes it as
Authorization: Bearer <token> on every delivery. Convenient for
API gateways but does not verify payload integrity or protect against replay.
For production use, always verify the signing secret.
Verifying the signing secret
Parse the X-Nexio-Signature header to extract the timestamp (t=) and one or more signatures
(v1=). Recompute the HMAC and compare using constant-time equality.
import { createHmac, timingSafeEqual } from 'crypto'
function verifyWebhook(secret, signatureHeader, rawBody) {
// Parse "t=<unix>,v1=<hex>,v1=<hex>" header
const parts = signatureHeader.split(',')
const timestamp = parts.find(p => p.startsWith('t=')).slice(2)
const signatures = parts.filter(p => p.startsWith('v1=')).map(p => p.slice(3))
// Reject stale requests (5 minute tolerance)
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10)
if (age > 300) throw new Error('Timestamp too old')
// Compute expected signature
const mac = createHmac('sha256', secret)
mac.update(timestamp)
mac.update('.')
mac.update(rawBody)
const expected = mac.digest('hex')
// Compare against each v1 signature (supports secret rotation overlap)
const match = signatures.some(sig =>
timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(sig, 'hex'))
)
if (!match) throw new Error('Invalid signature')
}
// In your webhook handler:
const rawBody = req.body // must be the raw string, not parsed JSON
const signature = req.headers['x-nexio-signature']
verifyWebhook('whsec_...', signature, rawBody)
Always verify against the raw request body (the exact bytes received), not a re-serialized
version of the parsed JSON. Re-serialization can change key order or whitespace, breaking the
signature.
Delivery Contract
- At least once delivery — your consumer must be idempotent
- Ordering not guaranteed — don’t depend on event order
- Same
X-Nexio-Delivery UUID across retries for the same endpoint delivery
- Immutable payload — the JSON snapshot is fixed at delivery creation
Retry schedule
Failed deliveries retry with exponential backoff: 1 min, 5 min, 30 min, 2 hours, 12 hours.
After all retries are exhausted, the delivery is dead-lettered.
| Header | Value |
|---|
Content-Type | application/json |
User-Agent | Nexio-Webhooks/1.0 |
X-Nexio-Event | run.completed or run.failed |
X-Nexio-Delivery | Stable delivery UUID |
X-Nexio-Timestamp | Unix timestamp used in signing |
X-Nexio-Webhook-Version | Payload version (e.g. 2026-03-22) |
X-Nexio-Signature | t=<unix>,v1=<hex> (multiple v1= during secret overlap) |
Authorization | Bearer <auth_token> when configured on the endpoint |
Example delivery payload (quote matching)
{
"id": "evt_123",
"type": "run.completed",
"webhook_version": "2026-03-22",
"created_at": "2026-03-22T12:00:00Z",
"data": {
"run": {
"run_id": "run_123",
"status": "completed",
"environment": "live",
"created_at": "2026-03-22T11:59:48Z",
"completed_at": "2026-03-22T12:00:00Z",
"output": {
"solutions_count": 2,
"top_label": "recommended",
"top_score": 3.75
},
"solutions": [
{
"id": "0b058b72-dec6-4ef9-915f-546fa6cb9377",
"rank": 1,
"cluster_label": "recommended",
"requirements_met": ["lob_auto", "lob_home", "lob_umbrella"],
"provider_count": 1,
"est_cost_low": 11985,
"est_cost_high": 11985,
"offerings": [
{ "id": "quote_line_carrier_c_auto_001", "provider_name": "Carrier C", "category": "auto" },
{ "id": "quote_line_carrier_c_home_001", "provider_name": "Carrier C", "category": "home" },
{ "id": "quote_line_carrier_c_umbrella_001", "provider_name": "Carrier C", "category": "umbrella" }
],
"scorecard": { "overall_level": 3.85 }
}
]
}
}
}
Example delivery payload (coverage analysis)
{
"id": "evt_456",
"type": "run.completed",
"webhook_version": "2026-03-22",
"created_at": "2026-03-22T12:05:00Z",
"data": {
"run": {
"run_id": "run_456",
"status": "completed",
"environment": "live",
"created_at": "2026-03-22T12:04:48Z",
"completed_at": "2026-03-22T12:05:00Z",
"output": {
"response_type": "COVERAGE_GAP_ANALYSIS",
"profile_summary": {
"state": "NJ",
"coverage_lines_present": ["HOMEOWNERS", "AUTO"],
"coverage_lines_missing": ["umbrella"]
},
"gaps": [
{
"id": "gap_missing_umbrella",
"severity": "HIGH",
"category": "MISSING_LINE",
"title": "No umbrella/excess liability policy",
"recommendation": "Add a $1M-$2M personal umbrella policy."
}
],
"summary": {
"total_gaps": 3,
"high_severity": 1,
"medium_severity": 2,
"low_severity": 0,
"lines_reviewed": 2,
"lines_recommended": 1,
"underwriting_flags": 0
}
}
}
}
}
Best Practices
- Return
2xx quickly and process downstream work asynchronously. Nexio retries on non-2xx
responses with exponential backoff.
- Deduplicate on
X-Nexio-Delivery — the same delivery UUID is sent across retries.
- Verify signatures using the code examples above.
- Reject stale timestamps older than 5 minutes to prevent replay attacks.
- Use
GET /api/v1/jobs/{run_id} as the canonical reconciliation read if you miss a delivery
or receive events out of order.