Skip to main content

Webhooks

Webhooks come in two flavors at Nexa:

  • Inbound — vendor systems (Pomelo, Amadeus, Hotelbeds, AeroAPI) calling Nexa to notify of state changes.
  • Outbound — Nexa calling registered partner endpoints to deliver platform events (case lifecycle, disruption detected, voucher issued).

Both use the same patterns: HMAC-SHA256 signing, idempotency keys, exponential retry, and replay endpoints.

Outbound webhooks (Nexa → partner)

Partners with *.events.subscribe scopes register a URL via Nexa Customer Success. Nexa delivers events to that URL with the following contract.

Request shape

POST https://your-system.example/nexa-webhook HTTP/1.1
Content-Type: application/json
User-Agent: Nexa-Webhook/1.0
X-Nexa-Signature: sha256=<base64-hmac>
X-Nexa-Signature-Timestamp: 1746489600
X-Nexa-Event-Type: case.resolved
X-Nexa-Event-Urn: urn:event:e-7f8e1
X-Nexa-Tenant: urn:airline:latam
X-Nexa-Correlation: urn:correlation:abc123

{
"type": "case.resolved",
"occurred_at": "2026-05-13T20:11:00Z",
"tenant": "urn:airline:latam",
"case_urn": "urn:case:c-7f8e1",
"data": {}
}

Signature verification

The signature is HMAC-SHA256(timestamp + "." + body, secret) base64-encoded. Verify in your endpoint:

import hmac, hashlib, base64, time

def verify(headers, body_bytes, secret):
sig_header = headers["x-nexa-signature"] # "sha256=<base64>"
timestamp = int(headers["x-nexa-signature-timestamp"])
if abs(time.time() - timestamp) > 300: # reject > 5 min skew
raise ValueError("stale signature")
payload = f"{timestamp}.".encode() + body_bytes
expected = base64.b64encode(
hmac.new(secret, payload, hashlib.sha256).digest()
).decode()
expected_header = f"sha256={expected}"
if not hmac.compare_digest(sig_header, expected_header):
raise ValueError("bad signature")

The 5-minute timestamp window prevents replay attacks. The HMAC is computed over timestamp + "." + body (not just body) so a replayed body with a fresh timestamp doesn't pass.

Idempotency

X-Nexa-Event-Urn is the idempotency key. Partners must dedupe — the same event may be delivered more than once during retries. The event URN is stable across all delivery attempts.

Retries

Failed deliveries (non-2xx response, connection error, timeout > 10s) retry with exponential backoff up to 24 hours:

AttemptDelay
1immediate
2+1 min
3+5 min
4+30 min
5+2 h
6+6 h
7+24 h

After attempt 7, the subscription is paused and the partner contact receives an in-band notification. Re-enabling requires a successful test delivery via POST /partner/v1/webhooks/test.

Replay

Partners can request a replay of any event within 7 days:

GET /partner/v1/events/replay?since=urn:event:e-1234&until=urn:event:e-1240
Authorization: Bearer <token>

Replay does not re-trigger the platform side effect — it just re-pushes the event payload to the registered webhook URL.

Outbound event types

Event typeWhenScope required
disruption.detectedflight-predictor flags a high-confidence disruption (probability > policy threshold)disruptions.events.subscribe
case.openedA case is provisioned (auto or manual)cases.events.subscribe
case.amendedManifest updated (added passengers, corrected next-flight)cases.events.subscribe
subcase.processingOperator submitted compensation; saga runningcases.events.subscribe
subcase.offer-readySaga succeeded; passenger has been notifiedcases.events.subscribe
subcase.resolvedPassenger acceptedcases.events.subscribe
subcase.declinedPassenger declined; saga rolling backcases.events.subscribe
subcase.failedVendor permanent failure; manual review queuedcases.events.subscribe
subcase.compensation-failedSaga rollback failed; manual reconciliation requiredcases.events.subscribe
case.resolvedAll sub-cases reached a terminal statecases.events.subscribe
voucher.issuedVoucher generated and dispatchedvouchers.read (subscribe gating uses the same scope)
wallet.card-issuedVirtual prepaid card issuedwallet.events.subscribe
wallet.card-txWallet authorization, settlement, reversalwallet.events.subscribe

Every event payload includes tenant, case_urn (where applicable), correlation_urn, and occurred_at. The data block carries event-specific fields.

Inbound webhooks (vendor → Nexa)

Several Nexa vendors deliver state changes via inbound webhook. These are configured at vendor onboarding by Nexa engineering, not by customers.

Pomelo (wallet)

Pomelo delivers transaction-state changes (auth, capture, reversal) via HMAC-signed webhooks to the per-tenant wallet deployable's webhook ingress. Each event carries the issued-card URN and is reconciled against Nexa's local card record. Authorization events update the held balance; capture events finalize the transaction; reversals release the hold.

Amadeus / Hotelbeds (booking)

Amadeus and Hotelbeds do not natively support outbound webhooks for booking state changes — these vendors operate on a request-response model. Where vendor-side state changes asynchronously (e.g., a hotel cancels a confirmed booking), Nexa polls the vendor's reconciliation API on a configurable interval (5–60 minutes per vendor) and emits internal events that map to outbound subcase.compensation-failed or voucher.amended events.

AeroAPI (flight predictor)

AeroAPI streams flight-state changes via a long-poll API. The flight-predictor worker consumes the stream continuously, normalizes flight events, and publishes them on the platform domain.flights topic. Disruptions detected here trigger the outbound disruption.detected event.

Twilio / SendGrid / WhatsApp Business (notifications)

Inbound delivery-status webhooks (delivered, bounced, replied, opted-out) are received by the per-tenant notifications deployable and reconciled against the per-passenger send log. Bounce/unsubscribe events update the passenger's contact preferences in the tenant's airline adapter cache.

AeroAPI delivery & retry

Inbound webhooks are received via the partner-api ingress for vendor-shape webhooks; signature validation happens at the proxy edge. Bad signatures return 401; vendor retries kick in. Nexa's own retry behavior on the read side (when polling rather than receiving) is governed by the per-vendor token bucket — see Integrations.

Webhook security checklist

Before going live with a webhook integration, confirm:

  • Signature verification runs on every request. Reject unsigned or bad-signature requests with 401.
  • Timestamp window is enforced. Reject signatures more than 5 minutes old.
  • Idempotency dedupe uses X-Nexa-Event-Urn (or the vendor-side equivalent). The same event may be delivered more than once.
  • Endpoint returns 2xx fast. Don't synchronously process the event in the webhook handler — enqueue it and 200 immediately. Webhook timeout is 10s; queue-based handling is the safe pattern.
  • TLS only. HTTP webhooks are not delivered.
  • Secret rotation is wired up. Both the OAuth client secret and the HMAC webhook secret rotate every 90 days; the dual-key window is 14 days.
  • Allowlist Nexa egress IPs in your firewall if you require IP-level allowlisting. The current list is published at https://docs.nexa.ai/static/egress-ips.txt.

Where to next

Was this helpful?