Skip to content
Seismic Orchestration

Webhooks

Outbound notifications. Seismic delivers a signed HTTPS POST to your configured endpoint(s) whenever a customer's verification status, an account, or an order changes state. Endpoint configuration, delivery history, retries, and signing-secret rotation are managed from a Seismic-hosted console reached via POST /org/webhooks/portal.

The hosted console manages endpoint configuration, signing secrets, retries, and per-endpoint delivery logs. It also publishes the full JSON Schema for every event type below, versioned under "1".

Event catalog

Twelve event types, grouped by the entity they describe. Each group has its own page with full payload examples and field tables.

EntityEventsPage
Customer (KYC / KYB status, virtual account approval)customer.kyc.status_changed, customer.kyb.status_changed, customer.virtual_account_approval.changedCustomer events
Account (fiat / crypto lifecycle)account.created, account.updated, account.deposit_liquidated, liquidation.settlement_recordedAccount events
Deposit (funds into a virtual account)deposit.status_changed, and the deprecated account.deposit_received, account.deposit_returnedAccount events
Order (accepted-quote lifecycle)order.status_changed, order.settlement_recordedOrder events

A KYC / KYB, order, or deposit event fires on any transition to a different status, not only a forward one: a payment that goes completed → refunded fires, and so does a customer released from a hold back into under_review. Only a transition to the same status is silent — with one exception: the customer events also fire when an additional verification approval completes without moving the status, marked by the payload's expanded flag (see Customer events). The deposit events are transition-fired too: Seismic registers every deposit reported into a virtual account and advances its status forward as it settles. deposit.status_changed fires once per transition, carrying the new status, so a hold is announced while it lasts rather than only once it resolves. (The per-status account.deposit_received / account.deposit_returned events still fire alongside it and are deprecated in its favour.) When a virtual account is configured to auto-liquidate, account.deposit_liquidated fires once as the deposit is converted and settled on-chain. Delivery waits for the settlement hash for up to 72 hours, then falls back to a hashless completion; an unrecoverable lookup triggers that fallback sooner. New liquidations do not emit liquidation.settlement_recorded. Registered deposits stay queryable via GET /deposits for their whole lifecycle.

Envelope

Every payload is a JSON envelope: an event_type discriminator, a created_at timestamp, and a data sub-object whose shape is picked by the type. additionalProperties is false on every variant. Treat unknown top-level keys in data as a contract violation.

{
  "event_type": "<one of the values in the catalog above>",
  "created_at": "2026-08-27T08:05:05.883Z",
  "data": {
    "...": "see the per-entity page for the full shape"
  }
}

created_at is an RFC-3339 UTC timestamp with millisecond precision, recording when the event was recorded — not when this particular delivery attempt was made. Retries and manual replays of the same event carry the value the event was first recorded with, so it is safe to use for ordering a backlog of deliveries or measuring how far behind a consumer has fallen. Compare it against the svix-timestamp header, which does move per attempt.

Events recorded before this field existed have no timestamp to replay, so a replay of one arrives without created_at. Treat the field as optional when parsing, and fall back to the svix-timestamp header for those.

The envelope created_at says when the event was recorded, never when the thing it describes came into being. Every event type also reports a timestamp of its own for the resource it is about, in the same RFC-3339 UTC millisecond form:

EventResource timestamps
account.created, account.updatedaccount.created_at, account.updated_at
deposit.status_changed, account.deposit_received, account.deposit_returneddeposit.created_at, deposit.updated_at, deposit.status_at
account.deposit_liquidatedliquidation.created_at, liquidation.updated_at
liquidation.settlement_recordedupdated_at — when the liquidation record captured the hash
order.status_changedorder.created_at
order.settlement_recordedupdated_at — the order record's, since no settlement instant is recorded
customer.kyc.status_changed, customer.kyb.status_changedupdated_at — when the verification record last changed
customer.virtual_account_approval.changedupdated_at — when the approval change was reported

A resource's created_at is stable across every event about it, so a deposit's is the same value on its deposit.status_changed and its account.deposit_returned. Events delivered before a given resource field existed replay without it, so treat each one as optional when parsing.

Delivery semantics

  • Signing: every request carries svix-id (unique message id), svix-timestamp (Unix seconds), and svix-signature (HMAC-SHA256 over id.timestamp.body, base64-encoded). Verify against the endpoint's signing secret before trusting the payload.
  • Idempotency: svix-id is stable across retries. Dedupe on it. A 12-hour replay window prevents a retry of an already-accepted message from delivering it twice.
  • Success criteria: only 2xx responses returned within a 15-second timeout count as delivered. Everything else (including 3xx redirects, 4xx, 5xx, and timeouts) counts as a delivery failure and triggers a retry.
  • Retries: failed deliveries are retried immediately, then 5s → 5m → 30m → 2h → 5h → 10h → 10h after the prior attempt, for 8 attempts total over ~28 hours. Each individual attempt has a 15-second timeout. After the final attempt the message is marked Failed, surfaced in the delivery log, and can be replayed manually from the portal. Endpoints that fail for 5+ days (with failures at least 12h apart) are automatically disabled and need to be re-enabled from the portal.
  • Ordering: none. Two state changes on the same entity can land out of order. Treat the payload as a fact about "the entity is at this state now," not a delta.

Parsing payloads

The TypeScript SDK ships typed Zod schemas for every event in the catalog, plus a one-call parser. See parseWebhookEvent. After verifying the delivery signature, parsing the body yields a discriminated union: narrowing on event_type narrows data with it, and the embedded resource (kyc_profile, account, order, deposit) is typed with the same schemas the rest of the SDK uses.

import { parseWebhookEvent } from "seismic-orchestration";
 
const event = parseWebhookEvent(body);
if (event.event_type === "account.deposit_received") {
  console.log(event.data.deposit.id);
}

Subscribing

  1. Call POST /org/webhooks/portal (owner / admin only) to mint a short-lived magic link.
  2. Open the link. Add one or more endpoint URLs, pick which event types each endpoint receives, and copy the signing secret.
  3. Verify every incoming request's signature against that secret before trusting the payload.

The portal exposes the full delivery history per endpoint, so failed deliveries can be replayed manually from the console.

Reference