Skip to main content

Overview

Webhooks let your integration receive events as they happen — an organization’s verification status changing, a transaction moving through its lifecycle — instead of polling the API. You register an HTTPS URL, we sign and POST each event to it, and you verify the signature before acting on it. All webhook management is scoped to your organization by your API key.

Quickstart

# 1. Register an endpoint
curl -X POST https://api.nxos.io/v1/webhooks/endpoints \
  -H "Authorization: Bearer $NXOS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://your-app.com/webhooks/nxos" }'
# → { "object": "webhook_endpoint", "id": "ep_…", … }

# 2. Fetch the signing secret
curl https://api.nxos.io/v1/webhooks/endpoints/ep_…/secret \
  -H "Authorization: Bearer $NXOS_API_KEY"
# → { "key": "whsec_…" }
On every delivery: verify the signature, then return any 2xx status.
Sandbox uses https://api.sandbox.nxos.io. Endpoints must be HTTPS.

Managing endpoints

MethodPathDescription
POST/v1/webhooks/endpointsRegister a URL to receive events
GET/v1/webhooks/endpointsList your endpoints
GET/v1/webhooks/endpoints/{id}Retrieve one
PATCH/v1/webhooks/endpoints/{id}Change the URL, event types, or enable/disable it
DELETE/v1/webhooks/endpoints/{id}Stop deliveries
GET/v1/webhooks/endpoints/{id}/secretGet the signing secret
POST/v1/webhooks/endpoints/{id}/secret/rotateRotate the signing secret
When creating an endpoint you can pass eventTypes to subscribe to a subset. Omit it to receive all event types, including ones added in the future.
{
  "url": "https://your-app.com/webhooks/nxos",
  "eventTypes": ["transaction.status.updated"],
  "description": "Production receiver"
}

Delivery format

Each delivery is an HTTPS POST. The body is the event envelope:
{
  "type": "transaction.status.updated",
  "timestamp": "2026-06-10T12:00:00.000Z",
  "data": {}
}
with these headers:
HeaderDescription
svix-idUnique message id. Use it to deduplicate retries.
svix-timestampUnix timestamp (seconds). Reject deliveries that are too old to prevent replay attacks.
svix-signatureSpace-delimited list of v1,<signature> entries (supports key rotation).

Verifying signatures

Verification is a plain HMAC-SHA256 check. No special library or SDK required — it works in any language.
  1. Take your endpoint secret, drop the whsec_ prefix, and base64-decode it. That’s your HMAC key.
  2. Build the signed content: svix-id + . + svix-timestamp + . + raw body.
  3. HMAC-SHA256 it with the key, base64-encode the result.
  4. Compare it (constant-time) to each v1,<signature> in the svix-signature header. If any matches, the delivery is authentic.
import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(secret: string, headers: Record<string, string>, rawBody: string): boolean {
  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
  const signedContent = `${headers['svix-id']}.${headers['svix-timestamp']}.${rawBody}`;
  const expected = createHmac('sha256', key).update(signedContent).digest('base64');
  const expectedBuf = Buffer.from(expected);

  return headers['svix-signature']
    .split(' ')
    .map((part) => part.split(',')[1])
    .filter(Boolean)
    .some((sig) => {
      const sigBuf = Buffer.from(sig);
      return sigBuf.length === expectedBuf.length && timingSafeEqual(sigBuf, expectedBuf);
    });
}
Always verify against the raw request body (before any JSON parsing), and reject deliveries whose svix-timestamp is outside a small tolerance (e.g. 5 minutes).

Retries & idempotency

A delivery is considered successful when your endpoint returns a 2xx status. Non-2xx responses or timeouts are retried with exponential backoff over several hours. Endpoints that keep failing are automatically disabled. Retries reuse the same svix-id, so make your handler idempotent: record the svix-id of each event you process and ignore repeats.

Rotating the signing secret

POST /v1/webhooks/endpoints/{id}/secret/rotate issues a new secret; the old one stops verifying immediately. Fetch the new value with GET …/secret. Because the secret is retrievable at any time, your API key is the credential to guard — if it leaks, rotate the API key and your webhook secrets.

Event catalog

organization.verification.updated

An organization’s verification (KYC/KYB) status changed.
{
  "type": "organization.verification.updated",
  "timestamp": "2026-06-10T12:00:00.000Z",
  "data": {
    "object": "organization",
    "organization_id": "org_…",
    "status": "APPROVED",
    "previous_status": "PENDING",
    "reason": null,
    "occurred_at": "2026-06-10T12:00:00.000Z"
  }
}
status is one of APPROVED, ON_HOLD, PENDING, RESUBMISSION_REQUIRED.
reason is populated when an org is placed on hold or asked to resubmit.

transaction.status.updated

A transaction changed status. Fires on every transition.
{
  "type": "transaction.status.updated",
  "timestamp": "2026-06-10T12:00:00.000Z",
  "data": {
    "object": "transaction",
    "transaction_id": "txn_…",
    "transaction_type": "FIAT_PAYOUT",
    "status": "COMPLETED",
    "previous_status": "LOCKED",
    "account_id": "acct_…",
    "organization_id": "org_…",
    "role": "SENDER",
    "occurred_at": "2026-06-10T12:00:00.000Z"
  }
}
status is one of EXPECTED, LOCKED, COMPLETED, DECLINED, REFUNDED. Payloads carry public IDs only. Call the relevant API endpoint to hydrate full details.

Brokers & sub-organizations

If your organization manages sub-organizations, webhook delivery follows your authorization relationship:
  • Every organization receives its own events on its own endpoints, scoped strictly to itself.
  • A broker additionally receives a sub-org’s events on the broker’s endpoints. The payload’s organization_id identifies which sub-org the event is about, so a broker can monitor its whole portfolio from one endpoint.
Broker visibility of a sub-org’s events is gated by the Letter of Authorization (LOA):
Sub-org eventWithout a signed LOAWith an active, signed LOA
organization.verification.updated✅ delivered (track onboarding)✅ delivered
transaction.status.updated🚫 suppressed✅ delivered
A broker can always see a sub-org’s onboarding status, but only sees its money movements once the customer has signed the LOA. See Cross-org access for how LOAs are established.