# Seismic Orchestration > On/off-ramp, wallet, and stablecoin swap orchestration behind a single REST API. Shipped as seismic-orchestration on npm. ## Authentication Seismic Orchestration accepts **two** auth schemes, mutually exclusive on a single request: | Scheme | Header | When to use | | ------------------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | **API key** (recommended) | `Api-Key: seismic_prod_…` | Server-to-server. Org-scoped (one key, one org). No login or refresh. | | **Access token** (dashboard sessions) | `Authorization: Bearer ` | Browsers logged into [dashboard.seismic.systems](https://dashboard.seismic.systems). Issued by the email/password/MFA flow below. | Sending both → **400 BothCredsProvided**. The SDK handles either via separate constructor options (`apiKey` vs `tokenStore`); see [`SeismicOrchestrationClient`](/sdk/typescript/orchestration-client#authenticating). For server-to-server integrations, you almost certainly want API keys — mint one in the dashboard (or via [`POST /auth/api-keys`](/api/auth/api-keys/mint)) and skip the rest of this page. Everything below describes the **dashboard / access-token** flow. *** The dashboard authenticates with `Authorization: Bearer `. Access tokens are short-lived (minutes); refresh tokens are long-lived (days) and live inside the `/auth/refresh` flow. ### Token lifecycle ``` signup ─┐ ├─► login ──► tokens ──────────────────► (use access until it 401s) │ │ │ │ └──► mfa_required ──► mfaVerify ──► tokens │ │ ▼ │ refresh ── tokens │ │ └────────────────────────────────────────────── logout ``` * **Access token** — minutes-long. Sent on every request. Stateless JWT, verified against `JWT_ACCESS_SECRET`. * **Refresh token** — days-long. Opaque. Stored server-side in `refresh_tokens`, one row per session. Rotated on every `refresh` call — the previous value is invalidated. * **MFA challenge id** — seconds-long. Issued by `login` when MFA is active; redeemed by `mfaVerify` for a real token pair. :::info[Single-flight refresh] If 50 concurrent requests 401 at the same time, the client refreshes **once**, not 50 times. See [`SeismicOrchestrationClient`](/sdk/typescript/orchestration-client#refresh-on-401) for the implementation. ::: ### Signup ```ts twoslash // @filename: client.ts import { SeismicOrchestrationClient, memoryTokenStore } from "seismic-orchestration"; export const api = new SeismicOrchestrationClient({ baseUrl: "", getAccessToken: () => null }); // @filename: main.ts // ---cut--- import { api } from "./client"; const { user_id } = await api.auth.signup( "alice@example.com", "hunter22!hunter22", ); ``` | Status | Meaning | | ------ | ------------------------------------------------------------- | | 200 | `{ user_id }` — account created. No tokens yet; call `login`. | | 400 | Password shorter than 10 chars, or invalid email. | | 409 | Email already in use. | ### Login (password-only) ```ts twoslash // @filename: client.ts import { SeismicOrchestrationClient, memoryTokenStore } from "seismic-orchestration"; export const api = new SeismicOrchestrationClient({ baseUrl: "", getAccessToken: () => null }); // @filename: main.ts // ---cut--- import { api } from "./client"; const res = await api.auth.login("alice@example.com", "hunter22!hunter22"); switch (res.status) { case "tokens": // Straight to a token pair — MFA not enrolled or not required. localStorage.setItem("access", res.access_token); break; case "mfa_required": // Punt the challenge_id to your MFA screen. redirect(`/login/mfa?challenge=${res.challenge_id}`); break; } function redirect(_: string) {} ``` The return type is a Zod-parsed discriminated union, so TypeScript narrows both arms for you. ### MFA Two flows, both using TOTP: 1. **Enroll** (signed-in user, one-time): `mfaEnroll` → scan QR → `mfaActivate(code)`. 2. **Challenge** (every login after activation): `login` returns `mfa_required` → `mfaVerify(challenge_id, code)` → token pair. #### Enrollment ```ts twoslash // @filename: client.ts import { SeismicOrchestrationClient, memoryTokenStore } from "seismic-orchestration"; export const api = new SeismicOrchestrationClient({ baseUrl: "", getAccessToken: () => null }); // @filename: main.ts // ---cut--- import { api } from "./client"; // Returns an otpauth:// URI + the plaintext TOTP secret for display. const enrollment = await api.auth.mfa.enroll(); renderQr(enrollment.provisioning_uri); // Prompt the user to type the 6-digit code from their authenticator app. const code = await promptForCode(); await api.auth.mfa.activate(code); // 204 No Content → MFA is now required on future logins. declare function renderQr(uri: string): void; declare function promptForCode(): Promise; ``` #### Challenge ```ts twoslash // @filename: client.ts import { SeismicOrchestrationClient, memoryTokenStore } from "seismic-orchestration"; export const tokens = memoryTokenStore(); export const api = new SeismicOrchestrationClient({ baseUrl: "", getAccessToken: () => tokens.accessToken(), }); // @filename: main.ts declare const email: string; declare const password: string; declare function promptForCode(): Promise; // ---cut--- import { api, tokens } from "./client"; const res = await api.auth.login(email, password); if (res.status === "mfa_required") { const code = await promptForCode(); const pair = await api.auth.mfa.verify(res.challenge_id, code); tokens.save(pair); } ``` :::tip[No SMS, by design] SMS-based 2FA is **not** supported — SIM-swap risk makes it unsuitable for the account-takeover threat model. If you need an escape hatch for ops, set `ALLOW_MFA_BYPASS=true` in the server's sandbox env; never in production. ::: ### Refresh You generally don't call this directly — the SDK catches `401`s on protected paths and refreshes for you. But if you're managing tokens in a background job or another process: ```ts twoslash // @filename: client.ts import { SeismicOrchestrationClient, memoryTokenStore } from "seismic-orchestration"; export const api = new SeismicOrchestrationClient({ baseUrl: "", getAccessToken: () => null }); // @filename: main.ts declare const currentRefreshToken: string; // ---cut--- import { api } from "./client"; const pair = await api.auth.refresh(currentRefreshToken); // pair.access_token, pair.refresh_token (new, old one is dead) ``` ### Logout Revokes the refresh token server-side. The access token is still technically valid until it expires (stateless JWTs can't be revoked without a denylist), so don't rely on logout as a security boundary — keep access TTLs short and audit `refresh_tokens` when it matters. ```ts twoslash // @filename: client.ts import { SeismicOrchestrationClient, memoryTokenStore } from "seismic-orchestration"; export const tokens = memoryTokenStore(); export const api = new SeismicOrchestrationClient({ baseUrl: "", getAccessToken: () => null }); // @filename: main.ts // ---cut--- import { api, tokens } from "./client"; const rt = tokens.refreshToken(); if (rt) await api.auth.logout(rt); tokens.clear(); ``` ### Inspecting the current user Two endpoints, two shapes — know which one you want: | Endpoint | Shape | When to use | | ---------- | ------------------- | ----------------------------------------------------------- | | `/auth/me` | JWT-derived claims | Fast, stateless. Good for session bootstrapping. | | `/user` | Full profile + orgs | Needs a DB round-trip. Use for account screens, dashboards. | ```ts twoslash // @filename: client.ts import { SeismicOrchestrationClient } from "seismic-orchestration"; export const api = new SeismicOrchestrationClient({ baseUrl: "", getAccessToken: () => null }); // @filename: main.ts // ---cut--- import { api } from "./client"; const claims = await api.auth.me(); // { user_id, mfa_passed } const profile = await api.users.me(); // also email, memberships[], mfa_active, created_at ``` ## Getting started Seismic Orchestration is dashboard-first for setup, API-first for everything else. You'll spend a few minutes in the dashboard once — signing up, getting your business KYB-checked, and minting an API key — then drop the key into the TypeScript SDK (or any HTTP client) and you're moving money. ### 1. Sign up + create an org Head to [dashboard.seismic.systems/signup](https://dashboard.seismic.systems/signup) — email + password, MFA enroll, then create an org for your business. You become the org's `owner`. See [Authentication](/authentication) for the underlying flow. ### 2. Submit KYB In the dashboard, fill out the unified **KYB profile** for your org — one form; we handle verification behind the scenes. Quoting and end-user onboarding unlock once business verification reaches the stage your corridor needs. (You can mint API keys at any time — KYB doesn't gate that.) The full walkthrough is at [Onboarding your business](/guides/onboarding-business). The same surface is API-callable under [`/orgs`](/api/orgs) once you have a key. ### 3. Mint an API key Settings → API keys → **New key** in the dashboard. Pick `production` or `sandbox`, label it for your environment (e.g. `ci-pipeline`, `local-dev`), copy the secret — it's shown once. Drop it into your secret store. For programmatic minting (rotation, multi-environment provisioning) see [`POST /auth/api-keys`](/api/auth/api-keys/mint). ### 4. Install the client :::code-group ```bash [bun] bun add seismic-orchestration ``` ```bash [npm] npm install seismic-orchestration ``` ```bash [pnpm] pnpm add seismic-orchestration ``` ```bash [yarn] yarn add seismic-orchestration ``` ::: The package has a single runtime dependency (`zod`) and works in the browser, Node ≥ 18, Bun, Deno, and service workers. There's no build step — it's a source-only ESM package. ### 5. Pick a base URL | Environment | Base URL | Notes | | ----------- | --------------------------------------------------- | ------------------------------------------ | | Sandbox | `https://orchestration-sandbox.seismic.systems/api` | Hits provider sandboxes.
Free to use. | | Production | *Coming soon.* | Real provider credentials. | All API endpoints live under `/api/v0/*` (Bridge-compat at `/api/compat/bridge/v0/*`). ### 6. Create a client + make a call Pass your API key to one of the environment factories — `baseUrl` and the `Api-Key:` header wiring are filled in for you. ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox({ apiKey: process.env.SEISMIC_ORCHESTRATION_API_KEY!, }); const me = await api.users.me(); console.log(me.email, me.memberships.map((m) => m.org_name)); ``` That's it. From here:
* [**Onboarding your end users**](/guides/end-user-onboarding) — provision per-user customer rows on each provider, drive their KYC, attach payout accounts. * [**Requesting quotes**](/guides/first-quote) — the three-call happy path from price to order. * [**SeismicOrchestrationClient**](/sdk/typescript/orchestration-client) — the full SDK surface, every namespace, every method.
### Using access-token auth instead If you're wiring `seismic-orchestration` into a dashboard frontend rather than a server-to-server backend, you'll authenticate as the logged-in user via access tokens — not API keys. Skip the `apiKey` config and configure a `tokenStore` instead. See [Authentication](/authentication) for the full flow. ## SDKs Every SDK wraps the same REST surface documented under [API Reference](/api/auth) — shared naming (`SeismicOrchestrationClient`), method shapes, and error codes. ### Availability | Language | Package | Status | Docs | | ---------- | ----------------------------- | --------- | ----------------------------------------------- | | TypeScript | `seismic-orchestration` (npm) | Available | [TypeScript overview](/sdk/typescript/overview) | Need an SDK for Python, Go, Rust, Ruby, Java, or similar? Open an issue on [GitHub](https://github.com/SeismicSystems/orchestration/issues) with your use case. Porting the client mostly means writing the auth + request layer (including the single-flight refresh-on-401 dance) and translating Zod schemas into the target language's validation idiom. ### Install :::code-group ```bash [npm] npm install seismic-orchestration ``` ```bash [bun] bun add seismic-orchestration ``` ```bash [pnpm] pnpm add seismic-orchestration ``` ```bash [yarn] yarn add seismic-orchestration ``` ::: ### Why use an SDK over plain HTTP * **Single-flight refresh-on-401.** The client catches expired access tokens and hits `/auth/refresh` once per refresh window, no matter how many concurrent calls raced. You don't write that. * **Runtime validation.** Responses are parsed against schemas mirrored from the Rust DTOs, so a server-side shape drift surfaces as a typed error at the call-site rather than a silent `undefined`. * **Normalized request shapes.** One unified body for `customers.create`, one for `createAccount` — no bespoke blobs to assemble by hand. The SDK parses responses into typed entries. You can always drop to curl for one-off probes — the [API reference](/api/auth) shows a curl example next to every SDK snippet. ### Design principles These hold for every SDK we ship:
#### 1. The wire format is the source of truth Schemas mirror the Rust DTOs field-for-field. Breaking changes surface as a typed failure, not a silent mismatch. #### 2. No framework dependency in the core The client is pure runtime — no React, no Django, no Express. Hook/framework integrations live in separate packages (e.g. internal app packages that wrap this client with TanStack Query hooks). #### 3. Refresh-on-401 is built in Any protected call that 401s triggers a single-flight refresh before retry. Concurrent 401s share one refresh round-trip. You wire up a `TokenStore` once. #### 4. One normalized request Customer + account creation expose a single normalized body — the server completes the routing and verification work your corridor needs and returns typed envelopes you can rely on.
### What's next * [**TypeScript SDK**](/sdk/typescript/overview) — install, configure, full `SeismicOrchestrationClient` reference. * [**API Reference**](/api/auth) — REST surface, same semantics, curl examples alongside every endpoint. ## `api.currencies` List every currency we accept on at least one rail, with the rails that carry it. Unauthenticated. See [`GET /currencies`](/api/currencies) for the documented response shape; the `CurrencyRail` type in `seismic-orchestration` includes any extra wire fields. ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- const { currencies } = await api.currencies(); const usdc = currencies.find((c) => c.currency === "USDC"); const usdcOnBase = usdc?.rails.find((r) => r.rail === "base"); ``` **Arguments** — none. **Returns** — `Promise<{ currencies: `[`CurrencyInfo`](/api/currencies#currencyinfo)`[] }>`. REST endpoint: [`GET /currencies`](/api/currencies). ## `SeismicOrchestrationClient` The primary export of `seismic-orchestration`. One class covers auth, users, orgs, KYB, API keys, quotes, customers, and accounts. ### Construction Three environment factories cover the common path. All three accept the same optional config (everything except `baseUrl`). ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const sandbox = SeismicOrchestrationClient.sandbox({ apiKey: process.env.SEISMIC_ORCHESTRATION_API_KEY }); const prod = SeismicOrchestrationClient.production({ apiKey: process.env.SEISMIC_ORCHESTRATION_API_KEY }); const fromEnv = SeismicOrchestrationClient.fromEnv(); // reads SEISMIC_ORCHESTRATION_ENV ``` For private deployments or tests, use the direct constructor: ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = new SeismicOrchestrationClient({ baseUrl: "https://orchestration.internal.example", apiKey: "seismic_prod_…", }); ``` #### Authenticating The client supports both auth modes from the [API auth model](/api/auth): * **API key** — pass `apiKey: "seismic_prod_…"` (or a getter). Sent as `Api-Key: `. Recommended for server-to-server. * **Access token (dashboard)** — leave `apiKey` unset; configure a `tokenStore` and call `api.auth.login(...)`. Tokens persist via the store; refresh-on-401 is automatic. Setting both is harmless on the client (API key wins, `Authorization` is never sent), but the server explicitly returns `400 BothCredsProvided` for any request that arrives with both — so don't try to send them yourself. #### `fromEnv()` details `SeismicOrchestrationClient.fromEnv()` reads `process.env.SEISMIC_ORCHESTRATION_ENV`. It must be exactly `"production"` or `"sandbox"` — anything else throws immediately, loud and early: ``` Error: SEISMIC_ORCHESTRATION_ENV must be "production" or "sandbox"; got "staging" ``` This is intentional. A silently-defaulted client is the kind of thing that prints real invoices on sandbox creds, or vice versa. ### Config ```ts twoslash import type { SeismicOrchestrationClientConfig } from "seismic-orchestration"; ``` | Field | Type | Default | Purpose | | ----------------- | ---------------------------------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | | `baseUrl` | `string` | (required) | API root. Filled in by the environment factories. | | `apiKey` | `string \| (() => string \| null)` | — | Org-scoped API key. Sent as `Api-Key: `. When set, `tokenStore` and refresh are bypassed. | | `tokenStore` | `"memory"` \| `"localStorage"` \| `TokenStore` | `"memory"` | Where access + refresh tokens live (dashboard sessions). Ignored when `apiKey` is set. See [Token stores](/sdk/typescript/token-stores). | | `onRefreshFailed` | `() => void` | — | Fires when refresh 401s (token-auth path). The token store is cleared first. | | `fetch` | `typeof fetch` | `globalThis.fetch` | Custom fetch (tracing, mocks, workers). | Advanced overrides (replace the `tokenStore`-derived wiring entirely): `getAccessToken`, `getRefreshToken`, `onTokensRefreshed` — all `() => …` callbacks. Skip unless you're doing something exotic (httpOnly cookies, cross-process coordination). ### Refresh-on-401 ``` request ─► 401 on protected path? │ yes ▼ is refresh in flight? │ no ──────┼──► start /auth/refresh │ │ │ ├─► success → persist pair, retry original │ └─► failure → clear tokenStore, onRefreshFailed yes ─────┴──► await the in-flight refresh, then retry ``` Login / signup / refresh / logout paths are excluded from this — refreshing against them either loops or is meaningless. ### Orgs | Method | Endpoint | | ---------------------------------------------------------------------------------- | ------------------------------------------- | | [`api.orgs.list()`](/sdk/typescript/orgs/list) | `GET /orgs` | | [`api.orgs.get()`](/sdk/typescript/orgs/retrieve) | `GET /org` | | [`api.orgs.update(req)`](/sdk/typescript/orgs/update) | `PATCH /org` | | [`api.orgs.members.list()`](/sdk/typescript/orgs/members/list) | `GET /org/members` | | [`api.orgs.members.invite(req)`](/sdk/typescript/orgs/members/invite) | `POST /org/members` | | [`api.orgs.members.updateRole(userId, role)`](/sdk/typescript/orgs/members/update) | `PATCH /org/members/{user_id}` | | [`api.orgs.members.remove(userId)`](/sdk/typescript/orgs/members/remove) | `DELETE /org/members/{user_id}` | | [`api.orgs.kyb.profile()`](/sdk/typescript/orgs/kyb/profile-get) | `GET /org/kyb/profile` | | [`api.orgs.kyb.setProfile(profile)`](/sdk/typescript/orgs/kyb/profile-set) | `PUT /org/kyb/profile` | | [`api.orgs.kyb.submit()`](/sdk/typescript/orgs/kyb/submit) | `POST /org/kyb/submit` | | [`api.orgs.kyb.providers()`](/sdk/typescript/orgs/kyb/providers) | `GET /org/kyb/providers` | | [`api.orgs.kyb.refreshLink(provider)`](/sdk/typescript/orgs/kyb/refresh-link) | `POST /org/kyb/providers/{provider}/link` | | [`api.orgs.kyb.resync(provider)`](/sdk/typescript/orgs/kyb/resync) | `POST /org/kyb/providers/{provider}/resync` | ### API keys | Method | Endpoint | | ----------------------------------------------------------- | ---------------------------- | | [`api.apiKeys.mint(req)`](/sdk/typescript/api-keys/mint) | `POST /auth/api-keys` | | [`api.apiKeys.list()`](/sdk/typescript/api-keys/list) | `GET /auth/api-keys` | | [`api.apiKeys.revoke(id)`](/sdk/typescript/api-keys/revoke) | `DELETE /auth/api-keys/{id}` | ### Auth | Method | Endpoint | | ---------------------------------------------------------------------------- | ------------------------- | | [`api.auth.signup(email, password)`](/sdk/typescript/auth/signup) | `POST /auth/signup` | | [`api.auth.login(email, password)`](/sdk/typescript/auth/login) | `POST /auth/login` | | [`api.auth.mfa.enroll()`](/sdk/typescript/auth/mfa-enroll) | `POST /auth/mfa/enroll` | | [`api.auth.mfa.activate(code)`](/sdk/typescript/auth/mfa-activate) | `POST /auth/mfa/activate` | | [`api.auth.mfa.verify(challenge_id, code)`](/sdk/typescript/auth/mfa-verify) | `POST /auth/mfa/verify` | | [`api.auth.refresh(refresh_token)`](/sdk/typescript/auth/refresh) | `POST /auth/refresh` | | [`api.auth.logout(refresh_token?)`](/sdk/typescript/auth/logout) | `POST /auth/logout` | | [`api.auth.me()`](/sdk/typescript/auth/me) | `GET /auth/me` | `login`, `mfa.verify`, and `refresh` persist the returned pair to the configured `tokenStore` automatically — no `tokens.save(res)` boilerplate. `logout()` uses the refresh token from the store if you don't pass one, then clears local state. ### Users | Method | Endpoint | | ------------------------------------------------------ | ------------------ | | [`api.users.me()`](/sdk/typescript/users/me) | `GET /user` | | [`api.users.myOrders()`](/sdk/typescript/users/orders) | `GET /user/orders` | ### Quotes | Method | Endpoint | | -------------------------------------------------------------------- | --------------------------------- | | [`api.quotes.create(req, { wait? })`](/sdk/typescript/quotes/create) | `POST /quotes[?wait=1]` | | [`api.quotes.get(id)`](/sdk/typescript/quotes/retrieve) | `GET /quotes/:id` | | [`api.quotes.acceptBest(id)`](/sdk/typescript/quotes/accept) | `POST /quotes/:id/accept` (best) | | [`api.quotes.accept(id, quoteId)`](/sdk/typescript/quotes/accept) | `POST /quotes/:id/accept` (by id) | ### Customers | Method | Endpoint | | ------------------------------------------------------------------------------------------ | ------------------------------ | | [`api.customers.create(req)`](/sdk/typescript/customers/create) | `POST /customers` | | [`api.customers.get(id)`](/sdk/typescript/customers/retrieve) | `GET /customers/:id` | | [`api.customers.refreshKycLinks(id, providers?)`](/sdk/typescript/customers/kyc-link) | `POST /customers/:id/kyc-link` | | [`api.customers.createAccount(customerId, req)`](/sdk/typescript/customers/create-account) | `POST /customers/:id/accounts` | ### Catalog | Method | Endpoint | | ------------------------------------------------ | ----------------- | | [`api.rails()`](/sdk/typescript/rails) | `GET /rails` | | [`api.currencies()`](/sdk/typescript/currencies) | `GET /currencies` | ### Health | Method | Endpoint | | -------------- | ------------- | | `api.health()` | `GET /health` | ### Errors Every method rejects with `ApiError` on non-2xx responses: ```ts twoslash import { SeismicOrchestrationClient, ApiError } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); try { await api.auth.login("alice@example.com", "wrong"); } catch (err) { if (err instanceof ApiError) { console.log(err.status); // 401 console.log(err.message); // "invalid credentials" } } ``` For the status-code landscape, see [API → Errors](/api/errors). ## TypeScript SDK **`seismic-orchestration`** is the official TypeScript client for the Seismic Orchestration REST API. It's the same code our own frontend runs — every feature documented here is dogfooded. ### Install :::code-group ```bash [bun] bun add seismic-orchestration ``` ```bash [npm] npm install seismic-orchestration ``` ```bash [pnpm] pnpm add seismic-orchestration ``` ```bash [yarn] yarn add seismic-orchestration ``` ::: Single runtime dependency (`zod`). Runs on Node ≥ 18, Bun, Deno, browsers, and service workers. Ships dual ESM + CJS; TS consumers get types for both `import` and `require`. ### Exports | Name | Kind | Purpose | | ---------------------------- | --------- | ----------------------------------------------------------------------------------------------- | | `SeismicOrchestrationClient` | class | The typed API client. See [`SeismicOrchestrationClient`](/sdk/typescript/orchestration-client). | | `ApiError` | class | Thrown for every non-2xx response. | | `localStorageTokenStore()` | factory | Browser-backed token store. | | `memoryTokenStore()` | factory | In-memory token store — Node, SSR, workers. | | `TokenStore` | interface | Shape you implement for bespoke storage. | | `*Schema` (Zod) | value | The Zod schemas used internally. Re-use if you want. | Response types (`LoginResponse`, `TokenPair`, `AsyncQuoteSnapshot`, `Customer`, …) are also exported. ### Quick-start ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); const res = await api.auth.login("alice@example.com", "hunter22!hunter22"); // Tokens are already persisted to the configured store. ``` Three environment factories cover the common shapes: ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; SeismicOrchestrationClient.sandbox(); // hits https://orchestration-sandbox.seismic.systems/api SeismicOrchestrationClient.production(); // hits https://orchestration.seismic.systems/api SeismicOrchestrationClient.fromEnv(); // reads SEISMIC_ORCHESTRATION_ENV ``` Need persistence across browser reloads? Opt in — the SDK won't touch `localStorage` otherwise: ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox({ tokenStore: "localStorage" }); ``` ### What's next * [`SeismicOrchestrationClient`](/sdk/typescript/orchestration-client) — every method, typed. * [Token stores](/sdk/typescript/token-stores) — the built-ins + how to roll your own. * [SDKs overview](/sdk/overview) — the language-agnostic view. ## `api.rails` List every settlement rail we support. Unauthenticated — calls work with or without an API key. See [`GET /rails`](/api/rails) for the documented response shape; the `RailInfo` type in `seismic-orchestration` includes any extra wire fields. ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- const { rails } = await api.rails(); const cryptoRails = rails.filter((r) => r.kind === "crypto"); ``` **Arguments** — none. **Returns** — `Promise<{ rails: `[`RailInfo`](/api/rails#railinfo)`[] }>`. REST endpoint: [`GET /rails`](/api/rails). ## Token stores The `tokenStore` config field decides where the client keeps tokens. Three shapes are accepted: | Value | When to use | | ---------------- | ------------------------------------------------------------ | | `"memory"` | Default. Node scripts, SSR, tests, short-lived workers. | | `"localStorage"` | Browser SPAs that want session survival across page reloads. | | A `TokenStore` | Cookies, SQLite, Cloudflare KV, or any bespoke backend. | Default is `"memory"` — the SDK never writes to browser storage without explicit consent. ### Memory (default) ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // tokenStore: "memory" const api2 = SeismicOrchestrationClient.sandbox({ tokenStore: "memory" }); ``` Tokens live in the client instance's closure. When the process exits (or the tab navigates away), they're gone. Fine for tests, CLI tools, and any flow where you log in once per run. ### localStorage ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox({ tokenStore: "localStorage" }); ``` Persists `access_token`, `refresh_token`, `access_expires_at`, `refresh_expires_at` under the `orchestration.*` key prefix. Survives page reloads and tab restores. :::warning[XSS exposure] `localStorage` is readable by any script on the origin. If your app loads third-party scripts you don't fully trust, pair this with a strict CSP or roll a custom httpOnly-cookie store. ::: ### Custom stores Implement the `TokenStore` interface and pass an instance: ```ts twoslash import { SeismicOrchestrationClient, type TokenStore } from "seismic-orchestration"; // Example: sessionStorage — cleared when the tab closes. function sessionTokenStore(): TokenStore { return { save(p) { sessionStorage.setItem("access", p.access_token); sessionStorage.setItem("refresh", p.refresh_token); sessionStorage.setItem("exp", p.access_expires_at); sessionStorage.setItem("rexp", p.refresh_expires_at); }, clear() { sessionStorage.clear(); }, accessToken: () => sessionStorage.getItem("access"), refreshToken: () => sessionStorage.getItem("refresh"), isAuthenticated: () => sessionStorage.getItem("access") !== null, }; } const api = SeismicOrchestrationClient.sandbox({ tokenStore: sessionTokenStore() }); ``` ### Seeding from existing tokens For machine users whose tokens live in env vars, build a memory store and hand it in: ```ts twoslash import { SeismicOrchestrationClient, memoryTokenStore } from "seismic-orchestration"; declare const env: { SEISMIC_ORCHESTRATION_ACCESS_TOKEN: string; SEISMIC_ORCHESTRATION_REFRESH_TOKEN: string; }; // ---cut--- const api = SeismicOrchestrationClient.production({ tokenStore: memoryTokenStore({ access_token: env.SEISMIC_ORCHESTRATION_ACCESS_TOKEN, refresh_token: env.SEISMIC_ORCHESTRATION_REFRESH_TOKEN, }), }); ``` ### Choosing | Scenario | Recommended | | -------------------------------------- | ----------------------------------------- | | Node CLI / script / cron | `"memory"` (default) or seeded from env | | Server-rendered app | `"memory"` per request + cookie plumbing | | SPA, low sensitivity | `"localStorage"` | | SPA, high sensitivity (banking, admin) | Custom httpOnly-cookie store + strict CSP | | Browser extension background worker | Custom — wrap `chrome.storage.session` | | Tests | `"memory"` or `memoryTokenStore({ … })` | ## `api.users.me` Full profile for the user behind the credential — row, org memberships, MFA state. For the lightweight JWT-only version, see [`auth.me`](/sdk/typescript/auth/me). ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- const me = await api.users.me(); for (const m of me.memberships) { console.log(`${m.role} at ${m.org_name}`); } ``` **Arguments** — none. **Returns** — `Promise` — fields documented under [`GET /user`](/api/users/retrieve). REST endpoint: [`GET /user`](/api/users/retrieve). ## `api.users.myOrders` Most-recent-first list of orders the authenticated user has kicked off, capped at 200. ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- const orders = await api.users.myOrders(); ``` **Arguments** — none. **Returns** — `Promise<`[`RampOrder`](/api/users/orders#ramporder)`[]>`. REST endpoint: [`GET /user/orders`](/api/users/orders). ## `api.quotes.accept` & `api.quotes.acceptBest` Two methods, one endpoint. Use `acceptBest` when you trust the server's ranking; use `accept` to pin a specific quote (e.g. the one the user picked in your UI). ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- // Accept the server's best pick. await api.quotes.acceptBest("9d8c…"); // Or pick a specific entry by its id. await api.quotes.accept("9d8c…", "q_9e4d…"); ``` ### `acceptBest(id)` | Argument | Type | | | -------- | ------ | ------------ | | `id` | string | Snapshot id. | ### `accept(id, quoteId)` | Argument | Type | | | --------- | ------ | -------------------------------------------------- | | `id` | string | | | `quoteId` | string | Must be one of the quote ids in `snapshot.quotes`. | **Returns** — `Promise` — includes `order_id`; see `seismic-orchestration` for the full type (extra wire fields omitted in REST samples on the accept page). **Throws** — `ApiError`. See the [accept endpoint](/api/quotes/accept) for the full code enum (`QuoteIdInvalid`, `QuoteNotFound`, `AlreadyAccepted`, `QuoteNotReady`, `QuoteExpired`). REST endpoint: [`POST /quotes/:id/accept`](/api/quotes/accept). ## `api.quotes.create` Create a quote request. We price the corridor and return a ranked snapshot — see the [Quotes overview](/api/quotes) for directionality, rails, and fee invariants. Pass `{ wait: true }` to block until the snapshot is ready; default is `false`, which returns a snapshot id you can poll with [`quotes.get`](/sdk/typescript/quotes/retrieve). ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- const quotes = await api.quotes.create( { amount: "1000", source: { currency: "USD" }, destination: { currency: "USDC", rail: "base" }, }, { wait: true }, ); ``` | Argument | Type | Notes | | ------------ | -------------- | ------------------------------------------------------ | | `req` | `QuoteRequest` | Body shape — see [`POST /quotes`](/api/quotes/create). | | `opts.wait?` | boolean | `true` → server blocks until the snapshot is ready. | **Returns** — `Promise`. **Throws** — `ApiError` with `status: 400` (invalid request) or `status: 422` (nothing priced this corridor right now). REST endpoint: [`POST /quotes`](/api/quotes/create). ## `api.quotes.get` Re-read a snapshot by id. Useful when [`quotes.create`](/sdk/typescript/quotes/create) was called without `wait: true` and returned a pending snapshot — poll until `status === "ready"`. ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- const snapshot = await api.quotes.get("9d8c…"); ``` | Argument | Type | | -------- | ------ | | `id` | string | **Returns** — `Promise`. **Throws** — `ApiError` with `status: 404` (unknown id, or not yours). REST endpoint: [`GET /quotes/:id`](/api/quotes/retrieve). ## `api.orgs.list` List orgs the calling user is a member of. Read-only — works with both API key and access-token auth. ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- const memberships = await api.orgs.list(); for (const { org, role } of memberships) { console.log(`${role} at ${org.name}`); } ``` **Arguments** — none. **Returns** — `Promise<{ org: Org; role: OrgRole }[]>`. REST endpoint: [`GET /orgs`](/api/orgs/list). ## `api.orgs.get` Read the org bound to the current credential — for an API key, that's the key's permanent scope. ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- const org = await api.orgs.get(); console.log(org.name); const approved = org.providers.filter((p) => p.kyb_status === "approved"); ``` **Arguments** — none. **Returns** — `Promise`. REST endpoint: [`GET /org`](/api/orgs/retrieve). ## `api.orgs.update` Update the current org's mutable fields. Admin/owner only. ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- const org = await api.orgs.update({ name: "Acme International Inc." }); ``` | Argument | Type | Notes | | -------- | ------------------- | ------------------------------------------------------------------------------------ | | `req` | `{ name?: string }` | Only fields you send are applied; `id`, `providers`, and timestamps aren't editable. | **Returns** — `Promise`. **Throws** — `ApiError` `400` (invalid name), `403` (not an admin/owner). REST endpoint: [`PATCH /org`](/api/orgs/update). ## `api.orgs.members.invite` Invite a user by email. Admin/owner only. ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- const member = await api.orgs.members.invite({ email: "carol@acme.com", role: "member", }); ``` | Argument | Type | Notes | | -------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `req` | `{ email: string; role?: "admin" \| "member" \| "viewer" }` | Defaults to `"member"`. Cannot invite as `"owner"` — promote via `updateRole` instead. Admins can only invite as `"member"` or `"viewer"`. | **Returns** — `Promise`. **Throws** — `ApiError` `400` (bad email/role), `403` (not allowed at this role), `409` (already a member). REST endpoint: [`POST /org/members`](/api/orgs/members/invite). ## `api.orgs.members.list` List members of the current org with their roles. ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- const members = await api.orgs.members.list(); ``` **Arguments** — none. **Returns** — `Promise`. REST endpoint: [`GET /org/members`](/api/orgs/members/list). ## `api.orgs.members.remove` Remove a member from the current org. Admin/owner only. **Soft delete** — the membership row is preserved server-side with a `deleted_at` tombstone, but the user loses access immediately and stops appearing in member listings. ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); declare const userId: string; // ---cut--- await api.orgs.members.remove(userId); ``` | Argument | Type | | -------- | -------- | | `userId` | `string` | **Returns** — `Promise`. **Throws** — `ApiError` `403` (not admin/owner), `404` (not a member), `409 LastOwner` (would leave the org with zero owners). REST endpoint: [`DELETE /org/members/{user_id}`](/api/orgs/members/remove). ## `api.orgs.members.updateRole` Change a member's role. ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); declare const userId: string; // ---cut--- const member = await api.orgs.members.updateRole(userId, "admin"); ``` | Argument | Type | | -------- | --------- | | `userId` | `string` | | `role` | `OrgRole` | **Returns** — `Promise`. **Throws** — `ApiError` `403` (not allowed to assign that role), `404` (not a member), `409 LastOwner` (would leave the org with zero owners). REST endpoint: [`PATCH /org/members/{user_id}`](/api/orgs/members/update). ## `api.orgs.kyb.profile` Read the unified KYB profile saved for the current org. Throws `ApiError` 404 if no profile has been saved yet. ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- const profile = await api.orgs.kyb.profile(); console.log(profile.business.legal_name); ``` **Arguments** — none. **Returns** — `Promise` — see [`KybProfile`](/api/orgs/kyb#kyb-profile) for the field-by-field schema. **Throws** — `ApiError` `404` (no profile saved yet). REST endpoint: [`GET /org/kyb/profile`](/api/orgs/kyb/profile-get). ## `api.orgs.kyb.setProfile` Replace the org's KYB profile with a complete document. Doesn't submit — call [`submit`](/sdk/typescript/orgs/kyb/submit) when ready. Admin/owner only. ```ts twoslash import { SeismicOrchestrationClient, type KybProfile } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); declare const profile: KybProfile; // ---cut--- const saved = await api.orgs.kyb.setProfile(profile); ``` | Argument | Type | | --------- | ------------ | | `profile` | `KybProfile` | **Returns** — `Promise` — the canonicalized saved profile. **Throws** — `ApiError` `400` (schema invalid), `403` (not admin/owner), `422 KybProfileIncomplete` (required fields missing). REST endpoint: [`PUT /org/kyb/profile`](/api/orgs/kyb/profile-set). ## `api.orgs.kyb.providers` KYB verification rows for the current org. Returns cached state from `org_provider_state`. ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- const states = await api.orgs.kyb.providers(); const approved = states.filter((s) => s.kyb_status === "approved"); ``` **Arguments** — none. **Returns** — `Promise`. See [`OrgProviderState`](/api/orgs/kyb#orgproviderstate). REST endpoint: [`GET /org/kyb/providers`](/api/orgs/kyb/providers). ## `api.orgs.kyb.refreshLink` Re-issue the hosted verification URL for a KYB row. Rows that do not use hosted flows return an error when hosted links are not supported. ```ts import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- declare const routingId: string; const link = await api.orgs.kyb.refreshLink(routingId); window.location.href = link.url; ``` | Argument | Type | | | ---------- | ------ | ---------------------------------------------------------- | | `provider` | string | Opaque routing id (historical parameter name on the wire). | **Returns** — `Promise<{ url: string; expires_at?: string }>`. **Throws** — `ApiError` `403` (not admin/owner), `404` (no submission yet), `422` (hosted links not supported for this row). REST endpoint: [`POST /org/kyb/providers/{provider}/link`](/api/orgs/kyb/refresh-link). ## `api.orgs.kyb.resync` Pull the latest KYB status for a verification row on demand. Use after a hosted session for instant feedback, or when a webhook was lost. ```ts import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- declare const routingId: string; const fresh = await api.orgs.kyb.resync(routingId); console.log(fresh.kyb_status, fresh.last_synced_at); ``` | Argument | Type | | | ---------- | ------ | ---------------------------------------------------------- | | `provider` | string | Opaque routing id (historical parameter name on the wire). | **Returns** — `Promise`. **Throws** — `ApiError` `403` (not admin/owner), `404` (no submission), `502 ProviderError` (temporary upstream error; cached state unchanged). REST endpoint: [`POST /org/kyb/providers/{provider}/resync`](/api/orgs/kyb/resync). ## `api.orgs.kyb.submit` Submit the saved KYB profile to the platform. Idempotent when there is nothing new to send. Admin/owner only. ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- const states = await api.orgs.kyb.submit(); for (const s of states) { console.log(s.kyb_status); if (s.kyb_link) console.log(` -> ${s.kyb_link.url}`); } ``` **Arguments** — none. **Returns** — `Promise` — verification rows after submit. **Throws** — `ApiError` `403` (not admin/owner), `422 KybProfileIncomplete`. REST endpoint: [`POST /org/kyb/submit`](/api/orgs/kyb/submit). ## `api.customers.createAccount` Attach a funding / payout account to a customer. Unlike customer creation, this is single-provider — bank accounts and wallets are tied to one provider's verification and settlement integration. Pass `provider` to pick which one; the customer must already be provisioned at that provider. The wire format requires `currencies` and `rails` as arrays — the client lets you pass either a **single string** or an **array** for each, and normalizes before sending. Use the single-string form when there's only one option; reach for the array form when an account legitimately holds multiple tokens or spans multiple rails. ```ts import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- // US bank — single currency, two rails const ach = await api.customers.createAccount("2f10…", { provider: "prv_…", account_type: "us_bank_account", currencies: "USD", rails: ["ach", "wire"], routing_number: "021000021", account_number: "0123456789", account_holder_name: "Acme Inc.", }); // USDC wallet — single token, single chain const wallet = await api.customers.createAccount("2f10…", { provider: "prv_…", account_type: "external_digital_asset_wallet", currencies: "USDC", rails: "base", address: "0xabc…", }); // EVM wallet — multiple tokens, multiple chains const evm = await api.customers.createAccount("2f10…", { provider: "prv_…", account_type: "external_digital_asset_wallet", currencies: ["USDC", "USDT"], rails: ["base", "ethereum"], address: "0xabc…", }); ``` | Argument | Type | Notes | | ------------ | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `customerId` | string | Customer id. | | `req` | `CreateAccountInput` | Body shape — `currencies` / `rails` accept `string \| string[]`. See [`POST /customers/:id/accounts`](/api/customers/create-account) for the full field table by `account_type`. | **Returns** — `Promise` (see [`Account`](/api/customers#account)). **Throws** — `ApiError` with `status: 400` (rail not allowed, missing required field, provider not attached to customer), `403`, or `404`. REST endpoint: [`POST /customers/:id/accounts`](/api/customers/create-account). ## `api.customers.create` Create an end-user customer. We persist a local row so every subsequent call uses our UUID. Returns the normalized [`Customer`](/api/customers#customer) plus verification and routing rows the platform attaches for this request. ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- const { customer } = await api.customers.create({ external_id: "user_abc123", type: "individual", email: "alice@example.com", first_name: "Alice", last_name: "Liddell", date_of_birth: "1990-04-12", address: { line1: "123 Main St", city: "Brooklyn", state: "NY", postal_code: "11201", country: "US", }, }); const { links } = await api.customers.refreshKycLinks(customer.id); const link = links.find((l) => l.link)?.link; if (link) window.location.href = link.url; ``` | Argument | Type | Notes | | -------- | ----------------------- | ------------------------------------------------------------------------------------- | | `req` | `CreateCustomerRequest` | Body shape — see [`POST /customers`](/api/customers/create) for the full field table. | **Returns** — `Promise` — see [`POST /customers`](/api/customers/create#response-200). **Throws** — `ApiError` with `status: 400` (missing required field; the `missing` array on the error body lists which), `409` (an existing customer already has this `(org, external_id)` pair), or `422` (this corridor cannot be fulfilled with the data supplied). REST endpoint: [`POST /customers`](/api/customers/create). ## `api.customers.refreshKycLinks` Refresh hosted verification URLs for the customer. Useful when the user closed the tab, the link expired, or the user needs to update submitted information. Send an empty body through the REST API (`{}`) to refresh every incomplete link; the TypeScript client omits the optional second argument for the same behavior. ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- const { links } = await api.customers.refreshKycLinks("2f10…"); const next = links.find((l) => l.link)?.link; if (next) window.location.href = next.url; ``` | Argument | Type | Notes | | ----------- | ----------- | --------------------------------------------------------------------------------------------- | | `id` | string | Customer id. | | `providers` | `string[]`? | Optional filter — see the `seismic-orchestration` package types if your integration needs it. | **Returns** — `Promise` — `{ links: … }` with `status` and hosted `link` on each entry. **Throws** — `ApiError` with `status: 404` (unknown customer id, or the filter references ids we do not have on file). REST endpoint: [`POST /customers/:id/kyc-link`](/api/customers/kyc-link). ## `api.customers.listAccounts` List every account attached to a single end-user customer. For an org-wide view, see [`api.accounts.list`](/sdk/typescript/accounts/list). Filters accept either a single string or an array — the client normalizes to the wire format. Use the single-string form when filtering by one option; reach for the array form when matching multiple. ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- const all = await api.customers.listAccounts("2f10…"); // USDC-capable, active only — single-string filter. const usdc = await api.customers.listAccounts("2f10…", { currencies: "USDC", status: "active", }); // Multi-rail filter — keep wallets reachable on Base or Ethereum. const evm = await api.customers.listAccounts("2f10…", { rails: ["base", "ethereum"], }); ``` **Arguments** | Param | Type | Notes | | ------------ | ------------------------------------------ | ---------------------------------------------- | | `customerId` | string | The customer UUID. | | `currencies` | string \| string\[]? | Keep accounts whose `currencies[]` intersects. | | `rails` | string \| string\[]? | Keep accounts whose `rails[]` intersects. | | `status` | `"pending"` \| `"active"` \| `"disabled"`? | Match exactly. | **Returns** — `Promise<`[`Account`](/api/customers#account)`[]>`. REST endpoint: [`GET /customers/:id/accounts`](/api/customers/list-accounts). ## `api.customers.orders` Org-wide feed of orders across every end-user customer the org has onboarded, capped at 200. For a single user's history, see [`api.users.myOrders`](/sdk/typescript/users/orders) instead. ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- const all = await api.customers.orders(); // Filter to a single customer. const justThisOne = await api.customers.orders({ customer_id: "cus_7c4b…", }); // Filter by direction + status. const completedOnRamps = await api.customers.orders({ direction: "on_ramp", status: "completed", }); ``` **Arguments** | Param | Type | Notes | | ------------- | ----------------------------------------------- | ------------------------------------ | | `customer_id` | string? | Filter to a single customer. | | `direction` | `"on_ramp"` \| `"off_ramp"`? | Filter by ramp direction. | | `status` | [`OrderStatus`](/api/users/orders#orderstatus)? | One of the canonical order statuses. | **Returns** — `Promise<`[`RampOrder`](/api/users/orders#ramporder)`[]>`. REST endpoint: [`GET /customers/orders`](/api/customers/orders). ## `api.customers.get` Read the normalized customer plus the latest verification and routing snapshot we attach to the response. ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- const snapshot = await api.customers.get("2f10…"); for (const row of snapshot.providers) { console.log(row.kyc.status); } ``` | Argument | Type | | -------- | ------ | | `id` | string | **Returns** — `Promise` — same shape as [`api.customers.create`](/sdk/typescript/customers/create). **Throws** — `ApiError` with `status: 403` (customer belongs to a different org) or `status: 404` (unknown id). REST endpoint: [`GET /customers/:id`](/api/customers/retrieve). ## `api.auth.login` Exchange credentials for a `TokenPair`. If the account has MFA active, the response is an `mfa_required` challenge instead — feed it to [`auth.mfa.verify`](/sdk/typescript/auth/mfa-verify). When the response is `tokens`, the pair is **automatically persisted** to the configured `tokenStore` — no `tokens.save(res)` boilerplate. ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- const res = await api.auth.login("alice@example.com", "hunter22!hunter22"); if (res.status === "tokens") { // Tokens are already in the store — fire requests. } else { // res.challenge_id → punt to the MFA verify step. } ``` | Argument | Type | | ---------- | ------ | | `email` | string | | `password` | string | **Returns** — `Promise` (discriminated union, see [Auth → Types](/api/auth#types)). **Throws** — `ApiError` with `status: 401` on bad credentials. REST endpoint: [`POST /auth/login`](/api/auth/login). ## `api.auth.logout` Revoke a refresh token server-side and clear the configured `tokenStore`. If you don't pass a token, the client uses the one currently in the store. ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- await api.auth.logout(); // uses the stored refresh token await api.auth.logout("rt_…"); // or pass an explicit one ``` | Argument | Type | Notes | | ---------------- | ------ | ---------------------------------------------- | | `refresh_token?` | string | Optional — defaults to the value in the store. | **Returns** — `Promise`. REST endpoint: [`POST /auth/logout`](/api/auth/logout). ## `api.auth.me` Lightweight identity claims pulled from the access token. Doesn't hit the database — for the full profile (email, memberships, created\_at), call [`users.me`](/sdk/typescript/users/me). ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- const claims = await api.auth.me(); // { user_id, mfa_passed } ``` **Arguments** — none. **Returns** — `Promise<{ user_id: string; mfa_passed: boolean }>`. REST endpoint: [`GET /auth/me`](/api/auth/me). ## `api.auth.mfa.activate` Confirm enrollment by providing the first valid TOTP code. After this, MFA is required on future logins. ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- await api.auth.mfa.activate("123456"); ``` | Argument | Type | | -------- | ------ | | `code` | string | **Returns** — `Promise` (the underlying endpoint returns `204`). REST endpoint: [`POST /auth/mfa/activate`](/api/auth/mfa-activate). ## `api.auth.mfa.enroll` Start TOTP enrollment. Returns a provisioning URI you can render as a QR (or surface `secret_base32` for manual entry). Confirm with [`auth.mfa.activate`](/sdk/typescript/auth/mfa-activate). ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- const enrollment = await api.auth.mfa.enroll(); // enrollment.provisioning_uri → render as QR // enrollment.secret_base32 → fallback for manual entry ``` **Arguments** — none. **Returns** — `Promise<{ provisioning_uri: string; secret_base32: string }>`. REST endpoint: [`POST /auth/mfa/enroll`](/api/auth/mfa-enroll). ## `api.auth.mfa.verify` Redeem the `challenge_id` from a `mfa_required` login response, plus the user's current TOTP code, for a `TokenPair`. Persists the pair to the configured `tokenStore` automatically. ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- const pair = await api.auth.mfa.verify("b4bd…", "123456"); ``` | Argument | Type | | -------------- | ------ | | `challenge_id` | string | | `code` | string | **Returns** — `Promise` (see [Auth → Types](/api/auth#types)). **Throws** — `ApiError` with `status: 401` on wrong code, expired challenge, or clock skew. REST endpoint: [`POST /auth/mfa/verify`](/api/auth/mfa-verify). ## `api.auth.refresh` Rotate a refresh token into a new `TokenPair`. The old refresh token is invalidated server-side. Persists the new pair to the configured `tokenStore` automatically. :::info[You usually don't call this directly] The client runs single-flight refresh-on-401 internally — see [Refresh-on-401](/sdk/typescript/orchestration-client#refresh-on-401). Reach for this method only when managing tokens outside the client (background jobs, cross-process handoff). ::: ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- const pair = await api.auth.refresh("rt_…"); ``` | Argument | Type | | --------------- | ------ | | `refresh_token` | string | **Returns** — `Promise`. **Throws** — `ApiError` with `status: 401` on revoked or expired refresh tokens. REST endpoint: [`POST /auth/refresh`](/api/auth/refresh). ## `api.auth.signup` Create a new account. Does **not** return tokens — call [`auth.login`](/sdk/typescript/auth/login) next. ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- const { user_id } = await api.auth.signup("alice@example.com", "hunter22!hunter22"); ``` | Argument | Type | | ---------- | ------ | | `email` | string | | `password` | string | **Returns** — `Promise<{ user_id: string }>`. **Throws** — `ApiError` with `status: 400` (password \< 10 chars, malformed email) or `status: 409` (email exists). REST endpoint: [`POST /auth/signup`](/api/auth/signup). ## `api.apiKeys.list` List all API keys for the current org, including revoked ones. Secrets are never included — just metadata. ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- const keys = await api.apiKeys.list(); const live = keys.filter( (k) => !k.revoked_at && k.environment === "production", ); ``` **Arguments** — none. **Returns** — `Promise`. REST endpoint: [`GET /auth/api-keys`](/api/auth/api-keys/list). ## `api.apiKeys.mint` Mint a new API key for the current org. Any member can mint. The plaintext `secret` is returned **exactly once** — store it immediately. ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- const key = await api.apiKeys.mint({ label: "ci-pipeline" }); console.log(key.secret); // "seismic_prod_…" — display, copy, then drop the variable. ``` | Argument | Type | | -------- | ------------------------------------------------------------ | | `req` | `{ label: string; environment?: "production" \| "sandbox" }` | **Returns** — `Promise` — the key metadata plus the plaintext `secret`. **Throws** — `ApiError` `400` (invalid label), `400 BothCredsProvided`. REST endpoint: [`POST /auth/api-keys`](/api/auth/api-keys/mint). ## `api.apiKeys.revoke` Revoke an API key. Effective immediately — in-flight requests using the key get `401 ApiKeyRevoked` on the next hit. ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); declare const keyId: string; // ---cut--- await api.apiKeys.revoke(keyId); ``` | Argument | Type | | -------- | -------- | | `id` | `string` | **Returns** — `Promise`. **Throws** — `ApiError` `404` (unknown id), `409 AlreadyRevoked`. REST endpoint: [`DELETE /auth/api-keys/{id}`](/api/auth/api-keys/revoke). ## `api.accounts.list` List every account the org has attached across all of its end-user customers. Optionally filter to a single customer or status. `currencies` and `rails` filters accept either a single string or an array — the client normalizes to the wire format. ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- const all = await api.accounts.list(); // Filter to a single customer. const justThisOnes = await api.accounts.list({ customer_id: "2f10…", }); // Only active USDC-capable accounts. const liveUsdc = await api.accounts.list({ currencies: "USDC", status: "active", }); ``` **Arguments** | Param | Type | Notes | | ------------- | ------------------------------------------ | ---------------------------------------------- | | `customer_id` | string? | Filter to a single customer. | | `currencies` | string \| string\[]? | Keep accounts whose `currencies[]` intersects. | | `rails` | string \| string\[]? | Keep accounts whose `rails[]` intersects. | | `status` | `"pending"` \| `"active"` \| `"disabled"`? | Filter by account status. | **Returns** — `Promise<`[`Account`](/api/customers#account)`[]>`. REST endpoint: [`GET /accounts`](/api/accounts/list). ## `api.accounts.get` Read a single account by its UUID. ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // ---cut--- const account = await api.accounts.get("5e3a…"); // Disambiguate a quote against this account if it's multi-currency. if (account.currencies.length > 1) { const snapshot = await api.quotes.create({ amount: "200", source: { account: account.id, currency: "USDC", rail: "base" }, destination: { currency: "USD", rail: "ach" }, }); } ``` **Arguments** — `id: string`. **Returns** — `Promise<`[`Account`](/api/customers#account)`>`. REST endpoint: [`GET /accounts/:id`](/api/accounts/retrieve). ## Accepting a quote Once you have an `AsyncQuoteSnapshot` with `status === "ready"`, accept it to kick off an order. The server re-validates the chosen leg, creates a `ramp_orders` row, and dispatches settlement for that rail path. ### Best default No payload = "take the winner the server picked": ```ts twoslash // @filename: client.ts import { SeismicOrchestrationClient } from "seismic-orchestration"; export const api = new SeismicOrchestrationClient({ baseUrl: "", getAccessToken: () => null }); // @filename: main.ts declare const snapshotId: string; // ---cut--- import { api } from "./client"; const result = await api.quotes.acceptBest(snapshotId); // result.order_id — see `AcceptQuoteResult` in seismic-orchestration for any extra fields. ``` ### Picking a specific quote When your UI lets the user pick a specific option from `snapshot.quotes[]`, pass that entry's `id` to `accept`: ```ts twoslash // @filename: client.ts import { SeismicOrchestrationClient } from "seismic-orchestration"; export const api = new SeismicOrchestrationClient({ baseUrl: "", getAccessToken: () => null }); // @filename: main.ts declare const snapshotId: string; // ---cut--- import { api } from "./client"; // `quoteId` is `snapshot.quotes[n].id` — pins one priced rail leg from the snapshot. await api.quotes.accept(snapshotId, "q_9e4d…"); ``` There is no shorthand for picking a leg without its id. The same **inbound rail** (for example ACH) can appear on multiple ranked rows with different economics; silently picking one would surprise the caller. Pass the exact `quote_id` your UI showed. The server returns **400** if `quoteId` isn't in the snapshot. ### Re-reading an order There's no dedicated `GET /orders/:id` yet — fetch the full list via `api.users.myOrders()` and filter client-side: ```ts twoslash // @filename: client.ts import { SeismicOrchestrationClient } from "seismic-orchestration"; export const api = new SeismicOrchestrationClient({ baseUrl: "", getAccessToken: () => null }); // @filename: main.ts // ---cut--- import { api } from "./client"; const orders = await api.users.myOrders(); // ^? // Returns up to 200 rows, most-recent first. ``` :::tip[Orders are append-only] An order's lifecycle (pending → submitted → settled / failed) updates the same row — you don't create a new row per state transition. Poll, or consume webhooks in a backend. ::: ### Idempotency Accepting the **same snapshot twice** returns **409 Conflict** — the first accept attaches the snapshot to an order and further accepts would create duplicates. If you need retry-safety around flaky network conditions, store `snapshot.id` alongside your request before calling `accept` — on retry, read the snapshot back and check whether it's already associated with an order. ### End-to-end example ```ts twoslash import { SeismicOrchestrationClient, ApiError } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); async function buyUsdc(amountUsd: string, destinationAccountId: string) { const quotes = await api.quotes.create( { amount: amountUsd, source: { currency: "USD", rail: "ach" }, destination: destinationAccountId, }, { wait: true }, ); if (quotes.quotes.length === 0) { throw new Error("nothing priced this corridor"); } try { return await api.quotes.acceptBest(quotes.id); } catch (err) { if (err instanceof ApiError && err.status === 409) { // Already accepted — treat as success. return null; } throw err; } } ``` ## Onboarding your end users Use this when **your product** onboards individual end users — a fintech app onboarding a consumer, a payroll product onboarding a contractor — and you need a per-user `customer` row so we can move funds on their behalf. If you're trying to onboard your own business (the org that holds the API key), you're in the wrong place — see [Onboarding your business](/guides/onboarding-business) instead. ### Prerequisites * Your org has completed business verification in the dashboard. * You've minted an API key from the dashboard. ### Three-step onboarding 1. **Create** the customer with `api.customers.create(...)` — we persist a Seismic-scoped row and handle verification work the corridor requires. 2. **Verification** — when we return a hosted URL, send the user there; otherwise verification may continue inline (for example from `government_id` on create) or over email. Poll `api.customers.get(id)` or call `api.customers.refreshKycLinks(id)` when you need a fresh link. 3. **Attach an account** with `api.customers.createAccount(id, ...)` — a bank account, IBAN, UPI VPA, or on-chain wallet. Pass the opaque routing id we returned for that customer (wire key `provider`). :::info[Our UUID, not internal ids] The `create` response includes a normalized `customer.id` (UUID). Use that for all subsequent calls. Other ids on the envelope are for Seismic routing — do not plumb them through your product UI or treat them as end-user identifiers. ::: ### Create a customer ```ts twoslash // @filename: client.ts import { SeismicOrchestrationClient } from "seismic-orchestration"; export const api = new SeismicOrchestrationClient({ baseUrl: "", getAccessToken: () => null }); // @filename: main.ts // ---cut--- import { api } from "./client"; const { customer } = await api.customers.create({ external_id: "user_abc123", type: "individual", email: "alice@example.com", first_name: "Alice", last_name: "Liddell", date_of_birth: "1990-04-12", address: { line1: "123 Main St", city: "Brooklyn", state: "NY", postal_code: "11201", country: "US", }, }); const customerId = customer.id; const { links } = await api.customers.refreshKycLinks(customerId); const next = links.find((l) => l.link)?.link; if (next) window.location.href = next.url; ``` :::warning[`UNIQUE(org_id, external_id)`] You can have at most one customer per `(org, external_id)` pair. Re-calling `create` with the same `external_id` returns **409 Conflict** with the existing `id` in the message — handle that by reading the row back with `api.customers.get(id)`. ::: ### Track verification Poll `api.customers.get(customerId)` until verification reaches **`approved`** on every row we return for that customer — then attach accounts and request quotes. ```ts twoslash // @filename: client.ts import { SeismicOrchestrationClient } from "seismic-orchestration"; export const api = new SeismicOrchestrationClient({ baseUrl: "", getAccessToken: () => null }); // @filename: main.ts declare const customerId: string; // ---cut--- import { api } from "./client"; const snapshot = await api.customers.get(customerId); const rows = snapshot.providers; const ready = rows.every((r) => r.kyc.status === "approved"); void ready; ``` `kyc.status` values, in order of forward motion: ``` not_started → submitted → under_review → approved ↘ rejected ``` If you need a fresh URL (the user closed the tab, the link expired, etc.): ```ts twoslash // @filename: client.ts import { SeismicOrchestrationClient } from "seismic-orchestration"; export const api = new SeismicOrchestrationClient({ baseUrl: "", getAccessToken: () => null }); // @filename: main.ts declare const customerId: string; // ---cut--- import { api } from "./client"; const { links } = await api.customers.refreshKycLinks(customerId); const next = links.find((l) => l.link)?.link; if (next) window.location.href = next.url; ``` ### Attach an account A customer needs at least one account to move money in or out. Pass the routing id we returned for that customer (wire key `provider`). :::code-group ```ts [Bank (US)] import { api } from "./client"; declare const customerId: string; declare const routingId: string; const account = await api.customers.createAccount(customerId, { provider: routingId, account_type: "us_bank_account", currencies: "USD", rails: ["ach", "wire"], routing_number: "021000021", account_number: "0123456789", account_holder_name: "Alice Liddell", }); ``` ```ts [USDC wallet] import { api } from "./client"; declare const customerId: string; declare const routingId: string; const account = await api.customers.createAccount(customerId, { provider: routingId, account_type: "external_digital_asset_wallet", currencies: "USDC", rails: "base", // only "base" | "polygon" are accepted for USDC address: "0xabc…", }); ``` ```ts [UPI (India)] import { api } from "./client"; declare const customerId: string; declare const routingId: string; const account = await api.customers.createAccount(customerId, { provider: routingId, account_type: "upi", currencies: "INR", rails: "upi", vpa: "alice@okicici", }); ``` ::: :::tip[`currencies` / `rails` are arrays on the wire] The wire format is always arrays. The TS client accepts a single string (`currencies: "USDC"`) or an array (`currencies: ["USDC", "USDT"]`) and normalizes — pick whichever reads better at the call site. ::: :::tip[Rail whitelist] For USDC wallets the server currently restricts `rails` to **`base`** or **`polygon`**. Other rails return 400. Swap-target rail support is tracked in the catalog — see [`GET /rails`](/api/rails) and [`GET /currencies`](/api/currencies) for the live list. ::: ### Tying it all together ```ts import { api } from "./client"; async function onboard(userEmail: string, externalId: string) { const { customer } = await api.customers.create({ external_id: externalId, type: "individual", email: userEmail, first_name: "Alice", last_name: "Liddell", }); const { links } = await api.customers.refreshKycLinks(customer.id); const hosted = links.find((l) => l.link)?.link; if (hosted) window.location.href = hosted.url; const snapshot = await api.customers.get(customer.id); const row = snapshot.providers.find((r) => r.kyc.status === "approved"); if (row) { await api.customers.createAccount(customer.id, { provider: row.provider, account_type: "external_digital_asset_wallet", currencies: "USDC", rails: "base", address: "0xabc…", }); } return customer.id; } ``` ## Requesting quotes In Seismic Orchestration, a quote snapshot is a **ranked comparison** across every **priced rail leg** for a given (source → destination, amount) trade — each row is identified by `in.rail` / `out.rail` plus a unique `id`. You get back a snapshot id, a list of quotes, and a pre-computed `best_quote_id`. Accept to turn a quote into an order. ### The whole flow in one snippet ```ts twoslash import { SeismicOrchestrationClient } from "seismic-orchestration"; const api = SeismicOrchestrationClient.sandbox(); // (Assume you've already logged in and MFA'd.) // 1. Price the corridor. wait=true blocks until the snapshot is ready. // Omit `rail` on the source to price every USD-in rail we support. const quotes = await api.quotes.create( { amount: "1000", source: { currency: "USD" }, destination: { currency: "USDC", rail: "base" }, }, { wait: true }, ); // 2. (Optional) Show the user the ranked list — compare rails and totals. for (const q of quotes.quotes) { console.log( `${q.in.rail.padEnd(8)} → ${q.out.rail.padEnd(8)} ` + `${q.out.amount} ${q.out.currency} rate=${q.rate} ` + `fee=${q.fees.total} ${q.fees.currency}`, ); } // 3. Accept the best pick; use `.accept(snapshotId, quoteId)` to override. const order = await api.quotes.acceptBest(quotes.id); // ^? ``` ### Sync vs. async: `wait=1` `create` has two modes, controlled by the `wait` option. :::code-group ```ts [Sync: wait=true] const quotes = await api.quotes.create(req, { wait: true }); // quotes.status === "ready" — pricing finished for this snapshot. ``` ```ts [Async: default] const quotes = await api.quotes.create(req); // quotes.status === "pending" — poll quotes.id. const result = await poll(() => api.quotes.get(quotes.id), (s) => s.status === "ready"); async function poll( read: () => Promise, done: (v: T) => boolean, ): Promise { while (true) { const v = await read(); if (done(v)) return v; await new Promise((r) => setTimeout(r, 500)); } } ``` ::: **When to use which:** | Mode | Pros | Cons | | --------------- | --------------------------------------------- | ------------------------------------------------------ | | `wait: true` | One network round-trip; simpler callsite. | Ties up a server connection; slowest priced path wins. | | Async (default) | Client can render partial data as it arrives. | Extra polls; you manage the UI state machine. | For a customer-facing quote preview, async lets you show partial results (for example "2 of 3 priced paths ready…") while the snapshot is still filling in. For a server-to-server backfill, `wait=1` is simpler. ### Picking a quote manually `acceptBest(snapshotId)` takes the server's recommendation (`best_quote_id`). To honor a user who picked a specific option — e.g. from a list your UI rendered — use `accept` with that quote's `id`: ```ts twoslash // @filename: client.ts import { SeismicOrchestrationClient } from "seismic-orchestration"; export const api = new SeismicOrchestrationClient({ baseUrl: "", getAccessToken: () => null }); // @filename: main.ts declare const snapshotId: string; // ---cut--- import { api } from "./client"; // The `quoteId` comes from `snapshot.quotes[n].id` — pins one priced rail leg from the snapshot. await api.quotes.accept(snapshotId, "q_9e4d…"); ``` There is no shorthand for disambiguating legs — if pricing produced multiple rows for the same **inbound rail** (for example ACH vs wire), silently picking one would surprise the caller. Pass the exact id the UI showed. The server returns **400** if `quote_id` isn't in the snapshot. ### Error handling ```ts twoslash // @filename: client.ts import { SeismicOrchestrationClient, ApiError } from "seismic-orchestration"; export const api = new SeismicOrchestrationClient({ baseUrl: "", getAccessToken: () => null }); // @filename: main.ts // ---cut--- import { api } from "./client"; import { ApiError } from "seismic-orchestration"; try { await api.quotes.create( { amount: "1000", source: { currency: "USD" }, destination: { currency: "USDC", rail: "base" }, }, { wait: true }, ); } catch (err) { if (err instanceof ApiError && err.status === 422) { // Nothing priced this corridor right now. } else { throw err; } } ``` The full error taxonomy is at [API reference → Errors](/api/errors). ### What's next * [**Onboarding your end users**](/guides/end-user-onboarding) — provision per-user customer rows before you execute against fiat accounts. * [**API reference → Quotes**](/api/quotes) — the underlying REST surface and every field. ## Onboarding your business Before you can call the Seismic Orchestration API for live money movement, we need to verify your business once. This guide walks the **dashboard-first** flow: log in to [dashboard.seismic.systems](https://dashboard.seismic.systems), submit one unified KYB form, and we handle verification behind the scenes. The same surface is exposed under [`/orgs`](/api/orgs) for automation, but you can't call it without an API key, and you can't mint a key until your org is set up — chicken-and-egg, so this guide stays in the dashboard. ### Step 1 — Sign up Create an individual account at [dashboard.seismic.systems/signup](https://dashboard.seismic.systems/signup) with email + password, then enable MFA. (See [Authentication](/authentication) for the underlying flow.) ### Step 2 — Create an org The dashboard's first-run experience asks you to create an org with a display name (e.g. "Acme Inc."). You become the org's `owner` automatically. :::tip[Multi-org setups] If you already belong to another org and want a separate sandbox, create a new org from the org switcher (top-left). All resources — keys, customers, KYB profile, quotes — are isolated per org. ::: ### Step 3 — Submit the unified KYB profile The KYB form collects the **superset** of fields **the platform's requirements** need — company details, addresses, beneficial owners, jurisdiction and funds-movement context. Fill it out once; we map it automatically. The form is split into the same subsections you'll see on the [API reference](/api/orgs/kyb#kyb-profile): * Business identity (legal name, entity type, tax id, formation date) * Addresses (registered + optional physical) * Contact * Operations (jurisdictions, source of funds, regulated status) * Volumes (monthly transaction buckets — used for risk profiling) * Counterparties * Associated persons (UBOs, control persons, signers) Save partial progress at any time; the dashboard keeps drafts client-side. When you click **Submit**, we send `PUT /org/kyb/profile` followed by `POST /org/kyb/submit`, and the org enters the `submitted` state. :::warning[Required fields] Some fields apply only to certain corridors, but we still collect them up front so the org does not get blocked when your product expands. The dashboard highlights what is load-bearing for **the platform's requirements**. ::: ### Step 4 — Finish hosted verification when prompted After submit, we may finish checks programmatically or send you to a hosted session for ID verification and document upload. The dashboard **KYB status** panel shows: * Current status — `submitted`, `under_review`, `approved`, `rejected`. * A "Continue KYB" button when there's a hosted link to follow. * A "Resync" button if you just finished a session and want immediate feedback. This panel is backed by [`GET /org/kyb/providers`](/api/orgs/kyb/providers); the buttons call [refresh-link](/api/orgs/kyb/refresh-link) and [resync](/api/orgs/kyb/resync) under the hood. ### Step 5 — Mint an API key API keys aren't gated on KYB — you can mint one as soon as the org exists. The key works for read-only and key-management endpoints right away; quoting, customer creation, and account creation start working once business verification reaches the stage your corridor requires. Settings → **API keys** in the dashboard. Mint a key — you'll see the secret exactly once; copy it into your secret store. From here you can: * Onboard your end users — [Onboarding your end users](/guides/end-user-onboarding). * Get your first quote — [Requesting quotes](/guides/first-quote). * Continue managing the org via API — for example, polling KYB status: ```bash curl "$SEISMIC_ORCHESTRATION_URL/v0/org/kyb/providers" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` The TypeScript SDK exposes the same call as `await api.orgs.kyb.providers()`. ### What's next * [API reference → Orgs](/api/orgs) — the full surface, for when you want to script org management. * [API reference → KYB](/api/orgs/kyb) — every field in the unified profile. * [API keys](/api/auth/api-keys) — rotate, revoke, list keys programmatically. ## Bridge compatibility Already built on Bridge? For the endpoints we implement, paths, field names, auth header, error envelope, and pagination all follow Bridge's conventions — the goal is that a base-URL swap gets most integrations that we support too running without code changes. This is best-effort, not a contract: if you hit an inconsistency that isn't already called out, or want an endpoint we haven't implemented, [open an issue](https://github.com/SeismicSystems/orchestration/issues) and we'll take a look. We don't use Bridge as a provider — we just know they're popular and have a nice API, so mirroring their shape lowers the switching cost for teams already integrated with them. :::tip[TL;DR] ```diff - https://api.bridge.xyz/v0 + https://orchestration.seismic.systems/api/compat/bridge/v0 ``` [What we cover](#whats-implemented) should mostly work after the base-URL swap. Everything else returns `501 not_implemented` — see [Not implemented](/bridge-compat/not-implemented). ::: ### Base URLs | Environment | Base URL | | ----------- | -------------------------------------------------------------------- | | Sandbox | `https://orchestration-sandbox.seismic.systems/api/compat/bridge/v0` | | Production | *Coming soon.* | The trailing `/v0` mirrors Bridge's own path versioning. ### Authentication Bridge-style `Api-Key` header. No `Bearer` prefix, no refresh dance. ```bash curl https://orchestration.seismic.systems/api/compat/bridge/v0/customers \ -H "Api-Key: seismic_sandbox_…" ``` Throughout the Bridge-compat pages we assume two environment variables in the curl snippets: ```bash export BASE_URL="https://orchestration-sandbox.seismic.systems/api/compat/bridge/v0" export SEISMIC_ORCHESTRATION_API_KEY="seismic_sandbox_…" ``` The same `SEISMIC_ORCHESTRATION_API_KEY` works against the [native API](/api/auth) — one key, two surfaces. Missing or invalid keys return `401` in Bridge's exact shape: ```json { "code": "required", "location": "header", "name": "Api-Key", "message": "Missing Api-Key header" } ``` ### Errors Bridge's envelope isn't uniform across endpoints; we mirror their shape per endpoint. The common cases: **Most 4xx** — flat `code` / `message`, optional `source`: ```json { "code": "bad_customer_request", "message": "fields missing from customer body.", "source": { "location": "body", "key": ["first_name", "ssn"] } } ``` **Auth errors (401)** — add `location` + `name`: ```json { "code": "invalid", "location": "header", "name": "Api-Key", "message": "Invalid Api-Key header" } ``` **5xx unexpected** — nested under `errors[]`: ```json { "errors": [ { "code": "unexpected", "message": "An unexpected error occurred, you may try again later" } ] } ``` Branch on `code` when you need programmatic handling. Treat an unknown code at a given status as a generic failure of that HTTP class. ### What's implemented The compat layer covers the endpoints our native API already supports. If your Bridge integration is scoped to KYC + fiat rails, you're covered. Every Bridge endpoint not listed here returns `501 not_implemented` — see [Not implemented](/bridge-compat/not-implemented) for the exhaustive list. **[Customers](/bridge-compat/customers)** * [`POST /customers`](/bridge-compat/customers/create) * [`GET /customers`](/bridge-compat/customers/list) * [`GET /customers/{id}`](/bridge-compat/customers/retrieve) * [`PUT /customers/{id}`](/bridge-compat/customers/update) * [`DELETE /customers/{id}`](/bridge-compat/customers/delete) * [`GET /customers/{id}/transfers`](/bridge-compat/customers/list-transfers) * [`GET /customers/{id}/kyc_link`](/bridge-compat/customers/kyc-link) * [TOS Links](/bridge-compat/customers/tos-links) * [`POST /customers/tos_links`](/bridge-compat/customers/tos-links#post-customerstos_links) * [`POST /customers/tos_acceptance_link`](/bridge-compat/customers/tos-links#post-customerstos_acceptance_link) (alias) * [`GET /customers/{id}/tos_acceptance_link`](/bridge-compat/customers/tos-links#get-customersidtos_acceptance_link) **[Associated Persons](/bridge-compat/associated-persons)** * [`GET /associated_persons/{id}`](/bridge-compat/associated-persons/retrieve) * [`PUT /associated_persons/{id}`](/bridge-compat/associated-persons/update) * [`DELETE /associated_persons/{id}`](/bridge-compat/associated-persons/delete) * [`POST /customers/{id}/associated_persons`](/bridge-compat/associated-persons/create) * [`GET /customers/{id}/associated_persons`](/bridge-compat/associated-persons/list) * `GET /customers/{customer-id}/associated_persons/{id}` (alias) * `PUT /customers/{customer-id}/associated_persons/{id}` (alias) * `DELETE /customers/{customer-id}/associated_persons/{id}` (alias) **[KYC Links](/bridge-compat/kyc-links)** * [`POST /kyc_links`](/bridge-compat/kyc-links/create) * [`GET /kyc_links`](/bridge-compat/kyc-links/list) * [`GET /kyc_links/{id}`](/bridge-compat/kyc-links/retrieve) **[External Accounts](/bridge-compat/external-accounts)** * [`GET /external_accounts`](/bridge-compat/external-accounts/list) * [`GET /external_accounts/{id}`](/bridge-compat/external-accounts/retrieve) * [`PUT /external_accounts/{id}`](/bridge-compat/external-accounts/update) * [`DELETE /external_accounts/{id}`](/bridge-compat/external-accounts/delete) * [`POST /external_accounts/{id}/reactivate`](/bridge-compat/external-accounts/reactivate) * [`POST /customers/{id}/external_accounts`](/bridge-compat/external-accounts/create) * [`GET /customers/{id}/external_accounts`](/bridge-compat/external-accounts/list-for-customer) **[Transfers](/bridge-compat/transfers)** * [`POST /transfers`](/bridge-compat/transfers/create) * [`GET /transfers`](/bridge-compat/transfers/list) * [`GET /transfers/{id}`](/bridge-compat/transfers/retrieve) * [`PUT /transfers/{id}`](/bridge-compat/transfers/update) * [`DELETE /transfers/{id}`](/bridge-compat/transfers/delete) **[Exchange Rates](/bridge-compat/exchange-rates)** * [`GET /exchange_rates`](/bridge-compat/exchange-rates/retrieve) **[Shared schemas](/bridge-compat/schemas)** — field definitions reused across endpoints. ### No SDK — bring your own HTTP client Bridge doesn't ship an official SDK, so the compat layer doesn't either. Use whatever you use against `api.bridge.xyz` today — `fetch`, `axios`, a generated OpenAPI client, whatever. If you're starting a new integration, we recommend the [native `SeismicOrchestrationClient`](/sdk/typescript/overview) instead — richer quote snapshots, richer fee breakdowns, typed error codes, actual TypeScript types. ### Versioning policy * The `v0` in the path is **Bridge's version**, not ours. When Bridge ships a breaking change to `v0` we'll do our best to keep up. * Additive Bridge changes (new optional fields, new enum values) flow through — we don't strip unknown fields. ### Feedback This is a best-effort surface, not a guarantee of full parity. If you run into: * A field we accept but silently handle differently than Bridge would * An endpoint we return `501` on that you need * An error shape that doesn't match what your existing Bridge code expects …[open an issue](https://github.com/SeismicSystems/orchestration/issues) with the request body (remember to redact secrets) and what you expected. We prioritize based on what real Bridge integrations actually hit. ## Not implemented Every Bridge endpoint below returns **`501 not_implemented`** when hit on `/api/compat/bridge/v0`. The response body mirrors Bridge's generic error envelope: ```json { "code": "not_implemented", "message": "This endpoint is not implemented by the Seismic Bridge compat layer." } ``` Implemented endpoints are on [Overview](/bridge-compat/intro). If something here is a blocker for your integration, [open an issue](https://github.com/SeismicSystems/orchestration/issues) on GitHub. ### Batch Settlements * `POST /batch_settlements` ### Bridge Wallets Bridge's custodial wallet product has no analogue in our surface. * `POST /bridge_wallets` * `GET /bridge_wallets` * `GET /bridge_wallets/{id}` * `GET /bridge_wallets/balances` * `GET /bridge_wallets/{id}/transactions` * `GET /customers/{id}/bridge_wallets` ### Cards We don't run a card program. * `POST /cards` * `GET /cards/{id}` * `PUT /cards/{id}` * `POST /cards/{id}/freeze` * `POST /cards/{id}/unfreeze` * `POST /cards/{id}/deposit_addresses` * `GET /cards/{id}/controls` * `GET /cards/designs` * `GET /cards/program` * `GET /customers/{id}/cards` * `POST /cards/withdrawal_requests` * `GET /cards/withdrawals` * `GET /cards/withdrawals/{id}` * `GET /cards/transactions` * `GET /cards/transactions/{id}` * `GET /cards/authorizations/pending` * `POST /cards/mobile_wallet_provisioning` * `POST /cards/pin_update_url` * `POST /cards/ephemeral_keys` * `POST /cards/statements` * `POST /cards/statements_stripe` ### Crypto Return Policies Our providers handle bounced/returned crypto sends internally, so we don't expose a configurable policy resource. * `POST /crypto_return_policies` * `GET /crypto_return_policies` * `PUT /crypto_return_policies/{id}` * `DELETE /crypto_return_policies/{id}` ### Customers — static templates Bridge's reusable transfer-template feature; depends on `/static_templates` as a separate resource, which we don't implement. * `GET /customers/{id}/static_templates` ### Developers We don't expose a tenant-level fee-configuration endpoint through the compat layer. Developer fees are supported as the per-transfer `developer_fee` field. * `POST /developers/fee_external_account` * `GET /developers/fee_external_account` * `GET /developers/fees` * `PUT /developers/fees` ### Fiat Payout Configuration * `GET /customers/{id}/fiat_payout_config` * `PUT /customers/{id}/fiat_payout_config` ### Funds Requests * `GET /funds_requests` ### Liquidation Addresses Bridge's crypto-to-fiat auto-liquidation primitive is not mirrored. * `POST /liquidation_addresses` * `GET /liquidation_addresses` * `GET /liquidation_addresses/{id}` * `PUT /liquidation_addresses/{id}` * `GET /liquidation_addresses/{id}/drains` * `GET /liquidation_addresses/{id}/balance` (Bridge has deprecated this) * `GET /liquidation_addresses/activity` * `GET /customers/{id}/liquidation_addresses` ### Lists * `GET /lists/countries` * `GET /lists/occupations` ### Plaid * `POST /plaid/link_token` * `POST /plaid/token_exchange` ### Prefunded Accounts * `GET /prefunded_accounts` * `GET /prefunded_accounts/{id}` * `GET /prefunded_accounts/{id}/funding_history` ### Rewards * `GET /rewards/summary/{stablecoin}` * `GET /customers/{id}/rewards/summary` * `GET /customers/{id}/rewards/history` ### Static Memos * `POST /static_memos` * `GET /static_memos` * `GET /static_memos/{id}` * `PUT /static_memos/{id}` * `GET /static_memos/{id}/activity` * `GET /static_memos/activity` * `GET /customers/{id}/static_memos` ### Transfers — templates Core transfers are mirrored. The templates endpoint isn't: * `GET /transfers/templates` ### Virtual Accounts Bridge's named IBAN / virtual-account pay-in product is not mirrored. * `POST /virtual_accounts` * `GET /virtual_accounts` * `GET /virtual_accounts/{id}` * `PUT /virtual_accounts/{id}` * `POST /virtual_accounts/{id}/deactivate` * `POST /virtual_accounts/{id}/reactivate` * `GET /virtual_accounts/{id}/activity` * `GET /virtual_accounts/activity` * `GET /customers/{id}/virtual_accounts` ### Webhooks Webhooks are table-stakes for a transfer API; we plan to ship them before GA but they are **not** available via the compat layer yet. * `POST /webhooks` * `GET /webhooks` * `PUT /webhooks/{id}` * `DELETE /webhooks/{id}` * `GET /webhooks/{id}/logs` * `GET /webhooks/{id}/upcoming_events` * `POST /webhooks/{id}/events/send` * `GET /webhooks/events` ## `POST /transfers` ### Request — minimal ```json { "on_behalf_of": "cust_01HZ…", "amount": "75.00", "source": { "currency": "usd", "payment_rail": "ach_push", "external_account_id": "ea_01HZ…" }, "destination": { "currency": "usdc", "payment_rail": "base", "to_address": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F" } } ``` ### Request — every option Same shape with every optional field set, so you can see exactly where each one lives in the body. `dry_run: true` returns the priced response without creating the transfer — perfect for a "review" screen before the user confirms. ```json { "on_behalf_of": "cust_01HZ…", "amount": "75.00", "developer_fee": "0.50", "client_reference_id": "order_9af2", "dry_run": true, "source": { "currency": "usd", "payment_rail": "ach_push", "external_account_id": "ea_01HZ…" }, "destination": { "currency": "usdc", "payment_rail": "base", "to_address": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F", "wire_message": "SEISMIC-TX-01HZ" }, "features": { "flexible_amount": false, "static_template": false, "allow_any_from_address": false }, "return_instructions": { "address": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F", "memo": "refund-tx-01HZ" } } ``` #### Flexible amount — pin the output instead of the input Set `features.flexible_amount: true`, drop the top-level `amount`, and put the target amount on `destination.amount`. The transfer prices backwards from output to input, so the `source_deposit_instructions.amount` is what the customer ends up funding. ```json { "on_behalf_of": "cust_01HZ…", "source": { "currency": "usd", "payment_rail": "ach_push", "external_account_id": "ea_01HZ…" }, "destination": { "currency": "usdc", "payment_rail": "base", "to_address": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F", "amount": "74.45" }, "features": { "flexible_amount": true } } ``` `amount` (top-level, source-pinned) and `destination.amount` (output-pinned) are mutually exclusive — pick one. ### Top-level fields | Field | Type | Required | Notes | | --------------------- | ------- | -------- | ---------------------------------------------------------------------- | | `on_behalf_of` | string | yes | Customer id. | | `amount` | string | yes | Source-currency denominated. String to avoid float drift. | | `source` | object | yes | See below. | | `destination` | object | yes | See below. | | `developer_fee` | string | no | Flat fee you take, in the source currency. Deducted from `out.amount`. | | `client_reference_id` | string | no | Your id — echoed on the response and all webhooks. | | `dry_run` | boolean | no | Returns a quote without creating the transfer. Useful for pricing UI. | | `features` | object | no | `flexible_amount`, `static_template`, `allow_any_from_address`. | | `return_instructions` | object | no | `{ address, memo? }` — where funds go on irrecoverable failures. | ### `source` / `destination` | Field | Type | Notes | | --------------------- | ------ | ------------------------------------------------------------------------------------ | | `currency` | string | See [supported currencies](/bridge-compat/transfers#currencies). | | `payment_rail` | string | See [rails](/bridge-compat/transfers#payment-rails). Required on at least one side. | | `external_account_id` | string | Bank-side — id from [External Accounts](/bridge-compat/external-accounts). | | `from_address` | string | Crypto-side — funding wallet. | | `to_address` | string | Crypto-side — destination wallet. | | `amount` | string | Destination-only — pins the **output** amount (Bridge: "flexible amount" transfers). | | `wire_message` | string | Destination-only — memo on the payout. | ### Response (201) ```json { "id": "tx_01HZ…", "client_reference_id": "order_9af2", "amount": "75.00", "currency": "usd", "on_behalf_of": "cust_01HZ…", "developer_fee": "0.50", "state": "awaiting_funds", "source": { "currency": "usd", "payment_rail": "ach_push", "external_account_id": "ea_01HZ…" }, "destination": { "currency": "usdc", "payment_rail": "base", "to_address": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F", "amount": "74.45" }, "source_deposit_instructions": { "payment_rail": "ach_push", "amount": "75.00", "currency": "usd", "bank_name": "Cross River Bank", "bank_routing_number": "021214891", "bank_account_number": "7438291011", "bank_beneficiary_name": "Seismic Systems Inc.", "deposit_message": "SEISMIC-TX-01HZ" }, "receipt": { "initial_amount": "75.00", "developer_fee": "0.50", "exchange_fee": "0.05", "subtotal_amount": "74.45", "gas_fee": "0.00", "final_amount": "74.45", "exchange_rate": "1.00" }, "created_at": "2026-04-24T12:00:00Z", "updated_at": "2026-04-24T12:00:00Z" } ``` ```bash curl -X POST "$BASE_URL/transfers" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "on_behalf_of": "cust_01HZ…", "amount": "75.00", "source": { "currency": "usd", "payment_rail": "ach_push", "external_account_id": "ea_01HZ…" }, "destination": { "currency": "usdc", "payment_rail": "base", "to_address": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F" } }' ``` Follow `source_deposit_instructions` in the response to fund the transfer, then poll [`GET /transfers/{id}`](/bridge-compat/transfers/retrieve) for state transitions. ## `DELETE /transfers/{id}` Cancel a transfer. Valid only in `awaiting_funds`. Once funds have been received the transfer must play out — use `return_instructions` to steer it back. Returns `204 No Content` on success. ```bash curl -X DELETE "$BASE_URL/transfers/tx_01HZ…" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ## Transfers Bridge's `/transfers` resource. The single most load-bearing endpoint — this is where onramps, offramps, and wallet-to-wallet transfers all live. :::info[How the compat layer maps this] Our native API splits this into a two-step flow: [`POST /quotes`](/api/quotes/create) (price discovery for the corridor) and [`POST /quotes/{id}/accept`](/api/quotes/accept) (settle the chosen quote). `POST /transfers` collapses both — we run pricing server-side and immediately accept the best result, so callers get a single synchronous response matching Bridge's shape. **Implication:** the returned `receipt.exchange_rate` and `receipt.*_fee` reflect the specific quote we chose, not a best-case. For explicit quote selection, use the native API. ::: ### Endpoints * [`POST /transfers`](/bridge-compat/transfers/create) — create a transfer. Returns funding instructions. * [`GET /transfers`](/bridge-compat/transfers/list) — list transfers with filter + pagination. * [`GET /transfers/{id}`](/bridge-compat/transfers/retrieve) — fetch a single transfer. * [`PUT /transfers/{id}`](/bridge-compat/transfers/update) — partial update, valid only while `awaiting_funds`. * [`DELETE /transfers/{id}`](/bridge-compat/transfers/delete) — cancel. Valid only in `awaiting_funds`. ### Shared reference #### Currencies **Supported** * Fiat: `brl`, `cop`, `eur`, `gbp`, `mxn`, `usd`. * Stablecoin: `usdc`. **Not supported** — `dai`, `eurc`, `pyusd`, `usdb`, `usdt`. Bridge lists these; we don't settle them today. Transfers referencing an unsupported currency return `422 unsupported_currency`. We're USDC-focused for stablecoins — if you need another, [open an issue](https://github.com/SeismicSystems/orchestration/issues) and we'll weigh it. #### Payment rails **Fiat:** `ach`, `ach_push`, `ach_same_day`, `wire`, `swift`, `sepa`, `faster_payments`, `pix`, `spei`, `bre_b`, `co_bank_transfer`. **Crypto (supported):** `base`, `ethereum`, `arbitrum`, `optimism`, `polygon`, `solana`, `avalanche_c_chain`, `celo`, `stellar`, `tron`, `tempo`. **Crypto (not supported):** `bridge_wallet` — Bridge's custodial wallet product has no analogue here; transfers referencing `bridge_wallet` return `422 unsupported_rail`. See [Bridge Wallets in Not implemented](/bridge-compat/not-implemented#bridge-wallets). #### States ``` awaiting_funds → funds_received → payment_submitted → payment_processed ↘ in_review ↗ ↘ undeliverable | returned → refund_in_flight → refunded ↘ refund_failed awaiting_funds → canceled ``` ## `GET /transfers` ### Query | Param | Type | Default | Notes | | ------------------- | ------- | ------- | ---------------------- | | `limit` | integer | `10` | | | `starting_after` | string | — | | | `ending_before` | string | — | | | `tx_hash` | string | — | | | `updated_after_ms` | integer | — | | | `updated_before_ms` | integer | — | | | `template_id` | string | — | | | `state` | string | — | | | `on_behalf_of` | string | — | Filter by customer id. | ```bash curl "$BASE_URL/transfers?state=payment_submitted&limit=100" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ### Response Array of [transfer objects](/bridge-compat/transfers/create#response-201). ## `GET /transfers/{id}` Fetch a single transfer. Response shape matches [the transfer object](/bridge-compat/transfers/create#response-201). ```bash curl "$BASE_URL/transfers/tx_01HZ…" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ## `PUT /transfers/{id}` Only valid while `state === "awaiting_funds"`. Mutable fields: `client_reference_id`, `return_instructions`. Attempting to change `amount`, `source`, or `destination` after creation returns `422 immutable_field`. ### Response Returns the updated [transfer object](/bridge-compat/transfers/create#response-201). ```bash curl -X PUT "$BASE_URL/transfers/tx_01HZ…" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "client_reference_id": "order_9af2_retry" }' ``` ## `Address` Used everywhere a physical location is required — residential addresses on individuals, registered / physical addresses on businesses, addresses on external accounts. | Field | Type | Required | Notes | | --------------- | -------------------------------------- | ----------- | ------------------------------------------------------------------ | | `street_line_1` | string (≥4) | yes | | | `street_line_2` | string (≥1) | no | | | `city` | string (≥1) | yes | | | `subdivision` | string (ISO 3166-2, e.g. `US-CA`) | conditional | Required for US and other countries that use subdivisions for KYC. | | `postal_code` | string | conditional | Required for countries that use postal codes. | | `country` | string (ISO 3166-1 alpha-3, exactly 3) | yes | | **Example** ```json { "street_line_1": "1 Market St", "city": "San Francisco", "subdivision": "US-CA", "postal_code": "94105", "country": "USA" } ``` ## `AssociatedPerson` Nested in `associated_persons[]` on business customers and accepted on the [Associated Persons endpoints](/bridge-compat/associated-persons). Each person represents a UBO (ultimate beneficial owner), controller, signer, or director. | Field | Type | Required | Notes | | ------------------------------------ | ----------------------------------------------------------------------------------- | ----------- | ----------------------------------------------------------------------- | | `first_name` | string (1–1024) | yes | | | `middle_name` | string | no | | | `last_name` | string (2–1024) | yes | | | `transliterated_first_name` | string | no | | | `transliterated_middle_name` | string | no | | | `transliterated_last_name` | string | no | | | `email` | string (1–1024) | yes | | | `phone` | string | no | | | `birth_date` | string (`YYYY-MM-DD`) | yes | | | `residential_address` | [`Address`](/bridge-compat/schemas/address) | yes | | | `transliterated_residential_address` | [`Address`](/bridge-compat/schemas/address) | no | | | `has_ownership` | boolean | yes | | | `has_control` | boolean | yes | | | `is_signer` | boolean | yes | | | `is_director` | boolean | no | | | `title` | string | conditional | Required when `has_control === true`. | | `ownership_percentage` | integer | no | Required when `has_ownership === true` and above `ownership_threshold`. | | `attested_ownership_structure_at` | string (ISO 8601) | no | | | `relationship_established_at` | string (`YYYY-MM-DD`) | no | | | `verified_govid_at` | string (ISO 8601) | no | | | `verified_selfie_at` | string (ISO 8601) | no | | | `completed_customer_safety_check_at` | string (ISO 8601) | no | | | `identifying_information` | array of [`IdentifyingInformation`](/bridge-compat/schemas/identifying-information) | no | | | `documents` | array of [`Document`](/bridge-compat/schemas/document) | no | | **Example** ```json { "first_name": "Alice", "last_name": "Zhang", "email": "alice@example.com", "birth_date": "1990-04-01", "residential_address": { "street_line_1": "1 Market St", "city": "San Francisco", "subdivision": "US-CA", "postal_code": "94105", "country": "USA" }, "has_ownership": true, "has_control": true, "is_signer": true, "title": "CEO", "ownership_percentage": 60 } ``` ## `BusinessDocument` Same field shape as [`Document`](/bridge-compat/schemas/document) — the only difference is the `purposes` enum, which accepts business-specific values. ### Fields | Field | Type | Required | Notes | | ------------- | -------------------------------------- | ----------- | ------------------------------------------ | | `purposes` | array of business purposes (see below) | yes | | | `file` | string (base64 data-uri) | yes | Min 200×200px, max 24 MB. | | `description` | string | conditional | Required when `purposes` includes `other`. | ### Business `purposes` * `aml_comfort_letter` * `business_formation` * `directors_registry` * `e_signature_certificate` * `evidence_of_good_standing` * `flow_of_funds` * `marketing_materials` * `ownership_chart` * `ownership_information` * `proof_of_account_purpose` * `proof_of_address` * `proof_of_entity_name_change` * `proof_of_nature_of_business` * `proof_of_signatory_authority` * `proof_of_source_of_funds` * `proof_of_source_of_wealth` * `proof_of_tax_identification` * `shareholder_register` * `other` **Example** ```json { "purposes": ["business_formation"], "file": "data:application/pdf;base64,…" } ``` ## `Document` Uploaded supporting file for individual customers. Business customers use the richer [`BusinessDocument`](/bridge-compat/schemas/business-document) shape which accepts a larger `purposes` enum. | Field | Type | Required | Notes | | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------ | | `purposes` | array of `proof_of_account_purpose` \| `proof_of_address` \| `proof_of_individual_name_change` \| `proof_of_relationship` \| `proof_of_source_of_funds` \| `proof_of_source_of_wealth` \| `proof_of_tax_identification` \| `other` | yes | | | `file` | string (base64 data-uri) | yes | Min 200×200px, max 24 MB. | | `description` | string | conditional | Required when `purposes` includes `other`. | **Example** ```json { "purposes": ["proof_of_address"], "file": "data:application/pdf;base64,…" } ``` ## `Endorsement` Rails / features the customer is requesting access to. Each endorsement has independent requirements the customer must clear during KYC. **Supported** * `base` — required. Basic account + crypto/USD rails. * `cop` — Colombian peso payouts via `co_bank_transfer`. * `faster_payments` — UK Faster Payments. * `pix` — Brazil Pix. * `sepa` — Eurozone SEPA. * `spei` — Mexico SPEI. **Not supported** * `cards` — we don't run a card program. Requesting this endorsement is accepted for shape parity but the capability will stay `inactive`; related `/cards/*` endpoints return `501 not_implemented` (see [Not implemented](/bridge-compat/not-implemented#cards)). Endorsements appear in two places: * On the **request**: an array of strings, e.g. `"endorsements": ["base", "sepa"]`. * On the **response**: an array of `EndorsementResponse` objects — the endorsement name plus status + requirements. ### `EndorsementResponse` | Field | Type | Notes | | ----------------------- | --------------------------------------- | -------------------------------------------------------- | | `name` | `Endorsement` | | | `status` | `incomplete` \| `approved` \| `revoked` | | | `requirements.complete` | array of string | Satisfied requirement ids. | | `requirements.pending` | array of string | Requirement ids currently in-flight. | | `requirements.missing` | object | Requirement-id keyed map of what still needs to be done. | | `requirements.issues` | array of string or object | Validation errors on submitted fields. | ## `IdentifyingInformation` Government-issued IDs and tax identifiers. Used on individual customers, business customers, and each `AssociatedPerson` on a business. | Field | Type | Required | Notes | | ----------------- | -------------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------ | | `type` | enum: `drivers_license`, `national_id`, `passport`, + 60+ tax-id types (`ssn`, `itin`, `nino`, `rfc`, …) | yes | Government IDs and tax identifiers share this array. | | `issuing_country` | string (ISO 3166-1 alpha-3) | yes | | | `number` | string | conditional | Required for government IDs; optional for some tax-id types. | | `description` | string | conditional | Required when `type === "other"`. | | `expiration` | string (`YYYY-MM-DD`) | no | | | `image_front` | string (base64 data-uri) | no | Min 200×200px, max 15 MB (combined with back: 24 MB). | | `image_back` | string (base64 data-uri) | no | | **Example — US SSN** ```json { "type": "ssn", "issuing_country": "USA", "number": "111-22-3333" } ``` **Example — passport with image** ```json { "type": "passport", "issuing_country": "USA", "number": "A12345678", "expiration": "2032-06-01", "image_front": "data:image/jpeg;base64,…" } ``` ## Shared schemas Shapes that appear in more than one Bridge-compat endpoint live here. Each type has its own page so you can deep-link to a specific schema from your integration notes. * [`Address`](/bridge-compat/schemas/address) — ISO-style postal address used everywhere a physical location is required. * [`Endorsement`](/bridge-compat/schemas/endorsement) — rail / feature capabilities customers can request. * [`IdentifyingInformation`](/bridge-compat/schemas/identifying-information) — government IDs and tax identifiers. * [`Document`](/bridge-compat/schemas/document) — uploaded supporting files for individuals. * [`BusinessDocument`](/bridge-compat/schemas/business-document) — business-customer variant with a richer `purposes` enum. * [`AssociatedPerson`](/bridge-compat/schemas/associated-person) — UBOs, controllers, signers, directors on a business customer. * [`PubliclyTradedListing`](/bridge-compat/schemas/publicly-traded-listing) — stock-exchange listing for publicly traded business customers. * [`RegulatedActivity`](/bridge-compat/schemas/regulated-activity) — regulator + license info for regulated businesses. ## `PubliclyTradedListing` Listed on publicly traded businesses. Accepted on customer create / update for shape parity with Bridge but **not forwarded** to our providers — see the business customer's [Accepted but ignored](/bridge-compat/customers/create#accepted-but-ignored-business) list. | Field | Type | Required | Notes | | ------------------------ | ------------------------------ | -------- | ----------------------- | | `market_identifier_code` | string (4-digit ISO 10383 MIC) | yes | e.g. `XNAS` for NASDAQ. | | `stock_number` | string (12-digit ISIN, no `-`) | yes | | | `ticker` | string | yes | | **Example** ```json { "market_identifier_code": "XNAS", "stock_number": "US0378331005", "ticker": "AAPL" } ``` ## `RegulatedActivity` Attached to business customers engaged in regulated activity (financial services, insurance, etc.). Accepted on customer create / update for shape parity with Bridge but **not forwarded** — our providers run their own regulator checks. | Field | Type | Required | | -------------------------------------- | --------------------------- | -------- | | `regulated_activities_description` | string | yes | | `primary_regulatory_authority_country` | string (ISO 3166-1 alpha-3) | yes | | `primary_regulatory_authority_name` | string | yes | | `license_number` | string | yes | **Example** ```json { "regulated_activities_description": "Money transmission services in New York.", "primary_regulatory_authority_country": "USA", "primary_regulatory_authority_name": "NYDFS", "license_number": "NYL-12345" } ``` ## `POST /kyc_links` Create a hosted KYC flow **and** the stub customer behind it — use this when you want the customer to do the typing. ### Request ```json { "type": "individual", "full_name": "Alice Zhang", "email": "alice@example.com", "endorsements": ["base", "sepa"], "redirect_uri": "https://yourapp.example.com/kyc-complete" } ``` | Field | Type | Required | Notes | | -------------- | ------------------------------------------------------------ | -------- | ----------------------------------------------------------------- | | `type` | `individual` \| `business` | yes | Governs the hosted form. | | `email` | string | yes | | | `full_name` | string | no | Pre-fills the form. | | `endorsements` | array of [`Endorsement`](/bridge-compat/schemas/endorsement) | no | `base`, `cards`, `cop`, `faster_payments`, `pix`, `sepa`, `spei`. | | `redirect_uri` | string | no | Bridge redirects here post-completion. | ### Response (200) ```json { "id": "kyc_link_01HZ…", "type": "individual", "customer_id": "cust_01HZ…", "full_name": "Alice Zhang", "email": "alice@example.com", "kyc_link": "https://kyc.seismic.systems/flow/abc123", "kyc_status": "not_started", "tos_link": "https://tos.seismic.systems/flow/def456", "tos_status": "pending", "rejection_reasons": [], "created_at": "2026-04-24T12:00:00Z" } ``` ```bash curl -X POST "$BASE_URL/kyc_links" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "individual", "full_name": "Alice Zhang", "email": "alice@example.com", "endorsements": ["base"], "redirect_uri": "https://yourapp.example.com/kyc-complete" }' ``` Redirect the user to `kyc_link` from the response, then poll [`GET /kyc_links/{id}`](/bridge-compat/kyc-links/retrieve). ## KYC Links Bridge's `/kyc_links` resource. One call creates a hosted KYC flow **and** the stub customer behind it — use this when you want the customer to do the typing. ### Endpoints * [`POST /kyc_links`](/bridge-compat/kyc-links/create) — create a hosted KYC + TOS link. * [`GET /kyc_links`](/bridge-compat/kyc-links/list) — list all KYC links in the tenant. * [`GET /kyc_links/{id}`](/bridge-compat/kyc-links/retrieve) — poll the status of a KYC link. ## `GET /kyc_links` List all KYC links you've created. ### Query | Param | Type | Default | | ---------------- | ------- | ------- | | `limit` | integer | `10` | | `starting_after` | string | — | | `ending_before` | string | — | ### Response ```json { "count": 3, "data": [ /* kyc_link objects */ ] } ``` Each item in `data` has the shape returned by [`POST /kyc_links`](/bridge-compat/kyc-links/create#response-200). ```bash curl "$BASE_URL/kyc_links?limit=50" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ## `GET /kyc_links/{id}` Check the current status. The path is the plain id — no `/status` suffix. Response shape matches [`POST /kyc_links`](/bridge-compat/kyc-links/create#response-200). ### `kyc_status` enum | Value | Meaning | | ------------------------ | ----------------------------------- | | `not_started` | Link issued; user hasn't opened it. | | `incomplete` | Started but not submitted. | | `awaiting_questionnaire` | Needs additional questionnaire. | | `awaiting_ubo` | Business: needs UBO details. | | `under_review` | Submitted; provider reviewing. | | `approved` | Done — customer is usable. | | `rejected` | Done — customer cannot proceed. | | `paused` | Manual hold. | | `offboarded` | Customer was removed. | `tos_status` is `"pending"` until the user accepts, then `"approved"`. The customer is safe to transact with once both `kyc_status === "approved"` and `tos_status === "approved"`. ```bash curl "$BASE_URL/kyc_links/kyc_link_01HZ…" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ## `POST /customers/{id}/external_accounts` Creation is **nested under the owning customer** — same path Bridge uses. ### Request (US bank) ```json { "currency": "usd", "bank_name": "Wells Fargo", "account_owner_name": "Alice Zhang", "account_type": "us", "account": { "account_number": "1210002481111", "routing_number": "121000248", "checking_or_savings": "checking" }, "address": { "street_line_1": "1 Market St", "city": "San Francisco", "state": "CA", "postal_code": "94102", "country": "USA" } } ``` ### Request (IBAN) ```json { "currency": "eur", "account_owner_name": "Alice Zhang", "account_type": "iban", "account": { "iban": "DE89370400440532013000", "bic": "COBADEFFXXX" } } ``` ### Enums | Field | Values | | ----------------------------- | -------------------------------------------------------------------------- | | `account_type` | `us`, `iban`, `clabe`, `pix`, `gb`, `bre_b`, `co_bank_transfer`, `unknown` | | `currency` | `usd`, `eur`, `mxn`, `brl`, `gbp`, `cop` | | `account.checking_or_savings` | `checking`, `savings` | `unknown` is passed through for shape parity — use it when none of the other account types fit. Internally our providers have their own account-type taxonomy; the compat layer translates both directions so the `account_type` you get back on `GET /external_accounts/{id}` always uses Bridge's enum (never a provider-specific value leaking through). ### Response (201) Returns the [external-account object](/bridge-compat/external-accounts/retrieve#the-external-account-object). Account details are redacted on read — only `last_4` comes back. ```bash curl -X POST "$BASE_URL/customers/cust_01HZ…/external_accounts" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "currency": "usd", "bank_name": "Wells Fargo", "account_owner_name": "Alice Zhang", "account_type": "us", "account": { "account_number": "1210002481111", "routing_number": "121000248", "checking_or_savings": "checking" }, "address": { "street_line_1": "1 Market St", "city": "San Francisco", "state": "CA", "postal_code": "94102", "country": "USA" } }' ``` ## `DELETE /external_accounts/{id}` Soft-delete — sets `active: false`. The underlying account is retained so existing transfers referencing it still resolve. Reverse via [`POST /external_accounts/{id}/reactivate`](/bridge-compat/external-accounts/reactivate). Returns `204 No Content` on success. ```bash curl -X DELETE "$BASE_URL/external_accounts/ea_01HZ…" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ## External Accounts Bridge's `/external_accounts` resource. Destinations a customer can receive payouts into, or fund transfers from. **Fiat only.** Bridge's `/external_accounts` covers bank rails (US, IBAN, CLABE, Pix, GB, etc.) — crypto destinations on Bridge are addressed inline on a transfer (`destination.payment_rail` + `to_address`), not stored as `/external_accounts` rows. Some of our underlying providers do support importing external crypto wallets, but that capability won't surface here — it lives on the native API. ### Endpoints * [`GET /external_accounts`](/bridge-compat/external-accounts/list) — list across all customers, tenant-scoped. * [`GET /external_accounts/{id}`](/bridge-compat/external-accounts/retrieve) — fetch by id. * [`PUT /external_accounts/{id}`](/bridge-compat/external-accounts/update) — partial update. * [`DELETE /external_accounts/{id}`](/bridge-compat/external-accounts/delete) — soft-delete. * [`POST /external_accounts/{id}/reactivate`](/bridge-compat/external-accounts/reactivate) — reverse a delete. * [`POST /customers/{id}/external_accounts`](/bridge-compat/external-accounts/create) — create, nested under the owning customer. * [`GET /customers/{id}/external_accounts`](/bridge-compat/external-accounts/list-for-customer) — list scoped to one customer. ## `GET /customers/{id}/external_accounts` List scoped to one customer. Same query params as [`GET /external_accounts`](/bridge-compat/external-accounts/list). ```bash curl "$BASE_URL/customers/cust_01HZ…/external_accounts" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ## `GET /external_accounts` List across all customers. Tenant-scoped. ### Query | Param | Type | Default | | ---------------- | ------- | ------- | | `limit` | integer | `10` | | `starting_after` | string | — | | `ending_before` | string | — | ```bash curl "$BASE_URL/external_accounts?limit=50" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ### Response Array of [external-account objects](/bridge-compat/external-accounts/retrieve#the-external-account-object). ## `POST /external_accounts/{id}/reactivate` Reverse of [`DELETE /external_accounts/{id}`](/bridge-compat/external-accounts/delete) — flips `active` back to `true`. ### Response Returns the updated [external-account object](/bridge-compat/external-accounts/retrieve#the-external-account-object). ```bash curl -X POST "$BASE_URL/external_accounts/ea_01HZ…/reactivate" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ## `GET /external_accounts/{id}` Fetch by id. ```bash curl "$BASE_URL/external_accounts/ea_01HZ…" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ### The external-account object Same shape returned by create, list, update, and retrieve. ```json { "id": "ea_01HZ…", "customer_id": "cust_01HZ…", "account_type": "us", "currency": "usd", "account_owner_name": "Alice Zhang", "bank_name": "Wells Fargo", "active": true, "beneficiary_address_valid": true, "account": { "last_4": "1111", "routing_number": "121000248", "checking_or_savings": "checking" }, "created_at": "2026-04-24T12:00:00Z", "updated_at": "2026-04-24T12:00:00Z" } ``` Account details are redacted on read — only `last_4` comes back. ## `PUT /external_accounts/{id}` Partial update. Only `account_owner_name`, `bank_name`, and `address` are mutable — other fields return `400 invalid_parameters` if you try to change them. ### Response Returns the updated [external-account object](/bridge-compat/external-accounts/retrieve#the-external-account-object). ```bash curl -X PUT "$BASE_URL/external_accounts/ea_01HZ…" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "bank_name": "Wells Fargo Bank, N.A." }' ``` ## Exchange Rates Bridge's `/exchange_rates` endpoint. Indicative quotes — the realized rate on a transfer depends on the provider, rail, and size. ### Endpoints * [`GET /exchange_rates`](/bridge-compat/exchange-rates/retrieve) :::info[Rates vs transfer pricing] This endpoint returns a book-keeping reference — it's not binding. A real transfer goes through a specific provider + rail, each of which has its own spread, fees, and min/max. To get a binding price, run [`POST /transfers`](/bridge-compat/transfers/create) with `dry_run: true`. ::: ## `GET /exchange_rates` ### Query | Param | Type | Required | Values | | ------ | ------ | -------- | ------------------------------------------------ | | `from` | string | yes | `brl`, `cop`, `eur`, `gbp`, `mxn`, `usd`, `usdt` | | `to` | string | yes | Same enum as `from`. | ### Response (200) ```json { "midmarket_rate": "1.0834", "buy_rate": "1.0901", "sell_rate": "1.0768" } ``` * **`midmarket_rate`** — no-fee reference rate. * **`buy_rate`** — what you'd pay to acquire `to` by spending `from`. Includes our indicative spread. * **`sell_rate`** — what you'd receive in `to` by selling `from`. All three are strings; values are multiplicative (`to_units = from_units × rate`). ```bash curl "$BASE_URL/exchange_rates?from=usd&to=eur" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` :::info[Rates vs transfer pricing] This endpoint returns a book-keeping reference — it's not binding. A real transfer goes through a specific provider + rail, each of which has its own spread, fees, and min/max. To get a binding price, run [`POST /transfers`](/bridge-compat/transfers/create) with `dry_run: true`. ::: ## `POST /customers` Create an individual or business customer. Every field below is accepted. Some are [forwarded to the underlying provider](#what-we-forward-vs-store), some are stored verbatim and echoed on `GET` but don't influence onboarding decisions — we list those explicitly in [Accepted but ignored (individual)](#accepted-but-ignored-individual) and [Accepted but ignored (business)](#accepted-but-ignored-business). ### Request — Individual (`type: "individual"`) | Field | Type | Required | Notes | | ------------------------------------ | -------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------- | | `type` | `"individual"` | yes | Discriminator. | | `first_name` | string (2–1024) | yes | | | `middle_name` | string (1–1024) | no | | | `last_name` | string (2–1024) | yes | | | `transliterated_first_name` | string (1–256) | no | Latin-script transliteration for non-Latin names. | | `transliterated_middle_name` | string (1–256) | no | | | `transliterated_last_name` | string (1–256) | no | | | `email` | string (1–1024) | yes | | | `phone` | string | no | E.164-ish, e.g. `+12223334444`. | | `birth_date` | string (`YYYY-MM-DD`) | yes | | | `residential_address` | [`Address`](/bridge-compat/schemas/address) | yes | | | `transliterated_residential_address` | [`Address`](/bridge-compat/schemas/address) | no | | | `account_purpose` | enum (see [account\_purpose (individual)](#account_purpose-individual)) | yes | Reason this customer is using the platform. | | `account_purpose_other` | string | conditional | Required if `account_purpose === "other"`. | | `employment_status` | `employed` \| `homemaker` \| `retired` \| `self_employed` \| `student` \| `unemployed` | yes | | | `expected_monthly_payments_usd` | `0_4999` \| `5000_9999` \| `10000_49999` \| `50000_plus` | yes | | | `acting_as_intermediary` | boolean | no | | | `most_recent_occupation` | string (occupation code) | no | Bridge uses their own occupational code list. | | `source_of_funds` | enum (see [source\_of\_funds (individual)](#source_of_funds-individual)) | yes | Where the money comes from. | | `nationality` | string (ISO 3166-1 alpha-3) | no | | | `verified_govid_at` | string (ISO 8601) | no | Caller attests they already verified gov ID. | | `verified_selfie_at` | string (ISO 8601) | no | Caller attests they already verified selfie. | | `completed_customer_safety_check_at` | string (ISO 8601) | no | Caller attests they ran their own safety check. | | `signed_agreement_id` | string (1–1024) | no | Bridge's internal agreement id — meaningless outside Bridge. | | `endorsements` | array of [`Endorsement`](/bridge-compat/schemas/endorsement) | no | Rails / features the customer wants access to. Empty = `base` only. | | `identifying_information` | array of [`IdentifyingInformation`](/bridge-compat/schemas/identifying-information) | no | | | `documents` | array of [`Document`](/bridge-compat/schemas/document) | no | | #### `account_purpose` (individual) * `charitable_donations` * `ecommerce_retail_payments` * `investment_purposes` * `operating_a_company` * `other` * `payments_to_friends_or_family_abroad` * `personal_or_living_expenses` * `protect_wealth` * `purchase_goods_and_services` * `receive_payment_for_freelancing` * `receive_salary` #### `source_of_funds` (individual) * `company_funds` * `ecommerce_reseller` * `gambling_proceeds` * `gifts` * `government_benefits` * `inheritance` * `investments_loans` * `pension_retirement` * `salary` * `sale_of_assets_real_estate` * `savings` * `someone_elses_funds` #### Example — Individual ```json { "type": "individual", "first_name": "Alice", "last_name": "Zhang", "email": "alice@example.com", "phone": "+14155550100", "birth_date": "1990-04-01", "residential_address": { "street_line_1": "1 Market St", "city": "San Francisco", "subdivision": "US-CA", "postal_code": "94105", "country": "USA" }, "account_purpose": "purchase_goods_and_services", "employment_status": "employed", "expected_monthly_payments_usd": "5000_9999", "source_of_funds": "salary", "nationality": "USA", "endorsements": ["base"], "identifying_information": [ { "type": "ssn", "issuing_country": "USA", "number": "111-22-3333" } ] } ``` ```bash curl -X POST "$BASE_URL/customers" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "individual", "first_name": "Alice", "last_name": "Zhang", "email": "alice@example.com", "birth_date": "1990-04-01", "residential_address": { "street_line_1": "1 Market St", "city": "San Francisco", "subdivision": "US-CA", "postal_code": "94105", "country": "USA" }, "account_purpose": "purchase_goods_and_services", "employment_status": "employed", "expected_monthly_payments_usd": "5000_9999", "source_of_funds": "salary" }' ``` #### Accepted but ignored (individual) The compat layer stores these on the customer row and echoes them on `GET`, but does **not** forward them to the underlying provider and they do **not** influence onboarding decisions. | Field | Why it's ignored | | ------------------------------------ | ------------------------------------------------------------------------------------------------ | | `transliterated_first_name` | We pass names as submitted to the provider; we don't run a second transliterated identity check. | | `transliterated_middle_name` | Same. | | `transliterated_last_name` | Same. | | `transliterated_residential_address` | Same. | | `acting_as_intermediary` | Our provider integrations don't expose an intermediary flag. | | `most_recent_occupation` | Bridge-specific occupation code list; not queried by our providers. | | `verified_govid_at` | We always run the provider's own ID verification — caller attestations don't short-circuit it. | | `verified_selfie_at` | Same. | | `completed_customer_safety_check_at` | Same. | | `signed_agreement_id` | Bridge's internal agreement id has no counterpart here. | ### Request — Business (`type: "business"`) | Field | Type | Required | Notes | | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------ | | `type` | `"business"` | yes | Discriminator. | | `business_legal_name` | string (1–1024) | yes | Registered legal entity name. | | `transliterated_business_legal_name` | string | no | | | `business_trade_name` | string (1–1024) | no | DBA / public-facing name. | | `transliterated_business_trade_name` | string | no | | | `business_description` | string (1–1024) | yes | What the business does. | | `email` | string (1–1024) | yes | | | `business_type` | `cooperative` \| `corporation` \| `llc` \| `other` \| `partnership` \| `sole_prop` \| `trust` | yes | | | `primary_website` | string (1–1024) | no | | | `other_websites` | array of string | no | | | `registered_address` | [`Address`](/bridge-compat/schemas/address) | yes | Legal / registration address. | | `transliterated_registered_address` | [`Address`](/bridge-compat/schemas/address) | no | | | `physical_address` | [`Address`](/bridge-compat/schemas/address) | no | Operating address if different from `registered_address`. | | `transliterated_physical_address` | [`Address`](/bridge-compat/schemas/address) | no | | | `is_dao` | boolean | no | | | `business_industry` | array of string (NAICS codes) | yes | | | `estimated_annual_revenue_usd` | `0_99999` \| `100000_999999` \| `1000000_9999999` \| `10000000_49999999` \| `50000000_249999999` \| `250000000_plus` | yes | | | `expected_monthly_payments_usd` | integer | yes | Note: individual uses an enum; business uses a plain integer. Bridge's choice. | | `expected_monthly_swift_transaction_volume` | integer | no | | | `operates_in_prohibited_countries` | boolean | no | | | `account_purpose` | enum (see [account\_purpose (business)](#account_purpose-business)) | yes | | | `account_purpose_other` | string | conditional | Required if `account_purpose === "other"`. | | `high_risk_activities` | array of `HighRiskActivity` (see [high\_risk\_activities](#high_risk_activities)) | yes | Include `none_of_the_above` if none apply. | | `high_risk_activities_explanation` | string | conditional | Required if `high_risk_activities` contains any non-`none_of_the_above` entry. | | `source_of_funds` | enum (see [source\_of\_funds (business)](#source_of_funds-business)) | yes | | | `source_of_funds_description` | string | no | | | `conducts_money_services` | boolean | no | | | `conducts_money_services_using_bridge` | boolean | conditional | Required if `conducts_money_services === true`. | | `conducts_money_services_description` | string | conditional | Required if `conducts_money_services === true`. | | `compliance_screening_explanation` | string | conditional | Required if `conducts_money_services === true`. | | `publicly_traded_listings` | array of [`PubliclyTradedListing`](/bridge-compat/schemas/publicly-traded-listing) | no | | | `ownership_threshold` | integer (5–25) | no | Default `25`. % ownership above which beneficial owners must be listed. | | `has_material_intermediary_ownership` | boolean | no | | | `acting_as_intermediary` | boolean | no | | | `signed_agreement_id` | string | no | | | `endorsements` | array of [`Endorsement`](/bridge-compat/schemas/endorsement) | no | | | `associated_persons` | array of [`AssociatedPerson`](/bridge-compat/schemas/associated-person) | yes | UBOs, controllers, signers, directors. | | `identifying_information` | array of [`IdentifyingInformation`](/bridge-compat/schemas/identifying-information) | no | | | `documents` | array of [`BusinessDocument`](/bridge-compat/schemas/business-document) | no | Business docs have a larger `purposes` enum than individual documents. | | `regulated_activity` | [`RegulatedActivity`](/bridge-compat/schemas/regulated-activity) | no | | #### `account_purpose` (business) * `charitable_donations` * `ecommerce_retail_payments` * `investment_purposes` * `other` * `payments_to_friends_or_family_abroad` * `payroll` * `personal_or_living_expenses` * `protect_wealth` * `purchase_goods_and_services` * `receive_payments_for_goods_and_services` * `tax_optimization` * `third_party_money_transmission` * `treasury_management` #### `high_risk_activities` * `adult_entertainment` * `gambling` * `hold_client_funds` * `investment_services` * `lending_banking` * `marijuana_or_related_services` * `money_services` * `nicotine_tobacco_or_related_services` * `operate_foreign_exchange_virtual_currencies_brokerage_otc` * `pharmaceuticals` * `precious_metals_precious_stones_jewelry` * `safe_deposit_box_rentals` * `third_party_payment_processing` * `weapons_firearms_and_explosives` * `none_of_the_above` #### `source_of_funds` (business) * `business_loans` * `grants` * `inter_company_funds` * `investment_proceeds` * `legal_settlement` * `owners_capital` * `pension_retirement` * `sale_of_assets` * `sales_of_goods_and_services` * `third_party_funds` * `treasury_reserves` #### Example — Business ```json { "type": "business", "business_legal_name": "Seismic Systems Inc.", "business_description": "Stablecoin payment infrastructure.", "email": "ops@example.com", "business_type": "corporation", "primary_website": "https://seismic.systems", "registered_address": { "street_line_1": "1 Market St", "city": "San Francisco", "subdivision": "US-CA", "postal_code": "94105", "country": "USA" }, "business_industry": ["522320"], "estimated_annual_revenue_usd": "1000000_9999999", "expected_monthly_payments_usd": 500000, "account_purpose": "treasury_management", "high_risk_activities": ["none_of_the_above"], "source_of_funds": "owners_capital", "ownership_threshold": 25, "endorsements": ["base"], "associated_persons": [ { "first_name": "Alice", "last_name": "Zhang", "email": "alice@example.com", "birth_date": "1990-04-01", "residential_address": { "street_line_1": "1 Market St", "city": "San Francisco", "subdivision": "US-CA", "postal_code": "94105", "country": "USA" }, "has_ownership": true, "has_control": true, "is_signer": true, "title": "CEO", "ownership_percentage": 60 } ] } ``` #### Accepted but ignored (business) Stored on the customer row and echoed on `GET`. Not forwarded to providers and not used for onboarding decisions. | Field | Why it's ignored | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | `transliterated_business_legal_name` | We pass names as submitted. | | `transliterated_business_trade_name` | Same. | | `transliterated_registered_address` | Same. | | `transliterated_physical_address` | Same. | | `is_dao` | No provider field for DAO disclosure. | | `expected_monthly_swift_transaction_volume` | We don't expose SWIFT-specific volume projections to providers. | | `operates_in_prohibited_countries` | Our compliance layer runs its own jurisdiction screen; this flag is not fed in. | | `conducts_money_services` | We don't expose MSB-status self-attestation to providers — they rerun it. | | `conducts_money_services_using_bridge` | Bridge-specific. | | `conducts_money_services_description` | Stored only. | | `compliance_screening_explanation` | Stored only. | | `publicly_traded_listings` | Not consumed by the native onboarding path. May be re-asked by the provider. | | `has_material_intermediary_ownership` | We require full ownership disclosure via `associated_persons` regardless. | | `acting_as_intermediary` | Same. | | `signed_agreement_id` | Bridge-internal. | | `regulated_activity` | Stored only; providers run their own regulator check. | | Per-`AssociatedPerson`: `transliterated_*`, `verified_govid_at`, `verified_selfie_at`, `completed_customer_safety_check_at`, `attested_ownership_structure_at`, `relationship_established_at` | See individual-level rationale above; the same holds for each associated person. | ### What we forward vs. store The compat layer's default is "accept every Bridge field and store it". What's *forwarded* to the underlying provider is a narrower core — identity, address, `birth_date`, `email`, `endorsements`, `account_purpose`, `source_of_funds`, `employment_status` (individual), `business_type` / `business_industry` / `estimated_annual_revenue_usd` (business), `identifying_information`, `documents`, and the full `associated_persons` array for businesses. Everything outside that set is listed above under **Accepted but ignored**. ### Response (201) Returns the [Customer object](/bridge-compat/customers/retrieve#the-customer-object). The response shape is the same as `GET /customers/{id}`. ## `DELETE /customers/{id}` Soft-delete. The row is retained so in-flight transfers referencing this customer still resolve. Returns `204 No Content` on success. ```bash curl -X DELETE "$BASE_URL/customers/cust_01HZ…" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ## Customers Bridge's `/customers` resource. Two customer types share one endpoint, discriminated by `type`: * **`individual`** — a natural person. \~24 fields. * **`business`** — a legal entity with associated beneficial owners. \~40 top-level fields plus nested `associated_persons[]`. Shared sub-objects — [`Address`](/bridge-compat/schemas/address), [`IdentifyingInformation`](/bridge-compat/schemas/identifying-information), [`Document`](/bridge-compat/schemas/document), [`BusinessDocument`](/bridge-compat/schemas/business-document), [`AssociatedPerson`](/bridge-compat/schemas/associated-person), [`Endorsement`](/bridge-compat/schemas/endorsement), [`PubliclyTradedListing`](/bridge-compat/schemas/publicly-traded-listing), [`RegulatedActivity`](/bridge-compat/schemas/regulated-activity) — live under [Shared schemas](/bridge-compat/schemas). ### Endpoints * [`POST /customers`](/bridge-compat/customers/create) — create an individual or business customer. * [`GET /customers`](/bridge-compat/customers/list) — list all customers in the tenant. * [`GET /customers/{id}`](/bridge-compat/customers/retrieve) — retrieve a single customer. * [`PUT /customers/{id}`](/bridge-compat/customers/update) — partial update. * [`DELETE /customers/{id}`](/bridge-compat/customers/delete) — soft-delete. * [`GET /customers/{id}/transfers`](/bridge-compat/customers/list-transfers) — alias of `GET /transfers?on_behalf_of={id}`. * [`GET /customers/{id}/kyc_link`](/bridge-compat/customers/kyc-link) — hosted KYC flow for an existing customer. * [TOS Links](/bridge-compat/customers/tos-links) * [`POST /customers/tos_links`](/bridge-compat/customers/tos-links#post-customerstos_links) * [`POST /customers/tos_acceptance_link`](/bridge-compat/customers/tos-links#post-customerstos_acceptance_link) (alias) * [`GET /customers/{id}/tos_acceptance_link`](/bridge-compat/customers/tos-links#get-customersidtos_acceptance_link) If something else doesn't match what you expect, [open an issue](https://github.com/SeismicSystems/orchestration/issues). ## `GET /customers/{id}/kyc_link` Hosted KYC flow for an **existing** customer. Distinct from [`POST /kyc_links`](/bridge-compat/kyc-links/create), which creates a fresh link + stub customer in one call — use this when the customer already exists and just needs to complete KYC. Response shape matches [`GET /kyc_links/{id}`](/bridge-compat/kyc-links/retrieve). ```bash curl "$BASE_URL/customers/cust_01HZ…/kyc_link" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ## `GET /customers/{id}/transfers` Alias of [`GET /transfers?on_behalf_of={id}`](/bridge-compat/transfers/list). Same query params, same response shape. ```bash curl "$BASE_URL/customers/cust_01HZ…/transfers?limit=50" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ## `GET /customers` List all customers in the tenant. ### Query | Param | Type | Default | Notes | | ---------------- | ------- | ------- | -------------------------------------- | | `limit` | integer | `10` | Max 100. | | `starting_after` | string | — | Cursor — returns items after this id. | | `ending_before` | string | — | Cursor — returns items before this id. | ### Response ```json { "count": 23, "data": [ /* customer objects */ ] } ``` Each item in `data` is a [Customer object](/bridge-compat/customers/retrieve#the-customer-object). ```bash curl "$BASE_URL/customers?limit=50" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ## `GET /customers/{id}` Retrieve a single customer by id. ```bash curl "$BASE_URL/customers/cust_01HZ…" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ### The Customer object Same shape returned by [`POST /customers`](/bridge-compat/customers/create), [`GET /customers`](/bridge-compat/customers/list), and [`PUT /customers/{id}`](/bridge-compat/customers/update). ```json { "id": "cust_01HZ…", "type": "individual", "email": "alice@example.com", "status": "awaiting_questionnaire", "first_name": "Alice", "last_name": "Zhang", "has_accepted_terms_of_service": false, "capabilities": { "payin_crypto": "pending", "payout_crypto": "pending", "payin_fiat": "pending", "payout_fiat": "pending" }, "endorsements": [ { "name": "base", "status": "incomplete", "requirements": { "complete": [], "pending": ["tos_acceptance"], "missing": {}, "issues": [] } } ], "requirements_due": ["tos_acceptance"], "future_requirements_due": [], "rejection_reasons": [], "created_at": "2026-04-24T12:00:00Z", "updated_at": "2026-04-24T12:00:00Z" } ``` #### Response fields | Field | Type | Notes | | ------------------------------- | --------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | `id` | string (UUID) | Compat id; distinct from any provider-side id. | | `type` | `individual` \| `business` | | | `email` | string | | | `status` | [`CustomerStatus`](#customerstatus) | | | `first_name`, `last_name` | string | Individual only; absent for business. | | `business_legal_name` | string | Business only. | | `persona_inquiry_type` | string | Bridge's internal Persona integration flag. Echoed for shape parity; no semantic here. | | `has_accepted_terms_of_service` | boolean | | | `capabilities` | `{ payin_crypto, payout_crypto, payin_fiat, payout_fiat }` with values in [`CapabilityState`](#capabilitystate) | | | `endorsements` | array of [`EndorsementResponse`](/bridge-compat/schemas/endorsement#endorsementresponse) | | | `requirements_due` | array of string | Actionable blockers right now. | | `future_requirements_due` | array of string | Blockers that will surface as KYC progresses (e.g. need more docs at a higher tier). | | `rejection_reasons` | array of [`RejectionReason`](#rejectionreason) | | | `associated_persons` | array of `{ id, email }` | Business only. Summary refs; full objects via the [Associated Persons](/bridge-compat/associated-persons) endpoints. | | `created_at`, `updated_at` | string (ISO 8601) | | #### `CustomerStatus` * `active` * `awaiting_questionnaire` * `awaiting_ubo` * `incomplete` * `not_started` * `offboarded` * `paused` * `rejected` * `under_review` #### `CapabilityState` * `pending` * `active` * `inactive` * `rejected` #### `RejectionReason` | Field | Type | Notes | | ------------------ | ------ | ------------------------ | | `developer_reason` | string | Machine-readable reason. | | `reason` | string | Human-readable detail. | ## TOS Links Three endpoints related to the hosted Terms of Service flow. All three return a TOS acceptance URL; the differences are *when* you call them and what you get back. * [`POST /customers/tos_links`](#post-customerstos_links) — create a fresh TOS link during onboarding. * [`POST /customers/tos_acceptance_link`](#post-customerstos_acceptance_link) — alias of `tos_links` (Bridge renamed the route; both still work). * [`GET /customers/{id}/tos_acceptance_link`](#get-customersidtos_acceptance_link) — read back the TOS link attached to an existing customer, with `accepted_at` filled in once the flow completes. ### `POST /customers/tos_links` Hosted URL for Terms of Service acceptance during new-customer onboarding. Send an `Idempotency-Key` header on repeated calls so you don't spawn duplicate links. Body: empty. **Response (200)** ```json { "url": "https://tos.seismic.systems/flow/abc123" } ``` ```bash curl -X POST "$BASE_URL/customers/tos_links" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" ``` ### `POST /customers/tos_acceptance_link` Alias of [`POST /customers/tos_links`](#post-customerstos_links). Same empty body, same response shape. Bridge exposes both route names for historical reasons — use whichever your existing Bridge code hits. ```bash curl -X POST "$BASE_URL/customers/tos_acceptance_link" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" ``` ### `GET /customers/{id}/tos_acceptance_link` Read back the TOS acceptance link attached to an existing customer — returns the same object [`POST /customers/tos_links`](#post-customerstos_links) originally produced, with acceptance state filled in. **Response (200)** ```json { "url": "https://tos.seismic.systems/flow/abc123", "accepted_at": "2026-04-24T12:00:00Z" } ``` `accepted_at` is `null` until the customer completes the hosted flow. ```bash curl "$BASE_URL/customers/cust_01HZ…/tos_acceptance_link" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ## `PUT /customers/{id}` Partial update. Any subset of the fields from [`POST /customers`](/bridge-compat/customers/create) is valid; unmentioned fields are left untouched. Same validation rules apply (e.g. conditionally-required fields stay conditionally required). The same [Accepted but ignored (individual)](/bridge-compat/customers/create#accepted-but-ignored-individual) / [Accepted but ignored (business)](/bridge-compat/customers/create#accepted-but-ignored-business) lists apply on update — sending a transliterated variant or an attested-at timestamp will be stored but won't change onboarding state. ### Response Returns the full [Customer object](/bridge-compat/customers/retrieve#the-customer-object). ```bash curl -X PUT "$BASE_URL/customers/cust_01HZ…" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "residential_address": { "street_line_1": "2 Market St", "city": "San Francisco", "subdivision": "US-CA", "postal_code": "94105", "country": "USA" } }' ``` ## `POST /customers/{id}/associated_persons` Add a new associated person to an existing business customer. Accepts the full [`AssociatedPerson`](/bridge-compat/schemas/associated-person) shape. ### Request ```json { "first_name": "Bob", "last_name": "Martinez", "email": "bob@example.com", "birth_date": "1985-07-14", "residential_address": { "street_line_1": "500 Market St", "city": "San Francisco", "subdivision": "US-CA", "postal_code": "94105", "country": "USA" }, "has_ownership": true, "has_control": false, "is_signer": false, "ownership_percentage": 25 } ``` ### Response (201) Same shape as [`GET /associated_persons/{id}`](/bridge-compat/associated-persons/retrieve#response-200). The parent customer's `status` may transition to `under_review` while the new person is verified. Returns `422 invalid_customer_type` if the parent customer is `type: "individual"`. ```bash curl -X POST "$BASE_URL/customers/cust_01HZ…/associated_persons" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "first_name": "Bob", "last_name": "Martinez", "email": "bob@example.com", "birth_date": "1985-07-14", "residential_address": { "street_line_1": "500 Market St", "city": "San Francisco", "subdivision": "US-CA", "postal_code": "94105", "country": "USA" }, "has_ownership": true, "has_control": false, "is_signer": false, "ownership_percentage": 25 }' ``` ## `DELETE /associated_persons/{id}` Soft-delete. The person is marked inactive on our side and excluded from future customer responses, but historical transfer and KYB records still resolve. If the associated person was already approved by the provider, provider-side state may still list them. See the KYB re-review note on the [Associated Persons overview](/bridge-compat/associated-persons). Returns `204 No Content` on success. ```bash curl -X DELETE "$BASE_URL/associated_persons/ap_01HZ…" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ### Alias `DELETE /customers/{customer-id}/associated_persons/{id}` resolves identically. ## Associated Persons Bridge's `/associated_persons` resource. Beneficial owners, controllers, signers, and directors attached to a business customer. The most common case — seeding them at onboarding — is covered by the [`associated_persons[]` array in the business customer create body](/bridge-compat/customers/create#request--business-type-business). The endpoints here are for post-onboarding lifecycle: adding a new owner, updating a title, removing someone who left. :::warning[KYB re-review] Mutating associated persons after a business customer is KYB-approved can retrigger provider review. The parent customer's `status` may drop back to `under_review` until the change is cleared. Treat these endpoints like a status-affecting operation, not a free-form edit. `DELETE` on an already-approved owner is especially rough — providers generally don't "forget" an approved person. We soft-delete on our side (mark inactive) so historical records remain consistent; the provider may continue to list them as an approved party. ::: The `AssociatedPerson` schema (all field definitions, enums, and nested sub-objects) lives under [Shared schemas](/bridge-compat/schemas/associated-person). ### Endpoints * [`GET /associated_persons/{id}`](/bridge-compat/associated-persons/retrieve) — fetch by id, no customer id required. * [`PUT /associated_persons/{id}`](/bridge-compat/associated-persons/update) — partial update. * [`DELETE /associated_persons/{id}`](/bridge-compat/associated-persons/delete) — soft-delete. * [`POST /customers/{id}/associated_persons`](/bridge-compat/associated-persons/create) — add a new person to an existing business customer. * [`GET /customers/{id}/associated_persons`](/bridge-compat/associated-persons/list) — list all persons on a customer. ### Nested path aliases Bridge also exposes the single-person endpoints scoped under the parent customer. These are **exact aliases** of the flat variants — same behavior, same response: * `GET /customers/{customer-id}/associated_persons/{id}` → [`GET /associated_persons/{id}`](/bridge-compat/associated-persons/retrieve) * `PUT /customers/{customer-id}/associated_persons/{id}` → [`PUT /associated_persons/{id}`](/bridge-compat/associated-persons/update) * `DELETE /customers/{customer-id}/associated_persons/{id}` → [`DELETE /associated_persons/{id}`](/bridge-compat/associated-persons/delete) If `{customer-id}` in the path doesn't match the person's actual `customer_id`, we return `404` — same as Bridge. ## `GET /customers/{id}/associated_persons` List all associated persons on a business customer. Tenant-scoped; returns `[]` for individual customers rather than `404`. ### Query | Param | Type | Default | | ---------------- | ------- | ------- | | `limit` | integer | `10` | | `starting_after` | string | — | | `ending_before` | string | — | ### Response Array of [associated-person objects](/bridge-compat/associated-persons/retrieve#response-200). ```bash curl "$BASE_URL/customers/cust_01HZ…/associated_persons" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ## `GET /associated_persons/{id}` Fetch by id — no customer id required. ```bash curl "$BASE_URL/associated_persons/ap_01HZ…" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ### Response (200) ```json { "id": "ap_01HZ…", "customer_id": "cust_01HZ…", "first_name": "Alice", "last_name": "Zhang", "email": "alice@example.com", "birth_date": "1990-04-01", "has_ownership": true, "has_control": true, "is_signer": true, "is_director": false, "title": "CEO", "ownership_percentage": 60, "status": "approved", "residential_address": { "street_line_1": "1 Market St", "city": "San Francisco", "subdivision": "US-CA", "postal_code": "94105", "country": "USA" }, "created_at": "2026-04-24T12:00:00Z", "updated_at": "2026-04-24T12:00:00Z" } ``` `status` mirrors the parent customer's capability states: `pending`, `under_review`, `approved`, `rejected`. All other fields are from the [`AssociatedPerson`](/bridge-compat/schemas/associated-person) schema. ### Alias `GET /customers/{customer-id}/associated_persons/{id}` resolves identically; the extra `{customer-id}` in the path just adds a scoping check (returns `404` if it doesn't match the person's `customer_id`). ## `PUT /associated_persons/{id}` Partial update. Any field from the [`AssociatedPerson`](/bridge-compat/schemas/associated-person) schema may be sent; omitted fields are left unchanged. Mutating person-identity fields (`first_name`, `last_name`, `birth_date`, `residential_address`, `identifying_information`) will typically retrigger KYC on that person and may drop the parent customer's status to `under_review`. Role-only changes (`title`, `ownership_percentage`, `is_signer`) are less likely to re-open review but aren't guaranteed not to. ### Response Returns the updated person object — same shape as [`GET /associated_persons/{id}`](/bridge-compat/associated-persons/retrieve#response-200). ```bash curl -X PUT "$BASE_URL/associated_persons/ap_01HZ…" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "Chief Executive Officer", "ownership_percentage": 55 }' ``` ### Alias `PUT /customers/{customer-id}/associated_persons/{id}` resolves identically. ## `GET /currencies` Every currency we accept on at least one rail, with the rails that carry it. **Unauthenticated.** No `Api-Key` or `Authorization` header required. **Response `200`** ```json { "currencies": [ { "currency": "USD", "kind": "fiat", "rails": [ { "rail": "ach" }, { "rail": "wire" } ] }, { "currency": "EUR", "kind": "fiat", "rails": [ { "rail": "sepa" }, { "rail": "swift" } ] }, { "currency": "INR", "kind": "fiat", "rails": [{ "rail": "upi" }] }, { "currency": "USDC", "kind": "stablecoin", "rails": [ { "rail": "base" }, { "rail": "ethereum" }, { "rail": "polygon" }, { "rail": "solana" } ] }, { "currency": "USDT", "kind": "stablecoin", "rails": [ { "rail": "ethereum" }, { "rail": "arbitrum" } ] } ] } ``` :::info[Full wire shape] Samples show `currency`, `kind`, and `rail` only. The live response can include extra opaque fields on each rail object used for pricing — see the `CurrencyRail` type exported from `seismic-orchestration` (or your OpenAPI client) for the complete schema. ::: ### `CurrencyInfo` | Field | Type | Notes | | ---------- | ---------------------------------------- | ---------------------------------------------------------------------------------- | | `currency` | string | ISO-4217 (`"USD"`, `"EUR"`) for fiat; ticker (`"USDC"`, `"USDT"`) for stablecoins. | | `kind` | `"fiat"` \| `"stablecoin"` \| `"crypto"` | Currency category. | | `rails` | array of [`CurrencyRail`](#currencyrail) | The rails that carry this currency. | #### `CurrencyRail` At minimum, each element includes: | Field | Type | Notes | | ------ | ------ | ------------------------------------------------- | | `rail` | string | Rail identifier — see [`GET /rails`](/api/rails). | **Example** ```bash curl "$SEISMIC_ORCHESTRATION_URL/v0/currencies" ``` ## Errors Every non-2xx response uses the same JSON envelope: ```json { "ok": false, "error": { "code": "AccountNotFound", "message": "No account found for id acc_1a2b" } } ``` * **`ok`** — literal `false`, present only on errors. Success responses are flat payloads (e.g. `{ "order_id": "f01e…" }`) with no `ok` field. Callers that want body-level dispatch can check `body.ok === false`; HTTP status is still authoritative. This mirrors the semantics of `fetch`'s `Response.ok`. * **`code`** — stable PascalCase enum value. Safe to switch on programmatically; will not change across minor releases. New codes may be added — treat unknown codes as a generic failure at that HTTP status. * **`message`** — human-readable detail with caller context. Surface to end users. ### Status codes The HTTP status categorises — the `code` field identifies. | Status | Meaning | Typical causes | | ------ | -------------------- | --------------------------------------------------------------------------- | | 400 | Bad request | Malformed body, invalid enum, rule violation (see codes below). | | 401 | Unauthenticated | Missing / expired access token — refresh and retry. | | 403 | Not allowed | Authed, but you don't own the resource. | | 404 | Not found | Unknown id, or scoped-away (also returned for "not yours"). | | 409 | Conflict | Duplicate row or idempotency violation. | | 422 | Unprocessable entity | Semantic failure — corridor cannot be fulfilled, snapshot not ready, … | | 5xx | Server-side | Temporary upstream issue, DB issue. Safe to retry with exponential backoff. | ### Error codes Codes group by surface. Not exhaustive — the server may add codes; unknown codes should be handled as a generic failure at their HTTP status. #### Orgs + KYB | `code` | Status | Meaning | | ---------------------- | ------ | ---------------------------------------------------------------------------- | | `OrgNotFound` | 404 | Unknown org id (or credential isn't scoped to it). | | `NotOrgMember` | 403 | Authed, but the user isn't a member of the requested org. | | `LastOwner` | 409 | Demote / remove would leave the org with zero owners. Promote someone first. | | `KybProfileIncomplete` | 422 | Required KYB fields are missing — see `missing` array on the error body. | | `KybNotApproved` | 422 | Action requires business verification to be further along. | #### Auth | `code` | Status | Meaning | | --------------------- | ------ | ------------------------------------------------------------------------------------------------ | | `PasswordTooShort` | 400 | Password below minimum length. | | `EmailAlreadyExists` | 409 | Signup email already registered. | | `InvalidCredentials` | 401 | Bad email/password on login. | | `MfaRequired` | 200¹ | Login succeeded but MFA challenge is required — handle the `LoginResponse` status, not an error. | | `MfaChallengeInvalid` | 401 | Wrong code, expired challenge, or clock skew > 30s. | | `RefreshTokenInvalid` | 401 | Refresh token expired or revoked. Send the user to login. | ¹ Not an error — included here to disambiguate. #### API keys | `code` | Status | Meaning | | ------------------- | ------ | --------------------------------------------------------------------------- | | `BothCredsProvided` | 400 | Request had both `Api-Key:` and `Authorization: Bearer` headers — pick one. | | `ApiKeyNotFound` | 404 | Unknown key id (or belongs to a different org). | | `ApiKeyRevoked` | 401 | Key was valid but is now revoked. Mint a fresh one. | | `AlreadyRevoked` | 409 | Revoke called on a key that was already revoked. | #### Customers + accounts | `code` | Status | Meaning | | ----------------------- | ------ | ----------------------------------------------------------------- | | `CustomerNotFound` | 404 | Unknown id (or belongs to a different org). | | `EndUserCustomerExists` | 409 | This org + external id is already onboarded. | | `KycNotApproved` | 422 | Account creation requires the customer's KYC to be `approved`. | | `AccountNotFound` | 404 | Unknown account id. | | `RailNotSupported` | 400 | Rail not in the whitelist for that currency (e.g. USDC rails). | | `ProviderMismatch` | 400 | The routing id on the request is not valid for this customer yet. | #### Quotes | `code` | Status | Meaning | | ----------------------------------------- | ------ | ---------------------------------------------------------------------------------------------------- | | `NoEligibleProviders` | 422 | Nothing priced this corridor right now. | | `QuoteNotFound` | 404 | Snapshot id unknown (or not yours). | | `QuoteNotReady` | 422 | Snapshot is still `pending` — poll `GET /quotes/:id`. | | `QuoteExpired` | 422 | The chosen quote's `expires_at` has passed. Re-create a snapshot. | | `QuoteIdInvalid` | 400 | `quote_id` in the accept body isn't in `snapshot.quotes[]`. | | `AlreadyAccepted` | 409 | Snapshot has already been turned into an order. | | [`AmbiguousCurrency`](#ambiguouscurrency) | 400 | Account-by-id reference matched an account with multiple `currencies[]`; no `currency` was supplied. | | [`AmbiguousRail`](#ambiguousrail) | 400 | Account-by-id reference matched an account with multiple `rails[]`; no `rail` was supplied. | ##### `AmbiguousCurrency` Returned when a quote endpoint references an account that supports more than one currency and the request didn't pin one. The error body lists the supported currencies so the caller can surface a picker: ```json { "ok": false, "error": { "code": "AmbiguousCurrency", "message": "Account acc_evm_wallet_1a2b supports multiple currencies — pin one via { account, currency }.", "account_id": "acc_evm_wallet_1a2b", "currencies": ["USDC", "USDT"] } } ``` Disambiguate by replacing the bare-string form with `{ account, currency, rail? }` — see [Quotes → Disambiguation](/api/quotes#disambiguation). ##### `AmbiguousRail` Same shape as [`AmbiguousCurrency`](#ambiguouscurrency) but for the rail axis — returned when the account supports >1 rail (e.g. an EVM wallet reachable on Base + Ethereum + Polygon, or a US bank account that accepts both ACH and wire) and no `rail` was pinned. ```json { "ok": false, "error": { "code": "AmbiguousRail", "message": "Account acc_evm_wallet_1a2b supports multiple rails for USDC — pin one via { account, currency, rail } or omit `rail` to price every supported rail for this account.", "account_id": "acc_evm_wallet_1a2b", "currency": "USDC", "rails": ["base", "ethereum", "polygon"] } } ``` Note: omitting `rail` on the `{ account, currency }` form is **not** ambiguous — the server prices every supported rail for that currency on that account and returns one quote per priced leg. `AmbiguousRail` only fires for the bare-string form, where the request does not carry enough structure to expand rails automatically. #### Generic | `code` | Status | Meaning | | ---------------- | ------ | ---------------------------------------------------------------------- | | `InvalidRequest` | 400 | Catch-all for malformed body / invalid enum. `message` has the detail. | | `Unauthorized` | 401 | Missing or invalid bearer token. | | `Forbidden` | 403 | Authed, but not permitted. | | `InternalError` | 500 | Unexpected server failure. Safe to retry. | | `ProviderError` | 502 | Temporary upstream dependency returned an error. Retryable. | | `Unknown` | — | Client-side fallback when the response body couldn't be parsed. | ### Retry guidance :::tip[Safe retries] * **4xx** — don't retry until the caller fixes the input. Idempotency shields you from double-charges on the rare 4xx-after-success case. * **5xx** — retry with exponential backoff, capped at \~5 attempts. Provider timeouts sometimes surface as `502`/`504`. * **`401`** — refresh the access token and retry. If it 401s again, treat as `RefreshTokenInvalid` and send the user to login. ::: ### Handling specific cases A few codes are worth dispatching on rather than treating as a generic failure: * **`QuoteExpired`** — re-create the snapshot, show the new price, let the user re-confirm. * **`AlreadyAccepted`** — treat as success: a prior request won the race. * **`QuoteIdInvalid`** — UI sent an id that isn't in the snapshot. Log + report. ## API Reference The native Seismic Orchestration API. All endpoints live under `/api/v0/*` — paths in the per-endpoint pages drop the `/v0/` prefix for readability. Curl snippets throughout assume `$SEISMIC_ORCHESTRATION_URL` and `$SEISMIC_ORCHESTRATION_API_KEY`. | Environment | Base URL | | ----------- | --------------------------------------------------- | | Sandbox | `https://orchestration-sandbox.seismic.systems/api` | | Production | *Coming soon.* | ### Sections * [**Orgs**](/api/orgs) — `/orgs/*`: tenants, memberships, the unified KYB profile, business verification state. * [**Auth**](/api/auth) — `/auth/*`: signup, login, MFA, refresh, logout, me, plus [API keys](/api/auth/api-keys) (mint / list / revoke). * [**Users**](/api/users) — `/user` and `/user/orders`: the user behind the credential plus their order history. * [**Customers**](/api/customers) — `/customers`: end-user customers owned by the org (NOT the org's own KYB — that's under Orgs). Includes `/customers/:id/accounts` for attaching funding / payout accounts. * [**Accounts**](/api/accounts) — `/accounts`: read-side endpoints for the accounts attached across the org's customers. Creation lives under Customers. * [**Quotes**](/api/quotes) — `/quotes`: priced legs for a corridor, explicit accept step, the richest surface we expose. * [**Catalog**](/api/rails) — [`/rails`](/api/rails) and [`/currencies`](/api/currencies): unauthenticated reference of supported rails + currencies. * [**Errors**](/api/errors) — envelope shape, error codes, retry semantics. ### Bridge compat If you already integrated [Bridge](https://bridge.xyz), we ship a best-effort compatibility layer at `/api/compat/bridge/v0/*` that mirrors Bridge's request / response shapes for the endpoints we implement (customers, KYC links, external accounts, transfers, exchange rates). A base-URL swap gets most integrations running without code changes. **Start here:** [Bridge compat overview](/bridge-compat/intro). The two surfaces share one `Api-Key` and one tenant — same account, two API shapes. If you're starting a new integration, use the native API above; the compat layer exists for existing Bridge callers. ## `GET /rails` Every settlement rail we support. Useful for filling rail pickers without round-tripping a quote first. **Unauthenticated.** No `Api-Key` or `Authorization` header required. **Response `200`** ```json { "rails": [ { "rail": "ach", "kind": "fiat" }, { "rail": "wire", "kind": "fiat" }, { "rail": "sepa", "kind": "fiat" }, { "rail": "swift", "kind": "fiat" }, { "rail": "upi", "kind": "fiat" }, { "rail": "pix", "kind": "fiat" }, { "rail": "base", "kind": "crypto" }, { "rail": "ethereum", "kind": "crypto" }, { "rail": "polygon", "kind": "crypto" }, { "rail": "solana", "kind": "crypto" }, { "rail": "arbitrum", "kind": "crypto" } ] } ``` :::info[Full wire shape] Samples show `rail` and `kind` only. The live response can include extra opaque fields used for pricing — see the `RailInfo` type exported from `seismic-orchestration` (or your OpenAPI client) for the complete schema. ::: ### `RailInfo` At minimum, each element includes: | Field | Type | Notes | | ------ | ---------------------- | ------------------------------------------------ | | `rail` | string | Stable rail identifier — `"ach"`, `"base"`, etc. | | `kind` | `"fiat"` \| `"crypto"` | Bank rail vs blockchain network. | The list is the union across all corridors and currencies — for the per-currency rail set, use [`GET /currencies`](/api/currencies). **Example** ```bash curl "$SEISMIC_ORCHESTRATION_URL/v0/rails" ``` ## Users The user behind the credential is the user who logged in (token auth) or the user who minted the API key. Both endpoints scope to that user. ### Endpoints * [`GET /user`](/api/users/retrieve) — full profile, memberships, MFA state. * [`GET /user/orders`](/api/users/orders) — most-recent-first order history. ## `GET /user/orders` Most-recent-first list of orders the authenticated user has kicked off, capped at 200. **Response `200`** — array of [`RampOrder`](#ramporder). ```json [ { "id": "ord_2f10…", "user_id": "0a2a3f…", "ramp_customer_id": "cus_7c4b…", "provider": "prv_…", "provider_order_id": "WP-9d8c0a1b", "direction": "on_ramp", "status": "completed", "source_currency": "USD", "source_amount": "1000.00", "destination_currency": "USDC", "destination_chain": "base", "destination_address": "0xabc…123", "source_account_id": "acc_5e3a…", "destination_account_id": null, "idempotency_key": "8b3c4d5e-…", "funding_instructions": null, "reference": "invoice-2026-04-001", "comment": null, "created_at": "2026-04-23T10:15:00Z", "updated_at": "2026-04-23T10:18:42Z" } ] ``` **Example** ```bash curl "$SEISMIC_ORCHESTRATION_URL/v0/user/orders" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` :::tip[Pagination roadmap] `limit=200` is hard-coded today. If you need older orders, open an issue — we'll add cursor-based pagination when the first user bumps the ceiling. ::: ### `RampOrder` | Field | Type | Notes | | ------------------------ | ----------------------------- | -------------------------------------------------------------------------------------- | | `id` | string (UUID) | Our id. | | `user_id` | string (UUID) | Order initiator. | | `ramp_customer_id` | string (UUID) | The end-user customer the order is for. | | `provider` | string | Opaque routing id for the order leg. | | `provider_order_id` | string \| null | Platform reference for this order while it settles. | | `direction` | `"on_ramp"` \| `"off_ramp"` | Fiat → crypto vs crypto → fiat. | | `status` | [`OrderStatus`](#orderstatus) | Logical status we normalize for callers. | | `source_currency` | string | ISO-4217 or stablecoin symbol. | | `source_amount` | string | String to avoid float drift. Fee-inclusive (matches the accepted quote's `in.amount`). | | `destination_currency` | string | | | `destination_chain` | string \| null | For crypto destinations — `"base"`, `"ethereum"`, etc. | | `destination_address` | string \| null | Wallet address when destination is a crypto wallet. | | `source_account_id` | string (UUID) \| null | Resolved attached account id, if the order originated from one. | | `destination_account_id` | string (UUID) \| null | Same on the destination side. | | `idempotency_key` | string | Echo of the key the order was placed under. | | `funding_instructions` | object \| null | Bank-rail orders return wire/ACH instructions here. | | `reference` | string \| null | Optional caller-supplied reference. | | `comment` | string \| null | Optional caller-supplied comment. | | `created_at` | string (ISO-8601) | | | `updated_at` | string (ISO-8601) | | #### `OrderStatus` A string enum — one of `"awaiting_funds"`, `"pending"`, `"processing"`, `"completed"`, `"expired"`, `"failed"`, `"canceled"`, `"unknown"`. We map external statuses into this set; if we cannot classify a state, we surface `"unknown"`. ## `GET /user` Full profile for the user behind the credential — user row + org memberships + TOTP activation state. **Response `200`** ```json { "id": "0a2a3f…", "email": "alice@example.com", "mfa_required": false, "mfa_active": true, "created_at": "2026-04-01T14:22:00Z", "memberships": [ { "org_id": "b3c7…", "org_name": "Acme Inc.", "role": "admin" } ] } ``` | Field | Notes | | --------------- | -------------------------------------------------------------- | | `mfa_required` | Whether the account is **required** to use MFA (admin-set). | | `mfa_active` | Whether the user has *actually* activated TOTP. | | `memberships[]` | One row per org the user belongs to. `role` is per-membership. | For an API key caller, "the user behind the credential" is the user who minted the key. **Example** ```bash curl "$SEISMIC_ORCHESTRATION_URL/v0/user" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ## `POST /quotes/:id/accept` Turn a snapshot into an order. ### Request body With no body (or `{}`), the snapshot's `best_quote_id` is accepted. To pick a specific quote, pass `{ "quote_id": "" }`. There is no shorthand keyed only by rail — the same inbound rail (for example ACH) can appear on more than one row with different fees or outcomes, so the caller must pass the exact `quote_id` from `snapshot.quotes[]`. ### Response `200` ```json { "order_id": "f01e…" } ``` :::info[Wire fields] The live body can include additional opaque routing metadata. See `AcceptQuoteResult` in `seismic-orchestration` for the full schema. ::: ### Errors See the [Errors reference](/api/errors) for the full `code` enum: | Code | Status | Meaning | | ----------------- | ------ | ----------------------------------------------- | | `QuoteIdInvalid` | 400 | `quote_id` isn't in the snapshot. | | `QuoteNotFound` | 404 | Snapshot id unknown. | | `AlreadyAccepted` | 409 | Snapshot has already been turned into an order. | | `QuoteNotReady` | 422 | Snapshot is still `pending`. | | `QuoteExpired` | 422 | The chosen quote's `expires_at` has passed. | ### Examples :::code-group ```bash [best] curl -X POST "$SEISMIC_ORCHESTRATION_URL/v0/quotes/9d8c…/accept" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" \ -H "content-type: application/json" \ -d '{}' ``` ```bash [by quote_id] curl -X POST "$SEISMIC_ORCHESTRATION_URL/v0/quotes/9d8c…/accept" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" \ -H "content-type: application/json" \ -d '{ "quote_id": "q_9e4d…" }' ``` ::: ## `POST /quotes` Create a quote request. We price the corridor, persist the snapshot in `ramp_quote_requests`, and return ranked legs. For the conceptual model — directionality, rails, fee invariants — see the [Quotes overview](/api/quotes). ### Query params | Name | Default | Meaning | | ------ | ------- | ---------------------------------------------------------------- | | `wait` | `0` | `1` / `true` / `yes` / `on` → block until the snapshot is ready. | ### Request body [`QuoteRequest`](/api/quotes#quoterequest). Common shapes: | Variant | Source | Destination | Notes | | ----------------------------------------------- | ----------------------- | ------------------------- | -------------------------------------- | | [Pricing preview, multi-rail](#multi-rail) | currency only | currency + rail | broadest comparison on the source side | | [Pricing preview, single rail](#single-rail) | currency + rail | currency + rail | rails pinned on both sides | | [Settlement-ready](#settlement-ready) | attached account | attached account | rails read off stored accounts | | [Multi-currency wallet](#multi-currency-wallet) | `{ account, currency }` | currency + rail | token pinned on the source account | | [Swap to one-off address](#one-off-address) | attached account | currency + rail + address | one-off crypto destination | | [Named-funding only](#named-funding) | currency \[+ rail] | currency + rail | named virtual account filter only | ##### Pricing preview, multi-rail No rail pinned on the source side — the server prices every USD-in rail we support for this corridor. Use this to compare prices before you've collected the user's funding choice. ```json { "amount": "1000", "source": { "currency": "USD" }, "destination": { "currency": "USDC", "rail": "base" } } ``` ##### Pricing preview, single rail Same corridor but pinned to ACH on the source side — one priced leg per opaque routing id that supports ACH for this corridor. ```json { "amount": "1000", "source": { "currency": "USD", "rail": "ach" }, "destination": { "currency": "USDC", "rail": "base" } } ``` ##### Settlement-ready (attached accounts both sides) Both legs reference attached accounts. Rail is read off each account; we price every routing context that matches the corridor. Accept the response to place a real order — no further account collection needed. ```json { "amount": "500", "source": "acc_usdc_base_1a2b", "destination": "acc_usd_ach_9c8d" } ``` ##### Multi-currency wallet When the account supports more than one currency (a typical EVM wallet holding both `USDC` and `USDT`), the bare-string form returns [`AmbiguousCurrency`](/api/errors#ambiguouscurrency) with the supported list. Disambiguate with the object form: ```json { "amount": "200", "source": { "account": "acc_evm_wallet_1a2b", "currency": "USDC", "rail": "base" }, "destination": { "currency": "USD", "rail": "ach" } } ``` Omit `rail` and the server fans out across the account's `rails[]` for that currency — useful when you want to compare the same `USDC` send across `base` / `ethereum` / `polygon`. ##### Swap to a one-off address Source is an attached account; destination is an inline crypto descriptor with an address. Useful for paying out to a wallet the customer hasn't pre-attached. ```json { "amount": "200", "source": "acc_usdc_base_1a2b", "destination": { "currency": "USDC", "rail": "ethereum", "address": "0xabc…" } } ``` ##### Named-funding only Add `"named": "require"` to keep only legs where fiat funding instructions are **named virtual accounts** for the customer — i.e. when the customer wires funds in, the bank statement shows the customer's name as the originator/payee. Stack on top of any of the variants above. The filter applies to the **fiat leg only** (the source on an on-ramp, the destination on an off-ramp). It's a no-op on crypto-to-crypto swaps, where it's silently ignored. ```json { "amount": "1000", "source": { "currency": "USD" }, "destination": { "currency": "USDC", "rail": "base" }, "named": "require" } ``` ### Response `200` [`AsyncQuoteSnapshot`](/api/quotes#asyncquotesnapshot). Below: the response to the first pricing-preview request (rail omitted on the source). The `quotes` array can contain **multiple rows for the same inbound fiat rail** (for example separate ACH vs wire legs), ranked by `out.amount`: ```json { "id": "9d8c…", "status": "ready", "created_at": "2026-04-23T10:20:00Z", "request": { "amount": "1000", "source": { "currency": "USD" }, "destination": { "currency": "USDC", "rail": "base" } }, "quotes": [ { "id": "q_7f3a…", "in": { "amount": "1000", "currency": "USD", "rail": "ach" }, "out": { "amount": "997", "currency": "USDC", "rail": "base" }, "fees": { "items": [{ "type": "flat", "amount": "3.00", "currency": "USD" }], "total": "3.00", "currency": "USD" }, "rate": "0.997", "expires_at": "2026-04-23T10:21:00Z" }, { "id": "q_2c1b…", "in": { "amount": "1000", "currency": "USD", "rail": "ach" }, "out": { "amount": "995", "currency": "USDC", "rail": "base" }, "fees": { "items": [{ "type": "flat", "amount": "2.50", "currency": "USD" }], "total": "2.50", "currency": "USD" }, "rate": "0.995" }, { "id": "q_9e4d…", "in": { "amount": "1000", "currency": "USD", "rail": "wire" }, "out": { "amount": "974", "currency": "USDC", "rail": "base" }, "fees": { "items": [{ "type": "flat", "amount": "25.00", "currency": "USD", "description": "Wire transfer fee" }], "total": "25.00", "currency": "USD" }, "rate": "0.974" } ], "best_quote_id": "q_7f3a…" } ``` :::info[Wire fields] Each quote row can include additional opaque routing metadata on the live response. See the `Quote` type in `seismic-orchestration` for the full schema. ::: When either side is an attached `account_id`, that leg's `in` / `out` additionally carries a compact `account` reference so the caller can render which account resolved where without a second round-trip — see [Endpoint shape](/api/quotes#endpoint-shape) on the overview for the fields. ### Errors `400` (invalid request, including [`AmbiguousCurrency`](/api/errors#ambiguouscurrency) / [`AmbiguousRail`](/api/errors#ambiguousrail) when an account-by-id reference doesn't pin the choice), `422` (nothing priced this corridor right now). ### Example ```bash curl -X POST "$SEISMIC_ORCHESTRATION_URL/v0/quotes?wait=1" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" \ -H "content-type: application/json" \ -d '{ "amount": "1000", "source": { "currency": "USD" }, "destination": { "currency": "USDC", "rail": "base" } }' ``` ## Quotes All endpoints require auth — either an `Api-Key: ` (org-scoped, recommended) or `Authorization: Bearer ` (dashboard session). Curl snippets assume `$SEISMIC_ORCHESTRATION_URL` and `$SEISMIC_ORCHESTRATION_API_KEY` — see [Auth](/api/auth) for the convention. ### Endpoints * [`POST /quotes`](/api/quotes/create) — create a quote request; we return ranked priced legs for your corridor. * [`GET /quotes/:id`](/api/quotes/retrieve) — re-read a snapshot by id. * [`POST /quotes/:id/accept`](/api/quotes/accept) — turn a snapshot into an order. ### Directionality The quote API is direction-agnostic: onramp, offramp, and stablecoin swap are all the same shape. You describe both sides of the trade and the amount to price. | Scenario | `source` | `destination` | | -------- | ---------------------------------- | ------------------------------------ | | Onramp | fiat (`USD` via `ach` / `wire`, …) | stablecoin (`USDC` on `base`, …) | | Offramp | stablecoin | fiat (`EUR` via `sepa`, …) | | Swap | stablecoin | stablecoin (possibly different rail) | Each side is one of three forms: * **A bare account id string** — `"acc_1a2b…"`. Shorthand for an attached [`Account`](/api/customers#account). Only valid when the account supports exactly one currency **and** one rail; otherwise the server returns [`AmbiguousCurrency`](/api/errors#ambiguouscurrency) / [`AmbiguousRail`](/api/errors#ambiguousrail) — see [Disambiguation](#disambiguation). * **An account with disambiguation** — `{ account, currency?, rail? }`. Use this for crypto wallets that hold multiple tokens or span multiple chains. The fields you set must be in the account's `currencies[]` / `rails[]`. * **An inline descriptor** — `{ currency, rail?, address? }`. Pricing-only quotes (no settlement account yet) or one-off destinations not attached to the customer. `rail` is the settlement path and unifies fiat + crypto into one field: | Kind | Examples | | ------ | --------------------------------------------------- | | Fiat | `ach`, `wire`, `swift`, `sepa`, `upi`, `pix` | | Crypto | `base`, `ethereum`, `polygon`, `solana`, `arbitrum` | #### Rails on descriptors On a descriptor, `rail` is **optional**: * **Specify `rail`** to pin a single settlement path (e.g. you only want ACH quotes). * **Omit `rail`** to compare every rail that corridor supports. The server returns one ranked quote per priced leg — the same opaque routing id can appear more than once when multiple rails price for the same corridor. When the endpoint is an account, the account's rail is used directly — unless the account supports multiple rails, in which case omitting `rail` prices every supported rail for that account + currency. #### Disambiguation A single account can support multiple currencies (a crypto wallet holding `USDC` + `USDT`) or multiple rails (a US bank account that accepts both `ach` and `wire`, an EVM address reachable on `base` + `ethereum`). When an account is referenced in a quote and the choice isn't unique, the server returns: * [`AmbiguousCurrency`](/api/errors#ambiguouscurrency) — the request didn't pin a currency and the account supports more than one. The error body includes `currencies: string[]`. * [`AmbiguousRail`](/api/errors#ambiguousrail) — the request didn't pin a rail and the account supports more than one. The error body includes `rails: string[]`. Disambiguate by replacing the bare-string form with `{ account, currency?, rail? }`: ```json { "amount": "200", "source": { "account": "acc_evm_wallet_1a2b", "currency": "USDC", "rail": "base" }, "destination": { "currency": "USD", "rail": "ach" } } ``` You can also omit `rail` and let the server price every entry in the account's `rails[]` for that currency — e.g. `{ account, currency: "USDC" }` returns one quote per supported chain. On the response, **every quote's `in` / `out` has `rail` populated** — the caller never has to disambiguate. For a crypto leg, `out` also carries the destination `address` once known. `amount` is always denominated in the **source** currency and passed as a **string** to avoid float drift. ### Amounts and fees — the invariant One rule, everywhere: * **`in.amount`** — what the user pays, **fee-inclusive**. All itemized fees are already part of this number. * **`out.amount`** — what lands at the destination. * **`fees.items[]`** — the *visible* slice of the difference between `in.amount × rate` and `out.amount` (developer fees you set, plus any explicit fees we itemize). * **Nothing is ever added on top.** Fees are always already deducted in getting from `in.amount` to `out.amount`. :::warning[Hidden spreads] Some priced legs bake margin into the quoted `rate` without itemizing it separately. In those cases `fees.total` will under-report the true cost — the spread is invisible but still affects `out.amount`. If you need full cost transparency, compare the effective rate (`out.amount / in.amount`) against a mid-market reference. ::: ### Endpoint shape Each `in` / `out`: | Field | Type | Notes | | ---------- | ------ | ----------------------------------------------------------------------------------------------------------------------------- | | `amount` | String | String to avoid float drift. Denominated in `currency`. | | `currency` | String | ISO-4217 or stablecoin symbol. | | `rail` | String | Always present. Read off the account or echoed from the descriptor. | | `address` | String | Crypto legs only. Echoed from the descriptor or from the resolved `Account`'s wallet. | | `account` | Object | Present when this side was resolved from an `account_id`. Compact reference (see below). Absent for pricing-only descriptors. | `account` carries just enough to render UI ("Paid from your Chase checking **…9c8d**") without forcing a second round-trip to the full `Account`: | Field | Type | Notes | | --------------------- | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | | `id` | string | Account UUID. | | `account_type` | `"us_bank_account"` \| `"external_digital_asset_wallet"` \| `"virtual_digital_asset_wallet"` \| `"iban"` \| `"upi"` | | | `status` | `"pending"` \| `"active"` \| `"disabled"` | | | `provider_account_id` | string | Opaque account id; useful for support and reconciliation. | For timestamps and full account metadata, the caller already holds the [`Account`](/api/customers#account) from `createAccount` — look it up by `account.id`. ### Fee breakdown Each quote's `fees` block has: | Field | Type | Meaning | | ---------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | | `items[]` | Array of tagged fee lines (`flat`, `percentage`, `network`, `other`) | One entry per distinct charge. Single-fee legs emit one `flat` line equal to `total`. | | `total` | String | Sum across `items`, denominated in `currency`. | | `currency` | String | Fee currency — typically the source currency (what the caller pays with). | `items[]` is a tagged discriminated union: * **`flat`** — `{ type, amount, currency, description? }`. Fixed charge. * **`percentage`** — `{ type, bps, amount, currency, description? }`. Basis points of the source amount; `amount` is the computed charge at quote time. * **`network`** — `{ type, amount, currency, description? }`. On-chain gas / settlement cost for crypto legs. * **`other`** — `{ type, amount, currency, description }`. Catch-all; `description` is required so the caller can render a label. ### Types #### `QuoteRequest` The body shape for [`POST /quotes`](/api/quotes/create). | Field | Type | Required | Notes | | ------------- | --------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `amount` | string | yes | Denominated in the **source** currency. String to avoid float drift. | | `source` | [`QuoteEndpoint`](#quoteendpoint) | yes | Account-id string, or inline descriptor. | | `destination` | [`QuoteEndpoint`](#quoteendpoint) | yes | Account-id string, or inline descriptor. | | `named` | `"require"` | no | **Fiat legs only** — applies to whichever side is fiat (the source on an on-ramp, the destination on an off-ramp). When set, only returns legs where fiat funding instructions are **named virtual accounts** for the customer (deposits show the customer's name on the bank statement). Omit for no filter. The enum will grow — `"forbid"`, `"prefer"`, etc. — when there's a use case. | #### `QuoteEndpoint` A discriminated union — one of: * **string** — an attached account id (`"acc_…"`). Currency + rail are read off the stored account; only valid when the account is unambiguous (exactly one currency + one rail). * **account-with-disambiguation** — `{ account, currency?, rail? }`. Required for accounts that support multiple currencies or rails. * **inline descriptor** — `{ currency, rail?, address? }`. Pricing-only quotes or one-off destinations. Account-with-disambiguation: | Field | Type | Required | Notes | | ---------- | ------ | -------------- | --------------------------------------------------------------------------- | | `account` | string | yes | Attached account UUID. | | `currency` | string | when ambiguous | Must appear in the account's `currencies[]`. | | `rail` | string | no | Must appear in the account's `rails[]`. Omit to price every supported rail. | Inline descriptor: | Field | Type | Required | Notes | | ---------- | ------ | -------- | ------------------------------------------------------------ | | `currency` | string | yes | ISO-4217 (`"USD"`) or stablecoin symbol (`"USDC"`). | | `rail` | string | no | Pin a rail. Omit to price every rail this corridor supports. | | `address` | string | no | Crypto destinations only. Wallet address. | #### `AsyncQuoteSnapshot` Returned by [`POST /quotes`](/api/quotes/create) and [`GET /quotes/:id`](/api/quotes/retrieve). | Field | Type | Notes | | --------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `id` | string (UUID) | Snapshot id. Pass to [`/accept`](/api/quotes/accept). | | `status` | `"pending"` \| `"ready"` \| `"expired"` | `pending` only with `?wait=0`; `ready` once every priced rail leg has landed in the snapshot. | | `quotes` | array of [`Quote`](#quote) | Ranked best-first. One entry per **priced leg** — disambiguate with `in.rail` / `out.rail` (and each row's `id` when accepting). | | `best_quote_id` | string (optional) | The server's recommended quote id. Matches `quotes[0].id` today. | | `request` | [`QuoteRequest`](#quoterequest) | Echo of the original request. | | `created_at` | string (ISO-8601) | | #### `Quote` A single priced leg for the requested corridor. **`in.rail`** and **`out.rail`** describe how money moves; each row has its own **`id`** for [`/accept`](/api/quotes/accept). | Field | Type | Notes | | ------------ | ---------------------------------------- | ---------------------------------------------------------------------------------- | | `id` | string | Pass to `/accept` to pick this exact leg. | | `provider` | string | Opaque routing id on the wire (treat as internal; prefer rails + `id` in your UI). | | `in` | [`QuoteEndpointAmount`](#endpoint-shape) | Source side. `amount` is fee-inclusive (what the user pays). | | `out` | [`QuoteEndpointAmount`](#endpoint-shape) | Destination side. `amount` is what lands. | | `fees` | [`QuoteFees`](#fee-breakdown) | Itemized fees + total. | | `rate` | string | Effective `out.amount / in.amount`. | | `expires_at` | string (ISO-8601, optional) | When this specific quote stops being honored. | ## `GET /quotes/:id` Re-read a snapshot by id. Useful when [`create`](/api/quotes/create) was called with `wait=0` and returned a pending snapshot — poll until `status === "ready"`. **Response `200`** — [`AsyncQuoteSnapshot`](/api/quotes#asyncquotesnapshot). Same shape as the [create response](/api/quotes/create#response-200). **Errors:** `404` (unknown id, or not yours). **Example** ```bash curl "$SEISMIC_ORCHESTRATION_URL/v0/quotes/9d8c…" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ## Orgs An **org** is the tenant. Customers, API keys, KYB submissions, quotes, and orders all hang off an org. A user can belong to one or more orgs; the common case is one. Multi-org users get a "Switch org" UI on the dashboard; multi-org API callers mint a separate key per org. When an org submits a KYB profile, we run the verification and routing work the platform requires — you fill one unified form in the dashboard; we handle the rest. ### Endpoints #### Org * [`GET /orgs`](/api/orgs/list) — list orgs the caller belongs to (dashboard sessions only). * [`GET /org`](/api/orgs/retrieve) — read the org bound to the current credential. * [`PATCH /org`](/api/orgs/update) — update display name and settings. #### Members * [`GET /org/members`](/api/orgs/members/list) * [`POST /org/members`](/api/orgs/members/invite) * [`PATCH /org/members/{user_id}`](/api/orgs/members/update) * [`DELETE /org/members/{user_id}`](/api/orgs/members/remove) #### KYB The unified KYB profile and verification endpoints — see [the KYB index](/api/orgs/kyb) for the schema and per-endpoint pages. ### Auth + scoping Endpoints accept either an `Api-Key: ` header (org-scoped key, recommended for automation) or `Authorization: Bearer ` (dashboard session). Sending both at once returns **400 BothCredsProvided** — see [Auth](/api/auth) for the full model. The URLs encode the scope: * **`/orgs`** (plural) — the caller's set of memberships. Read-only listing. With an API key, this enumerates the orgs the minting user belongs to. * **`/org`** (singular) — the *current* org. Whichever org the credential is scoped to. * With an API key: the org the key was minted in. Permanent — keys don't switch context. * With a dashboard session: the org from the `X-Org-Id` header, defaulting to the user's primary membership when the header is absent. There's no `/orgs/{id}/...` form — you don't pass an explicit id at all. To act on a different org from the dashboard, set `X-Org-Id`. To act on a different org from automation, mint a separate API key for it. ### Types #### `Org` | Field | Type | Notes | | ------------ | ------------------------------- | ------------------------------------------- | | `id` | string (UUID) | Our id. | | `name` | string | Display name. | | `providers` | [`OrgProvider[]`](#orgprovider) | Verification summary (historical wire key). | | `created_at` | string (ISO-8601) | | | `updated_at` | string (ISO-8601) | | #### `OrgProvider` Slim KYB status row surfaced on the org object. Richer state (hosted links, missing fields, rejection reasons, last sync) lives under [`GET /org/kyb/providers`](/api/orgs/kyb/providers). | Field | Type | Notes | | ------------ | ------------------------- | ----------------------- | | `provider` | string | Opaque routing id. | | `kyb_status` | [`KybStatus`](#kybstatus) | KYB stage for this row. | #### `OrgMembership` | Field | Type | Notes | | ------------ | --------------------- | --------------------------------------------- | | `org_id` | string (UUID) | | | `user_id` | string (UUID) | | | `email` | string | Member's email — denormalized for list views. | | `role` | [`OrgRole`](#orgrole) | See below. | | `created_at` | string (ISO-8601) | | #### `OrgRole` A string enum — one of `"owner"`, `"admin"`, `"member"`, `"viewer"`. Every org must have **at least one owner** at all times — a demote or remove that would leave the org with zero owners returns `409 LastOwner`. | Action | Owner | Admin | Member | Viewer | | ------------------------------------------------------ | :---: | :---: | :----: | :----: | | Read org, members, KYB, customers, orders | ✓ | ✓ | ✓ | ✓ | | Mint / list / revoke API keys | ✓ | ✓ | ✓ | | | Run quotes, accept quotes | ✓ | ✓ | ✓ | | | Create end-user customers + accounts | ✓ | ✓ | ✓ | | | Edit KYB profile, submit KYB, refresh KYB links | ✓ | ✓ | | | | Invite / remove members | ✓ | ✓ | | | | Billing | ✓ | ✓ | | | | Change roles (incl. promoting / demoting other owners) | ✓ | | | | | Delete org | ✓ | | | | Admins can change roles between `member` ↔ `viewer` only — anything that touches `admin` or `owner` is owner-only. #### `KybStatus` A string enum — one of `"not_started"`, `"submitted"`, `"under_review"`, `"approved"`, `"rejected"`. Each value describes one verification leg on the org; combine rows however your policy requires. | Value | Meaning | | -------------- | -------------------------------------------------------------------- | | `not_started` | We have not sent this leg yet. | | `submitted` | Profile is with the reviewer queue. | | `under_review` | Actively under review. | | `approved` | Cleared for live activity on this leg. | | `rejected` | Rejected — see `rejection_reason` on the detailed KYB state payload. | Detailed KYB state (link, missing fields, rejection reason, last sync) lives at [`GET /org/kyb/providers`](/api/orgs/kyb/providers). ## `GET /orgs` List orgs the calling user is a member of. Read-only — API keys can call this to enumerate the minting user's memberships (useful for discovering other orgs to mint keys for), but the key still can't mutate any org other than its own scope. **Response `200`** ```json { "orgs": [ { "org": { "id": "0a2a…", "name": "Acme Inc.", "created_at": "2026-04-01T10:00:00Z", "updated_at": "2026-04-23T10:00:00Z" }, "role": "owner" }, { "org": { "id": "1b3b…", "name": "Acme Labs", "created_at": "2026-04-22T10:00:00Z", "updated_at": "2026-04-23T10:00:00Z" }, "role": "admin" } ] } ``` **Errors:** `401` (no credential). **Example** ```bash curl "$SEISMIC_ORCHESTRATION_URL/v0/orgs" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ## `GET /org` Read the org the credential is currently scoped to. With an API key, that's the key's org. With a dashboard session, that's the org from `X-Org-Id` (or the user's primary membership if the header is absent). **Response `200`** — see [`Org`](/api/orgs#types). ```json { "id": "0a2a…", "name": "Acme Inc.", "created_at": "2026-04-01T10:00:00Z", "updated_at": "2026-04-23T10:00:00Z" } ``` **Errors:** `401` (no credential), `404` (credential scope is broken). **Example** ```bash curl "$SEISMIC_ORCHESTRATION_URL/v0/org" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ## `PATCH /org` Update the current org's mutable fields. Requires `admin` or `owner` role. **Request body** — all fields optional; only the keys you send are applied. ```json { "name": "Acme International Inc." } ``` | Field | Type | Notes | | ------ | ------ | ------------------------- | | `name` | string | Display name. 1–80 chars. | `providers`, `id`, and timestamps are not editable here. Per-provider state comes from the providers themselves; advance KYB via [`POST /org/kyb/submit`](/api/orgs/kyb/submit) and [`POST /org/kyb/providers/{provider}/resync`](/api/orgs/kyb/resync). **Response `200`** — the updated [`Org`](/api/orgs#types). **Errors:** `400` (invalid name), `403` (not an admin/owner). **Example** ```bash curl -X PATCH "$SEISMIC_ORCHESTRATION_URL/v0/org" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" \ -H "content-type: application/json" \ -d '{ "name": "Acme International Inc." }' ``` ## Members Endpoints for managing user ↔ org memberships. All scoped to the current org. ### Endpoints * [`GET /org/members`](/api/orgs/members/list) — list members with their roles. * [`POST /org/members`](/api/orgs/members/invite) — invite a user by email. * [`PATCH /org/members/{user_id}`](/api/orgs/members/update) — change a member's role. * [`DELETE /org/members/{user_id}`](/api/orgs/members/remove) — remove a member. ### Permissions | Action | Required role | | ------ | ----------------------------------------------------------------------------------------- | | List | any role | | Invite | `admin` or `owner` (admins cannot invite as `admin` or `owner`) | | Update | `owner` for any change involving `admin` or `owner`; `admin` can flip `member` ↔ `viewer` | | Remove | `admin` or `owner` (cannot remove the last `owner` — promote someone first) | See [`OrgRole`](/api/orgs#orgrole) for the full permissions matrix and [`OrgMembership`](/api/orgs#orgmembership) for the response shape. ## `POST /org/members` Invite a user by email. If they don't have a Seismic account, an invite email is sent and the row is created in `pending` state until they accept. If they already have an account, the membership is created immediately. Requires `admin` or `owner` role. **Request body** ```json { "email": "carol@acme.com", "role": "member" } ``` | Field | Type | Notes | | ------- | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `email` | string | Required. | | `role` | `"admin"` \| `"member"` \| `"viewer"` | Default: `"member"`. Cannot invite as `"owner"` — ownership transfers via [`PATCH`](/api/orgs/members/update). Admins can only invite as `"member"` or `"viewer"`; only an owner can invite an `"admin"`. | **Response `200`** — the new [`OrgMembership`](/api/orgs#orgmembership). **Errors:** `400` (invalid email/role), `403` (not allowed to invite at this role), `409` (user is already a member). **Example** ```bash curl -X POST "$SEISMIC_ORCHESTRATION_URL/v0/org/members" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" \ -H "content-type: application/json" \ -d '{ "email": "carol@acme.com", "role": "member" }' ``` ## `GET /org/members` List members of the current org. Any member can call this. **Response `200`** ```json { "members": [ { "org_id": "0a2a…", "user_id": "9d11…", "email": "alice@acme.com", "role": "owner", "created_at": "2026-04-01T10:00:00Z" }, { "org_id": "0a2a…", "user_id": "ad22…", "email": "bob@acme.com", "role": "admin", "created_at": "2026-04-12T10:00:00Z" } ] } ``` See [`OrgMembership`](/api/orgs#orgmembership). **Example** ```bash curl "$SEISMIC_ORCHESTRATION_URL/v0/org/members" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ## `DELETE /org/members/{user_id}` Remove a member from the current org. Requires `admin` or `owner`. Removing a member who is the sole `owner` is rejected — promote another member to `owner` first. **Soft delete.** The membership row is preserved with `deleted_at` set; the user loses access immediately and disappears from `GET /org/members`, but order history, audit trails, and any rows that reference the user-id stay intact and resolvable. Re-inviting the same email creates a fresh row. **Response `204`** — no body. **Errors:** `403` (not an admin/owner), `404` (user isn't an active member), `409 LastOwner` (would leave the org with zero owners). **Example** ```bash curl -X DELETE "$SEISMIC_ORCHESTRATION_URL/v0/org/members/ad22…" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ## `PATCH /org/members/{user_id}` Change a member's role. See the [permissions matrix](/api/orgs#orgrole) for which roles can assign what. * **Owner** can change anyone's role, including promoting members to `"owner"` or demoting other owners. An org can have multiple owners; only constraint is **at least one** at all times. * **Admin** can flip members between `"member"` and `"viewer"` only. Anything that touches `"admin"` or `"owner"` is owner-only. **Request body** ```json { "role": "admin" } ``` | Field | Type | Notes | | ------ | ------------------------------ | --------- | | `role` | [`OrgRole`](/api/orgs#orgrole) | New role. | **Response `200`** — the updated [`OrgMembership`](/api/orgs#orgmembership). **Errors:** `400` (invalid role), `403` (not allowed to assign that role), `404` (user isn't a member), `409 LastOwner` (would leave the org with zero owners). **Example** ```bash curl -X PATCH "$SEISMIC_ORCHESTRATION_URL/v0/org/members/ad22…" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" \ -H "content-type: application/json" \ -d '{ "role": "admin" }' ``` ## KYB KYB ("Know Your Business") is how we verify your org before live money movement. We collect a **unified superset** of fields once — aligned with **the platform's requirements** — then run the verification and hosted flows the corridor needs. The dashboard is the canonical entry point for this flow. The endpoints here let automation tools mirror it (status polling, programmatic resubmission) and back the dashboard itself. ### Lifecycle ``` draft (saved client-side) │ ▼ PUT /kyb/profile ──► server stores the unified profile │ ▼ POST /kyb/submit ──► verification work begins; status rows advance to "submitted" │ ▼ [ hosted verification / email steps the platform requires ] │ ▼ GET /kyb/providers ──► poll until your org is cleared for the product features you need ``` API keys can be minted at any point — they're separate from KYB. A key works for read-only and key-management endpoints from day one; quoting, customer creation, and account creation unlock once business verification reaches the stage your corridor requires. ### Endpoints * [`GET /org/kyb/profile`](/api/orgs/kyb/profile-get) * [`PUT /org/kyb/profile`](/api/orgs/kyb/profile-set) * [`POST /org/kyb/submit`](/api/orgs/kyb/submit) * [`GET /org/kyb/providers`](/api/orgs/kyb/providers) * [`POST /org/kyb/providers/{provider}/link`](/api/orgs/kyb/refresh-link) * [`POST /org/kyb/providers/{provider}/resync`](/api/orgs/kyb/resync) ### `KybProfile` The unified superset. Fields are grouped into subsections so partial saves stay manageable. #### `business` | Field | Type | Required | Notes | | ------------------------- | ---------------------- | ----------- | ------------------------------------------------------------------------------------------ | | `legal_name` | string | yes | Formal entity name as registered. | | `trade_name` | string | no | "Doing business as". | | `entity_type` | enum | yes | `corporation` \| `llc` \| `partnership` \| `sole_proprietorship` \| `nonprofit` \| `trust` | | `industry` | enum | yes | Canonical industry enum; mapped inside the platform. | | `registration_number` | string | yes | Government-issued company id (UK Companies House, etc.). | | `tax_id` | string | yes | EIN / equivalent. | | `country_of_registration` | string (ISO-3166 α-2) | yes | "US", "GB", etc. | | `state_of_incorporation` | string | conditional | Required when `country_of_registration === "US"`. | | `formation_date` | string (ISO-8601 date) | yes | When the entity was formed. | | `website` | string (URL) | no | Public-facing URL. | #### `addresses` | Field | Type | Required | Notes | | ------------ | --------------------- | -------- | ---------------------------------------------------------------------- | | `registered` | [`Address`](#address) | yes | Legal registered address. | | `physical` | [`Address`](#address) | no | Operating address if different. Defaults to `registered` when omitted. | #### `contact` | Field | Type | Required | Notes | | ------- | ------ | -------- | ------------------------- | | `email` | string | yes | Primary business contact. | | `phone` | string | yes | E.164 preferred. | #### `operations` Compliance and jurisdiction context. **The platform's requirements** may mark every field here as required — collecting the superset up front avoids blocking later when corridors expand. | Field | Type | Required | Notes | | ------------------------------ | --------------------- | ----------- | ---------------------------------------------------------------- | | `business_jurisdictions` | array of ISO-3166 α-2 | yes | Where the business operates. | | `funds_movement_jurisdictions` | array of ISO-3166 α-2 | yes | Where funds will be sent or received. | | `primary_source_of_funds` | enum | yes | `company_capital` \| `revenue` \| `investment` \| `other`. | | `regulated_status` | enum | yes | `registered` \| `unregistered` \| `exempt`. | | `regulated_activity` | string (free-form) | conditional | Required when `regulated_status === "registered"`. | | `account_purpose` | string | no | What the org plans to use Seismic Orchestration for. | | `estimated_annual_revenue_usd` | number (USD/year) | no | Exact number; mapped to internal reporting buckets where needed. | #### `volumes` Estimated transaction counts per month. Nonnegative integers; `0` is valid. | Field | Type | Required | Notes | | ----------------------------- | ------ | -------- | ----- | | `monthly_deposits` | number | yes | | | `monthly_withdrawals` | number | yes | | | `monthly_crypto_deposits` | number | no | | | `monthly_crypto_withdrawals` | number | no | | | `monthly_investment_deposits` | number | no | | #### `counterparties` | Field | Type | Required | Notes | | ---------------- | ------------- | -------- | -------------------------------------------------------------------------------------------------------------- | | `counterparties` | array of enum | yes | `merchants_suppliers` \| `customers` \| `friends_family` \| `employees_contractors` \| `investors` \| `other`. | #### `associated_persons` Beneficial owners + control persons. Collected once and mapped to whatever the platform requires downstream. | Field | Type | Required | Notes | | ------------------------- | ---------------------- | -------- | ------------------------------------------------------------------------------------- | | `first_name`, `last_name` | string | yes | | | `date_of_birth` | string (ISO-8601 date) | yes | | | `email` | string | yes | | | `phone` | string | yes | | | `address` | [`Address`](#address) | yes | Mailing address. | | `tax_id` | string | yes | SSN / equivalent. | | `nationality` | string (ISO-3166 α-2) | yes | | | `citizenship` | string (ISO-3166 α-2) | yes | May equal `nationality`. | | `ownership_percent` | number (0–100) | yes | Sum across all UBOs must reach 100% for compliant orgs. | | `roles` | object | yes | `{ has_control: bool, is_signer: bool, is_director: bool }` — mapped by the platform. | {/* ### `documents` :::info[Coming soon] File-upload mechanics aren't wired up yet. The schema reserves a `documents[]` field of `{ kind, url }` so callers can plumb references through, but the upload endpoint and signed-URL flow land in a later pass. For now, hosted KYB links cover document collection on the provider side. ::: */} ### `Address` Shared shape used in `addresses.registered`, `addresses.physical`, and `associated_persons[].address`. | Field | Type | Required | | ------------- | --------------------- | ------------------- | | `line1` | string | yes | | `line2` | string | no | | `city` | string | yes | | `state` | string | conditional (US/CA) | | `postal_code` | string | yes | | `country` | string (ISO-3166 α-2) | yes | PO boxes are not accepted for registered addresses. ### `OrgProviderState` Returned from [`GET /org/kyb/providers`](/api/orgs/kyb/providers). | Field | Type | Notes | | ---------------------- | ---------------------------------- | ------------------------------------------------------------------------ | | `provider` | string | Opaque routing id (historical wire key). | | `kyb_status` | [`KybStatus`](/api/orgs#kybstatus) | KYB stage for this row. | | `kyb_link` | object \| null | `{ url, expires_at }` when a hosted verification flow is pending. | | `missing_fields` | array of string | Dotted paths into the profile we still need. Empty after a clean submit. | | `last_synced_at` | string (ISO-8601) | When we last reconciled verification state. | | `provider_customer_id` | string \| null | Opaque id for this org on this leg. Null until first submit. | | `rejection_reason` | string \| null | Human-readable, when `kyb_status === "rejected"`. | ## `GET /org/kyb/profile` Read the unified KYB profile saved for the current org. Returns `404` if no profile has been saved yet. **Response `200`** — see [`KybProfile`](/api/orgs/kyb#kyb-profile) for the field-by-field schema. ```json { "business": { "legal_name": "Acme Inc.", "entity_type": "corporation", "industry": "software_blockchain_crypto", "registration_number": "1234567", "tax_id": "82-1234567", "country_of_registration": "US", "state_of_incorporation": "DE", "formation_date": "2020-04-12" }, "addresses": { "registered": { "line1": "1 Wall St", "city": "New York", "state": "NY", "postal_code": "10005", "country": "US" } }, "contact": { "email": "ops@acme.com", "phone": "+12025550100" }, "operations": { "business_jurisdictions": ["US"], "funds_movement_jurisdictions": ["US", "GB"], "primary_source_of_funds": "company_capital", "regulated_status": "unregistered" }, "volumes": { "monthly_deposits": 35, "monthly_withdrawals": 30 }, "counterparties": { "counterparties": ["customers", "merchants_suppliers"] }, "associated_persons": [ { "first_name": "Alice", "last_name": "Liddell", "date_of_birth": "1985-06-15", "email": "alice@acme.com", "phone": "+12025550100", "address": { "line1": "1 Wall St", "city": "New York", "state": "NY", "postal_code": "10005", "country": "US" }, "tax_id": "123-45-6789", "nationality": "US", "citizenship": "US", "ownership_percent": 100, "roles": { "has_control": true, "is_signer": true, "is_director": true } } ] } ``` **Errors:** `404` (no profile saved yet). **Example** ```bash curl "$SEISMIC_ORCHESTRATION_URL/v0/org/kyb/profile" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ## `PUT /org/kyb/profile` Replace the org's KYB profile with a complete document. The request body must contain every required field across all subsections — see [`KybProfile`](/api/orgs/kyb#kyb-profile). This **does not submit** the profile to providers — call [`POST /org/kyb/submit`](/api/orgs/kyb/submit) when you're ready. `PUT`-ing again before submit just overwrites the staged data; `PUT`-ing after a submit creates a new revision and the next submit fans the new shape out. Requires `admin` or `owner` role. **Request body** — full [`KybProfile`](/api/orgs/kyb#kyb-profile) document. See the [GET response](/api/orgs/kyb/profile-get) for an example shape. **Response `200`** — the saved [`KybProfile`](/api/orgs/kyb#kyb-profile) (canonicalized — defaults filled in, addresses normalized). **Errors:** * `400` — schema invalid. The error message names the bad path (e.g. `business.tax_id: required`). * `403` — not an admin/owner. * `422` — `KybProfileIncomplete`: required fields are missing. Shape: ```json { "ok": false, "error": { "code": "KybProfileIncomplete", "message": "5 required fields are missing", "missing": [ "business.formation_date", "operations.regulated_status", "associated_persons[0].tax_id", "associated_persons[0].nationality", "volumes.monthly_deposits" ] } } ``` **Example** — see [`GET`](/api/orgs/kyb/profile-get) for the full body. ```bash curl -X PUT "$SEISMIC_ORCHESTRATION_URL/v0/org/kyb/profile" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" \ -H "content-type: application/json" \ --data-binary @kyb-profile.json ``` ## `GET /org/kyb/providers` KYB verification rows for the current org. Use this to drive a **KYB status** panel in the dashboard, or to gate automation on approval. This endpoint returns cached state from `org_provider_state`, which is updated on submit, on webhook receipt, and on explicit [`POST /resync`](/api/orgs/kyb/resync) calls. Last-sync timestamp is on every row. **Response `200`** ```json { "providers": [ { "provider": "prv_a", "kyb_status": "approved", "kyb_link": null, "missing_fields": [], "last_synced_at": "2026-04-24T08:00:00Z", "provider_customer_id": "biz_…", "rejection_reason": null } ] } ``` See [`OrgProviderState`](/api/orgs/kyb#orgproviderstate). **Example** ```bash curl "$SEISMIC_ORCHESTRATION_URL/v0/org/kyb/providers" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ## `POST /org/kyb/providers/{provider}/link` Re-issue the hosted verification URL for a KYB row — for example when the user closed the tab or the link expired. Only rows that use a **hosted** flow return a fresh URL; others respond with an error when hosted links are not available. **Path parameters** | Param | Type | Notes | | ---------- | ------ | -------------------------------------------- | | `provider` | string | Opaque routing id (historical path segment). | **Request body** — empty. **Response `200`** ```json { "url": "https://…", "expires_at": "2026-04-25T11:00:00Z" } ``` **Errors:** * `403` — not an admin/owner. * `404` — the org has no submission for this provider yet (call [`POST /kyb/submit`](/api/orgs/kyb/submit) first). **Example** ```bash curl -X POST "$SEISMIC_ORCHESTRATION_URL/v0/org/kyb/providers/{provider}/link" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` Replace `{provider}` with the opaque provider id returned from [`GET /org/kyb/providers`](/api/orgs/kyb/providers). ## `POST /org/kyb/providers/{provider}/resync` Pull the latest KYB status for a verification row. Normally we react to webhooks, but resync exists for when a webhook was lost, the org just finished a hosted session and wants instant feedback, or operators want to verify the cached state matches reality. **Path parameters** | Param | Type | Notes | | ---------- | ------ | -------------------------------------------- | | `provider` | string | Opaque routing id (historical path segment). | **Request body** — empty. **Response `200`** — the freshly-synced [`OrgProviderState`](/api/orgs/kyb#orgproviderstate) for that row: ```json { "provider": "prv_b", "kyb_status": "approved", "kyb_link": null, "missing_fields": [], "last_synced_at": "2026-04-24T15:30:00Z", "provider_customer_id": "biz_…", "rejection_reason": null } ``` **Errors:** * `403` — not an admin/owner. * `404` — no submission for this provider. * `502 ProviderError` — provider returned an error; cached state is unchanged. Retryable. **Example** ```bash curl -X POST "$SEISMIC_ORCHESTRATION_URL/v0/org/kyb/providers/{provider}/resync" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` Replace `{provider}` with the opaque provider id from [`GET /org/kyb/providers`](/api/orgs/kyb/providers). ## `POST /org/kyb/submit` Submit the saved KYB profile to the platform. Idempotent — calling twice with the same profile is a no-op when there is nothing new to send. The server: 1. Reads the staged [`KybProfile`](/api/orgs/kyb#kyb-profile). 2. Validates required fields. Returns `422 KybProfileIncomplete` if anything is missing. 3. Maps the unified profile into the verification flows your corridors require and runs submission. 4. Stores verification state (ids, status, hosted-link URL if any) in `org_provider_state`. 5. Returns the resulting state list. Requires `admin` or `owner` role. **Request body** — empty. **Response `200`** — `{ "providers": … }` with [`OrgProviderState`](/api/orgs/kyb#orgproviderstate) entries — same fields as [`GET /org/kyb/providers`](/api/orgs/kyb/providers). **Errors:** * `403` — not an admin/owner. * `422 KybProfileIncomplete` — see the [`PUT`](/api/orgs/kyb/profile-set) page. **Example** ```bash curl -X POST "$SEISMIC_ORCHESTRATION_URL/v0/org/kyb/submit" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ## `POST /customers/:id/accounts` Attach a funding / payout account to a customer. Each account is tied to one routing context the platform already established for that customer. Pass the opaque routing id we returned on `GET /customers/:id` (wire key `provider`). ### Request body All fields are normalized. Some fields apply only to certain `account_type`s; see the Notes column. | Field | Type | Required | Notes | | --------------------- | ---------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `provider` | string | yes | Opaque routing id from the customer retrieve envelope (wire key `provider`). | | `account_type` | enum (see [`Account`](/api/customers#account)) | yes | | | `currencies` | string\[] | yes | ISO-4217 or stablecoin symbols. Always an array — pass a single-element array (`["USD"]`) for bank accounts; multi-token crypto wallets pass the full set (`["USDC", "USDT"]`). | | `rails` | string\[] | yes | Always an array. US bank → `["ach", "wire"]`; EVM wallet → `["base", "ethereum", "polygon"]`. Single-element arrays are fine. | | `account_number` | string | when bank account | For `iban`, this is the IBAN string. | | `routing_number` | string | when US bank | ACH routing number. | | `account_holder_name` | string | when bank account | | | `address` | string | when wallet | On-chain address. | | `vpa` | string | when UPI | Virtual Payment Address (e.g. `alice@okbank`). | **Validation (before the platform round-trip):** * Stablecoin wallet → every entry in `rails` must be in the whitelist for that token (e.g. `USDC` rails are `base` / `polygon`). * The routing id must match one we already returned for this customer. ### Response `200` [`Account`](/api/customers#account). ### Errors `400` (rail not allowed, missing required field, provider not attached to customer), `403`, `404`. ### Example — US bank account ```bash curl -X POST "$SEISMIC_ORCHESTRATION_URL/v0/customers/2f10…/accounts" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" \ -H "content-type: application/json" \ -d '{ "provider": "prv_…", "account_type": "us_bank_account", "currencies": ["USD"], "rails": ["ach", "wire"], "routing_number": "021000021", "account_number": "0123456789", "account_holder_name": "Acme Inc." }' ``` ### Example — USDC wallet (single token, single chain) ```bash curl -X POST "$SEISMIC_ORCHESTRATION_URL/v0/customers/2f10…/accounts" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" \ -H "content-type: application/json" \ -d '{ "provider": "prv_…", "account_type": "external_digital_asset_wallet", "currencies": ["USDC"], "rails": ["base"], "address": "0xabc…" }' ``` ### Example — multi-token EVM wallet The same address holds USDC + USDT and is reachable on Base + Ethereum: ```bash curl -X POST "$SEISMIC_ORCHESTRATION_URL/v0/customers/2f10…/accounts" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" \ -H "content-type: application/json" \ -d '{ "provider": "prv_…", "account_type": "external_digital_asset_wallet", "currencies": ["USDC", "USDT"], "rails": ["base", "ethereum"], "address": "0xabc…" }' ``` ### Example — UPI (India) ```bash curl -X POST "$SEISMIC_ORCHESTRATION_URL/v0/customers/2f10…/accounts" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" \ -H "content-type: application/json" \ -d '{ "provider": "prv_…", "account_type": "upi", "currencies": ["INR"], "rails": ["upi"], "vpa": "alice@okicici" }' ``` ## `POST /customers` Create an end-user customer. We persist a local `ramp_customers` row on success — every subsequent call uses our UUID. The customer is scoped to the org bound to your API key. The platform completes verification and routing work required for your corridor; you work with the normalized [`Customer`](/api/customers#customer) we return. ### Request body All fields are normalized. Supply the **superset** the platform documents below; we validate against **the platform's requirements** for your corridor. | Field | Type | Required | Notes | | --------------- | ----------------------------------- | --------------- | ---------------------------------------------------------------------------------------------------- | | `external_id` | string | yes | Your stable id for the user. Idempotency key for retries. | | `type` | `"individual"` \| `"business"` | yes | | | `email` | string | yes | | | `first_name` | string | when individual | | | `last_name` | string | when individual | | | `legal_name` | string | when business | Registered legal name for businesses. | | `country` | string | no | ISO-3166 alpha-2. For businesses, country of registration; for individuals prefer `address.country`. | | `date_of_birth` | string (`YYYY-MM-DD`) | when individual | Required for individual verification when the corridor asks for it. | | `address` | [`Address`](/api/customers#address) | when individual | Residential address for individuals. | | `phone` | string (E.164) | no | | | `tax_id` | string | when business | EIN / equivalent. | | `government_id` | [`GovernmentId`](#governmentid) | no | Optional inline ID documents for individuals. | **Validation:** if a field required by **the platform's requirements** for the selected corridor is missing, the request fails `400` with the missing field listed in the error body. #### `GovernmentId` | Field | Type | Required | Notes | | ------------- | ------------------------------------------------------------------------------ | -------- | ------------------------------------------------------------------ | | `type` | `"passport"` \| `"drivers_license"` \| `"national_id"` \| `"residence_permit"` | yes | | | `number` | string | yes | | | `country` | string | yes | ISO-3166 alpha-2 of the issuing country. | | `front_image` | string | yes | Base64 data URI. Max 50 MB. | | `back_image` | string | no | Base64 data URI. Required for `drivers_license` and `national_id`. | ```json { "external_id": "user_abc123", "type": "individual", "email": "alice@example.com", "first_name": "Alice", "last_name": "Liddell", "date_of_birth": "1990-04-12", "address": { "line1": "123 Main St", "city": "Brooklyn", "state": "NY", "postal_code": "11201", "country": "US" } } ``` ### Response `200` [`Customer`](/api/customers#customer) plus verification and routing rows the platform attaches for this request — see [`CustomerProvider`](/api/customers#customerprovider) on the [Customers index](/api/customers). ```json { "customer": { "id": "2f10…", "org_id": "0a2a…", "external_id": "user_abc123", "type": "individual", "email": "alice@example.com", "first_name": "Alice", "last_name": "Liddell", "created_at": "2026-04-23T10:20:00Z", "updated_at": "2026-04-23T10:20:00Z" } } ``` ### Errors `400` (missing required field — see `missing` in the error body), `409` (an existing customer already has this `(org, external_id)` pair), `422` (this corridor cannot be fulfilled with the data supplied). ### Example ```bash curl -X POST "$SEISMIC_ORCHESTRATION_URL/v0/customers" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" \ -H "content-type: application/json" \ -d '{ "external_id": "user_abc123", "type": "individual", "email": "alice@example.com", "first_name": "Alice", "last_name": "Liddell", "date_of_birth": "1990-04-12", "address": { "line1": "123 Main St", "city": "Brooklyn", "state": "NY", "postal_code": "11201", "country": "US" } }' ``` ## Customers These are **end-user customers** — individuals (or businesses) that *your product* onboards on top of Seismic Orchestration. They're scoped to your org. They are not the same thing as your org's own KYB onboarding — that's covered by [Orgs → KYB profile](/api/orgs). All endpoints under `/customers` require auth — either an `Api-Key: ` (org-scoped, recommended) or `Authorization: Bearer ` (dashboard session). Customers are filtered to the org bound to the credential. Curl snippets assume `$SEISMIC_ORCHESTRATION_URL` and `$SEISMIC_ORCHESTRATION_API_KEY` — see [Auth](/api/auth) for the full model. ### Endpoints * [`POST /customers`](/api/customers/create) — create a customer; we persist a Seismic-scoped row and run onboarding the platform requires for your corridor. * [`GET /customers/:id`](/api/customers/retrieve) — read the normalized customer and the latest verification snapshot we attach to the response. * [`POST /customers/:id/kyc-link`](/api/customers/kyc-link) — re-issue a hosted verification URL when the platform returns one. * [`POST /customers/:id/accounts`](/api/customers/create-account) — attach a funding / payout account. * [`GET /customers/:id/accounts`](/api/customers/list-accounts) — list every account attached to this customer. * [`GET /customers/orders`](/api/customers/orders) — org-wide feed of orders across every end-user customer. ### Responses Create and retrieve return the normalized [`Customer`](#customer) plus bookkeeping the platform uses for verification and routing. Use **`customer.id`** (UUID) for every subsequent call. Other ids on the envelope are for Seismic routing — do not treat them as end-user identifiers or display them in your product UI. ### Types #### `Customer` The normalized record. The `customer` field of the [create](/api/customers/create) and [retrieve](/api/customers/retrieve) responses. | Field | Type | Notes | | ------------- | ------------------------------ | --------------------------------- | | `id` | string (UUID) | Our id. | | `org_id` | string (UUID) | | | `external_id` | string | Your stable id for the user. | | `type` | `"individual"` \| `"business"` | | | `email` | string | | | `first_name` | string? | Set when `type === "individual"`. | | `last_name` | string? | Set when `type === "individual"`. | | `legal_name` | string? | Set when `type === "business"`. | | `created_at` | string (ISO-8601) | | | `updated_at` | string (ISO-8601) | | #### `CustomerProvider` Verification and routing row returned alongside [`Customer`](#customer) on create/retrieve. Keys are historical wire names; treat values as opaque ids unless Seismic asks you to pass them back. | Field | Type | Notes | | ---------------------- | ----------------------------------------- | --------------------------------------------------------------- | | `provider` | string | Opaque routing id. | | `provider_customer_id` | string | Opaque id linked to this customer; do not surface in your UI. | | `status` | `"pending"` \| `"active"` \| `"disabled"` | Onboarding status for this row. | | `kyc` | [`CustomerKyc`](#customerkyc) | Verification status + hosted link when the platform issues one. | #### `CustomerKyc` | Field | Type | Notes | | -------- | ------------------------------------------------------------------------------------ | ---------------------------------------------------------- | | `status` | `"not_started"` \| `"submitted"` \| `"under_review"` \| `"approved"` \| `"rejected"` | Verification outcome for this row. | | `link` | `{ url, expires_at? }` \| `null` | Hosted URL when the platform issues one; otherwise `null`. | #### `Address` | Field | Type | Required | Notes | | ------------- | ------ | -------- | ------------------------------------- | | `line1` | string | yes | | | `line2` | string | no | | | `city` | string | yes | | | `state` | string | when US | ISO-3166-2 subdivision (e.g. `"NY"`). | | `postal_code` | string | yes | | | `country` | string | yes | ISO-3166 alpha-2 (e.g. `"US"`). | #### `Account` Returned by [`POST /customers/:id/accounts`](/api/customers/create-account), [`GET /accounts/:id`](/api/accounts/retrieve), and [`GET /accounts`](/api/accounts/list); also embedded in quote responses when a leg resolves from an attached account. | Field | Type | Notes | | --------------------- | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | Our UUID. | | `ramp_customer_id` | string | Our customer UUID. | | `provider` | string | Opaque routing id. | | `provider_account_id` | string | Opaque id for this account; do not surface in your UI. | | `account_type` | `"us_bank_account"` \| `"external_digital_asset_wallet"` \| `"virtual_digital_asset_wallet"` \| `"iban"` \| `"upi"` | | | `currencies` | string\[] | ISO-4217 or stablecoin symbols. Plural because crypto wallets natively hold multiple tokens. Bank accounts return a single-element array. | | `rails` | string\[] | `"ach"` \| `"wire"` \| `"sepa"` \| `"base"` \| `"ethereum"` \| … Plural because most accounts support more than one (US bank → `["ach", "wire"]`; EVM wallet → `["base", "ethereum", "polygon"]`). | | `status` | `"pending"` \| `"active"` \| `"disabled"` | | | `created_at` | string | ISO-8601. | | `updated_at` | string | ISO-8601. | When an account that supports >1 currency or >1 rail is referenced in a quote, the caller must disambiguate — see [Quotes → Disambiguation](/api/quotes#disambiguation). ## `POST /customers/:id/kyc-link` Refresh hosted verification URLs for the customer. Useful when the user closed the tab, the link expired, or the user needs to update their submitted information. ### Request body Send `{}` when you want the platform to refresh every incomplete verification link. Advanced integrations can pass additional filters — see the TypeScript client types in `seismic-orchestration` if you need them. ```json {} ``` ### Response `200` `{ links }` — each entry includes verification `status` and a hosted `link` when the platform issues one (`null` otherwise). ```json { "links": [ { "status": "under_review", "link": { "url": "https://…", "expires_at": "2026-04-24T10:20:00Z" } } ] } ``` ### Errors `404` (unknown customer id, or the request references ids we do not have on file for this customer). ### Example ```bash curl -X POST "$SEISMIC_ORCHESTRATION_URL/v0/customers/2f10…/kyc-link" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" \ -H "content-type: application/json" \ -d '{}' ``` ## `GET /customers/:id/accounts` List every account attached to a single end-user customer. For an org-wide view across all customers, see [`GET /accounts`](/api/accounts/list) instead. ### Query params Every filter is repeatable — pass `?currency=USDC¤cy=USDT` to match accounts that support either. | Param | Type | Notes | | ---------- | ------------------------------------------ | --------------------------------------------------------- | | `currency` | string (repeatable) | Keep accounts whose `currencies[]` intersects the filter. | | `rail` | string (repeatable) | Keep accounts whose `rails[]` intersects the filter. | | `status` | `"pending"` \| `"active"` \| `"disabled"`? | Match exactly. | ### Response `200` ```json { "accounts": [ { "id": "5e3a…", "ramp_customer_id": "2f10…", "provider": "prv_…", "provider_account_id": "ext_…", "account_type": "external_digital_asset_wallet", "currencies": ["USDC", "USDT"], "rails": ["base", "ethereum"], "status": "active", "created_at": "2026-04-23T10:25:00Z", "updated_at": "2026-04-23T10:25:00Z" } ] } ``` Each entry is an [`Account`](/api/customers#account). ### Errors `403` (customer belongs to a different org), `404` (unknown customer id). ### Example Show the customer's USDC-capable accounts: ```bash curl "$SEISMIC_ORCHESTRATION_URL/v0/customers/2f10…/accounts?currency=USDC&status=active" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ## `GET /customers/orders` Most-recent-first feed of orders across **every** end-user customer the org has onboarded — capped at 200. This is the org-wide view; for a single user's history, see [`GET /user/orders`](/api/users/orders) instead. Each entry is a [`RampOrder`](/api/users/orders#ramporder) — same shape as `/user/orders`. Use `ramp_customer_id` to group client-side. ### Query params | Param | Type | Notes | | ------------- | ---------------------------- | --------------------------------------------------------------------------- | | `customer_id` | string? | Filter to a single customer. Omit to return orders across all of them. | | `direction` | `"on_ramp"` \| `"off_ramp"`? | Filter by ramp direction. | | `status` | `OrderStatus`? | One of [`OrderStatus`](/api/users/orders#orderstatus). Filters server-side. | ### Response `200` Array of [`RampOrder`](/api/users/orders#ramporder). ```json [ { "id": "ord_2f10…", "user_id": "0a2a3f…", "ramp_customer_id": "cus_7c4b…", "provider": "prv_…", "provider_order_id": "WP-9d8c0a1b", "direction": "on_ramp", "status": "completed", "source_currency": "USD", "source_amount": "1000.00", "destination_currency": "USDC", "destination_chain": "base", "destination_address": "0xabc…123", "source_account_id": "acc_5e3a…", "destination_account_id": null, "idempotency_key": "8b3c4d5e-…", "funding_instructions": null, "reference": "invoice-2026-04-001", "comment": null, "created_at": "2026-04-23T10:15:00Z", "updated_at": "2026-04-23T10:18:42Z" } ] ``` ### Example ```bash curl "$SEISMIC_ORCHESTRATION_URL/v0/customers/orders?customer_id=cus_7c4b…" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` :::tip[Pagination roadmap] `limit=200` is hard-coded today. If you need older orders, open an issue — we'll add cursor-based pagination when the first user bumps the ceiling. ::: ## `GET /customers/:id` Read the normalized customer plus the latest verification and routing snapshot we attach to the response. ### Response `200` Same shape as the [create response](/api/customers/create#response-200): [`Customer`](/api/customers#customer) plus [`CustomerProvider[]`](/api/customers#customerprovider) as documented on the [Customers index](/api/customers). ```json { "customer": { "id": "2f10…", "org_id": "0a2a…", "external_id": "user_abc123", "type": "individual", "email": "alice@example.com", "first_name": "Alice", "last_name": "Liddell", "created_at": "2026-04-23T10:20:00Z", "updated_at": "2026-04-23T10:25:00Z" } } ``` Use verification status on the response envelope when gating account creation or quotes. ### Errors `403` (customer belongs to a different org), `404` (unknown id). ### Example ```bash curl "$SEISMIC_ORCHESTRATION_URL/v0/customers/2f10…" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ## Auth All endpoints live under `/auth`. Request + response bodies are JSON. :::info[Auth model] Two ways to authenticate, mutually exclusive on a single request: * **`Api-Key: `** — The recommended path for server-to-server callers. Org-scoped; one key per org. * **`Authorization: Bearer `** — for dashboard sessions. Issued by [`POST /auth/login`](/api/auth/login) (after MFA, if enrolled), refreshed via [`POST /auth/refresh`](/api/auth/refresh). Sending **both** headers on the same request returns **400 BothCredsProvided** — pick one. The endpoints under this section (signup, login, MFA, refresh, logout) are how you obtain an access token and are themselves unauthenticated where it makes sense; they exist for the dashboard. API-key callers don't need them. ::: :::tip[Curl conventions used below] Examples on every endpoint page assume two environment variables: * `SEISMIC_ORCHESTRATION_URL` — e.g. `https://orchestration-sandbox.seismic.systems/api` * `SEISMIC_ORCHESTRATION_API_KEY` — your API key, for protected endpoints Set them once and the snippets are copy-paste ready. ::: ### Endpoints * [`POST /auth/signup`](/api/auth/signup) — create a new account. * [`POST /auth/login`](/api/auth/login) — exchange credentials for tokens (or an MFA challenge). * [`POST /auth/mfa/verify`](/api/auth/mfa-verify) — redeem an MFA challenge for tokens. * [`POST /auth/mfa/enroll`](/api/auth/mfa-enroll) — start TOTP enrollment. * [`POST /auth/mfa/activate`](/api/auth/mfa-activate) — confirm TOTP enrollment. * [`POST /auth/refresh`](/api/auth/refresh) — rotate a refresh token into a new pair. * [`POST /auth/logout`](/api/auth/logout) — revoke a refresh token server-side. * [`GET /auth/me`](/api/auth/me) — JWT identity claims. #### [API keys](/api/auth/api-keys) * [`POST /auth/api-keys`](/api/auth/api-keys/mint) — mint a new key. Returns the secret once. * [`GET /auth/api-keys`](/api/auth/api-keys/list) — list keys for the current org. * [`DELETE /auth/api-keys/{id}`](/api/auth/api-keys/revoke) — revoke a key. ### Types #### `TokenPair` Returned by [`login`](/api/auth/login) (when MFA is off), [`mfa/verify`](/api/auth/mfa-verify), and [`refresh`](/api/auth/refresh). | Field | Type | Notes | | -------------------- | ------ | ------------- | | `access_token` | string | JWT. | | `refresh_token` | string | | | `access_expires_at` | string | ISO-8601 UTC. | | `refresh_expires_at` | string | ISO-8601 UTC. | #### `LoginResponse` Discriminated union on `status` returned by [`login`](/api/auth/login). | `status` | Body | | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | `mfa_required` | `{ status: "mfa_required", challenge_id: string }`. `challenge_id` is `mfa_`, single-use, \~5 min TTL. Default — accounts have MFA on by default. | | `tokens` | All `TokenPair` fields above, alongside `status: "tokens"`. Returned only when the account has MFA disabled. | ## `POST /auth/login` Exchange credentials for an MFA challenge id. If MFA is disabled for the account, returns a token pair directly. **Request** ```json { "email": "alice@example.com", "password": "hunter22!hunter22" } ``` **Response `200`** — discriminated union on `status`: :::code-group ```json [mfa_required] { "status": "mfa_required", "challenge_id": "mfa_b4bd0c1d8e2f4a6b9d3c" } ``` ```json [tokens] { "status": "tokens", "access_token": "eyJhbGciOi…", "refresh_token": "rt_…", "access_expires_at": "2026-04-23T10:15:00Z", "refresh_expires_at": "2026-05-23T10:00:00Z" } ``` ::: The `challenge_id` is opaque, single-use, and expires \~5 minutes after issue. Redeem it at [`POST /auth/mfa/verify`](/api/auth/mfa-verify) with the user's TOTP code. **Errors:** `401` (bad creds). **Example** ```bash curl -X POST "$SEISMIC_ORCHESTRATION_URL/v0/auth/login" \ -H "content-type: application/json" \ -d '{ "email": "alice@example.com", "password": "hunter22!hunter22" }' ``` See [`LoginResponse`](/api/auth#types) for the full shape. ## `POST /auth/logout` Revoke a refresh token server-side. Access tokens remain valid until they expire — keep TTLs short. **Request** ```json { "refresh_token": "rt_…" } ``` **Response `204`**. **Example** ```bash curl -X POST "$SEISMIC_ORCHESTRATION_URL/v0/auth/logout" \ -H "content-type: application/json" \ -d '{ "refresh_token": "rt_…" }' ``` ## `GET /auth/me` Lightweight identity claims pulled from the JWT. Doesn't hit the DB. Useful for session bootstrapping in a browser app where you want to know "who am I" without paying for a full profile fetch. **Response `200`** ```json { "user_id": "0a2a3f…", "mfa_passed": true } ``` For the full profile (email, memberships, created\_at), call [`GET /user`](/api/users/retrieve). **Example** ```bash curl "$SEISMIC_ORCHESTRATION_URL/v0/auth/me" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ## `POST /auth/mfa/activate` Confirm enrollment by providing the first valid TOTP code. Requires auth. **Request** ```json { "code": "123456" } ``` **Response `204`** — MFA is now required on future logins. **Example** ```bash curl -X POST "$SEISMIC_ORCHESTRATION_URL/v0/auth/mfa/activate" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" \ -H "content-type: application/json" \ -d '{ "code": "123456" }' ``` ## `POST /auth/mfa/enroll` Start TOTP enrollment. Requires auth. **Response `200`** ```json { "provisioning_uri": "otpauth://totp/Seismic:alice@example.com?secret=…", "secret_base32": "JBSWY3DPEHPK3PXP" } ``` Render `provisioning_uri` as a QR, or surface `secret_base32` for manual entry. Then call [`mfa/activate`](/api/auth/mfa-activate) with the first valid code. **Example** ```bash curl -X POST "$SEISMIC_ORCHESTRATION_URL/v0/auth/mfa/enroll" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ## `POST /auth/mfa/verify` Redeem a `challenge_id` (from [`login`](/api/auth/login)) + TOTP code for a token pair. **Request** ```json { "challenge_id": "mfa_b4bd0c1d8e2f4a6b9d3c", "code": "123456" } ``` **Response `200`** — [`TokenPair`](/api/auth#types). **Errors:** `401` (wrong code, expired challenge, clock skew). **Example** ```bash curl -X POST "$SEISMIC_ORCHESTRATION_URL/v0/auth/mfa/verify" \ -H "content-type: application/json" \ -d '{ "challenge_id": "mfa_b4bd0c1d8e2f4a6b9d3c", "code": "123456" }' ``` ## `POST /auth/refresh` Rotate a refresh token into a new pair. Invalidates the old refresh token — don't hang onto it. **Request** ```json { "refresh_token": "rt_…" } ``` **Response `200`** — [`TokenPair`](/api/auth#types). **Errors:** `401` (revoked or expired refresh token). **Example** ```bash curl -X POST "$SEISMIC_ORCHESTRATION_URL/v0/auth/refresh" \ -H "content-type: application/json" \ -d '{ "refresh_token": "rt_…" }' ``` ## `POST /auth/signup` Create a new account. Does **not** return tokens — call [`login`](/api/auth/login) next. **Request** ```json { "email": "alice@example.com", "password": "hunter22!hunter22" } ``` **Response `200`** ```json { "user_id": "0a2a3f…" } ``` **Errors:** `400` (password \< 10 chars, malformed email), `409` (email exists). **Example** ```bash curl -X POST "$SEISMIC_ORCHESTRATION_URL/v0/auth/signup" \ -H "content-type: application/json" \ -d '{ "email": "alice@example.com", "password": "hunter22!hunter22" }' ``` ## API keys API keys are org-scoped credentials for server-to-server callers. One key, one org — to access multiple orgs from automation, mint a separate key for each. Revoking a key invalidates it immediately for all in-flight requests; existing access tokens are unaffected (different auth path). ### Endpoints * [`POST /auth/api-keys`](/api/auth/api-keys/mint) — mint a new key. Returns the secret once. * [`GET /auth/api-keys`](/api/auth/api-keys/list) — list keys for the current org (no secrets). * [`DELETE /auth/api-keys/{id}`](/api/auth/api-keys/revoke) — revoke a key. All three are callable with either auth scheme — `Api-Key:` or `Authorization: Bearer`. The bootstrap path is dashboard-login → mint, but anything is reachable once you have a key. Any member of the org can mint, list, or revoke — keys are operational, not administrative. ### Types #### `ApiKey` The shape returned from list + revoke responses, and the non-secret half of the mint response. | Field | Type | Notes | | -------------- | ------------------------- | ---------------------------------------------------------------------------------------------- | | `id` | string (UUID) | Our id for the key. | | `org_id` | string (UUID) | | | `prefix` | string | Public, non-sensitive prefix — e.g. `seismic_prod_abc12…`. Use to identify a key in logs / UI. | | `created_by` | string (UUID) | User id of the minting user. | | `created_at` | string (ISO-8601) | | | `last_used_at` | string (ISO-8601) \| null | Updated lazily on each authenticated request. | | `revoked_at` | string (ISO-8601) \| null | Set on revoke; revoked keys still appear in list. | #### `ApiKeyMinted` Returned only by [`POST /auth/api-keys`](/api/auth/api-keys/mint). Extends `ApiKey` with the plaintext `secret`. | Field | Type | Notes | | ----------------- | ------ | ------------------------------------------------------------------------------------------------------------- | | *all of `ApiKey`* | | | | `secret` | string | Full plaintext key — `seismic_prod_…`. Surfaced once; we hash and discard server-side. Treat like a password. | ## `GET /auth/api-keys` List all API keys for the current org, including revoked ones. Secrets are **never** returned — just metadata. Any member can call this. **Response `200`** ```json { "api_keys": [ { "id": "k_2f10…", "org_id": "0a2a…", "prefix": "seismic_prod_abc12…", "label": "ci-pipeline", "environment": "production", "created_by": "9d11…", "created_at": "2026-04-29T15:00:00Z", "last_used_at": "2026-04-30T09:14:22Z", "revoked_at": null }, { "id": "k_8b3a…", "org_id": "0a2a…", "prefix": "seismic_sandbox_xyz78…", "label": "local-dev (alice)", "environment": "sandbox", "created_by": "9d11…", "created_at": "2026-04-12T12:00:00Z", "last_used_at": null, "revoked_at": "2026-04-25T14:00:00Z" } ] } ``` See [`ApiKey`](/api/auth/api-keys#types). **Example** ```bash curl "$SEISMIC_ORCHESTRATION_URL/v0/auth/api-keys" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ## `POST /auth/api-keys` Mint a new API key scoped to the current org. Any member can mint. The plaintext `secret` is returned **exactly once** in the response. We hash and discard it server-side immediately — there is no "show key" endpoint. If a caller loses it, revoke and mint a new one. **Request body** ```json { "label": "ci-pipeline", "environment": "production" } ``` | Field | Type | Required | Notes | | ------------- | ----------------------------- | -------- | ------------------------------------------------------------------------------------------------------------ | | `label` | string | yes | Human-readable. 1–80 chars. Used in the dashboard list. | | `environment` | `"production"` \| `"sandbox"` | no | Defaults to the calling environment. Determines the secret prefix (`seismic_prod_…` vs `seismic_sandbox_…`). | **Response `200`** — see [`ApiKeyMinted`](/api/auth/api-keys#types). ```json { "id": "k_2f10…", "org_id": "0a2a…", "prefix": "seismic_prod_abc12…", "label": "ci-pipeline", "environment": "production", "created_by": "9d11…", "created_at": "2026-04-29T15:00:00Z", "last_used_at": null, "revoked_at": null, "secret": "seismic_prod_abc12_3f9d8e0a4b6c2e1d0f8a3b7c5d6e4f2a" } ``` **Errors:** * `400` — invalid label/environment. * `400 BothCredsProvided` — request had both `Api-Key:` and `Authorization: Bearer`. **Example** ```bash curl -X POST "$SEISMIC_ORCHESTRATION_URL/v0/auth/api-keys" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" \ -H "content-type: application/json" \ -d '{ "label": "ci-pipeline" }' ``` Or via dashboard session, e.g. when minting the very first key for an org: ```bash curl -X POST "$SEISMIC_ORCHESTRATION_URL/v0/auth/api-keys" \ -H "Authorization: Bearer $SEISMIC_DASHBOARD_TOKEN" \ -H "content-type: application/json" \ -d '{ "label": "ci-pipeline" }' ``` ## `DELETE /auth/api-keys/{id}` Revoke an API key. Effective immediately — in-flight requests using the key get **401 ApiKeyRevoked** on the next hit. The row is preserved with `revoked_at` set so audit trails stay readable. Any member of the org can revoke any key — including a key revoking itself, useful for "I think this leaked, kill it now" automation. **Response `204`** — no body. **Errors:** * `404` — unknown key id (or belongs to a different org). * `409 AlreadyRevoked` — the key is already revoked. Idempotent for the common retry case, but explicit so callers can distinguish "I just revoked it" from "it was already revoked yesterday". **Example** ```bash curl -X DELETE "$SEISMIC_ORCHESTRATION_URL/v0/auth/api-keys/k_2f10…" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ## Accounts These endpoints expose the accounts your end-user customers have attached — bank accounts, IBANs, UPI VPAs, and on-chain wallets. **Account creation lives under the customer that owns it** at [`POST /customers/:id/accounts`](/api/customers/create-account); the read-side endpoints live here so accounts can be addressed globally by their UUID. All endpoints require auth — either an `Api-Key: ` (org-scoped, recommended) or `Authorization: Bearer ` (dashboard session). Curl snippets assume `$SEISMIC_ORCHESTRATION_URL` and `$SEISMIC_ORCHESTRATION_API_KEY` — see [Auth](/api/auth) for the full model. ### Endpoints * [`GET /accounts`](/api/accounts/list) — list every account the org has attached, filterable by customer / status. * [`GET /accounts/:id`](/api/accounts/retrieve) — read a single account by UUID. * [`POST /customers/:id/accounts`](/api/customers/create-account) — create / attach (lives under Customers). ### `currencies` and `rails` are arrays A single attached account commonly supports more than one currency or rail: * **Crypto wallets** natively hold multiple tokens. A wallet at `0xabc…` on Base can receive `USDC`, `USDT`, `WETH`, etc. — and the same EVM-compatible address can also receive on `ethereum`, `polygon`, `arbitrum`, etc. * **Bank accounts** typically hold one currency but support multiple rails (e.g. a US checking account accepts both `ach` and `wire`). The Account record therefore exposes `currencies: string[]` and `rails: string[]`. When a quote endpoint references an account that supports more than one of either, the caller must disambiguate via `{ account, currency?, rail? }` — see [Disambiguation in quotes](/api/quotes#disambiguation). ### See also * [`Account`](/api/customers#account) — the type definition. * [Quotes → Disambiguation](/api/quotes#disambiguation) — how multi-currency / multi-rail accounts interact with quote requests. ## `GET /accounts` List every account the org has attached across all of its end-user customers. Optionally filter to a single customer or status. ### Query params `currency` and `rail` are repeatable — pass `?currency=USDC¤cy=USDT` to match accounts that support either. | Param | Type | Notes | | ------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `customer_id` | string? | Filter to a single customer. (For per-customer-only listings, [`GET /customers/:id/accounts`](/api/customers/list-accounts) is the more direct path.) | | `currency` | string (repeatable) | Keep accounts whose `currencies[]` intersects the filter. | | `rail` | string (repeatable) | Keep accounts whose `rails[]` intersects the filter. | | `status` | `"pending"` \| `"active"` \| `"disabled"`? | Match exactly. | ### Response `200` ```json { "accounts": [ { "id": "5e3a…", "ramp_customer_id": "2f10…", "provider": "prv_…", "provider_account_id": "wa_1a2b", "account_type": "external_digital_asset_wallet", "currencies": ["USDC", "USDT"], "rails": ["base", "polygon"], "status": "active", "created_at": "2026-04-23T10:25:00Z", "updated_at": "2026-04-23T10:25:00Z" } ] } ``` Each entry is an [`Account`](/api/customers#account). ### Example ```bash curl "$SEISMIC_ORCHESTRATION_URL/v0/accounts?customer_id=2f10…" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ``` ## `GET /accounts/:id` Read a single account by its UUID. Scoped to the org bound to the credential — accounts owned by a different org return `404`. ### Response `200` [`Account`](/api/customers#account). ```json { "id": "5e3a…", "ramp_customer_id": "2f10…", "provider": "prv_…", "provider_account_id": "wa_1a2b", "account_type": "external_digital_asset_wallet", "currencies": ["USDC", "USDT"], "rails": ["base", "polygon"], "status": "active", "created_at": "2026-04-23T10:25:00Z", "updated_at": "2026-04-23T10:25:00Z" } ``` ### Errors `404` (unknown id, or scoped to a different org). ### Example ```bash curl "$SEISMIC_ORCHESTRATION_URL/v0/accounts/5e3a…" \ -H "Api-Key: $SEISMIC_ORCHESTRATION_API_KEY" ```