Authentication
Two auth schemes, mutually exclusive on a single request:
| Scheme | Header | Use case |
|---|---|---|
| API key | Api-Key: seismic_prod_… | Server-to-server. Org-scoped (one key, one org). No login or refresh. |
| Access token | Authorization: Bearer <access-token> | Browsers logged into orchestration.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.
For server-to-server integrations, mint an API key in the dashboard (or via POST /auth/api-keys). The rest of this page describes the access-token flow used by the dashboard.
The dashboard authenticates with Authorization: Bearer <access_token>. 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 everyrefreshcall. The previous value is invalidated. - MFA challenge id: seconds-long. Issued by
loginwhen MFA is active. Redeemed bymfaVerifyfor a real token pair.
Signup
import { } from "./client";
const { } = await ..({
: "alice@example.com",
: "hunter22!hunter22",
: "Alice",
: "Anderson",
});| Status | Meaning |
|---|---|
| 200 | { user_id }: account created. No tokens yet. Call login. |
| 400 | Password shorter than 10 chars, invalid email, or missing name. |
| 409 | Email already in use. |
Login (password-only)
import { } from "./client";
const = await ..("alice@example.com", "hunter22!hunter22");
switch (.) {
case "tokens":
// Straight to a token pair: MFA not enrolled or not required.
.("access", .);
break;
case "mfa_required":
// Punt the challenge_id to your MFA screen.
(`/login/mfa?challenge=${.}`);
break;
}
function (: string) {}The return type is a Zod-parsed discriminated union, so TypeScript narrows both arms for you.
MFA
Two flows, both using TOTP:
- Enroll (signed-in user, one-time):
mfaEnroll→ scan QR →mfaActivate(code). - Challenge (every login after activation):
loginreturnsmfa_required→mfaVerify(challenge_id, code)→ token pair.
Enrollment
import { } from "./client";
// Returns an otpauth:// URI + the raw secret for display.
const = await ...();
(.);
// Prompt the user to type the 6-digit code from their authenticator app.
const = await ();
await ...();
// Returns a fresh TokenPair (mfa_passed=true); client persists it
// automatically, so this session continues without a re-login.
declare function (: string): void;
declare function (): <string>;Challenge
import { , } from "./client";
const = await ..(, );
if (. === "mfa_required") {
const = await ();
const = await ...(., );
.();
}SMS-based 2FA is not supported. For sandbox-only ops escape hatches, set ALLOW_MFA_BYPASS=true in the server env.
Refresh
The SDK catches 401s on protected paths and refreshes automatically. Call directly only when managing tokens in a background job or external process:
import { } from "./client";
const = await ..();
// pair.access_token, pair.refresh_token (new, old one is dead)Logout
Revokes the refresh token server-side. Access tokens remain valid until expiry (stateless JWTs aren't revocable without a denylist). Keep access TTLs short.
import { , } from "./client";
const = .();
if () await ..();
.();Inspecting the current user
GET /auth/me is the single canonical whoami: full profile, memberships, MFA state. Works equally for Api-Key: callers (returns the user who minted the key) and dashboard-session callers (returns the logged-in user).
import { } from "./client";
const = await ..();
// ^ id, email, mfa_required, mfa_active, mfa_passed, memberships[], created_at