Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.seismic-cards.systems/llms.txt

Use this file to discover all available pages before exploring further.

Seismic delivers real-time events to your server as signed HTTPS POST requests. Every webhook covers something you would otherwise have to poll for: KYC approvals, cardholder status changes, new card authorizations, and settlements. Every request is signed with HMAC-SHA256 so you can verify its origin before doing any work.

Events reference

EventWhen it firesWhat to do
KYC.UPDATEDA KYC decision is returnedMark the user approved or rejected. If approved, call POST /v1/accounts/{accountId}/init.
KYB.UPDATEDA business KYB decision is returnedSame as KYC.UPDATED — for B2B programs.
CARDHOLDER.CREATEDA new cardholder is createdOptional — record the creation in your system of record.
CARDHOLDER.UPDATEDA cardholder transitions state (e.g. PENDINGAPPROVED)Sync cardholder status in your database.
CARD.CREATEDA new card is issuedOptional — the card is also returned synchronously by POST /v1/budget-card.
CARD.UPDATEDA card’s status changes (frozen, closed, expired)Sync card status in your database.
CARD_TRANSACTION.CREATEDA new authorization arrives at the networkLock funds in your accounting system; show a pending charge in your UI.
CARD_TRANSACTION.UPDATEDAn auth is settled, reversed, or refundedUpdate the transaction status; release the funds lock if reversed.

Configuring your endpoint

1

Register your URL in the dashboard

In your Seismic dashboard, set your webhook URL — for example https://api.your-app.com/webhooks/seismic. This is where Seismic will send all events.
2

Generate and save your webhook secret

Generate a webhookSecret in the dashboard and copy it into your secret manager (e.g. AWS Secrets Manager, HashiCorp Vault, or an environment variable). You will use it to verify every incoming request.
3

Make your endpoint ready to receive events

Your endpoint must:
  • Be reachable from the public internet over HTTPS.
  • Respond within 10 seconds.
  • Return a 2xx status on success. Any other status triggers a retry.
Seismic retries failed deliveries with exponential backoff for up to 24 hours. Design your handler to be idempotent — see best practices below.

Webhook payload shape

Every webhook is a JSON POST with the same envelope:
{
  "id":         "evt_5f8b1c2d-3e4a-5678-9012-3456789abcde",
  "eventType":  "CARD_TRANSACTION.CREATED",
  "createTime": "2026-04-01T12:34:56Z",
  "apiVersion": "v1",
  "resource":   "{\"cardId\":\"d8eda079-...\",\"transactionId\":\"tx_...\",\"amount\":\"12.50\", ...}"
}
id
string
Unique event ID. Store this and use it for idempotency checks in your handler.
eventType
string
One of the eight event types listed in the events table above.
createTime
string
ISO-8601 timestamp of when Seismic generated the event.
apiVersion
string
The API version that produced the event, e.g. v1.
resource
string
A JSON-encoded string containing the event-specific payload. You must call JSON.parse(resource) before reading any fields from it.
resource is a string, not a nested JSON object. This is intentional: the HMAC signature is computed over the exact bytes of the resource string, so Seismic delivers it unparsed to avoid JSON re-serialization differences across languages and libraries.

Verifying the signature

Every request includes a Signature header containing a Base64-encoded HMAC-SHA256(webhookSecret, resource). Verify it before doing any work. Example signed request
POST /webhooks/seismic HTTP/1.1
Host: api.your-app.com
Signature:        QkFTRTY0X0VOQ09ERURfSE1BQ19TSEEyNTY=
Signature-Method: HMAC-SHA256
ApiKey:           <your apiKey>
Timestamp:        1714000000
Content-Type:     application/json

{
  "id":         "evt_...",
  "eventType":  "CARD_TRANSACTION.CREATED",
  "createTime": "2026-04-01T12:34:56Z",
  "resource":   "{\"cardId\":\"d8eda079-...\",\"transactionId\":\"tx_...\",\"amount\":\"12.50\",\"status\":\"CLOSED\"}"
}
Verification examples
import crypto from "node:crypto";

function verifySignature(
  resource:  string,
  signature: string,
  secret:    string,
): boolean {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(resource, "utf8")
    .digest("base64");
  return crypto.timingSafeEqual(
    Buffer.from(signature, "base64"),
    Buffer.from(expected,  "base64"),
  );
}

Reference handler

A complete Fastify handler that verifies the signature and dispatches to event-specific functions:
import Fastify from "fastify";
import crypto  from "node:crypto";

const app    = Fastify();
const SECRET = process.env.SEISMIC_WEBHOOK_SECRET!;

app.post("/webhooks/seismic", async (req, reply) => {
  const signature = req.headers["signature"] as string | undefined;
  const body      = req.body as { id: string; eventType: string; resource: string };

  if (!signature || !body.resource) {
    return reply.code(401).send({ error: "missing signature" });
  }

  const expected = crypto
    .createHmac("sha256", SECRET)
    .update(body.resource, "utf8")
    .digest("base64");

  const ok = crypto.timingSafeEqual(
    Buffer.from(signature, "base64"),
    Buffer.from(expected,  "base64"),
  );
  if (!ok) return reply.code(401).send({ error: "invalid signature" });

  const data = JSON.parse(body.resource);

  switch (body.eventType) {
    case "CARD_TRANSACTION.CREATED":
    case "CARD_TRANSACTION.UPDATED":
      await handleCardTxn(data);
      break;
    case "KYC.UPDATED":
      await handleKyc(data);
      break;
    case "CARDHOLDER.CREATED":
    case "CARDHOLDER.UPDATED":
      await handleCardholder(data);
      break;
  }

  return reply.send({ received: true });
});

app.listen({ port: 3000 });

Event payloads

CARD_TRANSACTION.CREATED and CARD_TRANSACTION.UPDATED

Fired on authorization, settlement, reversal, and refund. The same cardTransactionId can appear in both a CREATED and an UPDATED event — always upsert by cardTransactionId.
{
  "cardId":               "d8eda079-6ba7-409e-99c8-ab5f83566fbd",
  "cardTransactionId":    "tx_abcdef123456",
  "amount":               "12.50",
  "transactionAmount":    "12.50",
  "billingAmount":        "12.50",
  "currency":             "USD",
  "merchantName":         "AMZN Mktp US",
  "merchantCategoryCode": "5942",
  "merchantLogo":         "https://logo.clearbit.com/amazon.com",
  "status":               "PENDING",
  "type":                 "1",
  "createTime":           "1714000000000"
}
Transaction status values
statusMeaningAction
PENDINGAuthorization placed at the networkLock funds in your ledger.
CLOSEDAuthorization settledConvert the pending lock into a permanent debit.
FAILAuthorization declinedRelease the lock; surface a notification to the user.
REVERSEDAuthorization reversed before settlementRelease the lock; mark the transaction reversed.
REFUNDEDSettled charge has been refundedCredit the user’s balance.

KYC.UPDATED

{
  "accountId": "78ad30f2-5794-47c7-b413-62cc599ab203",
  "status":    "APPROVED",
  "caseId":    "abc-123-def-456"
}
statusAction
APPROVED, PASS, or COMPLETEDMark the user as KYC-verified; call POST /v1/accounts/{accountId}/init if you haven’t already.
REJECTEDSurface the result to the user; allow them to submit new documents and retry.
PENDINGNo action — wait for the next event.

CARDHOLDER.CREATED and CARDHOLDER.UPDATED

{
  "cardholderId": "1963922322985988097",
  "accountId":    "78ad30f2-5794-47c7-b413-62cc599ab203",
  "status":       "APPROVED"
}
Sync the cardholder’s status in your database when it changes. A cardholder typically moves from PENDING to APPROVED within seconds of creation.

CARD.CREATED and CARD.UPDATED

{
  "cardId":        "d8eda079-6ba7-409e-99c8-ab5f83566fbd",
  "accountId":     "78ad30f2-5794-47c7-b413-62cc599ab203",
  "status":        "FROZEN",
  "reasonCode":    "3001",
  "operationTime": "1714000000000"
}
Use reasonCode to understand why the card’s status changed:
CodeMeaning
7001Created via merchant portal
7002Created via API
1001Closed via API
1002Closed via merchant portal
1003Closed due to risk policy violation
2001Activated via API
3001Suspended via API
3002Suspended via merchant portal
5001Restricted by administrator
6001Unrestricted by administrator

Best practices

  1. Verify the signature first. Reject any request that fails signature verification before doing any work at all.
  2. Be idempotent. Look up the event id (or cardTransactionId) in your database before processing. If you’ve already handled it, return 200 and skip.
  3. Respond fast. Return a 2xx immediately, then process asynchronously by pushing the event to a queue or job runner.
  4. Log everything. Keep at least 30 days of raw webhook logs for reconciliation and debugging.
  5. Never block on slow downstream calls. Your handler must complete within 10 seconds — any longer triggers a retry.
  6. Allowlist Seismic’s IPs. Production webhooks originate from 47.88.0.0/16 and 47.89.0.0/16. IP allowlisting is an optional defense-in-depth layer on top of HMAC verification.