Skip to main content
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

EventFired when
run.completedA run finishes successfully with results
run.failedA 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.

Delivery headers

HeaderValue
Content-Typeapplication/json
User-AgentNexio-Webhooks/1.0
X-Nexio-Eventrun.completed or run.failed
X-Nexio-DeliveryStable delivery UUID
X-Nexio-TimestampUnix timestamp used in signing
X-Nexio-Webhook-VersionPayload version (e.g. 2026-03-22)
X-Nexio-Signaturet=<unix>,v1=<hex> (multiple v1= during secret overlap)
AuthorizationBearer <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.