Skip to content
Logo

Onboarding your end users

For per-user customer rows that move money on behalf of your end users.

To onboard your own organization (one that holds the API key), see Onboarding your organization.

Prerequisites

  • KYB-approved org.
  • API key.

Four-step onboarding

  1. Create the customer with api.customers.create(...). Polymorphic on type: individuals carry a KycProfile, businesses carry a KybProfile. The customer lands in draft.
  2. Submit with api.customers.submit(id). Nothing leaves draft without it — creating a customer files a profile, it doesn't file a verification. If the customer accepted the terms of service in your own UI, pass their IP address, user agent and device fingerprint as endUser — see Device fingerprinting.
  3. Verify: read submission state from api.customers.state(id). For hosted flows, redirect to the returned kyc_link.url (or kyb_link.url). For inline flows, verification runs against any documents you've uploaded, or over email. Poll until the status reaches approved.
  4. Create an account with one of the four api.customers.create*Account(id, ...) methods, fiat/crypto × virtual/external.

The create response includes the canonical customer id (UUID). Use that for all subsequent calls.

Create a customer

import {  } from "./client";
 
const  = await ..({
  : "individual",
  : {
    : {
      : "Alice",
      : "Liddell",
      : "1990-04-12",
    },
    : {
      : "alice@example.com",
    },
    : {
      : {
        : "123 Main St",
        : "Brooklyn",
        : "NY",
        : "11201",
        : "US",
      },
    },
  },
});
 
const  = .;
 
// File the verification, then drive it.
await ..();
const  = await ..();
if (. === "individual" && ..) {
  .. = ...;
}

api.customers.state(id) returns a tagged-union envelope. Narrow on env.type before reading env.state.kyc_link (individuals) or env.state.kyb_link (businesses).

Poll verification status

import {  } from "./client";
 
const  = await ..();
 
if (. === "individual") {
  .(..); // e.g. "approved"
 
  if (.. === "approved") {
    // Safe to create an account + start quoting.
  }
}

Status values, in order of forward motion:

draft → submitted → under_review → approved
              ↘ rejected (returned by review — correct and resubmit)
              ↘ terminally_rejected (declined by review — final)
                                       ↘ rejected (correct and resubmit)
                                       ↘ terminally_rejected (final)
                                       ↘ paused (compliance hold, reversible)

submitted is Seismic's own review of the submission, which runs before verification begins — the hosted link is null until the status moves to under_review. A submission rejected by that review reads rejected with the reason in rejection_reason; correct the profile and submit again. One the review declines reads terminally_rejected and cannot be resubmitted.

If you need a fresh URL (the user closed the tab, the link expired, etc.), re-read it from api.customers.state(id). The latest kyc_link.url / kyb_link.url is always on the state envelope.

Create an account

A customer needs at least one account to move money in or out. All four create endpoints require the customer to be approved first — an earlier status fails with CustomerNotVerified (409), so gate the call on the poll above rather than firing it optimistically. See Verification is required first.

import { api } from "./client";
 
const { request, account } = await api.customers.createExternalFiatAccount(customerId, {
  currency: "USD",
  account_holder: {
    type: "individual",
    first_name: "Alice",
    last_name: "Liddell",
    address: { country: "US" },
  },
  details: {
    routing_number: "021000021",
    account_number: "0123456789",
    bank_address: {
      line1: "1 Main St",
      city: "San Francisco",
      state: "CA",
      postal_code: "94104",
      country: "US",
    },
  },
});
// For external attaches, `account` is populated synchronously.

Currencies / rails accept either the human-readable name ("USDC", "base") or the numeric id from the seed JSON. The currency drives the inferred shape on fiat creates.

Tying it all together

import {  } from "./client";
 
async function (: string) {
  // 1. Create the customer.
  const  = await ..({
    : "individual",
    : {
      : {
        : "Alice",
        : "Liddell",
      },
      : {
        : ,
      },
    },
  });
 
  // 2. File the verification, then drive it if there's a hosted URL.
  await ..(.);
  const  = await ..(.);
  if (. === "individual" && ..) {
    .. = ...;
  }
 
  // 3. Once approved, attach a USDC wallet. Earlier statuses fail with
  //    CustomerNotVerified (409).
  if (. === "individual" && .. === "approved") {
    await ..(., {
      : "0x742d35Cc6634C0532925a3b844Bc9e7595f8b840",
      : "base",
      : "USDC",
      : {
        : "individual",
        : "Alice",
        : "Liddell",
        : { : "US" },
      },
    });
  }
 
  return .;
}