From 29c97f3d5602fdceab0a57013eb81280b331ca77 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Tue, 27 Oct 2026 21:52:17 -0400 Subject: [PATCH 1/4] test(mosaic): check fake FAPI responses against the OpenAPI spec --- .changeset/mosaic-invite-only-role.md | 2 + packages/mosaic/docs/testing.md | 36 + packages/mosaic/eslint.config.mjs | 6 +- packages/mosaic/package.json | 5 +- packages/mosaic/scripts/sync-fapi-spec.mjs | 38 + .../mosaic/src/__tests__/feature/fake-fapi.ts | 122 +- .../__tests__/feature/fake-fapi/enterprise.ts | 10 +- .../src/__tests__/feature/fake-fapi/mfa.ts | 22 +- .../feature/fake-fapi/organization-members.ts | 46 +- .../__tests__/feature/fake-fapi/passkeys.ts | 34 +- .../src/__tests__/feature/fake-fapi/shared.ts | 19 +- .../feature/fake-fapi/verification.ts | 19 +- .../feature/fapi-contract.feature.test.tsx | 162 + .../src/__tests__/feature/fapi-contract.ts | 265 + .../src/__tests__/feature/fapi-spec-gaps.ts | 103 + .../src/__tests__/feature/fapi-spec.yml | 18245 ++++++++++++++++ packages/mosaic/src/__tests__/feature/fapi.ts | 489 +- .../mosaic/src/__tests__/feature/raw.d.ts | 4 + .../__tests__/api-keys-table.feature.test.tsx | 40 +- ...on-profile-api-keys-panel.feature.test.tsx | 7 +- ...ation-profile-invitations.feature.test.tsx | 29 +- ...ion-profile-members-panel.feature.test.tsx | 21 +- ...nization-profile-requests.feature.test.tsx | 4 +- ...rganization-profile-members-panel.model.ts | 2 +- .../__tests__/reverification.feature.test.tsx | 10 +- .../__tests__/user-button.feature.test.tsx | 13 +- ...er-profile-api-keys-panel.feature.test.tsx | 13 +- ...le-active-devices-section.feature.test.tsx | 75 +- ...onnected-accounts.connect.feature.test.tsx | 8 +- ...er-profile-connected-accounts.fixtures.tsx | 19 +- ...ser-profile-email-section.feature.test.tsx | 5 +- ...erprise-accounts-contract.feature.test.tsx | 7 +- ...erprise-accounts-identity.feature.test.tsx | 27 +- ...ofile-enterprise-accounts.feature.test.tsx | 20 +- .../__tests__/mfa-feature-setup.tsx | 1 - ...-profile-mfa-interactions.feature.test.tsx | 12 +- ...er-profile-mfa-management.feature.test.tsx | 35 +- ...-profile-passkeys-section.feature.test.tsx | 2 +- ...ofile-passkeys-validation.feature.test.tsx | 2 +- ...ser-profile-phone-section.feature.test.tsx | 6 +- ...r-profile-profile-section.feature.test.tsx | 8 +- ...rofile-web3-wallets-races.feature.test.tsx | 14 +- ...file-web3-wallets-section.feature.test.tsx | 4 +- .../user-profile-web3-wallets.fixtures.tsx | 6 +- packages/mosaic/vitest.config.mts | 2 + packages/mosaic/vitest.setup.browser.mts | 5 +- pnpm-lock.yaml | 12 +- 47 files changed, 19695 insertions(+), 341 deletions(-) create mode 100644 .changeset/mosaic-invite-only-role.md create mode 100644 packages/mosaic/scripts/sync-fapi-spec.mjs create mode 100644 packages/mosaic/src/__tests__/feature/fapi-contract.feature.test.tsx create mode 100644 packages/mosaic/src/__tests__/feature/fapi-contract.ts create mode 100644 packages/mosaic/src/__tests__/feature/fapi-spec-gaps.ts create mode 100644 packages/mosaic/src/__tests__/feature/fapi-spec.yml create mode 100644 packages/mosaic/src/__tests__/feature/raw.d.ts diff --git a/.changeset/mosaic-invite-only-role.md b/.changeset/mosaic-invite-only-role.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/mosaic-invite-only-role.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/mosaic/docs/testing.md b/packages/mosaic/docs/testing.md index 8ddb38e614e..c8055d558ed 100644 --- a/packages/mosaic/docs/testing.md +++ b/packages/mosaic/docs/testing.md @@ -188,6 +188,42 @@ handler**, or on a `holdRequests` hold never released or failed. When a feature needs a new endpoint, add a handler to `serveFapi` that mirrors what FAPI returns. +### Matching the backend + +Every mocked response is checked against the FAPI OpenAPI spec, vendored as +`fapi-spec.yml`. The check is strict: a field the spec doesn't list fails, and +so does a missing required field or a wrong type. It reads the status and the +`_method` override, and checks a 4xx or 5xx the spec doesn't document against +`ClerkErrors`. A violation fails the test from the `afterEach`, with the +request and the JSON pointer of the bad field. + +When a test fails here, fix the builder or the handler so it sends what FAPI +sends. A builder can make a backend quirk part of its name, for example +`fapiSamlLinking(verification)` for the bare account that SAML linking +returns. + +When FAPI sends something the spec gets wrong, add an entry to +`fapi-spec-gaps.ts`: + +- `at` and `property` name the spec schema and the field. +- `schema` is what FAPI really sends. `replaces` is the spec's current schema, + when the gap widens a field the spec already has. +- `sentBy` names the `clerk_go` code that sends it. + +The list prunes itself. Once the spec adds the field, or changes the schema +that `replaces` records, the gap's test fails and the entry should go. + +A test that sends a response the spec forbids on purpose, such as a broken +server response, calls `allowSpecViolation(message)` with the exact violation. +The test also fails if that violation is never sent. + +To pick up backend changes, run the sync, then the feature tests: + +```bash +pnpm --filter @clerk/mosaic sync:fapi-spec # fetch the spec for SUPPORTED_FAPI_VERSION +pnpm --filter @clerk/mosaic test:feature +``` + ### Order matters Call `serveFapi`, then any `holdRequests`, then `renderWithClerk`. Handlers diff --git a/packages/mosaic/eslint.config.mjs b/packages/mosaic/eslint.config.mjs index c6cc71ac863..5f1adddfb40 100644 --- a/packages/mosaic/eslint.config.mjs +++ b/packages/mosaic/eslint.config.mjs @@ -5,4 +5,8 @@ import mosaicImportGuards from './eslint/config.mjs'; const root = path.resolve(import.meta.dirname, '../..'); -export default [...rootConfig.map(config => ({ ...config, basePath: root })), ...mosaicImportGuards]; +export default [ + ...rootConfig.map(config => ({ ...config, basePath: root })), + ...mosaicImportGuards, + { ignores: ['src/__tests__/feature/fapi-spec.yml'] }, +]; diff --git a/packages/mosaic/package.json b/packages/mosaic/package.json index a15e26ebdaa..4efb7ed02ce 100644 --- a/packages/mosaic/package.json +++ b/packages/mosaic/package.json @@ -57,6 +57,7 @@ "lint": "eslint src", "lint:attw": "attw --pack . --exclude-entrypoints styles.css --profile esm-only", "lint:publint": "publint", + "sync:fapi-spec": "node ./scripts/sync-fapi-spec.mjs", "test": "vitest run --project primitives --project mosaic", "test:browser": "vitest run --project browser", "test:ci": "vitest run --project primitives --project mosaic --maxWorkers=70%", @@ -82,6 +83,7 @@ "@types/react": "catalog:react", "@types/react-dom": "catalog:react", "@vitest/eslint-plugin": "1.6.28", + "ajv": "^8.20.0", "axe-core": "^4.11.3", "bundlewatch": "^0.4.2", "eslint-plugin-boundaries": "7.3.0", @@ -93,7 +95,8 @@ "react": "catalog:react", "react-dom": "catalog:react", "tsdown": "catalog:repo", - "vitest-axe": "^0.1.0" + "vitest-axe": "^0.1.0", + "yaml": "^2.9.0" }, "peerDependencies": { "react": "catalog:peer-react", diff --git a/packages/mosaic/scripts/sync-fapi-spec.mjs b/packages/mosaic/scripts/sync-fapi-spec.mjs new file mode 100644 index 00000000000..9bbb7dca3d0 --- /dev/null +++ b/packages/mosaic/scripts/sync-fapi-spec.mjs @@ -0,0 +1,38 @@ +import { readFile, writeFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; + +const constants = await readFile( + resolve(import.meta.dirname, '../../shared/src/internal/clerk-js/constants.ts'), + 'utf8', +); +const version = constants.match(/SUPPORTED_FAPI_VERSION = '([^']+)'/)?.[1]; +if (!version) { + throw new Error('Could not read SUPPORTED_FAPI_VERSION from @clerk/shared.'); +} + +const specPath = `fapi/${version}.yml`; +const headers = process.env.GITHUB_TOKEN ? { authorization: `Bearer ${process.env.GITHUB_TOKEN}` } : {}; +const commits = await fetchOk( + `https://api.github.com/repos/clerk/openapi-specs/commits?path=${specPath}&per_page=1`, + headers, +).then(response => response.json()); +const sha = commits[0]?.sha; +if (!sha) { + throw new Error(`clerk/openapi-specs has no ${specPath}.`); +} + +const spec = await fetchOk(`https://raw.githubusercontent.com/clerk/openapi-specs/${sha}/${specPath}`).then(response => + response.text(), +); +const target = resolve(import.meta.dirname, '../src/__tests__/feature/fapi-spec.yml'); +await writeFile(target, `# clerk/openapi-specs@${sha} ${specPath}\n${spec}`); + +console.log(`Synced ${specPath} at ${sha}`); + +async function fetchOk(url, headers = {}) { + const response = await fetch(url, { headers }); + if (!response.ok) { + throw new Error(`${url} answered ${response.status}`); + } + return response; +} diff --git a/packages/mosaic/src/__tests__/feature/fake-fapi.ts b/packages/mosaic/src/__tests__/feature/fake-fapi.ts index 691dcfb4819..71a36a540b9 100644 --- a/packages/mosaic/src/__tests__/feature/fake-fapi.ts +++ b/packages/mosaic/src/__tests__/feature/fake-fapi.ts @@ -1,22 +1,12 @@ import { OAUTH_PROVIDERS } from '@clerk/shared/oauth'; import type { ApiKeyJSON, - ClientJSON, - EmailAddressJSON, EnterpriseConnectionJSON, OAuthProvider, - OrganizationInvitationJSON, OrganizationJSON, OrganizationMembershipJSON, OrganizationMembershipRequestJSON, OrganizationSuggestionJSON, - PhoneNumberJSON, - RoleJSON, - SessionJSON, - SessionWithActivitiesJSON, - UserJSON, - UserOrganizationInvitationJSON, - Web3WalletJSON, } from '@clerk/shared/types'; import { http, HttpResponse } from 'msw'; import { setupWorker } from 'msw/browser'; @@ -33,20 +23,32 @@ import { verificationHandlers, } from './fake-fapi/verification'; import { + type FapiActiveSession, fapiApiKey, + fapiApiKeyId, + type FapiClient, fapiClient, + type FapiEmailAddress, fapiEmailAddress, type FapiEnvironment, fapiEnvironment, fapiExternalAccount, fapiMembership, fapiOrganization, + type FapiOrganizationInvitation, fapiPage, + type FapiPhoneNumber, fapiPhoneNumber, + type FapiRole, + type FapiSession, fapiToken, + type FapiUser, + type FapiUserInvitation, fapiVerification, + type FapiWeb3Wallet, fapiWeb3Wallet, } from './fapi'; +import { fapiContractViolations } from './fapi-contract'; const FAPI_DOMAIN = '12345.prod.lclclerk.com'; let fapiRun = 0; @@ -63,13 +65,13 @@ export const fapiUrl = (path: string) => `https://${fapiHost}${path}`; export interface FakeFapiState { environment: FapiEnvironment; - client: ClientJSON; + client: FapiClient; country?: string; memberships: OrganizationMembershipJSON[]; - roles: RoleJSON[]; + roles: FapiRole[]; hasRoleSetMigration: boolean; - invitations: UserOrganizationInvitationJSON[]; - organizationInvitations: OrganizationInvitationJSON[]; + invitations: FapiUserInvitation[]; + organizationInvitations: FapiOrganizationInvitation[]; organizationMembershipRequests: Array< Omit & { status: 'pending' | 'accepted' | 'rejected' } >; @@ -84,7 +86,7 @@ export interface FakeFapiState { deviceTrackingEnabled: boolean; } -export interface ActiveDeviceRecord extends SessionWithActivitiesJSON { +export interface ActiveDeviceRecord extends FapiActiveSession { ownerUserId: string; replacementSessionId?: string; inactivityTimeoutSeconds?: number; @@ -99,6 +101,8 @@ export type FakeFapiSeed = Partial[] = []; +const allowedViolations: string[] = []; export const worker = setupWorker( http.all(`https://*.${FAPI_DOMAIN}/*`, ({ request }) => @@ -117,10 +121,32 @@ export function startWorker() { }); } +worker.events.on('response:mocked', ({ request, response }) => { + if (new URL(request.url).host === fapiHost) { + contractChecks.push(fapiContractViolations(request, response)); + } +}); + export function takeUnhandledRequests(): string[] { return unhandled.splice(0); } +export function allowSpecViolation(violation: string) { + allowedViolations.push(violation); +} + +export async function takeContractViolations(): Promise { + const checks = contractChecks.splice(0); + const allowed = allowedViolations.splice(0); + const violations = (await Promise.all(checks)).flat(); + return [ + ...violations.filter(violation => !allowed.includes(violation)), + ...allowed + .filter(violation => !violations.includes(violation)) + .map(violation => `${violation} was allowed but never sent`), + ]; +} + function page(items: T[], url: URL) { const offset = Number(url.searchParams.get('offset') ?? 0); const limit = Number(url.searchParams.get('limit') ?? items.length); @@ -132,11 +158,11 @@ function withStatus(items: T[], url: URL): T[] { return statuses.length ? items.filter(item => statuses.includes(item.status)) : items; } -function activeUser(state: FakeFapiState): UserJSON | undefined { +function activeUser(state: FakeFapiState): FapiUser | undefined { return activeSession(state)?.user; } -function withoutOrganization(session: SessionJSON, organizationId: string): SessionJSON { +function withoutOrganization(session: FapiSession, organizationId: string): FapiSession { return { ...session, user: { @@ -182,11 +208,11 @@ function updateOrganization(state: FakeFapiState, organization: OrganizationJSON }; } -function findWeb3Wallet(state: FakeFapiState, id: unknown): Web3WalletJSON | undefined { +function findWeb3Wallet(state: FakeFapiState, id: unknown): FapiWeb3Wallet | undefined { return activeUser(state)?.web3_wallets.find(wallet => wallet.id === id); } -function updateWeb3Wallet(state: FakeFapiState, wallet: Web3WalletJSON): void { +function updateWeb3Wallet(state: FakeFapiState, wallet: FapiWeb3Wallet): void { const user = activeUser(state); if (user) { updateUser(state, { @@ -196,7 +222,7 @@ function updateWeb3Wallet(state: FakeFapiState, wallet: Web3WalletJSON): void { } } -function requestingSession(state: FakeFapiState, request: Request): SessionJSON | undefined { +function requestingSession(state: FakeFapiState, request: Request): FapiSession | undefined { const sessionId = new URL(request.url).searchParams.get('_clerk_session_id') ?? state.client.last_active_session_id; return findSession(state, sessionId); } @@ -240,10 +266,10 @@ const USER_FIELDS = [ 'primary_email_address_id', 'primary_phone_number_id', 'primary_web3_wallet_id', -] as const satisfies readonly (keyof UserJSON)[]; +] as const satisfies readonly (keyof FapiUser)[]; -function patchUser(user: UserJSON, body: URLSearchParams): UserJSON { - const patch: Partial> = {}; +function patchUser(user: FapiUser, body: URLSearchParams): FapiUser { + const patch: Partial> = {}; for (const field of USER_FIELDS) { if (body.has(field)) { patch[field] = body.get(field) || null; @@ -252,11 +278,11 @@ function patchUser(user: UserJSON, body: URLSearchParams): UserJSON { return { ...user, ...patch }; } -function activeSession(state: FakeFapiState): SessionJSON | undefined { +function activeSession(state: FakeFapiState): FapiSession | undefined { return state.client.sessions.find(session => session.id === state.client.last_active_session_id); } -function updateActiveUser(state: FakeFapiState, next: (user: UserJSON) => UserJSON): UserJSON | undefined { +function updateActiveUser(state: FakeFapiState, next: (user: FapiUser) => FapiUser): FapiUser | undefined { const session = activeSession(state); if (!session) { return undefined; @@ -269,22 +295,22 @@ function updateActiveUser(state: FakeFapiState, next: (user: UserJSON) => UserJS return user; } -function findPhone(state: FakeFapiState, id: unknown): PhoneNumberJSON | undefined { +function findPhone(state: FakeFapiState, id: unknown): FapiPhoneNumber | undefined { return activeSession(state)?.user.phone_numbers.find(phone => phone.id === id); } -function replacePhone(state: FakeFapiState, phone: PhoneNumberJSON) { +function replacePhone(state: FakeFapiState, phone: FapiPhoneNumber) { updateActiveUser(state, user => ({ ...user, phone_numbers: user.phone_numbers.map(p => (p.id === phone.id ? phone : p)), })); } -function findEmail(state: FakeFapiState, id: unknown): EmailAddressJSON | undefined { +function findEmail(state: FakeFapiState, id: unknown): FapiEmailAddress | undefined { return activeSession(state)?.user.email_addresses.find(email => email.id === id); } -function replaceEmail(state: FakeFapiState, email: EmailAddressJSON) { +function replaceEmail(state: FakeFapiState, email: FapiEmailAddress) { updateActiveUser(state, user => ({ ...user, email_addresses: user.email_addresses.map(e => (e.id === email.id ? email : e)), @@ -414,7 +440,10 @@ export function serveFapi(seed: FakeFapiSeed = {}): FakeFapiState { ...current, external_accounts: current.external_accounts.filter(item => item.id !== account.id), })); - return envelope({ ...account, object: 'external_account' }, state.client); + return envelope( + { object: `oauth_${account.provider}`, id: account.identification_id, deleted: true }, + state.client, + ); }), http.post(fapiUrl('/v1/me/web3_wallets'), async ({ request }) => { const user = activeUser(state); @@ -429,7 +458,7 @@ export function serveFapi(seed: FakeFapiSeed = {}): FakeFapiState { const wallet = fapiWeb3Wallet({ id: `wallet_${user.web3_wallets.length + 1}`, web3_wallet: address, - verification: fapiVerification('', { expire_at: 0 }), + verification: null, }); updateUser(state, { ...user, web3_wallets: [...user.web3_wallets, wallet] }); return envelope(wallet, state.client); @@ -561,15 +590,15 @@ export function serveFapi(seed: FakeFapiSeed = {}): FakeFapiState { const target = state.activeDevices.find(item => item.id === params.id); if (target?.id === requester.id) { const code = 'invalid_action_for_session'; - return HttpResponse.json({ errors: [{ code, message: code }] }, { status: 400 }); + return HttpResponse.json({ errors: [{ code, message: code, long_message: code }] }, { status: 400 }); } if (!target || target.ownerUserId !== requester.user.id) { const code = 'action_for_session_not_authorized'; - return HttpResponse.json({ errors: [{ code, message: code }] }, { status: 401 }); + return HttpResponse.json({ errors: [{ code, message: code, long_message: code }] }, { status: 401 }); } if (!canRevokeDevice(target)) { const code = 'invalid_action_for_session'; - return HttpResponse.json({ errors: [{ code, message: code }] }, { status: 400 }); + return HttpResponse.json({ errors: [{ code, message: code, long_message: code }] }, { status: 400 }); } target.status = 'revoked'; return envelope({ ...deviceSessionPayload(target), user: requester.user, status: 'revoked' }, state.client); @@ -649,11 +678,12 @@ export function serveFapi(seed: FakeFapiSeed = {}): FakeFapiState { return rejected('strategy_invalid', `Unsupported strategy ${strategy}`); } const domain = email.email_address.split('@')[1] ?? 'acme.co'; - const prepared: EmailAddressJSON = { + const prepared: FapiEmailAddress = { ...email, - verification: fapiVerification(strategy, { - external_verification_redirect_url: strategy === 'enterprise_sso' ? `https://idp.${domain}/sso` : undefined, - }), + verification: + strategy === 'enterprise_sso' + ? fapiVerification('saml', { external_verification_redirect_url: `https://idp.${domain}/sso` }) + : fapiVerification(strategy), }; replaceEmail(state, prepared); return envelope(prepared, state.client); @@ -667,7 +697,7 @@ export function serveFapi(seed: FakeFapiSeed = {}): FakeFapiState { if (body.get('code') !== VERIFICATION_CODE) { return rejected('form_code_incorrect', 'Incorrect code'); } - const verified: EmailAddressJSON = { + const verified: FapiEmailAddress = { ...email, verification: fapiVerification('email_code', { status: 'verified' }), }; @@ -733,15 +763,15 @@ export function serveFapi(seed: FakeFapiSeed = {}): FakeFapiState { return envelope({ object: 'phone_number', id: phone.id, deleted: true }, state.client); }), http.get(fapiUrl('/v1/me/organization_memberships'), ({ request }) => - envelope(page(state.memberships, new URL(request.url)), null), + envelope(page(state.memberships, new URL(request.url)), state.client), ), http.get(fapiUrl('/v1/me/organization_invitations'), ({ request }) => { const url = new URL(request.url); - return envelope(page(withStatus(state.invitations, url), url), null); + return envelope(page(withStatus(state.invitations, url), url), state.client); }), http.get(fapiUrl('/v1/me/organization_suggestions'), ({ request }) => { const url = new URL(request.url); - return envelope(page(withStatus(state.suggestions, url), url), null); + return envelope(page(withStatus(state.suggestions, url), url), state.client); }), http.post(fapiUrl('/v1/me/organization_memberships/:organizationId'), ({ params, request }) => { if (new URL(request.url).searchParams.get('_method') !== 'DELETE') { @@ -757,7 +787,7 @@ export function serveFapi(seed: FakeFapiSeed = {}): FakeFapiState { }), http.get(fapiUrl('/v1/organizations/:organizationId'), ({ params }) => { const membership = state.memberships.find(m => m.organization.id === params.organizationId); - return membership ? envelope(membership.organization, null) : missing(); + return membership ? envelope(membership.organization, state.client) : missing(); }), http.post(fapiUrl('/v1/organizations/:organizationId'), async ({ params, request }) => { const method = new URL(request.url).searchParams.get('_method'); @@ -841,7 +871,7 @@ export function serveFapi(seed: FakeFapiSeed = {}): FakeFapiState { } const now = Date.now(); const created = fapiApiKey({ - id: `ak_${state.apiKeys.length + 1}`, + id: fapiApiKeyId(state.apiKeys.length + 1), name: body.name, subject: body.subject, description: body.description ?? null, @@ -850,7 +880,7 @@ export function serveFapi(seed: FakeFapiSeed = {}): FakeFapiState { updated_at: now, }); state.apiKeys = [created, ...state.apiKeys]; - return HttpResponse.json({ ...created, secret: `ak_secret_${created.id}` }); + return HttpResponse.json({ ...created, secret: `ak_secret_${created.id}` }, { status: 201 }); }), http.post(fapiUrl('/api_keys/:id/revoke'), ({ params }) => { const key = state.apiKeys.find(k => k.id === params.id); @@ -933,7 +963,7 @@ export function holdRequests(method: 'get' | 'post', path: string, query: Record return undefined; } requests.push(request.clone()); - return gate; + return gate.then(response => response?.clone()); }), ); diff --git a/packages/mosaic/src/__tests__/feature/fake-fapi/enterprise.ts b/packages/mosaic/src/__tests__/feature/fake-fapi/enterprise.ts index 5e816264f00..14aac1e8ffe 100644 --- a/packages/mosaic/src/__tests__/feature/fake-fapi/enterprise.ts +++ b/packages/mosaic/src/__tests__/feature/fake-fapi/enterprise.ts @@ -1,9 +1,10 @@ -import type { ExternalAccountJSON, VerificationJSON } from '@clerk/shared/types'; +import type { VerificationJSON } from '@clerk/shared/types'; import { http, HttpResponse } from 'msw'; import type { FakeFapiState } from '../fake-fapi'; +import { type FapiExternalAccount, fapiSamlLinking } from '../fapi'; -export type EnterpriseExternalAccount = Omit & { provider: string }; +export type EnterpriseExternalAccount = Omit & { provider: string }; export type EnterprisePreparation = | { kind: 'saml'; verification: VerificationJSON } @@ -17,8 +18,9 @@ export interface FakeEnterpriseLinking { } function rejected(code: 'resource_not_found' | 'feature_not_enabled') { + const message = code === 'resource_not_found' ? 'not found' : 'Feature not enabled'; return HttpResponse.json( - { errors: [{ code, message: code === 'resource_not_found' ? 'not found' : 'Feature not enabled' }] }, + { errors: [{ code, message, long_message: message }] }, { status: code === 'resource_not_found' ? 404 : 403 }, ); } @@ -107,7 +109,7 @@ export function enterpriseHandlers(state: FakeFapiState, url: (path: string) => ); } return HttpResponse.json({ - response: { object: 'external_account', verification: preparation.verification }, + response: fapiSamlLinking(preparation.verification), client: state.client, }); } diff --git a/packages/mosaic/src/__tests__/feature/fake-fapi/mfa.ts b/packages/mosaic/src/__tests__/feature/fake-fapi/mfa.ts index 41f511abb5d..8e5de93733f 100644 --- a/packages/mosaic/src/__tests__/feature/fake-fapi/mfa.ts +++ b/packages/mosaic/src/__tests__/feature/fake-fapi/mfa.ts @@ -1,7 +1,13 @@ -import type { ClientJSON, PhoneNumberJSON, UserJSON } from '@clerk/shared/types'; import { http, HttpResponse } from 'msw'; -import { type FapiEnvironment, fapiPhoneNumber, fapiVerification } from '../fapi'; +import { + type FapiClient, + type FapiEnvironment, + type FapiPhoneNumber, + fapiPhoneNumber, + type FapiUser, + fapiVerification, +} from '../fapi'; import { envelope, missing, requestUser, updateUser, VERIFICATION_CODE } from './shared'; export interface FakeMfaState { @@ -31,19 +37,19 @@ export function createMfaState(): FakeMfaState { } export function mfaHandlers( - state: { client: ClientJSON; environment: FapiEnvironment; mfa: FakeMfaState }, + state: { client: FapiClient; environment: FapiEnvironment; mfa: FakeMfaState }, fapiUrl: (path: string) => string, ) { const backupEnabled = () => state.environment.user_settings.attributes.backup_code.enabled && state.environment.user_settings.attributes.backup_code.used_for_second_factor; - const usableFactors = (user: UserJSON) => + const usableFactors = (user: FapiUser) => Number(user.totp_enabled) + user.phone_numbers.filter(phone => phone.reserved_for_second_factor && phone.verification?.status === 'verified') .length; const error = (code: string, message: string, status = 400) => HttpResponse.json({ errors: [{ code, message, long_message: message }] }, { status }); - const updatePhone = (user: UserJSON, id: string, transform: (phone: PhoneNumberJSON) => PhoneNumberJSON) => { + const updatePhone = (user: FapiUser, id: string, transform: (phone: FapiPhoneNumber) => FapiPhoneNumber) => { const phone = user.phone_numbers.find(phone => phone.id === id); if (!phone) { return undefined; @@ -78,7 +84,7 @@ export function mfaHandlers( currentUser.phone_numbers.some(phone => phone.reserved_for_second_factor) && currentUser.backup_code_enabled, }); - return envelope({ object: 'deleted', id: 'totp_1' }, state.client); + return envelope({ object: 'totp', id: 'totp_1', deleted: true }, state.client); } if ( currentUser.totp_enabled || @@ -127,8 +133,10 @@ export function mfaHandlers( { object: 'totp', id: 'totp_1', + secret: null, + uri: null, verified: true, - ...(newCodes ? { backup_codes: state.mfa.codes } : {}), + backup_codes: newCodes ? state.mfa.codes : null, created_at: Date.now(), updated_at: Date.now(), }, diff --git a/packages/mosaic/src/__tests__/feature/fake-fapi/organization-members.ts b/packages/mosaic/src/__tests__/feature/fake-fapi/organization-members.ts index b2495f2046e..11c5b4fcb2b 100644 --- a/packages/mosaic/src/__tests__/feature/fake-fapi/organization-members.ts +++ b/packages/mosaic/src/__tests__/feature/fake-fapi/organization-members.ts @@ -1,8 +1,7 @@ -import type { OrganizationInvitationJSON } from '@clerk/shared/types'; import { http, HttpResponse } from 'msw'; import type { FakeFapiState } from '../fake-fapi'; -import { fapiMembership } from '../fapi'; +import { fapiMembership, type FapiOrganizationInvitation } from '../fapi'; import { envelope, missing, rejectUnknownParams, requestUser } from './shared'; const INVITATION_STATUSES = ['pending', 'accepted', 'revoked', 'expired']; @@ -18,7 +17,7 @@ async function createInvitations( state: FakeFapiState, organizationId: string, request: Request, -): Promise { +): Promise { const body = new URLSearchParams(await request.text()); const unknown = [...body.keys()].find(key => !key.startsWith('_') && !INVITATION_PARAMS.includes(key)); if (unknown) { @@ -124,7 +123,7 @@ async function createInvitations( } const now = Date.now(); const created = normalized.map( - (email, index): OrganizationInvitationJSON => ({ + (email, index): FapiOrganizationInvitation => ({ object: 'organization_invitation', id: `orginv_${now}_${state.organizationInvitations.length + index}`, email_address: email, @@ -133,6 +132,8 @@ async function createInvitations( status: 'pending', role: selectedRole.key, role_name: selectedRole.name, + url: null, + expires_at: null, created_at: now, updated_at: now, }), @@ -157,7 +158,16 @@ export function organizationMemberHandlers(state: FakeFapiState, fapiUrl: (path: const query = url.searchParams.get('query')?.toLowerCase(); if (query === '') { return HttpResponse.json( - { errors: [{ code: 'form_param_missing', message: 'Enter query.', meta: { param_name: 'query' } }] }, + { + errors: [ + { + code: 'form_param_missing', + message: 'Enter query.', + long_message: 'Enter query.', + meta: { param_name: 'query' }, + }, + ], + }, { status: 422 }, ); } @@ -175,14 +185,14 @@ export function organizationMemberHandlers(state: FakeFapiState, fapiUrl: (path: }); const offset = Number(url.searchParams.get('offset') ?? 0); const limit = Number(url.searchParams.get('limit') ?? 10); - return envelope({ data: matching.slice(offset, offset + limit), total_count: matching.length }, null); + return envelope({ data: matching.slice(offset, offset + limit), total_count: matching.length }, state.client); }), http.get(fapiUrl('/v1/organizations/:organizationId/roles'), ({ params }) => { const organizationExists = state.memberships.some(member => member.organization.id === params.organizationId); return organizationExists ? envelope( { data: state.roles, total_count: state.roles.length, has_role_set_migration: state.hasRoleSetMigration }, - null, + state.client, ) : missing(); }), @@ -201,6 +211,7 @@ export function organizationMemberHandlers(state: FakeFapiState, fapiUrl: (path: { code: 'form_param_value_invalid', message: `${invalidStatus} is not a valid value for status.`, + long_message: `${invalidStatus} is not a valid value for status.`, meta: { param_name: 'status' }, }, ], @@ -215,7 +226,7 @@ export function organizationMemberHandlers(state: FakeFapiState, fapiUrl: (path: ); const offset = Number(url.searchParams.get('offset') ?? 0); const limit = Number(url.searchParams.get('limit') ?? 10); - return envelope({ data: matching.slice(offset, offset + limit), total_count: matching.length }, null); + return envelope({ data: matching.slice(offset, offset + limit), total_count: matching.length }, state.client); }), http.get(fapiUrl('/v1/organizations/:organizationId/membership_requests'), ({ params, request }) => { const url = new URL(request.url); @@ -232,6 +243,7 @@ export function organizationMemberHandlers(state: FakeFapiState, fapiUrl: (path: { code: 'form_param_value_invalid', message: `${invalidStatus} is not a valid value for status.`, + long_message: `${invalidStatus} is not a valid value for status.`, meta: { param_name: 'status' }, }, ], @@ -245,7 +257,7 @@ export function organizationMemberHandlers(state: FakeFapiState, fapiUrl: (path: ); const offset = Number(url.searchParams.get('offset') ?? 0); const limit = Number(url.searchParams.get('limit') ?? 10); - return envelope({ data: matching.slice(offset, offset + limit), total_count: matching.length }, null); + return envelope({ data: matching.slice(offset, offset + limit), total_count: matching.length }, state.client); }), http.post(fapiUrl('/v1/organizations/:organizationId/membership_requests/:requestId/accept'), ({ params }) => { const request = state.organizationMembershipRequests.find( @@ -266,7 +278,7 @@ export function organizationMemberHandlers(state: FakeFapiState, fapiUrl: (path: public_user_data: request.public_user_data, }), ); - return envelope(accepted, null); + return envelope(accepted, state.client); }), http.post(fapiUrl('/v1/organizations/:organizationId/membership_requests/:requestId/reject'), ({ params }) => { const request = state.organizationMembershipRequests.find( @@ -280,15 +292,15 @@ export function organizationMemberHandlers(state: FakeFapiState, fapiUrl: (path: state.organizationMembershipRequests = state.organizationMembershipRequests.map(item => item.id === rejected.id ? rejected : item, ); - return envelope(rejected, null); + return envelope(rejected, state.client); }), http.post(fapiUrl('/v1/organizations/:organizationId/invitations'), async ({ params, request }) => { const created = await createInvitations(state, String(params.organizationId), request); - return created instanceof Response ? created : envelope(created[0] ?? null, null); + return created instanceof Response ? created : envelope(created[0] ?? null, state.client); }), http.post(fapiUrl('/v1/organizations/:organizationId/invitations/bulk'), async ({ params, request }) => { const created = await createInvitations(state, String(params.organizationId), request); - return created instanceof Response ? created : envelope(created, null); + return created instanceof Response ? created : envelope(created, state.client); }), http.post(fapiUrl('/v1/organizations/:organizationId/invitations/:invitationId/revoke'), ({ params }) => { const invitation = state.organizationInvitations.find( @@ -312,7 +324,7 @@ export function organizationMemberHandlers(state: FakeFapiState, fapiUrl: (path: state.organizationInvitations = state.organizationInvitations.map(item => item.id === revoked.id ? revoked : item, ); - return envelope(revoked, null); + return envelope(revoked, state.client); }), http.post(fapiUrl('/v1/organizations/:organizationId/memberships/:userId'), async ({ params, request }) => { const member = state.memberships.find( @@ -324,7 +336,7 @@ export function organizationMemberHandlers(state: FakeFapiState, fapiUrl: (path: const method = new URL(request.url).searchParams.get('_method'); if (method === 'DELETE') { state.memberships = state.memberships.filter(item => item.id !== member.id); - return envelope(member, null); + return envelope(member, state.client); } if (method !== 'PATCH') { return missing(); @@ -333,14 +345,14 @@ export function organizationMemberHandlers(state: FakeFapiState, fapiUrl: (path: const role = body.get('role'); if (!role) { return HttpResponse.json( - { errors: [{ code: 'form_param_missing', message: 'Role is required' }] }, + { errors: [{ code: 'form_param_missing', message: 'Role is required', long_message: 'Role is required' }] }, { status: 422 }, ); } const selected = state.roles.find(item => item.key === role); const updated = { ...member, role, role_name: selected?.name ?? role }; state.memberships = state.memberships.map(item => (item.id === member.id ? updated : item)); - return envelope(updated, null); + return envelope(updated, state.client); }), ]; } diff --git a/packages/mosaic/src/__tests__/feature/fake-fapi/passkeys.ts b/packages/mosaic/src/__tests__/feature/fake-fapi/passkeys.ts index efceddf7144..f449051e8aa 100644 --- a/packages/mosaic/src/__tests__/feature/fake-fapi/passkeys.ts +++ b/packages/mosaic/src/__tests__/feature/fake-fapi/passkeys.ts @@ -2,7 +2,7 @@ import type { PasskeyJSON } from '@clerk/shared/types'; import { http, HttpResponse } from 'msw'; import type { FakeFapiState } from '../fake-fapi'; -import { fapiPasskey } from '../fapi'; +import { fapiPasskey, fapiVerification } from '../fapi'; import { envelope, error, missing, requestUser, updateUser } from './shared'; export interface FakePasskeysSeed { @@ -49,22 +49,15 @@ export function passkeyHandlers( const passkey = fapiPasskey({ id: `passkey_${nextPasskeyId++}`, name: seed.name ?? 'Chrome on macOS', - verification: { - id: 'verification_passkey', - object: 'verification', - status: 'unverified', - verified_at_client: '', - strategy: 'passkey', - attempts: 0, + verification: fapiVerification('passkey', { expire_at: expiresAt, - error: { code: '', message: '' }, nonce: JSON.stringify({ challenge: 'Y2hhbGxlbmdl', rp: { name: 'Acme', id: 'localhost' }, user: { id: 'dXNlcg', name: 'user@example.com', displayName: 'Test user' }, pubKeyCredParams: [{ type: 'public-key', alg: -7 }], }), - }, + }), }); pendingPasskeys.set(passkey.id, { userId: user.id, passkey, expiresAt }); return envelope(passkey, state.client); @@ -86,7 +79,15 @@ export function passkeyHandlers( const body = new URLSearchParams(await request.text()); if (body.get('strategy') !== 'passkey' || !body.get('public_key_credential')) { return HttpResponse.json( - { errors: [{ code: 'form_param_missing', message: 'Passkey credential required.' }] }, + { + errors: [ + { + code: 'form_param_missing', + message: 'Passkey credential required.', + long_message: 'Passkey credential required.', + }, + ], + }, { status: 400 }, ); } @@ -94,14 +95,12 @@ export function passkeyHandlers( ...passkey, name: seed.authenticatorName || passkey.name, last_used_at: Date.now(), - verification: { - ...verification, - status: 'verified' as const, + verification: fapiVerification('passkey', { + status: 'verified', attempts: verification.attempts + 1, - verified_at_client: state.client.id, + expire_at: verification.expire_at, nonce: undefined, - error: undefined, - }, + }), }; pendingPasskeys.delete(passkey.id); updateUser(state, { ...user, passkeys: [...user.passkeys, verified] }); @@ -135,6 +134,7 @@ export function passkeyHandlers( { code: 'form_param_max_length_exceeded', message: 'Passkey name is too long.', + long_message: 'Passkey name is too long.', meta: { param_name: 'name' }, }, ], diff --git a/packages/mosaic/src/__tests__/feature/fake-fapi/shared.ts b/packages/mosaic/src/__tests__/feature/fake-fapi/shared.ts index bac91959e14..680f1a515fa 100644 --- a/packages/mosaic/src/__tests__/feature/fake-fapi/shared.ts +++ b/packages/mosaic/src/__tests__/feature/fake-fapi/shared.ts @@ -1,15 +1,16 @@ -import type { ClientJSON, SessionJSON, UserJSON } from '@clerk/shared/types'; import { HttpResponse, type JsonBodyType } from 'msw'; +import type { FapiClient, FapiSession, FapiUser } from '../fapi'; + export const VERIFICATION_CODE = '424242'; -type ClientState = { client: ClientJSON }; +type ClientState = { client: FapiClient }; -export function envelope(response: JsonBodyType, client: ClientJSON | null) { +export function envelope(response: JsonBodyType, client: FapiClient | null) { return HttpResponse.json({ response, client }); } -export function findSession(state: ClientState, id: unknown): SessionJSON | undefined { +export function findSession(state: ClientState, id: unknown): FapiSession | undefined { return state.client.sessions.find(session => session.id === id); } @@ -17,12 +18,12 @@ export function error(code: string, status = 400) { return HttpResponse.json({ errors: [{ code, message: code, long_message: code }] }, { status }); } -export function requestUser(state: ClientState, request: Request): UserJSON | undefined { +export function requestUser(state: ClientState, request: Request): FapiUser | undefined { const sessionId = new URL(request.url).searchParams.get('_clerk_session_id') ?? state.client.last_active_session_id; return findSession(state, sessionId)?.user; } -export function updateUser(state: ClientState, user: UserJSON): void { +export function updateUser(state: ClientState, user: FapiUser): void { state.client = { ...state.client, sessions: state.client.sessions.map(session => (session.user.id === user.id ? { ...session, user } : session)), @@ -30,7 +31,10 @@ export function updateUser(state: ClientState, user: UserJSON): void { } export function missing() { - return HttpResponse.json({ errors: [{ code: 'resource_not_found', message: 'not found' }] }, { status: 404 }); + return HttpResponse.json( + { errors: [{ code: 'resource_not_found', message: 'not found', long_message: 'not found' }] }, + { status: 404 }, + ); } const paginationParams = ['limit', 'offset', 'paginated']; @@ -46,6 +50,7 @@ export function rejectUnknownParams(url: URL, allowed: readonly string[]) { { code: 'form_param_unknown', message: `${unknown} is not a valid parameter for this request.`, + long_message: `${unknown} is not a valid parameter for this request.`, meta: { param_name: unknown }, }, ], diff --git a/packages/mosaic/src/__tests__/feature/fake-fapi/verification.ts b/packages/mosaic/src/__tests__/feature/fake-fapi/verification.ts index 6bc62122ef4..cd267b1aea0 100644 --- a/packages/mosaic/src/__tests__/feature/fake-fapi/verification.ts +++ b/packages/mosaic/src/__tests__/feature/fake-fapi/verification.ts @@ -1,6 +1,4 @@ import type { - ClientJSON, - SessionJSON, SessionVerificationLevel, SessionVerificationStatus, SignInFirstFactorJSON, @@ -9,7 +7,7 @@ import type { } from '@clerk/shared/types'; import { http, HttpResponse } from 'msw'; -import { fapiSessionVerification, fapiVerification } from '../fapi'; +import { type FapiClient, type FapiSession, fapiSessionVerification, fapiVerification } from '../fapi'; export type VerificationSecretStrategy = 'password' | 'email_code' | 'phone_code' | 'totp' | 'backup_code'; @@ -26,7 +24,7 @@ export interface FakeVerificationState { export type FakeVerificationSeed = Partial>; export interface VerificationHost { - client: ClientJSON; + client: FapiClient; verification: FakeVerificationState; } @@ -43,16 +41,19 @@ export function createVerificationState(seed: FakeVerificationSeed = {}): FakeVe }; } -function envelope(response: object, client: ClientJSON) { +function envelope(response: object, client: FapiClient) { return HttpResponse.json({ response, client }); } -function findSession(state: VerificationHost, id: unknown): SessionJSON | undefined { +function findSession(state: VerificationHost, id: unknown): FapiSession | undefined { return state.client.sessions.find(session => session.id === id); } function missing() { - return HttpResponse.json({ errors: [{ code: 'resource_not_found', message: 'not found' }] }, { status: 404 }); + return HttpResponse.json( + { errors: [{ code: 'resource_not_found', message: 'not found', long_message: 'not found' }] }, + { status: 404 }, + ); } type Stage = 'first' | 'second'; @@ -127,7 +128,7 @@ const verificationErrors = { codeIncorrect: () => apiError(422, 'form_code_incorrect', 'is incorrect', 'Incorrect code', 'code'), }; -function verificationReply(state: VerificationHost, session: SessionJSON) { +function verificationReply(state: VerificationHost, session: FapiSession) { const { level, status, firstFactors, secondFactors, firstFactorVerification, secondFactorVerification } = state.verification; const complete = status === 'complete'; @@ -152,7 +153,7 @@ function setStageVerification(state: VerificationHost, stage: Stage, verificatio } export function verificationHandlers(state: VerificationHost, url: (path: string) => string) { - const guard = (stage: Stage, action: string, session: SessionJSON | undefined, strategy: string | null) => { + const guard = (stage: Stage, action: string, session: FapiSession | undefined, strategy: string | null) => { if (!session) { return missing(); } diff --git a/packages/mosaic/src/__tests__/feature/fapi-contract.feature.test.tsx b/packages/mosaic/src/__tests__/feature/fapi-contract.feature.test.tsx new file mode 100644 index 00000000000..141efe388c8 --- /dev/null +++ b/packages/mosaic/src/__tests__/feature/fapi-contract.feature.test.tsx @@ -0,0 +1,162 @@ +import { http, HttpResponse } from 'msw'; +import { describe, expect, it } from 'vitest'; + +import { allowSpecViolation, fapiUrl, takeContractViolations, worker } from './fake-fapi'; +import { fapiClient, fapiEmailAddress, fapiSamlLinking, fapiSession, fapiUser, fapiVerification } from './fapi'; +import { fapiContractViolations, staleSpecGaps } from './fapi-contract'; + +const FAPI = 'https://clerk.example.com'; + +function request(method: string, path: string) { + return new Request(`${FAPI}${path}`, { method }); +} + +const user = fapiUser({ id: 'user_1', email_addresses: [fapiEmailAddress({ id: 'idn_1' })] }); +const client = fapiClient([fapiSession({ id: 'sess_1', user })]); + +function envelope(response: unknown, status = 200) { + return HttpResponse.json({ response, client }, { status }); +} + +describe('fapiContractViolations', () => { + it('accepts a response that matches the spec', async () => { + expect(await fapiContractViolations(request('GET', '/v1/me'), envelope(user))).toEqual([]); + }); + + it('accepts a whole client from the default builders', async () => { + expect(await fapiContractViolations(request('GET', '/v1/client'), envelope(client))).toEqual([]); + }); + + it('rejects a field the spec does not declare', async () => { + const violations = await fapiContractViolations( + request('GET', '/v1/me'), + envelope({ ...user, invented_field: true }), + ); + expect(violations).toEqual([expect.stringContaining('invented_field')]); + }); + + it('rejects a field with the wrong type', async () => { + const violations = await fapiContractViolations(request('GET', '/v1/me'), envelope({ ...user, first_name: 42 })); + expect(violations).toEqual([expect.stringContaining('/response/first_name')]); + }); + + it('rejects a response missing a required field', async () => { + const { object: _object, ...withoutObject } = user; + const violations = await fapiContractViolations(request('GET', '/v1/me'), envelope(withoutObject)); + expect(violations).toEqual([expect.stringContaining('object')]); + }); + + it('checks a POST sent with _method against the operation it stands for', async () => { + const deleted = { object: 'user', id: 'user_1', deleted: true }; + expect(await fapiContractViolations(request('POST', '/v1/me?_method=DELETE'), envelope(deleted))).toEqual([]); + expect(await fapiContractViolations(request('POST', '/v1/me?_method=PATCH'), envelope(deleted))).not.toEqual([]); + }); + + it('matches path parameters and ignores a trailing slash', async () => { + const email = fapiEmailAddress({ id: 'idn_1' }); + expect(await fapiContractViolations(request('GET', '/v1/me/email_addresses/idn_1/'), envelope(email))).toEqual([]); + }); + + it('checks a Clerk error against the error schema', async () => { + const valid = HttpResponse.json( + { errors: [{ code: 'not_found', message: 'gone', long_message: 'gone' }] }, + { status: 404 }, + ); + const invalid = HttpResponse.json({ errors: [{ code: 404 }] }, { status: 404 }); + expect(await fapiContractViolations(request('GET', '/v1/me'), valid)).toEqual([]); + expect(await fapiContractViolations(request('GET', '/v1/me'), invalid)).not.toEqual([]); + }); + + it('rejects a success status the operation does not document', async () => { + expect(await fapiContractViolations(request('GET', '/v1/me'), envelope(user, 201))).toEqual([ + expect.stringContaining('201'), + ]); + }); + + it('checks an error status the operation does not document against the error schema', async () => { + const valid = HttpResponse.json( + { errors: [{ code: 'conflict', message: 'taken', long_message: 'taken' }] }, + { status: 409 }, + ); + expect(await fapiContractViolations(request('GET', '/v1/me'), valid)).toEqual([]); + expect(await fapiContractViolations(request('GET', '/v1/me'), envelope(user, 409))).toEqual([ + expect.stringContaining('409'), + ]); + }); + + it('rejects an endpoint the spec does not have', async () => { + expect(await fapiContractViolations(request('GET', '/v1/me/invented'), envelope(user))).toEqual([ + expect.stringContaining('GET /v1/me/invented'), + ]); + }); + + it('accepts a field the spec is known to be missing', async () => { + const violations = await fapiContractViolations( + request('GET', '/v1/me'), + envelope({ ...user, enterprise_accounts: [] }), + ); + expect(violations).toEqual([]); + }); + + it('accepts a field the spec is known to be missing from an inline response schema', async () => { + const token = HttpResponse.json({ object: 'token', jwt: 'header.payload.signature' }); + expect(await fapiContractViolations(request('POST', '/v1/client/sessions/sess_1/tokens'), token)).toEqual([]); + }); + + it('accepts a field the spec is known to type too narrowly', async () => { + const linking = fapiSamlLinking(fapiVerification('saml')); + expect(await fapiContractViolations(request('POST', '/v1/me/external_accounts'), envelope(linking))).toEqual([]); + }); +}); + +describe('staleSpecGaps', () => { + it('has no known gap that the spec has since closed', () => { + expect(staleSpecGaps()).toEqual([]); + }); + + it('reports a known gap once the spec declares the field', () => { + const gap = { at: '#/components/schemas/Client.User', property: 'first_name', schema: {}, sentBy: 'user.go' }; + expect(staleSpecGaps([gap])).toEqual(['#/components/schemas/Client.User now declares first_name']); + }); + + it('reports a known gap whose schema left the spec', () => { + const gap = { at: '#/components/schemas/Client.Invented', property: 'field', schema: {}, sentBy: 'user.go' }; + expect(staleSpecGaps([gap])).toEqual(['#/components/schemas/Client.Invented is no longer in the spec']); + }); + + it('reports a known gap once the spec changes the field it widens', () => { + const gap = { + at: '#/components/schemas/Client.User', + property: 'first_name', + schema: {}, + replaces: { type: 'integer' }, + sentBy: 'user.go', + }; + expect(staleSpecGaps([gap])).toEqual(['#/components/schemas/Client.User changed first_name']); + }); +}); + +describe('takeContractViolations', () => { + const invalidMe = 'GET /v1/me 200: /response must be object {"type":"object"}'; + + async function fetchInvalidMe() { + worker.use(http.get(fapiUrl('/v1/me'), () => HttpResponse.json({ response: 42, client }))); + await fetch(fapiUrl('/v1/me')); + } + + it('reports a mocked response that does not match the spec', async () => { + await fetchInvalidMe(); + expect(await takeContractViolations()).toEqual([invalidMe]); + }); + + it('drops a violation the test allows', async () => { + allowSpecViolation(invalidMe); + await fetchInvalidMe(); + expect(await takeContractViolations()).toEqual([]); + }); + + it('reports an allowed violation that was never sent', async () => { + allowSpecViolation(invalidMe); + expect(await takeContractViolations()).toEqual([`${invalidMe} was allowed but never sent`]); + }); +}); diff --git a/packages/mosaic/src/__tests__/feature/fapi-contract.ts b/packages/mosaic/src/__tests__/feature/fapi-contract.ts new file mode 100644 index 00000000000..028bb5a3f64 --- /dev/null +++ b/packages/mosaic/src/__tests__/feature/fapi-contract.ts @@ -0,0 +1,265 @@ +import Ajv2019, { type ErrorObject, type ValidateFunction } from 'ajv/dist/2019'; +import { parse } from 'yaml'; + +import specSource from './fapi-spec.yml?raw'; +import { SPEC_GAPS, type SpecGap } from './fapi-spec-gaps'; + +export async function fapiContractViolations(request: Request, response: Response): Promise { + const url = new URL(request.url); + const method = (url.searchParams.get('_method') ?? request.method).toLowerCase(); + const path = url.pathname.replace(/(.)\/$/, '$1'); + const endpoint = `${method.toUpperCase()} ${path}`; + + const operation = operations.find(op => op.method === method && op.pattern.test(path)); + if (!operation) { + return [`${endpoint} is not in the Frontend API spec`]; + } + const schema = responseSchemaPointer(operation, response.status); + if (schema === undefined) { + return [`${endpoint} answered ${response.status}, which the spec does not document`]; + } + if (!schema || !response.headers.get('content-type')?.includes('json')) { + return []; + } + const validate = validator(schema); + if (validate(await response.clone().json())) { + return []; + } + return [`${endpoint} ${response.status}: ${[...new Set((validate.errors ?? []).map(describe))].join('; ')}`]; +} + +type Method = 'get' | 'post' | 'put' | 'patch' | 'delete'; +type JsonObject = Record; + +interface ResponseObject { + $ref?: string; + content?: Record; +} + +interface OperationObject { + responses: Record; +} + +interface Spec { + paths: Record>>; + components: { schemas: Record }; +} + +interface Operation { + method: Method; + template: string; + pattern: RegExp; + responses: Record; +} + +const METHODS: readonly Method[] = ['get', 'post', 'put', 'patch', 'delete']; +const JSON_SCHEMA = 'content/application~1json/schema'; +const CLERK_ERRORS = '#/components/schemas/ClerkErrors'; + +const spec: Spec = parse(specSource); + +const operations: Operation[] = Object.entries(spec.paths) + .flatMap(([template, item]) => + METHODS.flatMap(method => { + const operation = item[method]; + return operation ? [{ method, template, pattern: pathPattern(template), responses: operation.responses }] : []; + }), + ) + .sort((a, b) => parameterCount(a.template) - parameterCount(b.template)); + +const ajv = new Ajv2019({ strict: false, allErrors: true, validateFormats: false, validateSchema: false }); +ajv.addSchema(closeObjects(withSpecGaps(spec)), 'fapi'); + +const validators = new Map(); + +function validator(pointer: string): ValidateFunction { + const cached = validators.get(pointer); + if (cached) { + return cached; + } + const validate = ajv.compile({ $ref: `fapi${pointer}` }); + validators.set(pointer, validate); + return validate; +} + +function responseSchemaPointer(operation: Operation, status: number): string | null | undefined { + const key = [String(status), `${String(status)[0]}XX`, 'default'].find(k => k in operation.responses); + const response = key === undefined ? undefined : operation.responses[key]; + if (!response || key === undefined) { + return status >= 400 ? CLERK_ERRORS : undefined; + } + if (response.$ref) { + return `${response.$ref}/${JSON_SCHEMA}`; + } + if (!response.content?.['application/json']) { + return null; + } + return `#/paths/${pointerSegment(operation.template)}/${operation.method}/responses/${key}/${JSON_SCHEMA}`; +} + +export function staleSpecGaps(gaps: readonly SpecGap[] = SPEC_GAPS): string[] { + return gaps.flatMap(({ at, property, replaces }) => { + const schema = resolvePointer(spec, at); + if (!isObject(schema)) { + return [`${at} is no longer in the spec`]; + } + if (replaces) { + const current = isObject(schema.properties) ? schema.properties[property] : undefined; + return sameJson(current, replaces) ? [] : [`${at} changed ${property}`]; + } + return declaredProperties(schema).has(property) ? [`${at} now declares ${property}`] : []; + }); +} + +function withSpecGaps(source: Spec): Spec { + const patched: Spec = structuredClone(source); + for (const { at, property, schema } of SPEC_GAPS) { + const target = resolvePointer(patched, at); + if (isObject(target)) { + target.properties = { ...(isObject(target.properties) ? target.properties : {}), [property]: schema }; + } + } + return patched; +} + +function resolvePointer(root: unknown, pointer: string): unknown { + return pointer + .replace(/^#\//, '') + .split('/') + .map(segment => decodeURIComponent(segment).replace(/~1/g, '/').replace(/~0/g, '~')) + .reduce((node, key) => (isObject(node) ? node[key] : undefined), root); +} + +function declaredProperties(schema: JsonObject): Set { + const members = Array.isArray(schema.allOf) ? schema.allOf.filter(isObject) : []; + return new Set( + [schema, ...members].flatMap(member => (isObject(member.properties) ? Object.keys(member.properties) : [])), + ); +} + +function sameJson(a: unknown, b: unknown): boolean { + if (Array.isArray(a) && Array.isArray(b)) { + return a.length === b.length && a.every((value, index) => sameJson(value, b[index])); + } + if (isObject(a) && isObject(b)) { + const keys = Object.keys(a); + return keys.length === Object.keys(b).length && keys.every(key => sameJson(a[key], b[key])); + } + return a === b; +} + +function describe(error: ErrorObject): string { + return `${error.instancePath || '/'} ${error.message ?? 'is invalid'} ${JSON.stringify(error.params)}`; +} + +function closeObjects(source: Spec): Spec { + const composed = composedComponents(source.components.schemas); + const schemas = Object.fromEntries( + Object.entries(source.components.schemas).map(([name, schema]) => [name, closeSchema(schema, composed.has(name))]), + ); + const paths = Object.fromEntries( + Object.entries(source.paths).map(([template, item]) => [ + template, + Object.fromEntries( + Object.entries(item).map(([method, operation]) => [ + method, + { ...operation, responses: closeResponses(operation.responses) }, + ]), + ), + ]), + ); + return { ...source, paths, components: { ...source.components, schemas } }; +} + +function closeResponses(responses: Record): Record { + return Object.fromEntries( + Object.entries(responses).map(([status, response]) => { + const json = response.content?.['application/json']; + if (!isObject(json) || json.schema === undefined) { + return [status, response]; + } + return [ + status, + { ...response, content: { ...response.content, 'application/json': { schema: closeSchema(json.schema) } } }, + ]; + }), + ); +} + +function closeSchema(node: unknown, isAllOfMember = false): unknown { + if (!isObject(node)) { + return node; + } + const schema = { ...node }; + if (isObject(schema.properties)) { + schema.properties = Object.fromEntries( + Object.entries(schema.properties).map(([key, value]) => [key, closeSchema(value)]), + ); + } + if (isObject(schema.additionalProperties)) { + schema.additionalProperties = closeSchema(schema.additionalProperties); + } + schema.items = closeSchema(schema.items); + for (const keyword of ['oneOf', 'anyOf'] as const) { + if (Array.isArray(schema[keyword])) { + schema[keyword] = schema[keyword].map(member => closeSchema(member)); + } + } + if (Array.isArray(schema.allOf)) { + const composed = schema.allOf.length > 1; + schema.allOf = schema.allOf.map(member => closeSchema(member, composed)); + } + + const declaresFields = isObject(schema.properties) || (Array.isArray(schema.allOf) && schema.allOf.length > 1); + if (declaresFields && !isAllOfMember && schema.additionalProperties === undefined) { + schema.unevaluatedProperties = false; + } + if (schema.nullable === true) { + const { nullable: _nullable, ...nonNull } = schema; + return { anyOf: [{ type: 'null' }, nonNull] }; + } + if (schema.items === undefined) { + delete schema.items; + } + return schema; +} + +function composedComponents(schemas: Record): Set { + const names = new Set(); + const visit = (node: unknown): void => { + if (Array.isArray(node)) { + node.forEach(visit); + return; + } + if (!isObject(node)) { + return; + } + if (Array.isArray(node.allOf) && node.allOf.length > 1) { + for (const member of node.allOf) { + if (isObject(member) && typeof member.$ref === 'string') { + names.add(member.$ref.replace('#/components/schemas/', '')); + } + } + } + Object.values(node).forEach(visit); + }; + visit(schemas); + return names; +} + +function pathPattern(template: string): RegExp { + const escaped = template.replace(/[.*+?^$()|[\]\\]/g, '\\$&').replace(/\{[^}]+\}/g, '[^/]+'); + return new RegExp(`^${escaped}$`); +} + +function parameterCount(template: string): number { + return template.match(/\{/g)?.length ?? 0; +} + +function pointerSegment(segment: string): string { + return encodeURIComponent(segment.replace(/~/g, '~0').replace(/\//g, '~1')); +} + +function isObject(value: unknown): value is JsonObject { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} diff --git a/packages/mosaic/src/__tests__/feature/fapi-spec-gaps.ts b/packages/mosaic/src/__tests__/feature/fapi-spec-gaps.ts new file mode 100644 index 00000000000..31ff91a0f24 --- /dev/null +++ b/packages/mosaic/src/__tests__/feature/fapi-spec-gaps.ts @@ -0,0 +1,103 @@ +export interface SpecGap { + at: string; + property: string; + schema: Record; + replaces?: Record; + sentBy: string; +} + +const activeSession = component('Client.ActiveSession'); +const serializedSession = 'api/serialize/session.go sessionToClientAPI'; +const externalAccount = component('ExternalAccountWithVerification'); +const samlLinking = 'api/fapi/v1/users/service.go connectSAMLEnterpriseAccount'; +const accountVerifications = [ + { $ref: component('verification_oauth') }, + { $ref: component('verification_google_one_tap') }, +]; + +export const SPEC_GAPS: readonly SpecGap[] = [ + { + at: component('Client.User'), + property: 'enterprise_accounts', + schema: { type: 'array', items: { type: 'object' } }, + sentBy: 'api/serialize/user.go UserResponse', + }, + { + at: component('Client.SessionBase'), + property: 'tasks', + schema: { type: 'array', items: { $ref: '#/components/schemas/Client.SessionTask' } }, + sentBy: 'api/serialize/session.go SessionClientResponse', + }, + { + at: component('Client.OrganizationSettings'), + property: 'force_organization_selection', + schema: { type: 'boolean' }, + sentBy: 'api/serialize/auth_config.go organizationSettingsResponse', + }, + { + at: component('UserSettings.SignUp'), + property: 'mfa', + schema: { type: 'object', properties: { required: { type: 'boolean' } }, required: ['required'] }, + sentBy: 'pkg/usersettings/model/user_settings.go SignUp', + }, + { + at: activeSession, + property: 'last_active_organization_id', + schema: { type: 'string', nullable: true }, + sentBy: serializedSession, + }, + { at: activeSession, property: 'user', schema: { type: 'object', nullable: true }, sentBy: serializedSession }, + { + at: activeSession, + property: 'public_user_data', + schema: { type: 'object', nullable: true }, + sentBy: serializedSession, + }, + { + at: activeSession, + property: 'factor_verification_age', + schema: { type: 'array', items: { type: 'integer' } }, + sentBy: serializedSession, + }, + { at: activeSession, property: 'created_at', schema: { type: 'integer' }, sentBy: serializedSession }, + { at: activeSession, property: 'updated_at', schema: { type: 'integer' }, sentBy: serializedSession }, + { + at: component('EnterpriseConnectionResponse'), + property: 'object', + schema: { type: 'string', enum: ['enterprise_connection'] }, + sentBy: 'api/serialize/enterprise_connection.go EnterpriseConnectionResponse', + }, + { + at: '#/paths/~1v1~1client~1sessions~1{session_id}~1tokens/post/responses/200/content/application~1json/schema', + property: 'object', + schema: { type: 'string', enum: ['token'] }, + sentBy: 'api/serialize/token.go Token', + }, + { + at: externalAccount, + property: 'public_metadata', + schema: { type: 'object', additionalProperties: true, nullable: true }, + replaces: { type: 'object', additionalProperties: true }, + sentBy: samlLinking, + }, + { + at: externalAccount, + property: 'verification', + schema: { + type: 'object', + nullable: true, + oneOf: [...accountVerifications, { $ref: component('Stubs.Verification.SAML') }], + }, + replaces: { + type: 'object', + nullable: true, + oneOf: accountVerifications, + discriminator: { propertyName: 'object' }, + }, + sentBy: samlLinking, + }, +]; + +function component(name: string): string { + return `#/components/schemas/${name}`; +} diff --git a/packages/mosaic/src/__tests__/feature/fapi-spec.yml b/packages/mosaic/src/__tests__/feature/fapi-spec.yml new file mode 100644 index 00000000000..1b4b3e528a5 --- /dev/null +++ b/packages/mosaic/src/__tests__/feature/fapi-spec.yml @@ -0,0 +1,18245 @@ +# clerk/openapi-specs@0f00e5470e679a76b5fde999f2009e61a16f38c2 fapi/2026-08-20.yml +openapi: 3.0.3 +info: + title: Clerk Frontend API + version: v1 + description: |- + The Clerk REST Frontend API, meant to be accessed from a browser or native environment. + + This is a Form Based API and all the data must be sent and formatted according to the `application/x-www-form-urlencoded` content type. + + ### Versions + + When the API changes in a way that isn't compatible with older versions, a new version is released. + Each version is identified by its release date, e.g. `2021-02-05`. For more information, please see [Clerk API Versions](https://clerk.com/docs/backend-requests/versioning/overview). + + ### Using the Try It Console + + The `Try It` feature of the docs only works for **Development Instances** when using the `DevBrowser` security scheme. + To use it, first generate a dev instance token from the `/v1/dev_browser` endpoint. + + Please see https://clerk.com/docs for more information. + x-logo: + url: https://clerk.com/_next/image?url=%2Fimages%2Fclerk-logo.svg&w=96&q=75 + altText: Clerk docs + href: https://clerk.com/docs + contact: + email: support@clerk.com + name: Clerk Team + url: https://clerk.com/support + termsOfService: https://clerk.com/terms + license: + name: MIT + url: https://github.com/clerk/javascript/blob/main/LICENSE +servers: + - url: https://{domain}.clerk.accounts.dev + variables: + domain: + default: example-destined-camel-13 + description: Your Development Instance Frontend API Domain. +security: + - {} + - DevBrowser: [] + - ProductionBrowser: [] + - ProductionNativeApp: [] + ProductionNativeFlag: [] +tags: + - name: DevBrowser + description: Used to handle dev browsers. + - name: DevTools + description: Used to configure development instance settings. + - name: Well Known + description: Well-known endpoints like JWKS, deep linking, and openid-configuration. + - name: Health + description: Used to get the health status of the API. + - name: OAuth2 Identity Provider + description: Requests for the OAuth2 authorization flow. + - name: OAuth2 Callbacks + description: Used to receive callbacks from successful OAuth attempts. + - name: SAML + description: Used in authentication flows using SAML. + - name: Client + description: Used to interact with the Client Object. + - name: Sessions + description: Used to interact with the sessions of a client. + - name: Sign Ins + description: Used to sign in a user in the current client. + - name: Sign Ups + description: Used to sign up a new user in the current client. + - name: User + description: Used to interact with the properties of the current user. + - name: Active Sessions + description: Used to interact with the sessions of the current user. + - name: Email Addresses + description: Used to interact with the email addresses of the logged in user. + - name: Phone Numbers + description: Used to interact with the phone numbers of the logged in user. + - name: Web3 Wallets + description: Used to interact with the web3 wallets of the logged in user. + - name: Passkeys + description: Used to interact with the passkeys of the logged in user. + - name: Trusted Devices + description: Deprecated operations for native trusted-device credentials of the logged in user. + - name: Biometric Credentials + description: Used to interact with native biometric credentials of the logged in user. + - name: External Accounts + description: Used to interact with the external accounts of the current user. + - name: TOTP + description: Used to interact with One Time Password authenticators of the current user. + - name: Backup Codes + description: Used to interact with the two factor authentication backup codes of the current user. + - name: Organizations Memberships + description: Used to interact with the current user's organization memberships, invitations and suggestions. + - name: Organization + description: Used to interact with an organization and its properties. The current user must be an administrator to access them. + - name: Organization Creation Defaults + description: Used to get suggested defaults when creating a new organization. + - name: Invitations + description: Used to interact with the invitations of an organization. The current user must be an administrator to access them. + - name: Members + description: Used to interact with the members of an organization. The current user must be an administrator to access them. + - name: Membership Requests + description: Used to interact with the members of an organization. The current user must be an administrator to access them. + - name: Domains + description: Used to interact with the members of an organization. The current user must be an administrator to access them. + - name: Environment + description: Used to get and update the current Environment + - name: Waitlist + description: Used to interact with the waitlist. + - name: API Keys + description: Endpoints for managing API Keys + - name: Enterprise Connections + description: Endpoints for managing enterprise connections. + - name: SSO Bypass + description: Endpoints for managing who may sign in with an email code when their enterprise identity provider is unreachable. + - name: Plans + description: Used to interact with plans that users and organizations can purchase. + - name: Subscriptions + description: Used to interact with subscriptions belonging to users and organizations. + - name: Subscription Items + description: Used to interact with subscription items belonging to users and organizations. + - name: Checkouts + description: Used to interact with the checkout flow. + - name: Payment Methods + description: Used to interact with payment methods belonging to users and organizations. + - name: Statements + description: Used to interact with billing statements for users and organizations. + - name: Payment Attempts + description: Used to interact with payment attempts for users and organizations. +paths: + /.well-known/assetlinks.json: + get: + summary: Get Android Asset Links + description: Get Android asset links for universal/deep linking + operationId: getAndroidAssetLinks + security: [] + tags: + - Well Known + responses: + '200': + $ref: '#/components/responses/WellKnown.Assetlinks' + /.well-known/jwks.json: + get: + summary: JWKS + description: Retrieve the JSON Web Key Set of the instance + operationId: getJWKS + security: [] + tags: + - Well Known + responses: + '200': + $ref: '#/components/responses/WellKnown.JWKS' + '500': + $ref: '#/components/responses/ClerkErrors' + /.well-known/apple-app-site-association: + get: + summary: Apple App Site Association + description: Retrieve the Apple App Site Association file of the instance + operationId: getAppleAppSiteAssociation + security: [] + tags: + - Well Known + responses: + '200': + $ref: '#/components/responses/WellKnown.AppleAppSiteAssociation' + '500': + $ref: '#/components/responses/ClerkErrors' + /.well-known/openid-configuration: + get: + summary: Basic OpenID Configuration Payload + description: Get a basic OpenID configuration payload + operationId: getOpenIDConfiguration + security: [] + tags: + - Well Known + responses: + '200': + $ref: '#/components/responses/WellKnown.OpenIDConfiguration' + /.well-known/oauth-authorization-server: + get: + summary: OAuth 2.0 Authorization Server Metadata + description: Get the OAuth 2.0 Authorization Server Metadata according to RFC 8414 + operationId: getOAuth2AuthorizationServerMetadata + security: [] + tags: + - Well Known + responses: + '200': + $ref: '#/components/responses/WellKnown.OAuth2AuthorizationServerMetadata' + /.well-known/webauthn: + get: + summary: WebAuthn Related Origins + description: |- + Get the WebAuthn Related Origin Requests document. Only available when + the instance uses the auth_host passkey RP ID strategy; returns a not + found error otherwise. + operationId: getWebAuthnRelatedOrigins + security: [] + tags: + - Well Known + responses: + '200': + $ref: '#/components/responses/WellKnown.WebAuthn' + '404': + $ref: '#/components/responses/ClerkErrors' + /oauth/authorize: + get: + summary: Request OAuth2 authorization + description: Request OAuth2 authorization. If successful, receive authorization grant via redirect. + operationId: requestOAuthAuthorize + security: [] + tags: + - OAuth2 Identity Provider + parameters: + - name: response_type + in: query + required: true + description: The authorization flow type. Must be `code` for authorization code flow. + schema: + type: string + enum: + - code + - name: client_id + in: query + required: true + description: The OAuth2 client ID of the OAuth application. + schema: + type: string + - name: redirect_uri + in: query + required: false + description: The URI to redirect to after authorization. Must be registered for the OAuth application. + schema: + type: string + format: uri + - name: scope + in: query + required: false + style: spaceDelimited + explode: false + description: | + Space-separated list of scopes being requested. + Available scopes are `email`, `profile`, `openid`, `public_metadata`, `private_metadata`, `offline_access`, and `user:org:read` (when Organizations are enabled), and any custom scope keys assigned to the OAuth application. + Falls back to the OAuth application's registered scopes if not provided. + schema: + type: array + items: + type: string + - name: state + in: query + required: false + description: An opaque value used to maintain state between the request and callback (minimum 8 characters). Required to prevent CSRF attacks unless PKCE parameters (`code_challenge` and `code_challenge_method`) are provided. + schema: + type: string + minLength: 8 + - name: prompt + in: query + required: false + style: spaceDelimited + explode: false + description: Space-separated list of prompts. Supported values are `none` (no user interaction), `login` (force re-authentication), and `consent` (force consent screen). + schema: + type: array + items: + type: string + enum: + - none + - login + - consent + - name: code_challenge + in: query + required: false + description: The code challenge for PKCE (Proof Key for Code Exchange). Required for public clients. + schema: + type: string + - name: code_challenge_method + in: query + required: false + description: The method used to generate the code challenge. Must be `S256`. + schema: + type: string + enum: + - S256 + - name: response_mode + in: query + required: false + description: The method used to return authorization response parameters. Supported values are `query` (parameters in URL query string) and `form_post` (parameters in POST body). + schema: + type: string + enum: + - query + - form_post + - name: nonce + in: query + required: false + description: String value used to associate a client session with an ID Token and to mitigate replay attacks. Used in OpenID Connect flows. + schema: + type: string + - name: resource + in: query + required: false + description: |- + Absolute URI that identifies the resource server for the requested access token, as defined by RFC 8707. + The URI must not contain a fragment. Only one `resource` value is supported. + schema: + type: string + format: uri + responses: + '200': + description: |- + HTML `form_post` authorization response containing either a code or an OAuth error. + An invalid `resource` returns `error=invalid_target`. The response preserves a supplied `state` and includes `iss`. + '302': + description: Intermediate redirect for top-level navigation, sign-in, or consent. + '303': + description: |- + Authorization response redirect containing either a code or an OAuth error. + An invalid `resource` returns `error=invalid_target`. The redirect preserves a supplied `state` and includes `iss`. + '400': + description: Direct bad-request response used when the authorization server cannot safely return the error to the client's redirect URI. + '401': + description: Unauthorized error, for example the provided client is invalid. + '403': + description: Forbidden error, for example you are trying to use a satellite domain + post: + summary: Request OAuth2 authorization + description: Request OAuth2 authorization. If successful, receive authorization grant via redirect. + operationId: requestOAuthAuthorizePOST + security: [] + tags: + - OAuth2 Identity Provider + requestBody: + required: false + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + response_type: + type: string + description: The authorization flow type. Must be `code` for authorization code flow. + enum: + - code + client_id: + type: string + description: The OAuth2 client ID of the OAuth application. + redirect_uri: + type: string + format: uri + nullable: true + description: The URI to redirect to after authorization. Must be registered for the OAuth application. + scope: + type: string + nullable: true + description: | + Space-separated list of scopes being requested. + Available scopes are `email`, `profile`, `openid`, `public_metadata`, `private_metadata`, `offline_access`, and `user:org:read` (when Organizations are enabled), and any custom scope keys assigned to the OAuth application. + Falls back to the OAuth application's registered scopes if not provided. + state: + type: string + nullable: true + description: An opaque value used to maintain state between the request and callback (minimum 8 characters). Required to prevent CSRF attacks unless PKCE parameters (`code_challenge` and `code_challenge_method`) are provided. + minLength: 8 + prompt: + type: string + nullable: true + description: Space-separated list of prompts. Supported values are `none` (no user interaction), `login` (force re-authentication), and `consent` (force consent screen). Multiple values should be space-delimited (e.g., "login consent"). + code_challenge: + type: string + nullable: true + description: The code challenge for PKCE (Proof Key for Code Exchange). Required for public clients. + code_challenge_method: + type: string + nullable: true + description: The method used to generate the code challenge. Must be `S256`. + enum: + - S256 + response_mode: + type: string + nullable: true + description: The method used to return authorization response parameters. Supported values are `query` (parameters in URL query string) and `form_post` (parameters in POST body). + enum: + - query + - form_post + nonce: + type: string + nullable: true + description: String value used to associate a client session with an ID Token and to mitigate replay attacks. Used in OpenID Connect flows. + resource: + type: string + format: uri + nullable: true + description: |- + Absolute URI that identifies the resource server for the requested access token, as defined by RFC 8707. + The URI must not contain a fragment. Only one `resource` value is supported. + required: + - response_type + - client_id + responses: + '200': + description: |- + HTML `form_post` authorization response containing either a code or an OAuth error. + An invalid `resource` returns `error=invalid_target`. The response preserves a supplied `state` and includes `iss`. + '302': + description: Intermediate redirect for top-level navigation, sign-in, or consent. + '303': + description: |- + Authorization response redirect containing either a code or an OAuth error. + An invalid `resource` returns `error=invalid_target`. The redirect preserves a supplied `state` and includes `iss`. + '400': + description: Direct bad-request response used when the authorization server cannot safely return the error to the client's redirect URI. + '401': + description: Unauthorized error, for example the provided client is invalid. + '403': + description: Forbidden error, for example you are trying to use a satellite domain + /oauth/register: + post: + summary: Register OAuth 2.0 Client + description: |- + Registers a new OAuth 2.0 client with the authorization server according to RFC 7591 + (OAuth 2.0 Dynamic Client Registration Protocol). + + This endpoint allows clients to dynamically register by submitting client metadata. + Upon successful registration, the authorization server assigns a unique client identifier + and optionally a client secret (for confidential clients). + + **Authentication:** This endpoint does not require authentication as it is used for + initial client registration. However, the endpoint is disabled by default and must + be enabled in the Instance settings. + + **Client Types:** + - **Confidential clients** receive both a `client_id` and `client_secret` + - **Public clients** (using `token_endpoint_auth_method: none`) receive only a `client_id` + + **Key Points:** + - All dynamically registered clients must use the consent screen (cannot be disabled) + - PKCE is not required for dynamically registered clients + - The `redirect_uris` field is required and must contain at least one valid redirect URI + - If `client_name` is not provided, the `client_id` will be used as the display name + - If `scope` is not provided, default scopes will be assigned + - The `token_endpoint_auth_method` values `client_secret_basic` and `client_secret_post` are + treated equivalently in the current implementation; both methods can be used at the + token endpoint + - `application_type` accepts only `native` and `web`. Native clients may use custom URI + schemes, claimed `https` URIs ([RFC 8252 Section 7.2](https://datatracker.ietf.org/doc/html/rfc8252#section-7.2)), + and `http` only on loopback hosts. The field is optional; when omitted it defaults to + `web` ([OpenID Connect Dynamic Client Registration 1.0 §2](https://openid.net/specs/openid-connect-registration-1_0.html)) + and is returned as `web`. + + See [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591) for complete specification details. + operationId: registerOAuthClient + security: [] + tags: + - OAuth2 Identity Provider + requestBody: + description: |- + Client metadata as defined in RFC 7591 Section 2. The client metadata includes + information about the client such as its name, redirect URIs, and authentication method. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/OAuth.DynamicClientRegistrationRequest' + examples: + confidentialClient: + summary: Confidential Client Registration + description: Register a confidential client that will authenticate with client_secret_basic + value: + client_name: My Confidential App + client_uri: https://example.com + logo_uri: https://example.com/logo.png + redirect_uris: + - https://example.com/oauth/callback + token_endpoint_auth_method: client_secret_basic + scope: openid email profile + publicClient: + summary: Public Client Registration + description: Register a public client (no client secret) + value: + client_name: My Mobile App + client_uri: https://example.com + application_type: native + redirect_uris: + - myapp://oauth/callback + token_endpoint_auth_method: none + scope: openid email profile + minimalClient: + summary: Minimal Client Registration + description: Register a client with only required fields + value: + redirect_uris: + - https://example.com/callback + responses: + '201': + $ref: '#/components/responses/OAuth.DynamicClientRegistration' + '400': + $ref: '#/components/responses/OAuth.DynamicClientRegistrationError' + '422': + description: |- + Unprocessable entity error. The dynamic client registration feature may not be enabled + for this instance. + '500': + description: |- + Internal server error. The authorization server encountered an unexpected condition + that prevented it from fulfilling the registration request. + /oauth/device_authorization: + post: + summary: Start a device authorization request + description: |- + Starts an OAuth 2.0 Device Authorization Grant (RFC 8628). Display the returned + `user_code` and `verification_uri`, then poll `/oauth/token` with the returned + `device_code` at the specified `interval`. + operationId: createOAuthDeviceAuthorization + security: + - ClientCredentials: [] + - {} + tags: + - OAuth2 Identity Provider + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - client_id + properties: + client_id: + type: string + description: OAuth 2.0 client identifier. Must match the authenticated client. + client_secret: + type: string + nullable: true + description: OAuth 2.0 client secret when not using HTTP Basic Authentication. + scope: + type: string + nullable: true + description: Space-separated list of requested scopes. Must be a subset of the scopes allowed for the client. + nonce: + type: string + nullable: true + description: OpenID Connect nonce. Must be at least 8 characters when present. + responses: + '200': + $ref: '#/components/responses/OAuth.DeviceAuthorization' + '400': + $ref: '#/components/responses/OAuth.DeviceAuthorizationError400' + '401': + $ref: '#/components/responses/OAuth.TokenError401' + '403': + description: Forbidden error, for example you are trying to use a satellite domain + '422': + $ref: '#/components/responses/ClerkErrors' + '429': + $ref: '#/components/responses/TooManyRequests' + /oauth/token: + post: + summary: Get OAuth2 token + description: |- + Exchange an authorization grant for OAuth2 tokens (access token, refresh token, and optionally ID token). + + This endpoint implements the OAuth 2.0 Token Endpoint (RFC 6749) and OpenID Connect token endpoint. + It supports three grant types by default: `authorization_code`, `refresh_token`, and + `urn:ietf:params:oauth:grant-type:device_code`. Instances with token exchange enabled also support + `urn:ietf:params:oauth:grant-type:token-exchange`. + + #### Grant Types + + - **`authorization_code`**: Exchange an authorization code for tokens + - **`refresh_token`**: Refresh an access token using a refresh token + - **`urn:ietf:params:oauth:grant-type:device_code`**: Poll for the tokens of a device + authorization request (RFC 8628), which the client starts at + `/oauth/device_authorization` + - **`urn:ietf:params:oauth:grant-type:token-exchange`**: Exchange an access token for a + resource-scoped access token (RFC 8693). This grant is available only when enabled for the instance. + + #### Authentication + + - **Confidential clients**: Must authenticate using one of the following methods: + - HTTP Basic Authentication: `Authorization: Basic base64(client_id:client_secret)` + - Client credentials in request body: Include `client_id` and `client_secret` in form fields + - *Note: Both methods (`client_secret_basic` and `client_secret_post`) are treated equivalently* + - **Public clients** (PKCE flow): No client secret required. Must provide `client_id` and `code_verifier` in request body. + + #### PKCE (Proof Key for Code Exchange) + + Public clients and clients that cannot securely store a client secret must use PKCE (RFC 7636). + When using PKCE: + - Include `code_challenge` and `code_challenge_method` in the authorization request + - Include `code_verifier` in the token request (instead of `client_secret`) + + #### Scopes + + Available scopes: + - `openid` - Enables OpenID Connect features, returns an `id_token` in the response + - `email` - User's email address + - `profile` - Basic profile information (name, username, picture, etc.) + - `public_metadata` - Clerk custom scope for public metadata + - `private_metadata` - Clerk custom scope for private metadata + + Default scopes (if not specified): `email profile` + + #### Token Lifespans + + - **Access token**: 1 day (86400 seconds). Access tokens issued by token exchange are valid for up to + 5 minutes and never expire after the source token. + - **Refresh token**: 10 years + - **ID token**: 1 day (86400 seconds) + - **Authorization code**: 10 minutes (must be exchanged within this time) + + #### OpenID Connect + + When the `openid` scope is included in the authorization request, the token response will include + an `id_token` field containing a signed JWT with user identity information per the OpenID Connect specification. + The `id_token` will be refreshed along with the access token when using the `refresh_token` grant type. + operationId: getOAuthToken + security: + - ClientCredentials: [] + - {} + tags: + - OAuth2 Identity Provider + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - grant_type + properties: + grant_type: + type: string + description: |- + The grant type being used. + - `authorization_code`: Exchange an authorization code for tokens + - `refresh_token`: Refresh an access token + - `urn:ietf:params:oauth:grant-type:device_code`: Poll for tokens for a device authorization request + - `urn:ietf:params:oauth:grant-type:token-exchange`: Exchange an access token for a resource-scoped access token + enum: + - authorization_code + - refresh_token + - urn:ietf:params:oauth:grant-type:device_code + - urn:ietf:params:oauth:grant-type:token-exchange + device_code: + type: string + nullable: true + description: |- + The device code issued by the device authorization endpoint. + **Required when `grant_type=urn:ietf:params:oauth:grant-type:device_code`**. + code: + type: string + nullable: true + description: |- + The authorization code received from the authorization endpoint. + **Required when `grant_type=authorization_code`**. + redirect_uri: + type: string + nullable: true + description: |- + The redirect URI used in the authorization request. Must match exactly. + **Required when `grant_type=authorization_code`**. + code_verifier: + type: string + nullable: true + description: |- + The PKCE code verifier that corresponds to the `code_challenge` sent in the authorization request. + **Required for public clients using PKCE with `grant_type=authorization_code`**. + Confidential clients using `client_secret` should not include this parameter. + client_id: + type: string + nullable: true + description: |- + The OAuth 2.0 client identifier. + **Required for public clients** (those not using HTTP Basic Authentication). + For confidential clients, can be provided here or via HTTP Basic Authentication. + client_secret: + type: string + nullable: true + description: |- + The OAuth 2.0 client secret. + **Required for confidential clients** (unless using HTTP Basic Authentication). + Public clients using PKCE should not include this parameter. + refresh_token: + type: string + nullable: true + description: |- + The refresh token issued to the client. + **Required when `grant_type=refresh_token`**. + subject_token: + type: string + nullable: true + description: |- + The active access token to exchange. + **Required when `grant_type=urn:ietf:params:oauth:grant-type:token-exchange`**. + subject_token_type: + type: string + nullable: true + enum: + - urn:ietf:params:oauth:token-type:access_token + description: |- + The type of the subject token. + **Required when `grant_type=urn:ietf:params:oauth:grant-type:token-exchange`**. + requested_token_type: + type: string + nullable: true + enum: + - urn:ietf:params:oauth:token-type:access_token + description: The requested output token type for an RFC 8693 token exchange. + scope: + type: string + nullable: true + description: |- + Space-separated list of scopes for the access token. + **Optional when `grant_type=refresh_token`**. + If provided, the requested scope must not exceed the scope originally granted. + If omitted, the same scope as originally granted will be used. + resource: + type: string + format: uri + nullable: true + description: |- + Absolute URI that identifies the resource server for the access token, as defined by RFC 8707. + **Required when `grant_type=urn:ietf:params:oauth:grant-type:token-exchange`**. + For authorization code and refresh token grants, if omitted, the resource from the authorization grant is used. + If provided, it must exactly match that resource. + The URI must not contain a fragment. Only one `resource` value is supported. + examples: + authorizationCodeConfidential: + summary: Authorization Code - Confidential Client + description: Exchange an authorization code for tokens using a confidential client with client secret + value: + grant_type: authorization_code + code: clk_code_abc123xyz + redirect_uri: https://example.com/oauth/callback + client_id: client_abc123 + client_secret: secret_xyz789 + authorizationCodePKCE: + summary: Authorization Code - Public Client with PKCE + description: Exchange an authorization code for tokens using PKCE (public client, no client secret) + value: + grant_type: authorization_code + code: clk_code_abc123xyz + redirect_uri: https://example.com/oauth/callback + client_id: client_abc123 + code_verifier: dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk + refreshToken: + summary: Refresh Token + description: Refresh an access token using a refresh token + value: + grant_type: refresh_token + refresh_token: clk_refresh_abc123xyz + client_id: client_abc123 + client_secret: secret_xyz789 + tokenExchange: + summary: Token Exchange + description: Exchange an active access token for a short-lived resource-scoped access token + value: + grant_type: urn:ietf:params:oauth:grant-type:token-exchange + resource: https://api.example.com + subject_token: oat_subject_token + subject_token_type: urn:ietf:params:oauth:token-type:access_token + responses: + '200': + $ref: '#/components/responses/OAuth.Token' + '400': + $ref: '#/components/responses/OAuth.TokenError400' + '401': + $ref: '#/components/responses/OAuth.TokenError401' + '403': + $ref: '#/components/responses/OAuth.TokenError403' + /oauth/userinfo: + get: + summary: Get user info after OAuth2 flow + description: Get user info in exchange for a valid OAuth2 access token. + operationId: getOAuthUserInfo + security: [] + tags: + - OAuth2 Identity Provider + responses: + '200': + $ref: '#/components/responses/OAuth.UserInfo' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + post: + summary: Get user info after OAuth2 flow + description: Get user info in exchange for a valid OAuth2 access token. + operationId: getOAuthUserInfoPOST + security: [] + tags: + - OAuth2 Identity Provider + responses: + '200': + $ref: '#/components/responses/OAuth.UserInfo' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + /oauth/token_info: + post: + summary: Get information for an access or refresh token + description: Get information for an access or refresh token + operationId: getOAuthTokenInfo + security: [] + tags: + - OAuth2 Identity Provider + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + token: + type: string + description: The value of the access or the refresh token + token_type_hint: + type: string + description: A hint about the type of the token submitted for introspection. Can be one of the following `access_token` and `refresh_token` + nullable: true + scope: + type: string + description: The granted scopes for the token to check against + nullable: true + required: + - token + responses: + '200': + $ref: '#/components/responses/OAuth.TokenInfo' + '401': + description: Unauthorized error, for example the provided client is invalid. + '422': + description: You are currently using the legacy OAuth 2.0 provider, please migrate to the new one via Clerk Dashboard. + /oauth/token/revoke: + post: + summary: Revoke OAuth2 token + description: |- + Revoke OAuth2 token by token. + + #### Authentication + + - **Confidential clients**: Must authenticate using HTTP Basic Authentication with Client ID as username and Client Secret as password in the Authorization header. + operationId: revokeOAuthToken + security: + - ClientCredentials: [] + tags: + - OAuth2 Identity Provider + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + token: + type: string + description: The token to revoke. + nullable: false + token_type_hint: + type: string + description: A hint about the type of the token to be revoked. + nullable: true + enum: + - access_token + - refresh_token + responses: + '200': + description: Success, no content. + '400': + $ref: '#/components/responses/OAuth.TokenError400' + '401': + $ref: '#/components/responses/OAuth.TokenError401' + /v1/me/oauth/consent/{client_id}: + get: + summary: Get consent information + description: | + Returns the scopes for a given OAuth Application Client ID. + An optional `scope` query parameter can restrict the returned scopes to only those requested. + The OAuth application must have consent screen enabled. + operationId: getOAuthConsent + security: + - {} + - DevBrowser: [] + tags: + - OAuth2 Identity Provider + parameters: + - in: path + name: client_id + required: true + schema: + type: string + description: The OAuth Application Client ID to get consent information for. + - in: query + name: scope + required: false + schema: + type: string + description: Optional space-separated list of scopes to restrict the response to only those requested. + - in: query + name: redirect_uri + required: false + schema: + type: string + format: uri + description: | + Optional redirect URI from the original OAuth authorize request. + When provided and registered for the OAuth application, the response will include + a `redirect_domain` field containing the PSL-resolved root domain for display on the consent screen. + Unregistered or malformed URIs are silently ignored and `redirect_domain` will be null. + responses: + '200': + $ref: '#/components/responses/OAuth.ConsentInfo' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + post: + summary: Submit OAuth2 consent decision + description: | + Submits the user's consent decision for an OAuth2 authorization request. + + This endpoint allows building custom consent portals as a replacement for + the default account-portal-rendered consent screen. + + The OAuth application must have the consent screen enabled. On grant, the + flow continues just like the standard `/oauth/authorize` endpoint and + returns the appropriate redirect. + + In addition to the fields documented below, the request body must + re-submit the standard OAuth2 authorize parameters from the original + `/oauth/authorize` request: `response_type`, `redirect_uri`, `scope`, + `state`, `nonce`, `code_challenge`, `code_challenge_method`, and `resource`. These + are forwarded as-is to the authorize flow. + operationId: submitOAuthConsent + security: + - {} + - DevBrowser: [] + tags: + - OAuth2 Identity Provider + parameters: + - in: path + name: client_id + required: true + schema: + type: string + description: The OAuth Application Client ID the consent decision applies to. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - consented + properties: + consented: + type: boolean + description: | + The user's consent decision. `true` grants the requested + scopes and continues the authorization flow; any other value + is treated as a denial and returns an `access_denied` error + in the redirect. + organization_id: + type: string + nullable: true + description: | + Optional. The organization to scope the issued token to. The + authenticated user must be a member of this organization. If + omitted, the user's currently active organization is used. + resource: + type: string + format: uri + nullable: true + description: |- + Absolute URI from the original authorization request that identifies the resource server for the requested access token. + The URI must not contain a fragment. Only one `resource` value is supported. + responses: + '200': + description: |- + HTML `form_post` authorization response containing either a code or an OAuth error. + An invalid `resource` returns `error=invalid_target`. The response preserves a supplied `state` and includes `iss`. + '302': + description: Intermediate redirect for sign-in or consent. + '303': + description: |- + Authorization response redirect containing either a code or an OAuth error. + An invalid `resource` returns `error=invalid_target`. The redirect preserves a supplied `state` and includes `iss`. + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/me/oauth/device: + post: + summary: Submit an OAuth2 device authorization decision + description: Approves or denies a pending OAuth2 device authorization for the authenticated user. + operationId: submitOAuthDeviceAuthorizationDecision + security: + - DevBrowser: [] + - ProductionBrowser: [] + - ProductionNativeApp: [] + ProductionNativeFlag: [] + tags: + - OAuth2 Identity Provider + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + required: + - user_code + - approved + properties: + user_code: + type: string + description: The user code returned by the device authorization endpoint. + approved: + type: boolean + description: Whether the authenticated user approves the device authorization. + organization_id: + type: string + nullable: true + description: Optional organization context for the issued tokens. + responses: + '200': + $ref: '#/components/responses/OAuth.DeviceVerification' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + '429': + $ref: '#/components/responses/ClerkErrors' + /v1/me/oauth/device/lookup: + post: + summary: Look up an OAuth2 device authorization + description: Returns the OAuth application and requested scopes associated with a user code. + operationId: lookupOAuthDeviceAuthorization + security: + - DevBrowser: [] + - ProductionBrowser: [] + - ProductionNativeApp: [] + ProductionNativeFlag: [] + tags: + - OAuth2 Identity Provider + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + required: + - user_code + properties: + user_code: + type: string + description: The user code returned by the device authorization endpoint. + responses: + '200': + $ref: '#/components/responses/OAuth.DeviceAuthorizationInfo' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + '429': + $ref: '#/components/responses/ClerkErrors' + /v1/me/organization_creation_defaults: + get: + operationId: getOrganizationCreationDefaults + tags: + - Organization Creation Defaults + summary: Get organization creation defaults + description: | + Get suggested organization name and other defaults for creating a new organization. + Returns suggestions based on the user's email domain or configured naming templates. + responses: + '200': + description: Organization creation defaults + content: + application/json: + schema: + type: object + properties: + response: + type: object + properties: + form: + type: object + nullable: true + properties: + name: + type: string + nullable: true + description: Suggested organization name + logo: + type: string + nullable: true + description: Suggested organization logo URL + description: Suggested form values for creating an organization + advisory: + type: object + nullable: true + properties: + type: + type: string + enum: + - warning + - error + - info + description: Type of advisory message + message: + type: string + description: Advisory message text + description: Advisory message if applicable (e.g., organization already exists) + client: + $ref: '#/components/schemas/Client.Client' + '401': + $ref: '#/components/responses/AuthenticationInvalid' + '403': + $ref: '#/components/responses/AuthorizationInvalid' + /v1/account_portal: + get: + summary: Get account portal + description: Get users account portal + operationId: getAccountPortal + responses: + '200': + $ref: '#/components/responses/Client.AccountPortal' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + /v1/environment: + get: + summary: Get environment + description: Get the current environment. The environment contains information about the settings and features enabled for the current instance. + operationId: getEnvironment + security: + - {} + - DevBrowser: [] + tags: + - Environment + responses: + '200': + $ref: '#/components/responses/Client.Environment' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + patch: + summary: Update environment + description: Update environment using request origin + operationId: updateEnvironment + security: + - {} + - DevBrowser: [] + tags: + - Environment + parameters: + - in: header + name: Origin + description: Origin of the request + required: true + schema: + type: string + responses: + '200': + $ref: '#/components/responses/Client.Environment' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + /v1/saml/metadata/{saml_connection_id}.xml: + get: + summary: SAML Metadata + description: The Service Provider's SAML metadata + operationId: samlMetadata + security: + - {} + - DevBrowser: [] + tags: + - SAML + parameters: + - in: path + name: saml_connection_id + required: true + schema: + type: string + description: The ID of the SAML Connection + responses: + '200': + description: OK + '400': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + /v1/saml/acs/{saml_connection_id}: + post: + summary: SAML ACS + description: | + The SAML Assertion Consumer Service (ACS) endpoint, which processes SAML Responses by the IdP. + operationId: acs + security: + - {} + - DevBrowser: [] + tags: + - SAML + parameters: + - in: path + name: saml_connection_id + required: true + schema: + type: string + description: The ID of the SAML Connection + responses: + '302': + description: OK + '303': + description: OK + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + /v1/health: + get: + summary: Get Health + description: Get Health + security: + - {} + - DevBrowser: [] + tags: + - Health + operationId: getHealth + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + status: + type: string + enum: + - healthy + required: + - status + '503': + description: Service is unavailable + content: + application/json: + schema: + type: object + properties: + status: + type: string + enum: + - unhealthy + message: + type: string + required: + - status + - message + /v1/proxy-health: + get: + summary: Get Proxy Health + description: |- + Use this endpoint to check the validity of a proxy configuration for a domain. + Pass the instance ID and domain ID as query parameters. + operationId: getProxyHealth + parameters: + - in: query + name: domain_id + description: The ID of the domain + required: true + schema: + type: string + - in: header + name: Clerk-Proxy-Url + description: The URL of the proxy + required: true + schema: + type: string + - in: header + name: Clerk-Secret-Key + description: The secret key of the proxy + required: true + schema: + type: string + - in: header + name: X-Forwarded-For + description: The IP address of the client + required: true + schema: + type: string + responses: + '200': + description: Configuration is valid + content: + application/json: + schema: + oneOf: + - type: object + properties: + status: + type: string + enum: + - healthy + x_forwarded_for: + type: string + required: + - status + - x_forwarded_for + - type: object + properties: + status: + type: string + enum: + - unhealthy + message: + type: string + required: + - status + - message + '400': + $ref: '#/components/responses/ClerkErrors' + '422': + description: Configuration is invalid + content: + application/json: + schema: + type: object + properties: + status: + type: string + enum: + - unhealthy + message: + type: string + required: + - status + - message + '503': + description: The health check could not be completed + content: + application/json: + schema: + type: object + properties: + status: + type: string + enum: + - unhealthy + message: + type: string + required: + - status + - message + /v1/client: + get: + summary: Get Current Client + description: Returns the current client that is present either in the browser cookies or authorization header. + tags: + - Client + operationId: getClient + security: + - {} + - DevBrowser: [] + responses: + '200': + $ref: '#/components/responses/Client.Client' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + put: + summary: Create New Client + description: Creates a new Client and sets it either in the response cookies or the response authorization header. + security: + - {} + - DevBrowser: [] + tags: + - Client + operationId: putClient + responses: + '200': + $ref: '#/components/responses/Client.Client' + '307': + description: Redirect, no body. + post: + summary: Create New Client + description: Creates a new Client and sets it either in the response cookies or the response authorization header. + security: + - {} + - DevBrowser: [] + operationId: postClient + tags: + - Client + requestBody: + required: false + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + responses: + '200': + $ref: '#/components/responses/Client.Client' + '307': + description: Redirect, no body. + delete: + summary: Delete Client's Sessions + description: Ends all Sessions of the Current Client + tags: + - Client + operationId: deleteClientSessions + responses: + '200': + $ref: '#/components/responses/Client.DeleteSession' + '307': + description: Redirect, no body. + '404': + $ref: '#/components/responses/ClerkErrors' + /v1/client/sign_ins: + post: + security: + - {} + summary: Create a new Sign In or replace the current one. + description: |- + Creates or replaces the current Sign in object. + In order to authenticate a Sign in in as few requests as possible, + you can pass in parameters to this request that can identify and verify the Sign in. + + Parameter rules: + + If the strategy equals `phone_code`, `email_code`, `web3_[provider]_signature`, `reset_password_code` or `reset_password_phone_code` then an identifier is required. + + If the strategy equals `email_link` then an identifier is required and optionally redirect_url can be supplied. + + If the strategy equals `password` then both an identifier and a password is required. + + If the strategy equals `oauth_[provider]` or `saml` then a redirect_url is required, and an action_complete_redirect_url is optional. + + If the strategy equals `oauth_token_[provider]` then at least one of code (grant code) or token (openID token) is required. Passing only the token will probably retrieve minimal information about the user from the OAuth provider. You can pass both code and token for the best results. + + If the strategy equals `ticket` then ticket is required. + + If the strategy equals `passkey` then no identifier is provided. + + If the strategy equals `trusted_device` then trusted_device_id is required. + + If the strategy equals `google_one_tap` then token is required. + tags: + - Sign Ins + operationId: createSignIn + parameters: + - in: header + name: Origin + description: The origin of the request + schema: + type: string + requestBody: + required: false + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + strategy: + type: string + description: |- + Strategy used to sign in. + Can be one of `phone_code`, `email_code`, `ticket`, `web3_[provider]_signature` `reset_password_code`, `reset_password_phone_code`, `email_link`, `oauth_[provider]`, `oauth_token_[provider]`, `saml`, `password`, `passkey`, `trusted_device`, `google_one_tap` + nullable: true + identifier: + type: string + description: The unique identifier of the user. This changes depending on the strategy. + nullable: true + password: + type: string + description: The password of the user. Only used with password strategy. + nullable: true + ticket: + type: string + description: Ticket to be used for signing in. + nullable: true + redirect_url: + type: string + nullable: true + action_complete_redirect_url: + type: string + nullable: true + transfer: + type: boolean + nullable: true + timezone: + type: string + nullable: true + description: The IANA timezone reported by the client. + code: + type: string + description: The authorization or grant code for an OAuth exchange. Only used with `oauth_token_[provider]` strategies. + nullable: true + token: + type: string + description: The ID token from an OpenID Connect flow. Only used with `oauth_token_[provider]` and `google_one_tap` strategies. + nullable: true + oidc_login_hint: + type: string + description: Used with `oauth_[provider]`. The given value will be forwarded to the OIDC `login_hint` parameter of the generated redirect URL. + nullable: true + oidc_prompt: + type: string + description: Used with `oauth_[provider]` or `enterprise_sso`. The given value will be forwarded to the OIDC `prompt` parameter of the generated redirect URL. When using shared credentials, this value might be adjusted for security reasons. + nullable: true + proof_token: + type: string + description: Proof token produced by the Clerk Protect SDK. + nullable: true + trusted_device_id: + type: string + description: Used with the `trusted_device` strategy. + nullable: true + responses: + '200': + $ref: '#/components/responses/Client.SignIn' + '307': + description: Redirect, no body. + '400': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '409': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + '429': + $ref: '#/components/responses/ClerkErrors' + /v1/client/sign_ins/{sign_in_id}: + get: + summary: Retrieve sign-in + description: |- + Returns the sign-in with the given ID. + The sign in is returned only if it belongs to the requesting client and is not abandoned. + tags: + - Sign Ins + operationId: getSignIn + parameters: + - in: path + name: sign_in_id + required: true + schema: + type: string + description: The ID of the sign in to be retrieved. + responses: + '200': + $ref: '#/components/responses/Client.SignIn' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + /v1/client/sign_ins/{sign_in_id}/reset_password: + post: + summary: Reset password on sign-in + description: Reset password on sign-in. + tags: + - Sign Ins + operationId: resetPassword + parameters: + - in: path + name: sign_in_id + required: true + schema: + type: string + description: The ID of the sign in to be retrieved. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + password: + type: string + sign_out_of_other_sessions: + type: boolean + nullable: true + required: + - password + responses: + '200': + $ref: '#/components/responses/Client.SignIn-2' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/client/sign_ins/{sign_in_id}/prepare_first_factor: + post: + summary: Prepare First Factor Verification + description: |- + Prepares the verification object for the identified Sign in. + This step authenticates that the user is who they say they are. + Depending on the strategy, this request will do something different. + + Parameter actions: + If the strategy equals email_code then this request will send an email with an OTP code. + If the strategy equals phone_code then this request will send an SMS with an OTP code. + If the strategy equals oauth_[provider] then this request generate a URL that the User needs to visit in order to authenticate. + If the strategy equals passkey then this request will begin the passkey registration flow. + If the strategy equals trusted_device then this request will issue a trusted-device sign-in challenge. + + Parameter rules: + If the strategy equals `oauth_[provider]` then a redirect_url is required, and an action_complete_redirect_url is optional. + tags: + - Sign Ins + operationId: prepareSignInFactorOne + parameters: + - in: path + name: sign_in_id + required: true + schema: + type: string + description: The ID of the sign in to be retrieved. + - in: header + name: Origin + description: The origin of the request + schema: + type: string + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + strategy: + type: string + description: |- + The strategy to be prepared for first factor authentication. + + Can be one of the following `email_code`, `email_link`, + `phone_code`, `web3_metamask_signature`, `web3_base_signature`, `web3_coinbase_wallet_signature`, + `web3_okx_wallet_signature`, `reset_password_phone_code`, `reset_password_email_code`, + `oauth_[provider]`, `saml`, `passkey`, `trusted_device`, `enterprise_sso` + email_address_id: + type: string + description: Used with the `email_code`, `reset_password_email_code` and `email_link` strategies. Pass the `email_address_id` from this sign-in's `supported_first_factors` unchanged. + nullable: true + phone_number_id: + type: string + description: Used with the `phone_code` and `reset_password_phone_code` strategies. Pass the `phone_number_id` from this sign-in's `supported_first_factors` unchanged. + nullable: true + proof_token: + type: string + description: Proof token produced by the Clerk Protect SDK. + nullable: true + web3_wallet_id: + type: string + description: Used with the `web3_metamask_signature`, `web3_base_signature`, `web3_coinbase_wallet_signature` and `web3_okx_wallet_signature` strategies. Pass the `web3_wallet_id` from this sign-in's `supported_first_factors` unchanged. + nullable: true + passkey_id: + type: string + description: Used with the `passkey` strategy. + nullable: true + trusted_device_id: + type: string + description: Used with the `trusted_device` strategy. + nullable: true + redirect_url: + type: string + description: Used with `email_link`, `oauth_[provider]`, and `saml` strategies. + nullable: true + action_complete_redirect_url: + type: string + description: Used with `oauth_[provider]` and `saml` strategies. + nullable: true + oidc_login_hint: + type: string + description: Used with `oauth_[provider]`. The given value will be forwarded to the OIDC `login_hint` parameter of the generated redirect URL. + nullable: true + oidc_prompt: + type: string + description: Used with `oauth_[provider]` or `enterprise_sso`. The given value will be forwarded to the OIDC `prompt` parameter of the generated redirect URL. When using shared credentials this value might be adjusted for security reasons. + nullable: true + required: + - strategy + responses: + '200': + $ref: '#/components/responses/Client.SignIn' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + '429': + $ref: '#/components/responses/ClerkErrors' + /v1/client/sign_ins/{sign_in_id}/attempt_first_factor: + post: + summary: Attempt First Factor Verification + description: |- + Attempt the first verification. + Requires the sign in attempt to be identified, and the first factor verification to be prepared, unless you're using a password. + + Parameter rules: + If the strategy equals `email_code` or `phone_code` then a code is required. + If the strategy equals `password` then a password is required. + If the strategy equals `trusted_device` then trusted_device_id, client_data, signature and algorithm are required. + tags: + - Sign Ins + operationId: attemptSignInFactorOne + parameters: + - in: path + name: sign_in_id + required: true + schema: + type: string + description: The ID of the sign in. + - in: header + name: Origin + description: The origin of the request + schema: + type: string + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + strategy: + type: string + description: |- + The strategy to be used for first factor authentication. + + Can be one of the following `email_code`, `email_link`, `password`, `phone_code`, + `web3_metamask_signature`, `web3_base_signature`, `web3_coinbase_wallet_signature`, `web3_okx_wallet_signature`, + `reset_password_phone_code`, `reset_password_email_code`, `passkey`, `trusted_device`, `google_one_tap` + code: + type: string + description: The code that was sent to the email. Used with the `email_code`, `phone_code`, and `email_link` strategies. + nullable: true + password: + type: string + description: Used with the `password` and `reset_password_phone_code` strategies. + nullable: true + signature: + type: string + description: Used with the `web3_metamask_signature`, `web3_base_signature`, `web3_coinbase_wallet_signature`, `web3_okx_wallet_signature`, and `trusted_device` strategies. + nullable: true + token: + type: string + description: The ID token from an OpenID Connect flow. Only used with `oauth_token_[provider]` and `google_one_tap` strategies. + nullable: true + ticket: + type: string + deprecated: true + nullable: true + public_key_credential: + type: string + description: Used with the `passkey` strategy. + nullable: true + trusted_device_id: + type: string + description: Used with the `trusted_device` strategy. + nullable: true + client_data: + type: string + description: Canonical trusted-device challenge payload returned by prepare. + nullable: true + algorithm: + type: string + enum: + - ES256 + description: Signature algorithm used with the `trusted_device` strategy. + nullable: true + required: + - strategy + responses: + '200': + $ref: '#/components/responses/Client.SignIn' + '307': + description: Redirect, no body. + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + '429': + $ref: '#/components/responses/ClerkErrors' + /v1/client/sign_ins/{sign_in_id}/prepare_second_factor: + post: + summary: Prepare Second Factor Verification + description: |- + Prepare the second verification. + Requires the sign in attempt `status` to be equal to `needs_second_factor` or `needs_client_trust`. + tags: + - Sign Ins + operationId: prepareSignInFactorTwo + parameters: + - in: path + name: sign_in_id + required: true + schema: + type: string + description: The ID of the sign in. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + strategy: + type: string + description: The strategy to be prepared for second factor authentication. + nullable: true + enum: + - phone_code + - email_code + - email_link + - passkey + phone_number_id: + type: string + description: Used with the `phone_code` strategy. + nullable: true + proof_token: + type: string + description: Proof token produced by the Clerk Protect SDK. + nullable: true + email_address_id: + type: string + description: Used with the `email_code` and `email_link` strategies. + nullable: true + redirect_url: + type: string + description: Used with the `email_link` strategy. The redirect URL after email link verification. + nullable: true + responses: + '200': + $ref: '#/components/responses/Client.SignIn' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + '429': + $ref: '#/components/responses/ClerkErrors' + /v1/client/sign_ins/{sign_in_id}/attempt_second_factor: + post: + summary: Attempt Second Factor Verification + description: |- + Attempt the second verification. + Requires the sign in attempt `status` to be equal to `needs_second_factor` or `needs_client_trust`, and for the preparation step to have been called. + tags: + - Sign Ins + operationId: attemptSignInFactorTwo + parameters: + - in: path + name: sign_in_id + required: true + schema: + type: string + description: The ID of the sign in. + - in: header + name: Origin + description: The origin of the request. Used with the `passkey` strategy. + schema: + type: string + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + strategy: + type: string + description: The strategy to be attempted for second factor authentication. + enum: + - email_code + - phone_code + - totp + - backup_code + - passkey + code: + type: string + description: Used with the `phone_code`, `totp` and `backup_code` strategies. + nullable: true + public_key_credential: + type: string + description: Used with the `passkey` strategy. + nullable: true + responses: + '200': + $ref: '#/components/responses/Client.SignIn' + '307': + description: Redirect, no body. + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/client/sign_ups: + post: + summary: Create Sign-up + description: Creates or replaces the sign-up on the current Client object. + tags: + - Sign Ups + operationId: createSignUps + parameters: + - in: header + name: Origin + description: The origin of the request + schema: + type: string + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + transfer: + type: boolean + nullable: true + password: + type: string + nullable: true + first_name: + type: string + nullable: true + last_name: + type: string + nullable: true + username: + type: string + nullable: true + email_address: + type: string + nullable: true + phone_number: + type: string + nullable: true + email_address_or_phone_number: + type: string + nullable: true + unsafe_metadata: + type: string + nullable: true + strategy: + type: string + description: |- + Strategy used to sign up. + Can be one of `email_code`, `email_link`, `enterprise_sso`, `google_one_tap`, `oauth_[provider]`, `oauth_token_[provider]`, `phone_code`, `saml`, `ticket`, `web3_[provider]_signature` + nullable: true + action_complete_redirect_url: + type: string + nullable: true + redirect_url: + type: string + nullable: true + ticket: + type: string + nullable: true + web3_wallet: + type: string + nullable: true + token: + type: string + description: The ID token from an OpenID Connect flow. Only used with `oauth_token_[provider]` and `google_one_tap` strategies. + nullable: true + code: + type: string + description: The authorization or grant code for an OAuth exchange. Only used with `oauth_token_[provider]` strategies. + nullable: true + captcha_token: + type: string + nullable: true + captcha_error: + type: string + nullable: true + captcha_widget_type: + type: string + nullable: true + legal_accepted: + type: boolean + nullable: true + description: Has the value `true` if the user has accepted the legal requirements. + timezone: + type: string + nullable: true + description: The IANA timezone reported by the client. + oidc_login_hint: + type: string + description: Used with `oauth_[provider]`. The given value will be forwarded to the OIDC `login_hint` parameter of the generated redirect URL. + nullable: true + oidc_prompt: + type: string + description: Used with `oauth_[provider]` or `enterprise_sso`. The given value will be forwarded to the OIDC `prompt` parameter of the generated redirect URL. When using shared credentials this value might be adjusted for security reasons. + nullable: true + proof_token: + type: string + description: Proof token produced by the Clerk Protect SDK. + nullable: true + responses: + '200': + $ref: '#/components/responses/Client.SignUp' + '307': + description: Redirect, no body. + '400': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '409': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + '429': + $ref: '#/components/responses/ClerkErrors' + /v1/client/sign_ups/{sign_up_id}: + get: + summary: Get Sign-up + description: Returns the sign-up by ID. Must be associated with the current Client object. + tags: + - Sign Ups + operationId: getSignUps + parameters: + - in: path + name: sign_up_id + required: true + schema: + type: string + description: The sign up unique identifier. + responses: + '200': + $ref: '#/components/responses/Client.SignUp' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + patch: + summary: Update Sign-up + description: Updates the sign-up object specified by ID, with the supplied parameters. + tags: + - Sign Ups + operationId: updateSignUps + parameters: + - in: path + name: sign_up_id + required: true + schema: + type: string + description: The sign up unique identifier. + - in: header + name: Origin + description: The origin of the request + schema: + type: string + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + password: + type: string + nullable: true + first_name: + type: string + nullable: true + last_name: + type: string + nullable: true + username: + type: string + nullable: true + email_address: + type: string + nullable: true + phone_number: + type: string + nullable: true + email_address_or_phone_number: + type: string + nullable: true + unsafe_metadata: + type: string + nullable: true + strategy: + type: string + description: |- + Strategy used to sign up. + Can be one of `email_code`, `email_link`, `enterprise_sso`, `google_one_tap`, `oauth_[provider]`, `oauth_token_[provider]`, `phone_code`, `saml`, `ticket`, `web3_[provider]_signature` + nullable: true + redirect_url: + type: string + nullable: true + action_complete_redirect_url: + type: string + nullable: true + ticket: + type: string + nullable: true + web3_wallet: + type: string + nullable: true + token: + type: string + description: The ID token from an OpenID Connect flow. Only used with `oauth_token_[provider]` and `google_one_tap` strategies. + nullable: true + code: + type: string + description: The authorization or grant code for an OAuth exchange. Only used with `oauth_token_[provider]` strategies. + nullable: true + legal_accepted: + type: boolean + nullable: true + description: | + Has the value `true` if the user has accepted the legal requirements. + oidc_login_hint: + type: string + description: Used with `oauth_[provider]`. The given value will be forwarded to the OIDC `login_hint` parameter of the generated redirect URL. + nullable: true + oidc_prompt: + type: string + description: Used with `oauth_[provider]` or `enterprise_sso`. The given value will be forwarded to the OIDC `prompt` parameter of the generated redirect URL. When using shared credentials this value might be adjusted for security reasons. + nullable: true + proof_token: + type: string + description: Proof token produced by the Clerk Protect SDK. + nullable: true + responses: + '200': + $ref: '#/components/responses/Client.SignUp' + '307': + description: Redirect, no body. + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + '429': + $ref: '#/components/responses/ClerkErrors' + /v1/client/sign_ups/{sign_up_id}/prepare_verification: + post: + summary: Prepare Sign-up Identification Verification + description: | + Prepares verification for the sign-up specified by `{id}`. + + Depending on the given strategy, the API will prepare the verification for the current sign-up. + In particular, + * for `email_code`, the API will send a verification email to the email address currently load up in the sign-up + * for `phone_code`, the API will send a verification SMS to the phone number currently load up in the sign-up + tags: + - Sign Ups + operationId: prepareSignUpsVerification + parameters: + - in: path + name: sign_up_id + required: true + schema: + type: string + description: The sign up unique identifier. + - in: header + name: Origin + description: The origin of the request + schema: + type: string + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + strategy: + type: string + description: |- + The verification strategy + + - email_code + - email_link + - phone_code + - web3_metamask_signature + - web3_base_signature + - web3_coinbase_wallet_signature + - web3_okx_wallet_signature + - saml + - oauth + redirect_url: + type: string + nullable: true + action_complete_redirect_url: + type: string + nullable: true + oidc_login_hint: + type: string + description: Used with `oauth_[provider]`. The given value will be forwarded to the OIDC `login_hint` parameter of the generated redirect URL. + nullable: true + oidc_prompt: + type: string + description: Used with `oauth_[provider]` or `enterprise_sso`. The given value will be forwarded to the OIDC `prompt` parameter of the generated redirect URL. When using shared credentials this value might be adjusted for security reasons. + nullable: true + proof_token: + type: string + description: Proof token produced by the Clerk Protect SDK. + nullable: true + responses: + '200': + $ref: '#/components/responses/Client.SignUp' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + '429': + $ref: '#/components/responses/ClerkErrors' + /v1/client/sign_ups/{sign_up_id}/attempt_verification: + post: + summary: Attempt Sign-up Identification Verification + description: Attempts to verify the identification that corresponds to the given strategy using the given verification code. + tags: + - Sign Ups + operationId: attemptSignUpsVerification + parameters: + - in: path + name: sign_up_id + required: true + schema: + type: string + description: The sign up unique identifier. + - in: header + name: Origin + description: The origin of the request + schema: + type: string + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + strategy: + type: string + description: The verification strategy + enum: + - email_code + - email_link + - phone_code + - web3_metamask_signature + - web3_base_signature + - web3_coinbase_wallet_signature + - web3_okx_wallet_signature + - web3_solana_signature + - google_one_tap + code: + nullable: true + type: string + description: The verification code + signature: + nullable: true + type: string + description: The verification web3 signature + token: + type: string + description: The ID token from an OpenID Connect flow. Only used with `oauth_token_[provider]` and `google_one_tap` strategies. + nullable: true + responses: + '200': + $ref: '#/components/responses/Client.SignUp' + '307': + description: Redirect, no body. + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '409': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + '429': + $ref: '#/components/responses/ClerkErrors' + /v1/client/sync: + get: + summary: Sync clients across multiple domains + description: Start the syncing process between a satellite and primary domain. + operationId: syncClient + parameters: + - in: query + name: link_domain + schema: + type: string + description: The satellite domain which should be synced with its primary. + - in: query + name: redirect_url + schema: + type: string + description: The URL to redirect to after the syncing process has been completed. + responses: + '307': + description: Redirect + '400': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/client/link: + get: + summary: Link clients across multiple domains + description: Complete a syncing process between a satellite and primary domains by linking their clients. + operationId: linkClient + parameters: + - in: query + name: __clerk_token + schema: + type: string + description: The token generated from a sync request. + responses: + '307': + description: Redirect + '400': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/client/handshake: + get: + summary: Generate and return a new session token for a given client. + description: When the authentication status cannot be determined from the current session token, we initiate a handshake to refresh the token and send it back to the application. Called in server environments. + tags: + - Client + operationId: handshakeClient + parameters: + - in: header + name: Clerk-Proxy-Url + description: The URL of the proxy + schema: + type: string + - in: header + name: Clerk-Secret-Key + description: The secret key of the proxy + schema: + type: string + - in: query + name: redirect_url + schema: + type: string + description: The URL to redirect back to after the handshake + - in: query + name: format + allowEmptyValue: false + required: false + schema: + type: string + enum: + - nonce + - token + default: token + description: The supported format of the handshake payload. + - in: query + name: organization_id + schema: + type: string + allowEmptyValue: true + description: | + The organization ID or slug to attempt to set as active for the session. If this param is present but has no value, + the personal account will be set as active. If the organization cannot be set as active (because it does not + exist, or the user is not a member), the active organization for the session will not change. + - in: query + name: satellite_fapi + description: The Frontend API of the satellite domain + schema: + type: string + responses: + '307': + description: Redirect + '400': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + description: Request was not successful + content: + application/json: + schema: + $ref: '#/components/schemas/ClerkErrors' + text/html: + schema: + type: string + '500': + $ref: '#/components/responses/ClerkErrors' + /v1/client/trusted_devices/validate: + post: + deprecated: true + summary: Validate Trusted Device Sign-In Credential + description: Validates that a local trusted-device credential can be used by the current native client. + operationId: validateTrustedDevice + tags: + - Trusted Devices + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + trusted_device_id: + type: string + description: The trusted-device credential ID stored by the native SDK. + required: + - trusted_device_id + responses: + '200': + description: Returns whether the trusted-device credential is valid for the current client. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedTrustedDeviceValidation' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/client/biometric_credentials/validate: + post: + summary: Validate Biometric Sign-In Credential + description: Validates that a local biometric credential can be used by the current native client. + operationId: validateBiometricCredential + tags: + - Biometric Credentials + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + trusted_device_id: + type: string + description: The biometric credential ID stored by the native SDK. + required: + - trusted_device_id + responses: + '200': + description: Returns whether the biometric credential is valid for the current client. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedBiometricCredentialValidation' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/agents/tasks: + get: + x-speakeasy-group: agentTasks + x-speakeasy-name-override: create + summary: Create session from agent task + description: | + Creates a new session directly from an agent task without requiring navigation to the sign-in page. + Agent tasks are single-use tokens created via the Backend API that bypass MFA requirements. + + The endpoint returns a redirect with a handshake cookie containing all session cookies, + enabling cookie sync to the app domain in a single step. This is useful for browser + automation tools (Puppeteer, Playwright) that need cookies on the application domain. + tags: + - Client + operationId: createAgentTask + security: + - {} + - DevBrowser: [] + parameters: + - in: query + name: ticket + required: true + schema: + type: string + description: The agent task ticket value obtained from the Backend API. + responses: + '307': + description: | + Redirect to the `redirect_url` specified when the agent task was created. + + Before redirecting, the URL is validated: + - Must be a valid absolute URL with an `https` scheme in production. + In development instances, `http` is also permitted. + - The URL's domain must belong to one of the instance's associated domains + (primary or satellite). + + In production, the handshake payload is set as a cookie. + In development, the handshake payload is passed as a query parameter. + '400': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + '500': + $ref: '#/components/responses/ClerkErrors' + /v1/dev_browser: + post: + summary: Create a Dev Browser token + description: |- + Generate a Dev Browser API token. + + This is used to authenticate Development Instances with the `DevBrowser` scheme. + It must be set before making any request to a dev instance, even for endpoints that are public. + operationId: createDevBrowser + security: [] + tags: + - DevBrowser + responses: + '200': + description: A DevBrowser was created. + '400': + description: The hostname is invalid. + /v1/dev_browser/set_first_party_cookie: + post: + summary: Post Dev Browser init set cookie + description: post dev_browser/set_first_party_cookie + operationId: postDevBrowserInitSetCookie + responses: + '200': + description: A cookie for the dev browser was set. + /v1/dev_browser/init: + get: + summary: Initialize dev browser for development and staging instances + description: get dev_browser/init + operationId: getDevBrowserInit + parameters: + - in: query + name: origin + description: The origin of the request + schema: + type: string + responses: + '200': + description: Successfully initialized the dev browser. + '400': + $ref: '#/components/responses/ClerkErrors' + '500': + $ref: '#/components/responses/ClerkErrors' + /v1/oauth_callback: + get: + operationId: getOauthCallback + summary: OAuth Callback + description: The endpoint where the OAuth providers redirect to after a successful authentication attempt. + tags: + - OAuth2 Callbacks + parameters: + - in: query + name: scope + schema: + type: string + description: returned scopes from OAuth provider. + - in: query + name: code + schema: + type: string + description: returned exchange code from OAuth provider. + - in: query + name: state + schema: + type: string + description: returned state from OAuth provider. + - in: query + name: error + schema: + type: string + description: returned error state from OAuth provider, if applicable + responses: + '200': + description: '[AuthV2] HTML-based redirection in dev instances' + '299': + description: FAKE SUCCESS + '301': + description: Redirect + '303': + description: Redirect + '307': + description: Temporary redirect to complete the OAuth callback flow. + '400': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + post: + operationId: postOauthCallback + summary: OAuth Post Callback + description: Converts a POST to a GET, because if it's a POST coming from an external account. This is necessary because some providers (e.g. Apple) issue a POST instead of a GET request. + tags: + - OAuth2 Callbacks + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: true + properties: + code: + description: returned exchange code from OAuth provider. + type: string + scope: + description: returned scopes from OAuth provider. + type: string + nullable: true + state: + description: returned state from OAuth provider. + type: string + error: + description: returned error state from OAuth provider, if applicable + type: string + nullable: true + responses: + '303': + description: Redirect to GET /v1/oauth_callback with same parameters + /v1/clear-site-data: + get: + summary: Set the Clear-Site-Data header + description: Clear browsing data (cookies, storage, cache) associated with the requesting website + operationId: clearSiteData + responses: + '200': + description: The Clear-Site-Data header was set with wildcard value. + /v1/client/sessions: + delete: + summary: Remove client's sessions + description: Removes all the sessions of the current client without removing the __client cookie + tags: + - Sessions + operationId: removeClientSessionsAndRetainCookie + responses: + '200': + $ref: '#/components/responses/Client.DeleteSession' + '401': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + /v1/client/sessions/{session_id}: + get: + operationId: getSession + summary: Get Session + description: Returns the session with the given id + tags: + - Sessions + parameters: + - in: path + name: session_id + required: true + schema: + type: string + description: The user session ID. + responses: + '200': + $ref: '#/components/responses/Client.Session' + '401': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + /v1/client/sessions/{session_id}/touch: + post: + operationId: touchSession + summary: Touch session + description: |- + Specify the active session for the client. + + When force organization selection is enabled and `active_organization_id` is sent as null or empty string, + the session will keep the previous active organization and will not attempt to switch to a personal account. + tags: + - Sessions + parameters: + - in: path + name: session_id + required: true + schema: + type: string + description: The user session ID. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + active_organization_id: + type: string + description: |- + The ID or slug of the organization to activate. + + When force organization selection is enabled and this value is sent as null or empty string, + the session will keep the previous active organization and will not attempt to switch to a personal account. + nullable: true + intent: + type: string + description: Indicates why the touch request was triggered. + enum: + - focus + - select_session + - select_org + nullable: true + responses: + '200': + $ref: '#/components/responses/Client.TouchSession' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/client/sessions/{session_id}/end: + post: + operationId: endSession + summary: End Session + description: Marks the given session as ended. + tags: + - Sessions + parameters: + - in: path + name: session_id + required: true + schema: + type: string + description: The user session ID. + responses: + '200': + $ref: '#/components/responses/Client.Session' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + /v1/client/sessions/{session_id}/remove: + post: + operationId: removeSession + summary: Remove Session + description: Delete the given session. + tags: + - Sessions + parameters: + - in: path + name: session_id + required: true + schema: + type: string + description: The user session ID. + responses: + '200': + $ref: '#/components/responses/Client.Session' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + /v1/me/sessions/active: + get: + summary: Get Active Sessions + description: Returns all active session for user, including activity information. + tags: + - Active Sessions + operationId: getSessions + parameters: + - in: query + name: _clerk_session_id + schema: + type: string + description: The session_id associated with the requesting user. + responses: + '200': + $ref: '#/components/responses/Client.ActiveSessions' + '401': + $ref: '#/components/responses/ClerkErrors' + /v1/me/sessions/{session_id}/revoke: + post: + summary: Revoke given session + description: Revokes given session. + tags: + - Active Sessions + operationId: revokeSession + parameters: + - in: query + name: _clerk_session_id + schema: + type: string + description: The session_id associated with the requesting user. + - in: path + name: session_id + required: true + schema: + type: string + description: The session ID to be revoked. + responses: + '200': + $ref: '#/components/responses/Client.Session' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + /v1/client/sessions/{session_id}/tokens: + post: + summary: Create Session Token + description: |- + Create a session JWT for the authenticated requested user. + + When force organization selection is enabled and `organization_id` is sent as null or empty string, + the token will be created with the previous active organization and will not attempt to switch to a personal account. + operationId: createSessionToken + tags: + - Sessions + parameters: + - in: path + name: session_id + required: true + schema: + type: string + description: The user session ID. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + organization_id: + type: string + description: |- + The organization ID to associate with the token. The user must be a member of the organization. + If present but empty, the personal account will be set as active. + If absent, the previous active organization for the session will be used. + + When force organization selection is enabled and this value is sent as null or empty string, + the token will be created with the previous active organization and will not attempt to switch to a personal account. + nullable: true + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + jwt: + type: string + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + /v1/client/sessions/{session_id}/tokens/{template_name}: + post: + summary: Create Session Token With JWT Template + description: Create a session JWT for the authenticated requested user. + operationId: createSessionTokenWithTemplate + tags: + - Sessions + parameters: + - in: path + name: session_id + required: true + schema: + type: string + description: The user session ID. + - in: path + name: template_name + required: true + schema: + type: string + description: the template name + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + jwt: + type: string + '401': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + /v1/client/sessions/{session_id}/verify: + post: + summary: Start a new session reverification + description: |- + Start a new session reverification flow by providing a verification level. + + If the requested level equals 'secondFactor' or 'multiFactor' and the associated user + doesn't have any available second factor, then we fall back to 'firstFactor' + tags: + - Sessions + operationId: startSessionReverification + parameters: + - in: path + name: session_id + required: true + schema: + type: string + description: the user session ID. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + level: + type: string + description: The level of authentication that the user needs to go through + enum: + - first_factor + - second_factor + - multi_factor + required: + - level + responses: + '200': + $ref: '#/components/responses/Client.SessionReverification' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/client/sessions/{session_id}/verify/prepare_first_factor: + post: + summary: Prepare session reverification first factor + description: |- + Prepare the first factor verification. + Depending on the strategy, this request will do something different. + + Parameter actions: + If the strategy equals email_code then this request will send an email with an OTP code. + If the strategy equals phone_code then this request will send an SMS with an OTP code. + tags: + - Sessions + operationId: prepareSessionReverificationFirstFactor + parameters: + - in: header + name: Origin + description: The origin of the request + schema: + type: string + - in: path + name: session_id + required: true + schema: + type: string + description: the user session ID. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + trusted_device_id: + type: string + description: The biometric credential enrolled on this native client. Required with `trusted_device`. + strategy: + type: string + description: The strategy to be prepared for first factor authentication. + enum: + - email_code + - phone_code + - passkey + - enterprise_sso + - trusted_device + email_address_id: + type: string + description: Used with the `email_code` strategy. + nullable: true + phone_number_id: + type: string + description: Used with the `phone_code` strategy. + nullable: true + proof_token: + type: string + description: Proof token produced by the Clerk Protect SDK. + nullable: true + responses: + '200': + $ref: '#/components/responses/Client.SessionReverification' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + '429': + $ref: '#/components/responses/ClerkErrors' + /v1/client/sessions/{session_id}/verify/attempt_first_factor: + post: + summary: Attempt session reverification first factor + description: |- + Attempt the first factor verification. + Requires the first factor verification to be prepared, unless you're using a password. + + Parameter rules: + If the strategy equals `email_code` then a code is required. + If the strategy equals `password` then a password is required. + tags: + - Sessions + operationId: attemptSessionReverificationFirstFactor + parameters: + - in: header + name: Origin + description: The origin of the request + schema: + type: string + - in: path + name: session_id + required: true + schema: + type: string + description: the user session ID. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + trusted_device_id: + type: string + description: The biometric credential enrolled on this native client. Required with `trusted_device`. + client_data: + type: string + description: The exact server-issued challenge data. Required with `trusted_device`. + signature: + type: string + description: The base64url-encoded ES256 signature. Required with `trusted_device`. + algorithm: + type: string + enum: + - ES256 + description: Required with `trusted_device`. + strategy: + type: string + description: The strategy to be used for first factor authentication. + enum: + - email_code + - password + - phone_code + - passkey + - trusted_device + code: + type: string + description: The code that was sent to the email. Used with the `email_code` and `phone_code` strategies. + nullable: true + password: + type: string + description: Used with the `password` strategy. + nullable: true + public_key_credential: + type: string + description: Used with the `passkey` strategy. + nullable: true + required: + - strategy + responses: + '200': + $ref: '#/components/responses/Client.SessionReverification' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + '429': + $ref: '#/components/responses/ClerkErrors' + /v1/client/sessions/{session_id}/verify/prepare_second_factor: + post: + summary: Prepare session reverification second factor + description: |- + Prepare the second factor verification. + Requires the `status` to be equal to `needs_second_factor`. + tags: + - Sessions + operationId: prepareSessionReverificationSecondFactor + parameters: + - in: path + name: session_id + required: true + schema: + type: string + description: the user session ID. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + trusted_device_id: + type: string + description: The biometric credential enrolled on this native client. Required with `trusted_device`. + strategy: + type: string + description: The strategy to be prepared for second factor authentication. + nullable: true + enum: + - phone_code + - passkey + - trusted_device + phone_number_id: + type: string + description: Used with the `phone_code` strategy. + nullable: true + proof_token: + type: string + description: Proof token produced by the Clerk Protect SDK. + nullable: true + responses: + '200': + $ref: '#/components/responses/Client.SessionReverification' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + '429': + $ref: '#/components/responses/ClerkErrors' + /v1/client/sessions/{session_id}/verify/attempt_second_factor: + post: + summary: Attempt session reverification second factor + description: |- + Attempt the second factor verification. + Requires the `status` to be equal to `needs_second_factor` and for the preparation step to have been called. + tags: + - Sessions + operationId: attemptSessionReverificationSecondFactor + parameters: + - in: path + name: session_id + required: true + schema: + type: string + description: the user session ID. + - in: header + name: Origin + description: The origin of the request. Used with the `passkey` strategy. + schema: + type: string + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + trusted_device_id: + type: string + description: The biometric credential enrolled on this native client. Required with `trusted_device`. + client_data: + type: string + description: The exact server-issued challenge data. Required with `trusted_device`. + signature: + type: string + description: The base64url-encoded ES256 signature. Required with `trusted_device`. + algorithm: + type: string + enum: + - ES256 + description: Required with `trusted_device`. + strategy: + type: string + description: The strategy to be attempted for second factor authentication. + enum: + - phone_code + - totp + - backup_code + - passkey + - trusted_device + code: + type: string + description: Used with the `phone_code`, `totp` and `backup_code` strategies. + nullable: true + public_key_credential: + type: string + description: Used with the `passkey` strategy. + nullable: true + responses: + '200': + $ref: '#/components/responses/Client.SessionReverification' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + '429': + $ref: '#/components/responses/ClerkErrors' + /v1/me/sessions: + get: + summary: Get Sessions + description: Get all the sessions of the current user. + tags: + - Active Sessions + operationId: getUsersSessions + parameters: + - in: query + name: _clerk_session_id + schema: + type: string + description: The session_id associated with the requesting user. + responses: + '200': + $ref: '#/components/responses/Server.Sessions' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + /v1/organizations/{organizationID}/enterprise_connections: + get: + operationId: listOrganizationEnterpriseConnections + summary: List enterprise connections for the active organization + description: | + Returns the active enterprise connections visible to the requesting user within the active organization + context. The active organization is taken from the session, not the URL path. Unless the + `with_organization_account_linking` query is `true`, the result set is filtered to connections whose domains + match the user's verified primary email address. + tags: + - Enterprise Connections + parameters: + - in: path + name: organizationID + required: true + schema: + type: string + description: The ID of the active organization + - in: query + name: _clerk_session_id + schema: + type: string + description: The session_id associated with the requesting user. + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedEnterpriseConnections' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + post: + operationId: createOrganizationEnterpriseConnection + summary: Create enterprise connection for an organization + description: | + Creates an enterprise connection bound to the organization in the URL path. The caller's session must be pinned + to that organization and the caller must hold the `org:sys_entconns:manage` permission. + tags: + - Enterprise Connections + parameters: + - in: path + name: organizationID + required: true + schema: + type: string + description: The ID of the active organization + - in: query + name: _clerk_session_id + schema: + type: string + description: The session_id associated with the requesting user. + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + required: + - provider + - domains + properties: + provider: + type: string + enum: + - saml_custom + - saml_okta + - saml_google + - saml_microsoft + - oidc_custom + - oidc_github_enterprise + - oidc_gitlab + description: SSO provider type + name: + type: string + description: | + Optional display name for the enterprise connection. When omitted (or sent empty), + the name defaults to the requesting user's primary email address domain, falling + back to the organization's name. + domains: + type: array + items: + type: string + minItems: 1 + description: | + Domains served by this enterprise connection. Repeated values are sent as multiple + `domains` form fields (e.g. `domains=acme.test&domains=beta.acme.test`). Each entry is lowercased, + trimmed, and deduplicated server-side. Every supplied domain must already exist as one of the + organization's domains with verified ownership (see the organization domains verification flow) + and the `enterprise_sso` enrollment mode; the whole request is rejected with a 422 if any domain + is missing, unverified, or configured with a different enrollment mode. + saml_idp_entity_id: + type: string + nullable: true + description: SAML IdP entity ID (only when using a SAML provider) + saml_idp_sso_url: + type: string + nullable: true + description: SAML IdP SSO URL (only when using a SAML provider) + saml_idp_certificate: + type: string + nullable: true + deprecated: true + description: Deprecated, use saml_idp_certificates. SAML IdP X.509 certificate, one or several concatenated PEM certificates; replaces the connection's whole certificate set (only when using a SAML provider) + saml_idp_certificates: + type: array + description: SAML IdP X.509 signing certificates, one per value, PEM or bare base64. Replaces the connection's whole certificate set and takes precedence over saml_idp_certificate (only when using a SAML provider) + minItems: 1 + maxItems: 5 + items: + type: string + saml_idp_metadata_url: + type: string + nullable: true + description: SAML IdP metadata URL (only when using a SAML provider) + saml_idp_metadata: + type: string + nullable: true + description: SAML IdP metadata XML document (only when using a SAML provider) + saml_attribute_mapping: + type: string + nullable: true + description: | + JSON-encoded IdP attribute mapping (only when using a SAML provider). + Shape depends on provider. + saml_allow_subdomains: + type: boolean + nullable: true + saml_allow_idp_initiated: + type: boolean + nullable: true + saml_force_authn: + type: boolean + nullable: true + oidc_client_id: + type: string + nullable: true + description: OIDC client ID (only when using an OIDC provider) + oidc_client_secret: + type: string + nullable: true + description: OIDC client secret (only when using an OIDC provider) + oidc_discovery_url: + type: string + nullable: true + description: OIDC discovery URL (only when using an OIDC provider) + oidc_auth_url: + type: string + nullable: true + description: OIDC authorization endpoint URL (only when using an OIDC provider) + oidc_token_url: + type: string + nullable: true + description: OIDC token endpoint URL (only when using an OIDC provider) + oidc_user_info_url: + type: string + nullable: true + description: OIDC user info endpoint URL (only when using an OIDC provider) + oidc_requires_pkce: + type: boolean + nullable: true + responses: + '201': + $ref: '#/components/responses/Client.ClientWrappedEnterpriseConnection' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '402': + $ref: '#/components/responses/PaymentRequired' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ResourceNotFound' + '409': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/UnprocessableEntity' + /v1/organizations/{organizationID}/enterprise_connections/{enterprise_connection_id}: + patch: + operationId: updateOrganizationEnterpriseConnection + summary: Update enterprise connection for an organization + description: | + Updates an enterprise connection bound to the organization in the URL path. The caller's session must be pinned + to that organization and the caller must hold the `org:sys_entconns:manage` permission. + tags: + - Enterprise Connections + parameters: + - in: path + name: organizationID + required: true + schema: + type: string + description: The ID of the active organization + - in: path + name: enterprise_connection_id + required: true + schema: + type: string + description: The ID of the enterprise connection + - in: query + name: _clerk_session_id + schema: + type: string + description: The session_id associated with the requesting user. + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + name: + type: string + nullable: true + domains: + type: array + nullable: true + items: + type: string + description: | + Replacement domains for this enterprise connection. Repeated values are sent as multiple + `domains` form fields (e.g. `domains=acme.test&domains=beta.acme.test`). Every supplied domain + must already exist as one of the organization's domains with verified ownership and the + `enterprise_sso` enrollment mode. + active: + type: boolean + nullable: true + sync_user_attributes: + type: boolean + nullable: true + disable_additional_identifications: + type: boolean + nullable: true + custom_attributes: + type: string + nullable: true + description: | + JSON-encoded custom attributes (only when enabled for the instance). + saml_idp_entity_id: + type: string + nullable: true + saml_idp_sso_url: + type: string + nullable: true + saml_idp_certificate: + type: string + nullable: true + deprecated: true + saml_idp_certificates: + type: array + description: SAML IdP X.509 signing certificates, one per value, PEM or bare base64. Replaces the connection's whole certificate set and takes precedence over saml_idp_certificate (only when using a SAML provider) + minItems: 1 + maxItems: 5 + items: + type: string + saml_idp_metadata_url: + type: string + nullable: true + saml_idp_metadata: + type: string + nullable: true + saml_attribute_mapping: + type: string + nullable: true + description: JSON-encoded IdP attribute mapping. Shape depends on provider. + saml_allow_subdomains: + type: boolean + nullable: true + saml_allow_idp_initiated: + type: boolean + nullable: true + saml_force_authn: + type: boolean + nullable: true + oidc_client_id: + type: string + nullable: true + oidc_client_secret: + type: string + nullable: true + oidc_discovery_url: + type: string + nullable: true + oidc_auth_url: + type: string + nullable: true + oidc_token_url: + type: string + nullable: true + oidc_user_info_url: + type: string + nullable: true + oidc_requires_pkce: + type: boolean + nullable: true + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedEnterpriseConnection' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '402': + $ref: '#/components/responses/PaymentRequired' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ResourceNotFound' + '422': + $ref: '#/components/responses/UnprocessableEntity' + delete: + operationId: deleteOrganizationEnterpriseConnection + summary: Delete enterprise connection for an organization + description: | + Deletes an enterprise connection bound to the organization in the URL path. The caller's session must be pinned + to that organization and the caller must hold the `org:sys_entconns:manage` permission. + tags: + - Enterprise Connections + parameters: + - in: path + name: organizationID + required: true + schema: + type: string + description: The ID of the active organization + - in: path + name: enterprise_connection_id + required: true + schema: + type: string + description: The ID of the enterprise connection to delete + - in: query + name: _clerk_session_id + schema: + type: string + description: The session_id associated with the requesting user. + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedDeletedObject' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '402': + $ref: '#/components/responses/PaymentRequired' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ResourceNotFound' + '422': + $ref: '#/components/responses/UnprocessableEntity' + /v1/organizations/{organizationID}/enterprise_connections/{enterprise_connection_id}/test_runs: + get: + operationId: listOrganizationEnterpriseConnectionTestRuns + summary: List enterprise connection test runs for an organization + description: | + Returns a paginated list of SAML or OIDC debug test runs for an organization-bound enterprise connection. + tags: + - Enterprise Connections + parameters: + - in: path + name: organizationID + required: true + schema: + type: string + description: The ID of the active organization + - in: path + name: enterprise_connection_id + required: true + schema: + type: string + description: The ID of the enterprise connection + - in: query + name: _clerk_session_id + schema: + type: string + description: The session_id associated with the requesting user. + - in: query + name: status + required: false + schema: + type: array + items: + type: string + enum: + - pending + - success + - failed + style: form + explode: true + description: Filter test runs by status (may be repeated) + - $ref: '#/components/parameters/LimitParameter' + - $ref: '#/components/parameters/OffsetParameter' + - $ref: '#/components/parameters/Paginated' + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedEnterpriseConnectionTestRunsList' + '401': + $ref: '#/components/responses/ClerkErrors' + '402': + $ref: '#/components/responses/PaymentRequired' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ResourceNotFound' + '422': + $ref: '#/components/responses/UnprocessableEntity' + post: + operationId: createOrganizationEnterpriseConnectionTestRun + summary: Create enterprise connection test run for an organization + description: | + Returns a short-lived URL that starts the IdP test flow for an organization-bound enterprise connection. + tags: + - Enterprise Connections + parameters: + - in: path + name: organizationID + required: true + schema: + type: string + description: The ID of the active organization + - in: path + name: enterprise_connection_id + required: true + schema: + type: string + description: The ID of the enterprise connection + - in: query + name: _clerk_session_id + schema: + type: string + description: The session_id associated with the requesting user. + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedEnterpriseConnectionTestRun' + '401': + $ref: '#/components/responses/ClerkErrors' + '402': + $ref: '#/components/responses/PaymentRequired' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ResourceNotFound' + /v1/organizations/{organizationID}/sso_bypass_allowlist_users: + get: + operationId: listOrganizationSSOBypassAllowlistUsers + summary: List the organization's SSO bypass allowlist + description: | + Returns the members who may verify an email code instead of reaching their identity provider + when one of the organization's enterprise connections is unreachable. The caller must hold the + `org:sys_entconns_sso_bypass:manage` permission in the organization. + tags: + - SSO Bypass + parameters: + - in: path + name: organizationID + required: true + schema: + type: string + description: The ID of the organization + - in: query + name: _clerk_session_id + schema: + type: string + description: The session_id associated with the requesting user. + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedSSOBypassAllowlistUsers' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ResourceNotFound' + post: + operationId: createOrganizationSSOBypassAllowlistUser + summary: Add a member to the organization's SSO bypass allowlist + description: | + Puts a member on the allowlist. The request is rejected unless the user is a member of the + organization and holds a verified email address on a domain one of the organization's enterprise + connections serves. The caller must hold the `org:sys_entconns_sso_bypass:manage` permission. + tags: + - SSO Bypass + parameters: + - in: path + name: organizationID + required: true + schema: + type: string + description: The ID of the organization + - in: query + name: _clerk_session_id + schema: + type: string + description: The session_id associated with the requesting user. + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + required: + - user_id + properties: + user_id: + type: string + description: The ID of the member to allowlist. + responses: + '201': + $ref: '#/components/responses/Client.ClientWrappedSSOBypassAllowlistUser' + '401': + $ref: '#/components/responses/ClerkErrors' + '402': + $ref: '#/components/responses/PaymentRequired' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ResourceNotFound' + '422': + $ref: '#/components/responses/UnprocessableEntity' + /v1/organizations/{organizationID}/sso_bypass_allowlist_users/bulk: + post: + operationId: createOrganizationSSOBypassAllowlistUsersBulk + summary: Add up to 100 members to the organization's SSO bypass allowlist + description: | + Puts a batch of members on the allowlist in one request. The response is a partial-success + payload: members who were added, or were already on the list, are returned in `data`, while a + user who is not a member of the organization, or holds no verified email address on a domain one + of the organization's enterprise connections serves, is returned in `errors` with the code the + single-user endpoint returns for them. Duplicate ids in the request are collapsed. The caller must + hold the `org:sys_entconns_sso_bypass:manage` permission. + tags: + - SSO Bypass + parameters: + - in: path + name: organizationID + required: true + schema: + type: string + description: The ID of the organization + - in: query + name: _clerk_session_id + schema: + type: string + description: The session_id associated with the requesting user. + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + required: + - user_id + properties: + user_id: + type: array + maxItems: 100 + items: + type: string + description: The IDs of the members to allowlist. + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedSSOBypassAllowlistBulkCreate' + '401': + $ref: '#/components/responses/ClerkErrors' + '402': + $ref: '#/components/responses/PaymentRequired' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ResourceNotFound' + '422': + $ref: '#/components/responses/UnprocessableEntity' + /v1/organizations/{organizationID}/sso_bypass_allowlist_users/{user_id}: + delete: + operationId: deleteOrganizationSSOBypassAllowlistUser + summary: Remove a member from the organization's SSO bypass allowlist + description: | + Removes the user from the allowlist of every enterprise connection the organization owns. The + caller must hold the `org:sys_entconns_sso_bypass:manage` permission. + tags: + - SSO Bypass + parameters: + - in: path + name: organizationID + required: true + schema: + type: string + description: The ID of the organization + - in: path + name: user_id + required: true + schema: + type: string + description: The ID of the allowlisted user + - in: query + name: _clerk_session_id + schema: + type: string + description: The session_id associated with the requesting user. + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedDeletedObject' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ResourceNotFound' + /v1/me/enterprise_connections: + get: + operationId: getMeEnterpriseConnections + deprecated: true + summary: List enterprise connections for the current user + description: | + **Deprecated.** Use `GET /v1/organizations/{organizationID}/enterprise_connections` instead. + + Returns active enterprise connections on the instance whose domains match the **verified** primary email address + of the current user. If the user has no primary email or it is not verified, the list is empty. + tags: + - Enterprise Connections + parameters: + - in: query + name: _clerk_session_id + schema: + type: string + description: The session_id associated with the requesting user. + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedEnterpriseConnections' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + post: + operationId: createMeEnterpriseConnection + deprecated: true + summary: Create enterprise connection + description: | + **Deprecated.** Use `POST /v1/organizations/{organizationID}/enterprise_connections` instead. + + Creates an enterprise connection for the instance. + tags: + - Enterprise Connections + parameters: + - in: query + name: _clerk_session_id + schema: + type: string + description: The session_id associated with the requesting user. + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + required: + - provider + - domains + properties: + provider: + type: string + enum: + - saml_custom + - saml_okta + - saml_google + - saml_microsoft + - oidc_custom + - oidc_github_enterprise + - oidc_gitlab + description: SSO provider type + name: + type: string + description: | + Optional display name for the enterprise connection. When omitted, + the name defaults to the requesting user's primary email address domain, falling + back to the current user's active organization's name. + domains: + type: array + items: + type: string + minItems: 1 + description: | + Domains served by this enterprise connection. Repeated values are sent as multiple + `domains` form fields (e.g. `domains=acme.test&domains=beta.acme.test`). Each entry is lowercased, + trimmed, and deduplicated server-side. Every supplied domain must already exist as one of the + organization's domains with verified ownership and the `enterprise_sso` enrollment mode; the whole + request is rejected with a 422 if any domain is missing, unverified, or configured with a different + enrollment mode. + organization_id: + type: string + nullable: true + description: Optional organization ID to associate with this connection + saml_idp_entity_id: + type: string + nullable: true + description: SAML IdP entity ID (only when using a SAML provider) + saml_idp_sso_url: + type: string + nullable: true + description: SAML IdP SSO URL (only when using a SAML provider) + saml_idp_certificate: + type: string + nullable: true + deprecated: true + description: Deprecated, use saml_idp_certificates. SAML IdP X.509 certificate, one or several concatenated PEM certificates; replaces the connection's whole certificate set (only when using a SAML provider) + saml_idp_certificates: + type: array + description: SAML IdP X.509 signing certificates, one per value, PEM or bare base64. Replaces the connection's whole certificate set and takes precedence over saml_idp_certificate (only when using a SAML provider) + minItems: 1 + maxItems: 5 + items: + type: string + saml_idp_metadata_url: + type: string + nullable: true + description: SAML IdP metadata URL (only when using a SAML provider) + saml_idp_metadata: + type: string + nullable: true + description: SAML IdP metadata XML document (only when using a SAML provider) + saml_attribute_mapping: + type: string + nullable: true + description: | + JSON-encoded IdP attribute mapping (only when using a SAML provider). + Shape depends on provider. + saml_allow_subdomains: + type: boolean + nullable: true + saml_allow_idp_initiated: + type: boolean + nullable: true + saml_force_authn: + type: boolean + nullable: true + oidc_client_id: + type: string + nullable: true + description: OIDC client ID (only when using an OIDC provider) + oidc_client_secret: + type: string + nullable: true + description: OIDC client secret (only when using an OIDC provider) + oidc_discovery_url: + type: string + nullable: true + description: OIDC discovery URL (only when using an OIDC provider) + oidc_auth_url: + type: string + nullable: true + description: OIDC authorization endpoint URL (only when using an OIDC provider) + oidc_token_url: + type: string + nullable: true + description: OIDC token endpoint URL (only when using an OIDC provider) + oidc_user_info_url: + type: string + nullable: true + description: OIDC user info endpoint URL (only when using an OIDC provider) + oidc_requires_pkce: + type: boolean + nullable: true + responses: + '201': + $ref: '#/components/responses/Client.ClientWrappedEnterpriseConnection' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '402': + $ref: '#/components/responses/PaymentRequired' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ResourceNotFound' + '409': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/UnprocessableEntity' + /v1/me/enterprise_connections/{enterprise_connection_id}: + patch: + operationId: updateMeEnterpriseConnection + deprecated: true + summary: Update enterprise connection + description: | + **Deprecated.** Use `PATCH /v1/organizations/{organizationID}/enterprise_connections/{enterprise_connection_id}` instead. + + Updates an enterprise connection the current user may manage (verified primary email domain must match the connection). + tags: + - Enterprise Connections + parameters: + - in: path + name: enterprise_connection_id + required: true + schema: + type: string + description: The ID of the enterprise connection + - in: query + name: _clerk_session_id + schema: + type: string + description: The session_id associated with the requesting user. + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + name: + type: string + nullable: true + domains: + type: array + nullable: true + items: + type: string + description: | + Replacement domains for this enterprise connection. Repeated values are sent as multiple + `domains` form fields (e.g. `domains=acme.test&domains=beta.acme.test`). Every supplied domain + must already exist as one of the organization's domains with verified ownership and the + `enterprise_sso` enrollment mode. + active: + type: boolean + nullable: true + sync_user_attributes: + type: boolean + nullable: true + disable_additional_identifications: + type: boolean + nullable: true + organization_id: + type: string + nullable: true + custom_attributes: + type: string + nullable: true + description: | + JSON-encoded custom attributes (only when enabled for the instance). + saml_idp_entity_id: + type: string + nullable: true + saml_idp_sso_url: + type: string + nullable: true + saml_idp_certificate: + type: string + nullable: true + deprecated: true + saml_idp_certificates: + type: array + description: SAML IdP X.509 signing certificates, one per value, PEM or bare base64. Replaces the connection's whole certificate set and takes precedence over saml_idp_certificate (only when using a SAML provider) + minItems: 1 + maxItems: 5 + items: + type: string + saml_idp_metadata_url: + type: string + nullable: true + saml_idp_metadata: + type: string + nullable: true + saml_attribute_mapping: + type: string + nullable: true + description: JSON-encoded IdP attribute mapping. Shape depends on provider. + saml_allow_subdomains: + type: boolean + nullable: true + saml_allow_idp_initiated: + type: boolean + nullable: true + saml_force_authn: + type: boolean + nullable: true + oidc_client_id: + type: string + nullable: true + oidc_client_secret: + type: string + nullable: true + oidc_discovery_url: + type: string + nullable: true + oidc_auth_url: + type: string + nullable: true + oidc_token_url: + type: string + nullable: true + oidc_user_info_url: + type: string + nullable: true + oidc_requires_pkce: + type: boolean + nullable: true + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedEnterpriseConnection' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '402': + $ref: '#/components/responses/PaymentRequired' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ResourceNotFound' + '422': + $ref: '#/components/responses/UnprocessableEntity' + delete: + operationId: deleteMeEnterpriseConnection + deprecated: true + summary: Delete enterprise connection + description: | + **Deprecated.** Use `DELETE /v1/organizations/{organizationID}/enterprise_connections/{enterprise_connection_id}` instead. + + Deletes an enterprise connection the current user may manage (verified primary email domain must match the connection). + tags: + - Enterprise Connections + parameters: + - in: path + name: enterprise_connection_id + required: true + schema: + type: string + description: The ID of the enterprise connection to delete + - in: query + name: _clerk_session_id + schema: + type: string + description: The session_id associated with the requesting user. + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedDeletedObject' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '402': + $ref: '#/components/responses/PaymentRequired' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ResourceNotFound' + '422': + $ref: '#/components/responses/UnprocessableEntity' + /v1/me/enterprise_connections/{enterprise_connection_id}/test_runs: + get: + operationId: listMeEnterpriseConnectionTestRuns + deprecated: true + summary: List enterprise connection test runs + description: | + **Deprecated.** Use `GET /v1/organizations/{organizationID}/enterprise_connections/{enterprise_connection_id}/test_runs` instead. + + Returns a paginated list of SAML or OIDC debug test runs for the connection. + tags: + - Enterprise Connections + parameters: + - in: path + name: enterprise_connection_id + required: true + schema: + type: string + description: The ID of the enterprise connection + - in: query + name: _clerk_session_id + schema: + type: string + description: The session_id associated with the requesting user. + - in: query + name: status + required: false + schema: + type: array + items: + type: string + enum: + - pending + - success + - failed + style: form + explode: true + description: Filter test runs by status (may be repeated) + - $ref: '#/components/parameters/LimitParameter' + - $ref: '#/components/parameters/OffsetParameter' + - $ref: '#/components/parameters/Paginated' + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedEnterpriseConnectionTestRunsList' + '401': + $ref: '#/components/responses/ClerkErrors' + '402': + $ref: '#/components/responses/PaymentRequired' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ResourceNotFound' + '422': + $ref: '#/components/responses/UnprocessableEntity' + post: + operationId: createMeEnterpriseConnectionTestRun + deprecated: true + summary: Create enterprise connection test run + description: | + **Deprecated.** Use `POST /v1/organizations/{organizationID}/enterprise_connections/{enterprise_connection_id}/test_runs` instead. + + Returns a short-lived URL that starts the IdP test flow for this connection. + tags: + - Enterprise Connections + parameters: + - in: path + name: enterprise_connection_id + required: true + schema: + type: string + description: The ID of the enterprise connection + - in: query + name: _clerk_session_id + schema: + type: string + description: The session_id associated with the requesting user. + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedEnterpriseConnectionTestRun' + '401': + $ref: '#/components/responses/ClerkErrors' + '402': + $ref: '#/components/responses/PaymentRequired' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ResourceNotFound' + /v1/enterprise_connections/{enterpriseConnectionID}/init_test_run: + get: + operationId: initEnterpriseConnectionTestRun + security: [] + tags: + - Enterprise Connections + summary: Initiate enterprise connection test run + description: | + Initiates a test run for an enterprise connection. + Validates the provided ticket and redirects to the Identity Provider for authentication. + parameters: + - name: enterpriseConnectionID + in: path + required: true + schema: + type: string + description: The ID of the enterprise connection to test + - name: ticket + in: query + required: true + schema: + type: string + description: A ticket that authorizes and expires this test run request + responses: + '301': + description: Redirect to Identity Provider for authentication + '400': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ResourceNotFound' + '422': + $ref: '#/components/responses/UnprocessableEntity' + /v1/enterprise_connections/{enterpriseConnectionID}/test_runs/{testRunID}: + get: + operationId: getEnterpriseConnectionTestRunResult + security: [] + tags: + - Enterprise Connections + summary: Get enterprise connection test run result + description: | + Returns the result of an enterprise connection test run as an HTML page. + Displays success or error status with detailed information about the test run. + parameters: + - name: enterpriseConnectionID + in: path + required: true + schema: + type: string + description: The ID of the enterprise connection + - name: testRunID + in: path + required: true + schema: + type: string + description: The ID of the test run (SAML debug run or OAuth debug run) + - name: token + in: query + required: true + schema: + type: string + description: A token that authorizes access to view the test run results + responses: + '200': + description: HTML page displaying the test run result + content: + text/html: + schema: + type: string + '400': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ResourceNotFound' + /v1/me/email_addresses: + get: + operationId: getEmailAddresses + summary: Get Email Addresses + description: Retrieve all the email addresses associated with the current user. + tags: + - Email Addresses + parameters: + - in: query + name: _clerk_session_id + schema: + type: string + description: The session_id associated with the requesting user. + responses: + '200': + $ref: '#/components/responses/Server.UserEmailAddresses' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + post: + summary: Create Email Address + description: Add an email address to the current user. The address then needs to be verified using the `prepare_verification` and `attempt_verification` endpoints. + operationId: createEmailAddresses + tags: + - Email Addresses + parameters: + - in: query + name: _clerk_session_id + schema: + type: string + description: The session_id associated with the requesting user. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + email_address: + type: string + description: The email address to be added to the user. + required: + - email_address + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedEmailAddress' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/me/email_addresses/{email_id}/attempt_verification: + post: + operationId: verifyEmailAddress + description: Attempt to verify an email address that was previously created. + summary: Attempt Email Address Verification + tags: + - Email Addresses + parameters: + - in: path + required: true + name: email_id + schema: + type: string + description: The email_id. + - in: query + name: _clerk_session_id + schema: + type: string + description: The session_id associated with the requesting user. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + code: + type: string + description: The code that was previously sent to the email address. + required: + - code + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedEmailAddress' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/me/email_addresses/{email_id}/prepare_verification: + post: + description: |- + Depending on the given strategy, the API will prepare the verification for the email address. + In particular, * for `email_code`, the API will send a verification email to the address containing a code. * for `email_link`, the API will send a verification email to the address containing a link to the verification attempt endpoint. * for `enterprise_sso`, the API will send an external redirect URL to the browser containing a link to the SSO verification endpoint. + summary: Prepare Email Address Verification + tags: + - Email Addresses + operationId: sendVerificationEmail + parameters: + - in: path + name: email_id + required: true + schema: + type: string + description: The email_id. + - in: query + name: _clerk_session_id + schema: + type: string + description: The session_id associated with the requesting user. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + strategy: + type: string + description: The strategy to be prepared for email verification. + enum: + - email_code + - email_link + - enterprise_sso + redirect_url: + type: string + description: Used with the `email_link` strategy. + nullable: true + action_complete_redirect_url: + type: string + description: Used with `oauth_[provider]` and `saml` strategies. + nullable: true + required: + - strategy + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedEmailAddress' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + '429': + $ref: '#/components/responses/ClerkErrors' + /v1/me/email_addresses/{email_id}: + get: + summary: Get Email Address + description: Retrieve an email address by ID. + tags: + - Email Addresses + operationId: getEmailAddress + parameters: + - in: path + name: email_id + required: true + schema: + type: string + description: The email_id. + - in: query + name: _clerk_session_id + schema: + type: string + description: The session_id associated with the requesting user. + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedEmailAddress' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + delete: + summary: Delete email address + description: Delete an email address by ID. + tags: + - Email Addresses + operationId: DeleteEmailAddress + parameters: + - in: path + name: email_id + required: true + schema: + type: string + description: The email_id. + - in: query + name: _clerk_session_id + schema: + type: string + description: The session_id associated with the requesting user. + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedDeletedObject' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + /v1/me/phone_numbers: + get: + summary: Get Phone Numbers + description: Retrieve all phone numbers associated with the current user. + operationId: getPhoneNumbers + tags: + - Phone Numbers + parameters: + - in: query + name: _clerk_session_id + schema: + type: string + description: The session_id associated with the requesting user. + responses: + '200': + $ref: '#/components/responses/Server.UserPhoneNumbers' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + post: + summary: Create Phone Number + description: Add a phone number to the current user. The phone number then needs to be verified using the sms `prepare_verification` and `attempt_verification` endpoints. + tags: + - Phone Numbers + operationId: postPhoneNumbers + parameters: + - in: query + name: _clerk_session_id + schema: + type: string + description: The session_id associated with the requesting user. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + phone_number: + type: string + description: The phone number to be added to the user. + reserved_for_second_factor: + type: boolean + description: Whether the phone number is reserved for multi-factor authentication. + nullable: true + required: + - phone_number + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedPhoneNumber' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/me/phone_numbers/{phone_number_id}/attempt_verification: + post: + summary: Attempt Phone Number Verification + description: Attempt to verify the given phone number using the code sent during the `prepare_verification` step. + tags: + - Phone Numbers + operationId: verifyPhoneNumber + parameters: + - in: path + required: true + name: phone_number_id + schema: + type: string + description: The phone_number_id. + - in: query + name: _clerk_session_id + schema: + type: string + description: The session_id associated with the requesting user. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + code: + type: string + description: Strategy used to verify the phone number. + required: + - code + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedPhoneNumber' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + '429': + $ref: '#/components/responses/ClerkErrors' + /v1/me/phone_numbers/{phone_number_id}/prepare_verification: + post: + summary: Prepare Phone Number Verification + description: Sends an SMS to the phone number with the code needed to verify it. + tags: + - Phone Numbers + operationId: sendVerificationSMS + parameters: + - in: path + required: true + name: phone_number_id + schema: + type: string + description: The phone_number_id. + - in: query + name: _clerk_session_id + schema: + type: string + description: The session_id associated with the requesting user. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + strategy: + type: string + description: Strategy used to verify the phone number. + enum: + - phone_code + proof_token: + type: string + description: Proof token produced by the Clerk Protect SDK. + nullable: true + required: + - strategy + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedPhoneNumber' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '429': + $ref: '#/components/responses/ClerkErrors' + /v1/me/phone_numbers/{phone_number_id}: + get: + summary: Retrieve Phone Number + description: Retrieve all properties for the given phone number. + tags: + - Phone Numbers + operationId: ReadPhoneNumber + parameters: + - in: path + required: true + name: phone_number_id + schema: + type: string + description: The phone_number_id. + - in: query + name: _clerk_session_id + schema: + type: string + description: The session_id associated with the requesting user. + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedPhoneNumber' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + patch: + summary: Update Phone Number + description: Update the properties of the given phone number. + tags: + - Phone Numbers + operationId: UpdatePhoneNumber + parameters: + - in: path + required: true + name: phone_number_id + schema: + type: string + description: The phone_number_id. + - in: query + name: _clerk_session_id + schema: + type: string + description: The session_id associated with the requesting user. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + reserved_for_second_factor: + type: boolean + description: Whether the phone number is reserved for multi-factor authentication. + nullable: true + default_second_factor: + type: boolean + description: Marks the phone number as the default that will be used in multi-factor authentication. + nullable: true + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedPhoneNumber' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + delete: + summary: Delete Phone Number + description: Deletes the phone number with the given ID. + operationId: DeletePhoneNumber + tags: + - Phone Numbers + parameters: + - in: path + required: true + name: phone_number_id + schema: + type: string + description: The phone_number_id. + - in: query + name: _clerk_session_id + schema: + type: string + description: The session_id associated with the requesting user. + responses: + '200': + $ref: '#/components/responses/Client.DeletedPhoneNumber' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + /v1/me/web3_wallets: + get: + summary: Get All Web3 Wallets + description: Retrieve all web3 wallets associated with the current user. + operationId: getWeb3Wallets + tags: + - Web3 Wallets + parameters: + - in: query + name: _clerk_session_id + schema: + type: string + description: The session_id associated with the requesting user. + responses: + '200': + $ref: '#/components/responses/Server.UserWeb3Wallets' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + post: + summary: Create Web3 Wallet + description: Create a new web3 wallet. + operationId: postWeb3Wallets + tags: + - Web3 Wallets + parameters: + - in: query + name: _clerk_session_id + schema: + type: string + description: The session_id associated with the requesting user. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + web3_wallet: + type: string + description: The web3 wallet to be added to the user. + required: + - web3_wallet + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedWeb3Wallet' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/me/web3_wallets/{web3_wallet_id}: + get: + summary: Get Web3 Wallet + description: Retrieve all properties associated with a specific web3 wallet. + operationId: readWeb3Wallet + tags: + - Web3 Wallets + parameters: + - in: path + required: true + name: web3_wallet_id + schema: + type: string + description: The web3 wallet ID. + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedWeb3Wallet' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + delete: + summary: Delete Web3 Wallet + description: Delete a web3 wallet by ID. + operationId: deleteWeb3Wallet + tags: + - Web3 Wallets + parameters: + - in: path + required: true + name: web3_wallet_id + schema: + type: string + description: The web3 wallet ID. + responses: + '200': + $ref: '#/components/responses/Client.DeletedPhoneNumber' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + /v1/me/web3_wallets/{web3_wallet_id}/prepare_verification: + post: + summary: Prepare Web3 Wallet Verification + description: Prepares verification for the provided web3 wallet. + operationId: prepareWeb3WalletVerification + tags: + - Web3 Wallets + parameters: + - in: path + required: true + name: web3_wallet_id + description: The web3 wallet ID. + schema: + type: string + - in: header + name: Origin + description: The origin of the request. + schema: + type: string + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + strategy: + type: string + description: The strategy used to verify the web3 wallet. + pattern: ^web3_\w+_signature$ + redirect_url: + type: string + description: The redirect URL to redirect the user to after verification. + nullable: true + required: + - strategy + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedWeb3Wallet' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/me/web3_wallets/{web3_wallet_id}/attempt_verification: + post: + summary: Attempt Web3 Wallet Verification + description: Attempts to verify the provided web3 wallet. + operationId: attemptWeb3WalletVerification + tags: + - Web3 Wallets + parameters: + - in: path + required: true + name: web3_wallet_id + schema: + type: string + description: The web3 wallet ID. + - in: header + name: Origin + description: The origin of the request. + schema: + type: string + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + signature: + type: string + description: The signature that was generated from your Web3 Wallet to sign the message + required: + - signature + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedWeb3Wallet' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/me/passkeys: + post: + summary: Create Passkey + description: Create a new passkey. + operationId: postPasskey + tags: + - Passkeys + parameters: + - in: query + name: _clerk_session_id + description: The session_id associated with the requesting user. + schema: + type: string + - in: header + name: Origin + description: The origin of the request. + schema: + type: string + - in: header + name: X-Original-Host + description: The original host of the request. + schema: + type: string + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedPasskey' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + /v1/me/passkeys/{passkey_id}: + get: + summary: Get Passkey + description: Retrieve all properties associated with a specific passkey. + operationId: readPasskey + tags: + - Passkeys + parameters: + - in: path + required: true + name: passkey_id + schema: + type: string + description: The passkey identification ID. + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedPasskey' + '401': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + patch: + summary: Update Passkey + description: Update properties of a specific passkey. + operationId: patchPasskey + tags: + - Passkeys + parameters: + - in: path + required: true + name: passkey_id + schema: + type: string + description: The passkey identification ID. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + name: + type: string + nullable: true + description: The name of the passkey. + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedPasskey' + '401': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + delete: + summary: Delete Passkey + description: Delete a passkey by ID. + operationId: deletePasskey + tags: + - Passkeys + parameters: + - in: path + required: true + name: passkey_id + schema: + type: string + description: The passkey identification ID. + responses: + '200': + $ref: '#/components/responses/Client.DeletedPasskey' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + /v1/me/passkeys/{passkey_id}/attempt_verification: + post: + summary: Attempt Passkey Verification + description: Attempts to verify the provided passkey. + operationId: attemptPasskeyVerification + tags: + - Passkeys + parameters: + - in: path + required: true + name: passkey_id + schema: + type: string + description: The passkey identification ID. + - in: header + name: Origin + description: The origin of the request. + schema: + type: string + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + strategy: + type: string + description: The strategy used to connect the external account. + nullable: true + enum: + - passkey + public_key_credential: + type: string + description: The public key credential of the passkey. + nullable: true + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedPasskey' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/me/trusted_devices: + get: + deprecated: true + summary: List Trusted Devices + description: Retrieve active trusted-device credentials associated with the current user. + operationId: listTrustedDevices + tags: + - Trusted Devices + responses: + '200': + description: Returns the current user's trusted-device credentials. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedTrustedDevices' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + /v1/me/trusted_devices/prepare: + post: + deprecated: true + summary: Prepare Trusted Device Enrollment + description: Issues a one-time challenge for enrolling a native trusted-device credential. + operationId: prepareTrustedDevice + tags: + - Trusted Devices + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + platform: + type: string + enum: + - ios + - android + app_identifier: + type: string + description: Bundle identifier or package name for the native application. + name: + type: string + nullable: true + description: Optional user-facing device name. + algorithm: + type: string + enum: + - ES256 + public_key_jwk: + type: string + description: Canonical public JWK for the device-bound P-256 key. + required: + - platform + - app_identifier + - algorithm + - public_key_jwk + responses: + '200': + description: Returns the enrollment challenge that must be signed by the native private key. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedTrustedDeviceChallenge' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/me/trusted_devices/attempt: + post: + deprecated: true + summary: Attempt Trusted Device Enrollment + description: Verifies a signed enrollment challenge and creates a trusted-device credential. + operationId: attemptTrustedDevice + tags: + - Trusted Devices + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + platform: + type: string + enum: + - ios + - android + app_identifier: + type: string + description: Bundle identifier or package name for the native application. + name: + type: string + nullable: true + description: Optional user-facing device name. + algorithm: + type: string + enum: + - ES256 + public_key_jwk: + type: string + description: Canonical public JWK for the device-bound P-256 key. + client_data: + type: string + description: Canonical JSON challenge payload returned from prepare. + signature: + type: string + description: Base64url raw ECDSA P-256 signature over `client_data`. + required: + - platform + - app_identifier + - algorithm + - public_key_jwk + - client_data + - signature + responses: + '200': + description: Returns the created trusted-device credential. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedTrustedDevice' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/me/trusted_devices/{trusted_device_id}: + delete: + deprecated: true + summary: Revoke Trusted Device + description: Revokes a trusted-device credential so it can no longer authenticate. + operationId: revokeTrustedDevice + tags: + - Trusted Devices + parameters: + - in: path + required: true + name: trusted_device_id + schema: + type: string + description: The trusted-device credential ID. + responses: + '200': + description: Returns the revoked trusted-device credential. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedTrustedDevice' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + /v1/me/biometric_credentials: + get: + summary: List Biometric Credentials + description: Retrieve active biometric credentials associated with the current user. + operationId: listBiometricCredentials + tags: + - Biometric Credentials + responses: + '200': + description: Returns the current user's biometric credentials. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedBiometricCredentials' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + /v1/me/biometric_credentials/prepare: + post: + summary: Prepare Biometric Credential Enrollment + description: Issues a one-time challenge for enrolling a native biometric credential. + operationId: prepareBiometricCredential + tags: + - Biometric Credentials + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + platform: + type: string + enum: + - ios + - android + app_identifier: + type: string + description: Bundle identifier or package name for the native application. + name: + type: string + nullable: true + description: Optional user-facing credential name. + algorithm: + type: string + enum: + - ES256 + public_key_jwk: + type: string + description: Canonical public JWK for the device-bound P-256 key. + required: + - platform + - app_identifier + - algorithm + - public_key_jwk + responses: + '200': + description: Returns the enrollment challenge that must be signed by the native private key. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedBiometricCredentialChallenge' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/me/biometric_credentials/attempt: + post: + summary: Attempt Biometric Credential Enrollment + description: Verifies a signed enrollment challenge and creates a biometric credential. + operationId: attemptBiometricCredential + tags: + - Biometric Credentials + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + platform: + type: string + enum: + - ios + - android + app_identifier: + type: string + description: Bundle identifier or package name for the native application. + name: + type: string + nullable: true + description: Optional user-facing credential name. + algorithm: + type: string + enum: + - ES256 + public_key_jwk: + type: string + description: Canonical public JWK for the device-bound P-256 key. + client_data: + type: string + description: Canonical JSON challenge payload returned from prepare. + signature: + type: string + description: Base64url raw ECDSA P-256 signature over `client_data`. + required: + - platform + - app_identifier + - algorithm + - public_key_jwk + - client_data + - signature + responses: + '200': + description: Returns the created biometric credential. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedBiometricCredential' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/me/biometric_credentials/{biometric_credential_id}: + delete: + summary: Revoke Biometric Credential + description: Revokes a biometric credential so it can no longer authenticate. + operationId: revokeBiometricCredential + tags: + - Biometric Credentials + parameters: + - in: path + required: true + name: biometric_credential_id + schema: + type: string + description: The biometric credential ID. + responses: + '200': + description: Returns the revoked biometric credential. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedBiometricCredential' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + /v1/me/external_accounts: + post: + summary: Connect OAuth Accounts + description: Connect a new External Account from the OAuth providers enabled. + tags: + - External Accounts + operationId: postOAuthAccounts + parameters: + - in: header + name: Origin + description: The origin of the request. + schema: + type: string + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + strategy: + type: string + description: |- + The OAuth strategy that the external account provider supports. Optional when `enterprise_connection_id` is provided. + Can be one of `oauth_[provider]` or `oauth_token_[provider]`. + The `oauth_[provider]` strategy can be used for regular OAuth flows with redirects and a `redirect_url` parameter is also required. + The `oauth_token_[provider]` strategy can be used for native flows, along with a `token` or `code` parameter. + pattern: ^oauth_(?:(?:token_)|(?:custom_))?[a-z0-9_]+$ + enterprise_connection_id: + type: string + description: The ID of an enterprise connection to link. When provided, strategy is not required. + nullable: true + redirect_url: + type: string + nullable: true + action_complete_redirect_url: + type: string + nullable: true + additional_scope: + type: string + nullable: true + code: + type: string + description: The authorization or grant code that an OAuth provider returns during authentication. Can be used with `oauth_token_[provider]` strategies. + nullable: true + token: + type: string + description: The ID token that an OpenID Connect provider returns during authentication. Can be used with `oauth_token_[provider]` strategies. + nullable: true + oidc_login_hint: + type: string + description: Used with `oauth_[provider]`. The given value will be forwarded to the OIDC `login_hint` parameter of the generated redirect URL. + nullable: true + oidc_prompt: + type: string + description: Used with `oauth_[provider]` or `enterprise_sso`. The given value will be forwarded to the OIDC `prompt` parameter of the generated redirect URL. When using shared credentials this value might be adjusted for security reasons. + nullable: true + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedExternalAccount' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + '500': + $ref: '#/components/responses/ClerkErrors' + /v1/me/external_accounts/{external_account_id}/reauthorize: + patch: + summary: Reauthorize External Account + operationId: reauthorizeExternalAccount + description: Reauthorize an external account by ID. + tags: + - External Accounts + parameters: + - name: external_account_id + in: path + description: External account ID + required: true + schema: + type: string + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + additional_scope: + type: array + items: + type: string + nullable: true + redirect_url: + type: string + action_complete_redirect_url: + type: string + nullable: true + oidc_login_hint: + type: string + description: Used with `oauth_[provider]`. The given value will be forwarded to the OIDC `login_hint` parameter of the generated redirect URL. + nullable: true + oidc_prompt: + type: string + description: Used with `oauth_[provider]` or `enterprise_sso`. The given value will be forwarded to the OIDC `prompt` parameter of the generated redirect URL. When using shared credentials this value might be adjusted for security reasons. + nullable: true + required: + - redirect_url + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedExternalAccount' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/me/external_accounts/{external_account_id}: + delete: + summary: Delete External Account + operationId: deleteExternalAccount + description: Delete an external account by ID. + tags: + - External Accounts + parameters: + - name: external_account_id + in: path + description: External account ID + required: true + schema: + type: string + responses: + '200': + $ref: '#/components/responses/Client.DeletedExternalAccount' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + /v1/me/external_accounts/{external_account_id}/tokens: + delete: + summary: Revoke OAuth tokens + operationId: revokeExternalAccountTokens + description: Revoke the OAuth access and refresh token of an external account by ID, if supported by the provider. + tags: + - External Accounts + parameters: + - name: external_account_id + in: path + description: External account ID + required: true + schema: + type: string + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedUser' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + /v1/me/totp: + post: + summary: Create TOTP + description: Initializes TOTP by generating a new secret. This then needs to be added to an authenticator app and verified. A user can have only one TOTP secret and this endpoint will return an error if it is already present. + tags: + - TOTP + operationId: postTOTP + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedTOTP' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '500': + $ref: '#/components/responses/ClerkErrors' + delete: + summary: Delete TOTP + description: Deletes the current TOTP secret. This will invalidate all authenticators that where previously added. + tags: + - TOTP + operationId: deleteTOTP + responses: + '200': + $ref: '#/components/responses/Client.DeletedTOTP' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '500': + $ref: '#/components/responses/ClerkErrors' + /v1/me/totp/attempt_verification: + post: + summary: Attempt TOTP Verification + description: Attempts to verify the TOTP secret previously created using a code from an authenticator. + tags: + - TOTP + operationId: verifyTOTP + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + code: + type: string + nullable: false + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedTOTP' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + '500': + $ref: '#/components/responses/ClerkErrors' + /v1/me/backup_codes: + post: + summary: Create Backup Codes + description: |- + Create two factor authentication backup codes for the current user. + A two factor authentication method must be enabled for the environment. Otherwise the endpoint will return an error. + tags: + - Backup Codes + operationId: createBackupCodes + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedBackupCodes' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '500': + $ref: '#/components/responses/ClerkErrors' + /v1/me: + get: + summary: Get User + description: Returns all attributes of the current user. + tags: + - User + operationId: getUser + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedUser' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + patch: + summary: Update User + description: |- + Update the current user with the given attributes. + + `unsafe_metadata` is not accepted on this endpoint as of API version 2026-05-12. + Use `PATCH /v1/me/metadata` to update `unsafe_metadata`. + tags: + - User + operationId: patchUser + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + username: + nullable: true + type: string + first_name: + nullable: true + type: string + last_name: + nullable: true + type: string + primary_email_address_id: + type: string + nullable: true + primary_phone_number_id: + nullable: true + type: string + primary_web3_wallet_id: + nullable: true + type: string + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedUser' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + delete: + summary: Delete User + description: Delete the current user. + tags: + - User + operationId: deleteUser + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedDeletedObject' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '500': + $ref: '#/components/responses/ClerkErrors' + /v1/me/metadata: + patch: + summary: Merge and update the current user's metadata + description: |- + Update the current user's metadata attributes by merging existing values with the provided parameters. + + This endpoint behaves differently than the *Update User* endpoint. + Metadata values will not be replaced entirely. + Instead, a deep merge will be performed. + Deep means that any nested JSON objects will be merged as well. + + You can remove metadata keys at any level by setting their value to `null`. + + Only `unsafe_metadata` can be updated through the Frontend API. `public_metadata` and `private_metadata` must be modified through the Backend API. + tags: + - User + operationId: patchUserMetadata + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - unsafe_metadata + properties: + unsafe_metadata: + type: string + description: |- + A stringified JSON object containing the unsafe metadata patch to merge into the current user's `unsafe_metadata`. + Existing keys are deep-merged; keys at any level with `null` values are removed. + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedUser' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/me/tokens: + post: + description: Create a JWT for the requested user. + operationId: createServiceToken + tags: + - User + parameters: + - in: query + name: _clerk_session_id + description: The session_id associated with the requesting user. + schema: + type: string + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + service: + type: string + required: + - service + responses: + '200': + $ref: '#/components/responses/Token' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/me/profile_image: + post: + summary: Update Profile Image + description: Update the current user's profile image. + tags: + - User + operationId: updateProfileImage + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + application/octet-stream: + schema: + format: base64 + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedImage' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + delete: + summary: Delete Profile Image + description: Remove the current user's profile image. + operationId: deleteProfileImage + tags: + - User + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedDeletedObject' + '401': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + /v1/me/change_password: + post: + summary: Update Password + description: Update the current user's password. + operationId: changePassword + tags: + - User + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + current_password: + type: string + nullable: true + new_password: + type: string + sign_out_of_other_sessions: + type: boolean + nullable: true + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedUser-2' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/me/remove_password: + post: + summary: Delete Password + description: Removes the current user's password. + operationId: removePassword + tags: + - User + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + current_password: + type: string + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedUser-2' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/me/billing/subscription: + get: + operationId: GetUserMainBillingSubscription + x-speakeasy-group: billing + x-speakeasy-name-override: getUserMainSubscription + tags: + - Subscriptions + summary: Get user's main billing subscription + description: Returns the main billing subscription for the current user. + responses: + '200': + $ref: '#/components/responses/Client.BillingSubscription' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/AuthenticationInvalid' + '404': + $ref: '#/components/responses/ResourceNotFound' + /v1/me/billing/subscription_items: + get: + operationId: GetUserBillingSubscriptionItems + x-speakeasy-group: billing + x-speakeasy-name-override: listUserSubscriptionItems + tags: + - Subscription Items + summary: List user's subscription items + description: Returns a list of subscription items for the current user. + parameters: + - $ref: '#/components/parameters/LimitParameter' + - $ref: '#/components/parameters/OffsetParameter' + - $ref: '#/components/parameters/Paginated' + responses: + '200': + $ref: '#/components/responses/Client.BillingSubscriptionItemList' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/AuthenticationInvalid' + /v1/me/billing/subscription_items/{subscriptionItemID}: + delete: + operationId: DeleteUserBillingSubscriptionItem + x-speakeasy-group: billing + x-speakeasy-name-override: cancelUserSubscriptionItem + tags: + - Subscription Items + summary: Cancel user's subscription item + description: Cancels a subscription item for the current user. + parameters: + - name: subscriptionItemID + in: path + description: The ID of the subscription item to cancel + required: true + schema: + type: string + responses: + '200': + $ref: '#/components/responses/Client.BillingSubscriptionItem' + '204': + description: Subscription item canceled successfully + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/AuthenticationInvalid' + '403': + $ref: '#/components/responses/AuthorizationInvalid' + '404': + $ref: '#/components/responses/ResourceNotFound' + '409': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/UnprocessableEntity' + '500': + $ref: '#/components/responses/ClerkErrors' + /v1/me/billing/payment_methods: + get: + operationId: GetUserPaymentMethods + x-speakeasy-group: billing + x-speakeasy-name-override: listUserPaymentMethods + tags: + - Payment Methods + summary: List user payment methods + description: Returns a list of all payment methods for the current user. + parameters: + - $ref: '#/components/parameters/LimitParameter' + - $ref: '#/components/parameters/OffsetParameter' + - $ref: '#/components/parameters/Paginated' + responses: + '200': + $ref: '#/components/responses/Client.BillingPaymentMethodList' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/AuthenticationInvalid' + '403': + $ref: '#/components/responses/AuthorizationInvalid' + '404': + $ref: '#/components/responses/ResourceNotFound' + post: + operationId: CreateUserPaymentMethod + x-speakeasy-group: billing + x-speakeasy-name-override: createUserPaymentMethod + tags: + - Payment Methods + summary: Create user payment method + description: Creates a new payment method for the current user. + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + gateway: + type: string + description: Payment gateway to use + payment_token: + type: string + nullable: true + description: Payment token for the method + org_id: + type: string + nullable: true + description: Organization ID (optional) + required: + - gateway + - payment_token + responses: + '200': + $ref: '#/components/responses/Client.BillingPaymentMethod' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/AuthenticationInvalid' + '422': + $ref: '#/components/responses/UnprocessableEntity' + /v1/me/billing/payment_methods/initialize: + post: + operationId: InitializeUserPaymentMethod + x-speakeasy-group: billing + x-speakeasy-name-override: initializeUserPaymentMethod + tags: + - Payment Methods + summary: Initialize user payment method + description: Initializes a payment method setup process for the current user. + responses: + '200': + $ref: '#/components/responses/Client.BillingPaymentMethodInitialize' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/AuthenticationInvalid' + '403': + $ref: '#/components/responses/AuthorizationInvalid' + '404': + $ref: '#/components/responses/ResourceNotFound' + '422': + $ref: '#/components/responses/UnprocessableEntity' + /v1/me/billing/payment_methods/{paymentMethodID}: + delete: + operationId: DeleteUserPaymentMethod + x-speakeasy-group: billing + x-speakeasy-name-override: deleteUserPaymentMethod + tags: + - Payment Methods + summary: Delete user payment method + description: Deletes a specific payment method for the current user. + parameters: + - name: paymentMethodID + in: path + description: The ID of the payment method to delete + required: true + schema: + type: string + responses: + '200': + $ref: '#/components/responses/Client.BillingPaymentMethod' + '204': + description: Payment method deleted successfully + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/AuthenticationInvalid' + '403': + $ref: '#/components/responses/AuthorizationInvalid' + '404': + $ref: '#/components/responses/ResourceNotFound' + '409': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/UnprocessableEntity' + '500': + $ref: '#/components/responses/ClerkErrors' + /v1/me/billing/checkouts: + post: + operationId: CreateUserBillingCheckout + x-speakeasy-group: billing + x-speakeasy-name-override: createUserCheckout + tags: + - Checkouts + summary: Create billing checkout + description: Creates a new billing checkout for the current user. + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + plan_id: + type: string + description: The ID of the plan to checkout + plan_period: + type: string + enum: + - month + - annual + description: The billing period for the plan + default: month + org_id: + type: string + nullable: true + description: Organization ID (if creating org checkout) + payment_method_id: + type: string + nullable: true + description: Payment method ID to use + required: + - plan_id + - plan_period + responses: + '200': + $ref: '#/components/responses/Client.BillingCheckout' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/AuthenticationInvalid' + '403': + $ref: '#/components/responses/ClerkErrors' + '409': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/UnprocessableEntity' + /v1/me/billing/checkouts/{checkoutID}: + get: + operationId: GetUserBillingCheckout + x-speakeasy-group: billing + x-speakeasy-name-override: getUserCheckout + tags: + - Checkouts + summary: Get billing checkout + description: Returns a billing checkout for the current user. + parameters: + - name: checkoutID + in: path + description: The ID of the checkout + required: true + schema: + type: string + responses: + '200': + $ref: '#/components/responses/Client.BillingCheckout' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/AuthenticationInvalid' + '404': + $ref: '#/components/responses/ResourceNotFound' + '410': + $ref: '#/components/responses/ClerkErrors' + patch: + operationId: UpdateUserBillingCheckout + x-speakeasy-group: billing + x-speakeasy-name-override: updateUserCheckout + tags: + - Checkouts + summary: Update billing checkout + description: |- + Applies or removes a promo code on an in-progress billing checkout for the current user. + A non-blank promo_code applies the code; a blank value clears it. + parameters: + - name: checkoutID + in: path + description: The ID of the checkout to update + required: true + schema: + type: string + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - promo_code + properties: + promo_code: + type: string + nullable: true + description: |- + The promo code to apply to this checkout. + Send a blank value to clear the currently applied promo code. + responses: + '200': + $ref: '#/components/responses/Client.BillingCheckout' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/AuthenticationInvalid' + '404': + $ref: '#/components/responses/ResourceNotFound' + '409': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/UnprocessableEntity' + /v1/me/billing/checkouts/{checkoutID}/confirm: + patch: + operationId: ConfirmUserBillingCheckout + x-speakeasy-group: billing + x-speakeasy-name-override: confirmUserCheckout + tags: + - Checkouts + summary: Confirm billing checkout + description: Confirms a billing checkout for the current user. + parameters: + - name: checkoutID + in: path + description: The ID of the checkout to confirm + required: true + schema: + type: string + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + payment_method_id: + type: string + nullable: true + description: The ID of the payment method to use for this checkout + gateway: + type: string + nullable: true + description: The payment gateway to use (e.g., stripe) + payment_token: + type: string + nullable: true + description: A payment token for processing the payment + use_test_card: + type: boolean + nullable: true + description: Whether to use a test card for payment processing + responses: + '200': + $ref: '#/components/responses/Client.BillingCheckout' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/AuthenticationInvalid' + '404': + $ref: '#/components/responses/ResourceNotFound' + '409': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/UnprocessableEntity' + /v1/me/billing/payment_attempts: + get: + operationId: GetUserPaymentAttempts + x-speakeasy-group: billing + x-speakeasy-name-override: listUserPaymentAttempts + tags: + - Payment Attempts + summary: List user payment attempts + description: Returns a list of payment attempts for the current user. + parameters: + - $ref: '#/components/parameters/LimitParameter' + - $ref: '#/components/parameters/OffsetParameter' + - $ref: '#/components/parameters/Paginated' + responses: + '200': + $ref: '#/components/responses/Client.BillingPaymentAttemptList' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/AuthenticationInvalid' + '403': + $ref: '#/components/responses/AuthorizationInvalid' + /v1/me/billing/payment_attempts/{paymentAttemptID}: + get: + operationId: GetUserPaymentAttempt + x-speakeasy-group: billing + x-speakeasy-name-override: getUserPaymentAttempt + tags: + - Payment Attempts + summary: Get user payment attempt + description: Returns details of a specific payment attempt for the current user. + parameters: + - name: paymentAttemptID + in: path + description: The ID of the payment attempt to retrieve + required: true + schema: + type: string + responses: + '200': + $ref: '#/components/responses/Client.BillingPaymentAttempt' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/AuthenticationInvalid' + '403': + $ref: '#/components/responses/AuthorizationInvalid' + '404': + $ref: '#/components/responses/ResourceNotFound' + /v1/me/billing/payers/default_payment_method: + put: + operationId: SetUserDefaultPaymentMethod + x-speakeasy-group: billing + x-speakeasy-name-override: setUserDefaultPaymentMethod + tags: + - Payment Methods + summary: Set user default payment method + description: Sets the default payment method for the current user. + responses: + '200': + $ref: '#/components/responses/Client.BillingPaymentMethod' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/AuthenticationInvalid' + '403': + $ref: '#/components/responses/AuthorizationInvalid' + '422': + $ref: '#/components/responses/UnprocessableEntity' + /v1/me/billing/statements: + get: + operationId: GetUserStatements + x-speakeasy-group: billing + x-speakeasy-name-override: listUserStatements + tags: + - Statements + summary: List user statements + description: Returns a list of billing statements for the current user. + parameters: + - $ref: '#/components/parameters/LimitParameter' + - $ref: '#/components/parameters/OffsetParameter' + - $ref: '#/components/parameters/Paginated' + responses: + '200': + $ref: '#/components/responses/Client.BillingStatementList' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/AuthenticationInvalid' + '403': + $ref: '#/components/responses/AuthorizationInvalid' + /v1/me/billing/statements/{statementID}: + get: + operationId: GetUserStatement + x-speakeasy-group: billing + x-speakeasy-name-override: getUserStatement + tags: + - Statements + summary: Get user statement + description: Returns details of a specific billing statement for the current user. + parameters: + - name: statementID + in: path + description: The ID of the statement to retrieve + required: true + schema: + type: string + responses: + '200': + $ref: '#/components/responses/Client.BillingStatement' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/AuthenticationInvalid' + '403': + $ref: '#/components/responses/AuthorizationInvalid' + '404': + $ref: '#/components/responses/ResourceNotFound' + /v1/organizations/{organizationID}/billing/checkouts: + post: + operationId: CreateOrganizationBillingCheckout + x-speakeasy-group: billing + x-speakeasy-name-override: createOrganizationCheckout + tags: + - Checkouts + summary: Create organization billing checkout + description: Creates a new billing checkout for the organization. + parameters: + - name: organizationID + in: path + description: The ID of the organization + required: true + schema: + type: string + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + plan_id: + type: string + description: The ID of the plan to checkout + plan_period: + type: string + enum: + - month + - annual + description: The billing period for the plan + default: month + payment_method_id: + type: string + nullable: true + description: Payment method ID to use + required: + - plan_id + - plan_period + responses: + '200': + $ref: '#/components/responses/Client.BillingCheckout' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/AuthenticationInvalid' + '403': + $ref: '#/components/responses/AuthorizationInvalid' + '409': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/UnprocessableEntity' + /v1/organizations/{organizationID}/billing/checkouts/{checkoutID}: + get: + operationId: GetOrganizationBillingCheckout + x-speakeasy-group: billing + x-speakeasy-name-override: getOrganizationCheckout + tags: + - Checkouts + summary: Get organization billing checkout + description: Returns a billing checkout for the organization. + parameters: + - name: organizationID + in: path + description: The ID of the organization + required: true + schema: + type: string + - name: checkoutID + in: path + description: The ID of the checkout + required: true + schema: + type: string + responses: + '200': + $ref: '#/components/responses/Client.BillingCheckout' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/AuthenticationInvalid' + '403': + $ref: '#/components/responses/AuthorizationInvalid' + '404': + $ref: '#/components/responses/ResourceNotFound' + '410': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/UnprocessableEntity' + patch: + operationId: UpdateOrganizationBillingCheckout + x-speakeasy-group: billing + x-speakeasy-name-override: updateOrganizationCheckout + tags: + - Checkouts + summary: Update organization billing checkout + description: |- + Applies or removes a promo code on an in-progress billing checkout for the organization. + A non-blank promo_code applies the code; a blank value clears it. + parameters: + - name: organizationID + in: path + description: The ID of the organization + required: true + schema: + type: string + - name: checkoutID + in: path + description: The ID of the checkout to update + required: true + schema: + type: string + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - promo_code + properties: + promo_code: + type: string + nullable: true + description: |- + The promo code to apply to this checkout. + Send a blank value to clear the currently applied promo code. + responses: + '200': + $ref: '#/components/responses/Client.BillingCheckout' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/AuthenticationInvalid' + '403': + $ref: '#/components/responses/AuthorizationInvalid' + '404': + $ref: '#/components/responses/ResourceNotFound' + '409': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/UnprocessableEntity' + /v1/organizations/{organizationID}/billing/checkouts/{checkoutID}/confirm: + patch: + operationId: ConfirmOrganizationBillingCheckout + x-speakeasy-group: billing + x-speakeasy-name-override: confirmOrganizationCheckout + tags: + - Checkouts + summary: Confirm organization billing checkout + description: Confirms a billing checkout for the organization. + parameters: + - name: organizationID + in: path + description: The ID of the organization + required: true + schema: + type: string + - name: checkoutID + in: path + description: The ID of the checkout to confirm + required: true + schema: + type: string + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + payment_method_id: + type: string + nullable: true + description: The ID of the payment method to use for this checkout + gateway: + type: string + nullable: true + description: The payment gateway to use (e.g., stripe) + payment_token: + type: string + nullable: true + description: A payment token for processing the payment + use_test_card: + type: boolean + nullable: true + description: Whether to use a test card for payment processing + responses: + '200': + $ref: '#/components/responses/Client.BillingCheckout' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/AuthenticationInvalid' + '403': + $ref: '#/components/responses/AuthorizationInvalid' + '404': + $ref: '#/components/responses/ResourceNotFound' + '422': + $ref: '#/components/responses/UnprocessableEntity' + /v1/organizations/{organizationID}/billing/payment_methods: + get: + operationId: GetOrganizationPaymentMethods + x-speakeasy-group: billing + x-speakeasy-name-override: getOrganizationPaymentMethods + tags: + - Payment Methods + summary: List organization payment methods + description: Lists all payment methods for the organization. + parameters: + - name: organizationID + in: path + description: The ID of the organization + required: true + schema: + type: string + responses: + '200': + $ref: '#/components/responses/Client.BillingPaymentMethodList' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/AuthenticationInvalid' + '403': + $ref: '#/components/responses/AuthorizationInvalid' + '404': + $ref: '#/components/responses/ResourceNotFound' + post: + operationId: CreateOrganizationPaymentMethod + x-speakeasy-group: billing + x-speakeasy-name-override: createOrganizationPaymentMethod + tags: + - Payment Methods + summary: Create organization payment method + description: Creates a new payment method for the organization. + parameters: + - name: organizationID + in: path + description: The ID of the organization + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + additionalProperties: false + properties: + payment_token: + type: string + description: The payment token for the payment method + gateway: + type: string + description: The payment gateway to use + enum: + - stripe + required: + - payment_token + - gateway + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + payment_token: + type: string + description: The payment token for the payment method + gateway: + type: string + description: The payment gateway to use + enum: + - stripe + required: + - payment_token + - gateway + responses: + '200': + $ref: '#/components/responses/Client.BillingGenericObject' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/AuthenticationInvalid' + '403': + $ref: '#/components/responses/AuthorizationInvalid' + '404': + $ref: '#/components/responses/ResourceNotFound' + '422': + $ref: '#/components/responses/UnprocessableEntity' + /v1/organizations/{organizationID}/billing/payment_methods/initialize: + post: + operationId: InitializeOrganizationPaymentMethod + x-speakeasy-group: billing + x-speakeasy-name-override: initializeOrganizationPaymentMethod + tags: + - Payment Methods + summary: Initialize organization payment method + description: Initializes a payment method for the organization. + parameters: + - name: organizationID + in: path + description: The ID of the organization + required: true + schema: + type: string + responses: + '200': + $ref: '#/components/responses/Client.BillingPaymentMethodInitialize' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/AuthenticationInvalid' + '403': + $ref: '#/components/responses/AuthorizationInvalid' + '404': + $ref: '#/components/responses/ResourceNotFound' + '422': + $ref: '#/components/responses/UnprocessableEntity' + /v1/organizations/{organizationID}/billing/payment_methods/{paymentMethodID}: + delete: + operationId: DeleteOrganizationPaymentMethod + x-speakeasy-group: billing + x-speakeasy-name-override: deleteOrganizationPaymentMethod + tags: + - Payment Methods + summary: Delete organization payment method + description: Deletes a payment method for the organization. + parameters: + - name: organizationID + in: path + description: The ID of the organization + required: true + schema: + type: string + - name: paymentMethodID + in: path + description: The ID of the payment method to delete + required: true + schema: + type: string + responses: + '200': + $ref: '#/components/responses/Client.BillingGenericObject' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/AuthenticationInvalid' + '403': + $ref: '#/components/responses/AuthorizationInvalid' + '404': + $ref: '#/components/responses/ResourceNotFound' + /v1/organizations/{organizationID}/billing/statements: + get: + operationId: GetOrganizationStatements + x-speakeasy-group: billing + x-speakeasy-name-override: getOrganizationStatements + tags: + - Statements + summary: List organization statements + description: Lists all statements for the organization. + parameters: + - name: organizationID + in: path + description: The ID of the organization + required: true + schema: + type: string + responses: + '200': + $ref: '#/components/responses/Client.BillingGenericArray' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/AuthenticationInvalid' + '403': + $ref: '#/components/responses/AuthorizationInvalid' + '404': + $ref: '#/components/responses/ResourceNotFound' + /v1/organizations/{organizationID}/billing/statements/{statementID}: + get: + operationId: GetOrganizationStatement + x-speakeasy-group: billing + x-speakeasy-name-override: getOrganizationStatement + tags: + - Statements + summary: Get organization statement + description: Gets a specific statement for the organization. + parameters: + - name: organizationID + in: path + description: The ID of the organization + required: true + schema: + type: string + - name: statementID + in: path + description: The ID of the statement to retrieve + required: true + schema: + type: string + responses: + '200': + $ref: '#/components/responses/Client.BillingGenericObject' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/AuthenticationInvalid' + '403': + $ref: '#/components/responses/AuthorizationInvalid' + '404': + $ref: '#/components/responses/ResourceNotFound' + /v1/organizations/{organizationID}/billing/payers/default_payment_method: + put: + operationId: SetOrganizationDefaultPaymentMethod + x-speakeasy-group: billing + x-speakeasy-name-override: setOrganizationDefaultPaymentMethod + tags: + - Payment Methods + summary: Set organization default payment method + description: Sets the default payment method for the organization. + parameters: + - name: organizationID + in: path + description: The ID of the organization + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + additionalProperties: false + properties: + payment_method_id: + type: string + description: The ID of the payment method to set as default + required: + - payment_method_id + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + payment_method_id: + type: string + description: The ID of the payment method to set as default + required: + - payment_method_id + responses: + '200': + $ref: '#/components/responses/Client.BillingGenericObject' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/AuthenticationInvalid' + '403': + $ref: '#/components/responses/AuthorizationInvalid' + '404': + $ref: '#/components/responses/ResourceNotFound' + '422': + $ref: '#/components/responses/UnprocessableEntity' + /v1/organizations/{organizationID}/billing/subscription: + get: + operationId: GetOrganizationBillingSubscription + x-speakeasy-group: billing + x-speakeasy-name-override: getOrganizationSubscription + tags: + - Subscriptions + summary: Get organization's billing subscription + description: Returns the billing subscription for the organization. + parameters: + - name: organizationID + in: path + description: The ID of the organization + required: true + schema: + type: string + responses: + '200': + $ref: '#/components/responses/Client.BillingSubscription' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/AuthenticationInvalid' + '403': + $ref: '#/components/responses/AuthorizationInvalid' + '404': + $ref: '#/components/responses/ResourceNotFound' + /v1/organizations/{organizationID}/billing/subscription_items: + get: + operationId: GetOrganizationBillingSubscriptionItems + x-speakeasy-group: billing + x-speakeasy-name-override: listOrganizationSubscriptionItems + tags: + - Subscription Items + summary: List organization's subscription items + description: Returns a list of subscription items for the organization. + parameters: + - name: organizationID + in: path + description: The ID of the organization + required: true + schema: + type: string + responses: + '200': + $ref: '#/components/responses/Client.BillingSubscriptionItemList' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/AuthenticationInvalid' + '403': + $ref: '#/components/responses/AuthorizationInvalid' + '404': + $ref: '#/components/responses/ResourceNotFound' + /v1/organizations/{organizationID}/billing/subscription_items/{subscriptionItemID}: + delete: + operationId: DeleteOrganizationBillingSubscriptionItem + x-speakeasy-group: billing + x-speakeasy-name-override: cancelOrganizationSubscriptionItem + tags: + - Subscription Items + summary: Cancel organization's subscription item + description: Cancels a subscription item for the organization. + parameters: + - name: organizationID + in: path + description: The ID of the organization + required: true + schema: + type: string + - name: subscriptionItemID + in: path + description: The ID of the subscription item to cancel + required: true + schema: + type: string + responses: + '200': + $ref: '#/components/responses/Client.BillingSubscriptionItem' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/AuthenticationInvalid' + '403': + $ref: '#/components/responses/AuthorizationInvalid' + '404': + $ref: '#/components/responses/ResourceNotFound' + /v1/billing/plans: + get: + operationId: GetBillingPlanList + x-speakeasy-group: billing + x-speakeasy-name-override: listPlans + tags: + - Plans + summary: List billing plans + description: Returns a list of all publicly visible billing plans. + parameters: + - in: query + name: payer_type + description: Filter plans by payer type + required: false + schema: + type: string + enum: + - user + - org + responses: + '200': + $ref: '#/components/responses/BillingPlanList' + '400': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/UnprocessableEntity' + /v1/billing/plans/{planIdOrSlug}: + get: + operationId: GetBillingPlan + x-speakeasy-group: billing + x-speakeasy-name-override: getPlan + tags: + - Plans + summary: Get a billing plan + description: Returns a billing plan by ID or slug. + parameters: + - name: planIdOrSlug + in: path + description: The ID or slug of the plan + required: true + schema: + type: string + responses: + '200': + $ref: '#/components/responses/BillingPlan' + '400': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ResourceNotFound' + '422': + $ref: '#/components/responses/UnprocessableEntity' + /v1/me/organization_memberships: + get: + summary: Get Organization Memberships + description: Get a list of the organization memberships of the current user. + operationId: getOrganizationMemberships + tags: + - Organizations Memberships + parameters: + - $ref: '#/components/parameters/LimitParameter' + - $ref: '#/components/parameters/OffsetParameter' + - $ref: '#/components/parameters/Paginated' + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedOrganizationMemberships' + '401': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/me/organization_memberships/{organization_id}: + delete: + summary: Delete Organization Membership + description: Delete the membership of the given organization. + operationId: deleteOrganizationMemberships + tags: + - Organizations Memberships + parameters: + - in: path + name: organization_id + required: true + schema: + type: string + description: The ID of the organization for which the membership will be deleted + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedDeletedObject' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/me/organization_invitations: + get: + summary: Get Current User's Organization Invitations + description: Get a list of the organization invitations of the current user. + operationId: getUsersOrganizationInvitations + tags: + - Organizations Memberships + parameters: + - $ref: '#/components/parameters/LimitParameter' + - $ref: '#/components/parameters/OffsetParameter' + - in: query + name: status + schema: + type: string + description: The status of the organization invitations to filter by + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedOrganizationInvitationsUserContext' + '401': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/me/organization_invitations/{invitation_id}/accept: + post: + summary: Accept Organization invitation + description: Accept an organization invitation for the current user. + operationId: acceptOrganizationInvitation + tags: + - Organizations Memberships + parameters: + - in: path + name: invitation_id + required: true + schema: + type: string + description: The ID of the organization invitation to accept + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedOrganizationInvitationUserContext' + '401': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/me/organization_suggestions: + get: + summary: Get Organization Suggestions + description: Get a list of the organization suggestions of the current user. + operationId: getOrganizationSuggestions + tags: + - Organizations Memberships + parameters: + - $ref: '#/components/parameters/LimitParameter' + - $ref: '#/components/parameters/OffsetParameter' + - in: query + name: status + schema: + type: string + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedOrganizationSuggestions' + '401': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/me/organization_suggestions/{suggestion_id}/accept: + post: + summary: Accept Organization Suggestion + description: Accept an organization suggestion for the current user. + operationId: acceptOrganizationSuggestion + tags: + - Organizations Memberships + parameters: + - in: path + name: suggestion_id + required: true + schema: + type: string + description: The ID of the organization suggestion to accept + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedOrganizationSuggestion' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/organizations: + post: + summary: Create Organization + description: Create an organization and add the current user as admin. + operationId: createOrganization + tags: + - Organization + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + name: + type: string + description: |- + The organization name. + May not contain URLs or HTML. + slug: + type: string + description: The organization slug + nullable: true + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedOrganization' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/organizations/{organization_id}: + get: + summary: Get Organization + description: Retrieve all properties of an organization that the current user is member of. + tags: + - Organization + operationId: getOrganization + parameters: + - in: path + required: true + name: organization_id + schema: + type: string + description: The organization ID. + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedOrganization' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + patch: + summary: Update Organization + description: |- + Update the given properties of an organization. + + The current user must be an admin in the organization. + operationId: updateOrganization + tags: + - Organization + parameters: + - in: path + name: organization_id + required: true + schema: + type: string + description: The ID of the organization to update + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + name: + type: string + description: |- + The organization name. + May not contain URLs or HTML. + nullable: true + slug: + type: string + description: The organization slug + nullable: true + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedOrganization' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + delete: + summary: Delete Organization + description: |- + Delete the given organization. + + The current user must be an admin in the organization. + operationId: deleteOrganization + tags: + - Organization + parameters: + - in: path + name: organization_id + required: true + schema: + type: string + description: The ID of the organization to delete + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedDeletedObject' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/organizations/{organization_id}/logo: + put: + summary: Update Organization Logo + description: |- + Update the organization logo. + + The current user must be an admin in the organization. + operationId: updateOrganizationLogo + tags: + - Organization + parameters: + - in: path + name: organization_id + required: true + schema: + type: string + description: The ID of the organization. + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + application/octet-stream: + schema: + format: base64 + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedOrganization-2' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '413': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + delete: + summary: Delete Organization Logo + description: |- + Delete the organization logo. + + The current user must be an admin in the organization. + operationId: deleteOrganizationLogo + tags: + - Organization + parameters: + - in: path + name: organization_id + required: true + schema: + type: string + description: The ID of the organization. + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedDeletedObject' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + /v1/organizations/{organization_id}/invitations: + post: + summary: Create Organization Invitation + description: |- + Create an invitation for a user to join an organization. + + The current user must have permissions to manage the members of the organization. + operationId: createOrganizationInvitations + tags: + - Invitations + parameters: + - in: path + name: organization_id + required: true + schema: + type: string + description: The ID of the organization for which the invitation will be created. + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + email_address: + type: string + description: The email address the invitation will be sent to. + nullable: false + role: + type: string + description: |- + The role that will be assigned to the user after joining. + This can be one of the predefined roles (`org:admin`, `org:basic_member`) or a custom role. + nullable: false + notify: + type: boolean + description: |- + Optional flag which denotes whether an email invitation should be sent to the given email address. + Defaults to `true`. + nullable: true + required: + - email_address + - role + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedOrganizationInvitation' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + get: + summary: Get All Organization Invitations + description: |- + Retrieve all invitations for an organization. + + The current user must have permissions to manage the members of the organization. + operationId: getOrganizationInvitations + tags: + - Invitations + parameters: + - $ref: '#/components/parameters/LimitParameter' + - $ref: '#/components/parameters/OffsetParameter' + - in: path + name: organization_id + required: true + schema: + type: string + description: The ID of the organization for which the invitation will be retrieved. + - in: query + name: status + required: false + schema: + type: string + enum: + - pending + - accepted + - revoked + - expired + - invalid + - completed + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedOrganizationInvitations' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/organizations/{organization_id}/invitations/bulk: + post: + summary: Bulk Create Organization Invitations + description: |- + Create an invitation for a user to join an organization. + + The current user must have permissions to manage the members of the organization. + operationId: bulkCreateOrganizationInvitations + tags: + - Invitations + parameters: + - in: path + name: organization_id + required: true + schema: + type: string + description: The ID of the organization for which the invitations will be created. + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + email_address: + type: array + description: An array of email addresses the invitations will be sent to. + nullable: false + items: + type: string + role: + type: string + description: |- + The role that will be assigned to each of the users after joining. + This can be one of the predefined roles (`org:admin`, `org:basic_member`) or a custom role. + nullable: false + notify: + type: boolean + description: |- + Optional flag which denotes whether email invitations should be sent to the given email addresses. + Defaults to `true`. + nullable: true + required: + - email_address + - role + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedOrganizationInvitations' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/organizations/{organization_id}/invitations/pending: + get: + summary: Get All Pending Organization Invitations + deprecated: true + description: |- + Get a list of pending invitations for an organization. + + This endpoint is deprecated. Instead use the `/v1/organizations/{organization_id}/invitations` + with a query parameter of `status=pending`. + operationId: getAllPendingOrganizationInvitations + tags: + - Invitations + parameters: + - in: path + name: organization_id + required: true + schema: + type: string + description: The ID of the organization for which the invitations will be retrieved. + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedOrganizationInvitations' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/organizations/{organization_id}/invitations/{invitation_id}/revoke: + post: + summary: Revoke Pending Organization Invitation + description: |- + Revoke a pending organization invitation. + + The current user must have permissions to manage the members of the organization. + operationId: revokePendingOrganizationInvitation + tags: + - Invitations + parameters: + - in: path + name: organization_id + required: true + schema: + type: string + description: The ID of the organization for which the invitations will be retrieved. + - in: path + name: invitation_id + required: true + schema: + type: string + description: The ID of the invitation to revoke. + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedOrganizationInvitation' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + /v1/organizations/{organization_id}/memberships: + post: + summary: Create Organization Membership + description: |- + Create a new organization membership. + + The current user must have permissions to manage the members of the organization. + tags: + - Members + operationId: CreateOrganizationMembership + parameters: + - in: path + required: true + name: organization_id + schema: + type: string + description: The organization ID. + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + user_id: + type: string + description: The ID of the user to be added as a member. + nullable: true + role: + type: string + description: |- + The role that will be assigned to the user after joining. + This can be one of the predefined roles (`org:admin`, `org:basic_member`) or a custom role defined. + nullable: true + required: + - user_id + - role + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedOrganizationMembership' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + get: + summary: Get All Organization Members + description: |- + Retrieve all members of an organization. + + The current user must have permissions to manage the members of the organization. + operationId: ListOrganizationMemberships + tags: + - Members + parameters: + - $ref: '#/components/parameters/LimitParameter' + - $ref: '#/components/parameters/OffsetParameter' + - $ref: '#/components/parameters/Paginated' + - in: path + required: true + name: organization_id + schema: + type: string + description: The organization ID. + - in: query + required: false + name: query + schema: + type: string + description: |- + Returns members that match the given query. + For possible matches, we check for any of the user's identifier, usernames, user IDs, first and last names. + The query value doesn't need to match the exact value you are looking for, it is capable of partial matches as well. + - in: query + required: false + name: role + description: Filter by roles. This can be one of the predefined roles (`org:admin`, `org:basic_member`) or a custom role defined. + schema: + type: string + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedOrganizationMemberships' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/organizations/{organization_id}/memberships/{user_id}: + patch: + summary: Update Organization Membership + description: |- + Update the attributes of an organization membership for a specific user. + The current user must have permissions to manage the members of the organization. + tags: + - Members + operationId: UpdateOrganizationMembership + parameters: + - in: path + required: true + name: organization_id + schema: + type: string + description: The organization ID. + - in: path + required: true + name: user_id + schema: + type: string + description: The member ID. + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + role: + type: string + description: |- + The new role that will be assigned to the member. + This can be one of the predefined roles (`org:admin`, `org:basic_member`) or a custom role defined. + nullable: false + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedOrganizationMembership' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + delete: + summary: Remove Organization Member + description: |- + Remove a member from an organization. + + The current user must have permissions to manage the members of the organization. + operationId: removeOrganizationMember + tags: + - Members + parameters: + - in: path + required: true + name: organization_id + schema: + type: string + description: The organization ID. + - in: path + required: true + name: user_id + schema: + type: string + description: The member ID. + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedOrganizationMembership' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/organizations/{organization_id}/domains: + post: + summary: Create Organization Domain + description: |- + Create a new organization domain. + + The current user must have permissions to manage the domains of the organization. + tags: + - Domains + operationId: CreateOrganizationDomain + parameters: + - in: path + required: true + name: organization_id + schema: + type: string + description: The organization ID. + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + name: + type: string + description: The name of the new domain + nullable: false + enrollment_mode: + type: string + description: The enrollment mode of the new domain. Defaults to `manual_invitation`. + nullable: true + enum: + - manual_invitation + - automatic_invitation + - automatic_suggestion + - enterprise_sso + required: + - name + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedOrganizationDomain' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + get: + summary: Get All Organization Domains + description: |- + Retrieves a list of all the domains in an organization + + The current user must have permissions to manage the domains of the organization. + tags: + - Domains + operationId: ListOrganizationDomains + parameters: + - $ref: '#/components/parameters/LimitParameter' + - $ref: '#/components/parameters/OffsetParameter' + - in: path + name: organization_id + description: The organization ID. + required: true + schema: + type: string + - in: query + name: verified + description: Filter by whether a domain is verified + required: false + schema: + type: boolean + - in: query + name: enrollment_mode + description: Filter by enrollment mode + required: false + schema: + type: string + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedOrganizationDomains' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/organizations/{organization_id}/domains/bulk: + post: + summary: Bulk Create Organization Domains + description: |- + Creates up to 10 organization domains by name. Duplicate names in the + request are skipped. The response is a partial-success payload: + successfully created domains are returned in `data`, while per-name + failures are returned in `errors`. + + The current user must have permissions to manage the domains of the + organization. Requires the self-serve SSO feature to be enabled on the + instance. + tags: + - Domains + operationId: createOrganizationDomainsBulk + parameters: + - in: path + required: true + name: organization_id + schema: + type: string + description: The organization ID. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + names: + type: array + maxItems: 10 + items: + type: string + description: The domain names to create. + enrollment_mode: + type: string + description: The enrollment mode for the new domains. Defaults to `manual_invitation`. + nullable: true + enum: + - manual_invitation + - automatic_invitation + - automatic_suggestion + - enterprise_sso + required: + - names + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedOrganizationDomainBulkCreate' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '402': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/organizations/{organization_id}/domains/prepare_ownership_verification/bulk: + post: + summary: Bulk Prepare Organization Domain Ownership Verification + description: |- + Collection-level bulk counterpart of the single + `/domains/{domain_id}/prepare_ownership_verification` endpoint. Accepts a + set of `organization_domain_ids` and issues a fresh TXT challenge for + each. The plaintext TXT record value for each prepared domain is returned + in the response payload and is only stored as a bcrypt digest on the + server. Re-preparing replaces any prior outstanding TXT challenge for the + same domain. + + The current user must have permissions to manage the domains of the + organization. Requires the self-serve SSO feature to be enabled on the + instance. + tags: + - Domains + operationId: prepareOrganizationDomainsOwnershipVerification + parameters: + - in: path + required: true + name: organization_id + schema: + type: string + description: The organization ID. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + organization_domain_ids: + type: array + items: + type: string + description: The organization domain IDs to prepare ownership verification for. + required: + - organization_domain_ids + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedOrganizationDomainBulkOwnershipVerification' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '402': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/organizations/{organization_id}/domains/attempt_ownership_verification/bulk: + post: + summary: Bulk Attempt Organization Domain Ownership Verification + description: |- + Collection-level bulk counterpart of the single + `/domains/{domain_id}/attempt_ownership_verification` endpoint. Accepts a + set of `organization_domain_ids` and runs the same TXT ownership attempt + for each. + tags: + - Domains + operationId: attemptOrganizationDomainsOwnershipVerification + parameters: + - in: path + required: true + name: organization_id + schema: + type: string + description: The organization ID. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + organization_domain_ids: + type: array + items: + type: string + description: The organization domain IDs to attempt ownership verification for. + required: + - organization_domain_ids + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedOrganizationDomainBulkOwnershipVerification' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '402': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/organizations/{organization_id}/domains/{domain_id}: + get: + summary: Get Organization Domain + description: |- + Retrieve all properties for a domain of an organization. + + The current user must have permissions to manage the domains of the organization. + tags: + - Domains + operationId: GetOrganizationDomain + parameters: + - in: path + required: true + name: organization_id + schema: + type: string + description: The organization ID. + - in: path + required: true + name: domain_id + schema: + type: string + description: The domain ID. + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedOrganizationDomain' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + delete: + summary: Delete Organization Domain + description: |- + Remove a domain from an organization. + + The current user must have permissions to manage the domains of the organization. + tags: + - Domains + operationId: deleteOrganizationDomain + parameters: + - in: path + required: true + name: organization_id + schema: + type: string + description: The organization ID. + - in: path + required: true + name: domain_id + schema: + type: string + description: The domain ID. + responses: + '200': + $ref: '#/components/responses/Client.DeletedOrganizationDomain' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/organizations/{organization_id}/domains/{domain_id}/update_enrollment_mode: + post: + summary: Update Organization Enrollment Mode + description: |- + Update the enrollment mode for an organization domain. + + This can be either `automatic_invitation` or `automatic_suggestion`. + + The current user must have permissions to manage the domains of the organization. + tags: + - Domains + operationId: UpdateOrganizationDomainEnrollmentMode + parameters: + - in: path + required: true + name: organization_id + schema: + type: string + description: The organization ID. + - in: path + required: true + name: domain_id + schema: + type: string + description: The domain ID. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + enrollment_mode: + type: string + nullable: false + delete_pending: + type: boolean + nullable: true + required: + - enrollment_mode + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedOrganizationDomain' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/organizations/{organization_id}/domains/{domain_id}/prepare_affiliation_verification: + post: + summary: Prepare Organization Domain Affiliation Verification + description: |- + Prepares affiliation verification for an organization domain. + + The current user must have permissions to manage the domains of the organization. + tags: + - Domains + operationId: prepareOrganizationDomainVerification + parameters: + - in: path + required: true + name: organization_id + schema: + type: string + description: The organization ID. + - in: path + required: true + name: domain_id + schema: + type: string + description: The domain ID. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + affiliation_email_address: + type: string + nullable: false + required: + - affiliation_email_address + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedOrganizationDomain' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/organizations/{organization_id}/domains/{domain_id}/attempt_affiliation_verification: + post: + summary: Attempt Organization Domain Affiliation Verification + description: |- + Attempts affiliation verification for organization domain + + The current user must have permissions to manage the domains of the organization. + tags: + - Domains + operationId: attemptOrganizationDomainVerification + parameters: + - in: path + required: true + name: organization_id + schema: + type: string + description: The organization ID. + - in: path + required: true + name: domain_id + schema: + type: string + description: The domain ID. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + code: + type: string + description: The code that was sent to the email address. + nullable: false + required: + - code + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedOrganizationDomain' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/organizations/{organization_id}/domains/{domain_id}/prepare_ownership_verification: + post: + summary: Prepare Organization Domain Ownership Verification + description: |- + Issues a fresh TXT challenge for the organization domain. The plaintext + TXT record value is returned in the response payload and is only stored + as a bcrypt digest on the server. Re-preparing replaces any prior + outstanding TXT challenge for the same domain. + + The current user must have permissions to manage the domains of the + organization. Requires the self-serve SSO feature to be enabled on the + instance. + tags: + - Domains + operationId: prepareOrganizationDomainOwnershipVerification + parameters: + - in: path + required: true + name: organization_id + schema: + type: string + description: The organization ID. + - in: path + required: true + name: domain_id + schema: + type: string + description: The domain ID. + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedOrganizationDomain' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '402': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/organizations/{organization_id}/domains/{domain_id}/attempt_ownership_verification: + post: + summary: Attempt Organization Domain Ownership Verification + description: |- + Resolves the TXT record for the organization domain and, on a successful + match against the outstanding challenge, marks the domain as + ownership-verified and emits an `organization_domain.ownership_verified` + audit event. + + The current user must have permissions to manage the domains of the + organization. Requires the self-serve SSO feature to be enabled on the + instance. + tags: + - Domains + operationId: attemptOrganizationDomainOwnershipVerification + parameters: + - in: path + required: true + name: organization_id + schema: + type: string + description: The organization ID. + - in: path + required: true + name: domain_id + schema: + type: string + description: The domain ID. + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedOrganizationDomain' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '402': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/organizations/{organization_id}/membership_requests: + get: + summary: Get Organization Membership Requests + description: |- + Retrieve a list of all membership requests of an organization. + + The current user must have permissions to manage the members of the organization. + tags: + - Membership Requests + operationId: listOrganizationMembershipRequests + parameters: + - $ref: '#/components/parameters/LimitParameter' + - $ref: '#/components/parameters/OffsetParameter' + - in: path + required: true + name: organization_id + schema: + type: string + description: The organization ID. + - in: query + required: false + name: status + schema: + type: string + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedOrganizationMembershipRequests' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/organizations/{organization_id}/membership_requests/{request_id}/accept: + post: + summary: Accept Organization Membership Request + description: |- + Accepts an organization membership request. + + The current user must have permissions to manage the members of the organization. + tags: + - Membership Requests + operationId: acceptOrganizationMembershipRequest + parameters: + - in: path + required: true + name: organization_id + schema: + type: string + description: The organization ID. + - in: path + required: true + name: request_id + schema: + type: string + description: The request ID. + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedOrganizationMembershipRequest' + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/organizations/{organization_id}/membership_requests/{request_id}/reject: + post: + summary: Reject Organization Membership Request + description: |- + Rejects an organization membership request + + The current user must have permissions to manage the members of the organization. + tags: + - Membership Requests + operationId: rejectOrganizationMembershipRequest + parameters: + - in: path + required: true + name: organization_id + schema: + type: string + description: The organization ID. + - in: path + required: true + name: request_id + schema: + type: string + description: The request ID. + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedOrganizationMembershipRequest' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/organizations/{organization_id}/roles: + get: + summary: Get Organization Roles + description: Retrieve a list of all roles of an organization. + tags: + - Roles + operationId: ListOrganizationRoles + parameters: + - $ref: '#/components/parameters/LimitParameter' + - $ref: '#/components/parameters/OffsetParameter' + - in: path + required: true + name: organization_id + schema: + type: string + description: The organization ID. + responses: + '200': + $ref: '#/components/responses/Client.ClientWrappedRoles' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + /v1/redirect: + get: + summary: Redirect to a validated URL + description: Validates and redirects to a safe URL. If no URL is provided, redirects to a default URL. + tags: + - Redirect + operationId: redirectToUrl + parameters: + - name: redirect_url + in: query + description: The URL to redirect to (optional). If empty or not provided, redirects to the default URL. + required: false + allowEmptyValue: true + schema: + type: string + responses: + '307': + description: Temporary redirect to the validated URL or default URL + headers: + Location: + description: The URL to redirect to + schema: + type: string + format: uri + '400': + $ref: '#/components/responses/ClerkErrors' + '401': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /v1/tickets/accept: + get: + summary: Accept ticket + description: |- + Parses a ticket JWT and performs the necessary actions depending on the ticket's source type. + Depending on the ticket source type, a successful response can either redirect to a new location + with the ticket in the query string, or respond directly with a text/html content type for the response body. + operationId: acceptTicket + tags: + - Sign Ins + parameters: + - in: query + name: ticket + required: true + schema: + type: string + description: The JWT with verification information + responses: + '200': + description: Successful response with a plain text or HTML body. + '302': + description: Redirect + '303': + description: Redirect + '400': + $ref: '#/components/responses/ClerkErrors' + '404': + $ref: '#/components/responses/ClerkErrors' + /v1/verify: + get: + summary: Attempt Email Link Verification + description: Attempt to verify a verification with email_link strategy. + operationId: verify + tags: + - Sign Ins + parameters: + - in: query + name: token + required: true + schema: + type: string + description: The JWT with verification information + responses: + '200': + description: OK + '303': + description: Redirect + '400': + $ref: '#/components/responses/ClerkErrors' + /v1/waitlist: + post: + summary: Join Waitlist + description: |- + Adds an email address to the waitlist. If the email is already on the waitlist, the request will still succeed, + returning the ID of the existing waitlist entry. + operationId: joinWaitlist + security: + - {} + - DevBrowser: [] + tags: + - Waitlist + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + additionalProperties: false + properties: + email_address: + type: string + nullable: false + required: + - email_address + responses: + '200': + $ref: '#/components/responses/Client.WaitlistEntry' + '400': + $ref: '#/components/responses/ClerkErrors' + '403': + $ref: '#/components/responses/ClerkErrors' + '422': + $ref: '#/components/responses/ClerkErrors' + /api_keys: + get: + operationId: getApiKeys + summary: Get API Keys + tags: + - API Keys + parameters: + - schema: + type: string + minLength: 3 + default: api_key + required: false + name: type + in: query + - schema: + type: string + pattern: ^(user|org)_\w{27}$ + required: true + name: subject + in: query + - schema: + type: string + enum: + - 'true' + - 'false' + default: 'false' + required: false + name: include_invalid + in: query + - schema: + type: number + minimum: 1 + maximum: 100 + default: 10 + required: false + name: limit + in: query + - schema: + type: number + nullable: true + minimum: 0 + default: 0 + required: false + name: offset + in: query + - schema: + type: string + required: false + name: query + in: query + responses: + '200': + description: 200 OK + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + type: object + properties: + object: + type: string + enum: + - api_key + id: + type: string + pattern: ^ak_[0-9A-Fa-f]{32}$ + example: ak_3beecc9c60adb5f9b850e91a8ee1e992 + type: + type: string + minLength: 3 + example: api_key + subject: + type: string + pattern: ^(user|org|mch)_\w{27}$ + example: user_2xhFjEI5X2qWRvtV13BzSj8H6Dk + name: + type: string + example: MY_SERVICE_API_KEY + description: + type: string + nullable: true + maxLength: 255 + example: This is my API Key + claims: + nullable: true + example: + foo: bar + scopes: + type: array + items: + type: string + example: + - read + - write + revoked: + type: boolean + example: false + revocation_reason: + type: string + nullable: true + example: Revoked by user + expired: + type: boolean + example: false + expiration: + type: number + nullable: true + description: The timestamp for when the API key will expire, in milliseconds + example: 1716883200 + created_by: + type: string + nullable: true + pattern: ^user_\w{27}$ + example: user_2xhFjEI5X2qWRvtV13BzSj8H6Dk + last_used_at: + type: number + nullable: true + description: The timestamp for when the API key was last used, in milliseconds + example: 1716883200 + created_at: + type: number + description: The timestamp for when the API key was created, in milliseconds + example: 1716883200 + updated_at: + type: number + description: The timestamp for when the API key was last updated, in milliseconds + example: 1716883200 + required: + - object + - id + - type + - subject + - name + - claims + - scopes + - revoked + - revocation_reason + - expired + - expiration + - created_by + - last_used_at + - created_at + - updated_at + additionalProperties: false + total_count: + type: number + required: + - data + - total_count + '400': + description: 400 Bad Request + content: + application/json: + schema: + type: object + properties: + errors: + type: array + items: + type: object + properties: + message: + type: string + example: Bad Request + long_message: + type: string + example: "Invalid 'url_parameter.example': Failed regex check" + code: + type: string + example: bad_request + required: + - message + - long_message + - code + required: + - errors + '404': + description: 404 Not Found + content: + application/json: + schema: + type: object + properties: + errors: + type: array + items: + type: object + properties: + message: + type: string + example: some details about the error + long_message: + type: string + example: some details about the error + code: + type: string + example: some_error_code + required: + - message + - long_message + - code + required: + - errors + post: + operationId: createApiKey + summary: Create an API Key + tags: + - API Keys + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + type: + type: string + minLength: 3 + maxLength: 25 + default: api_key + name: + type: string + minLength: 3 + pattern: ^(\S(.*\S)?)?$ + description: + type: string + nullable: true + maxLength: 255 + subject: + type: string + pattern: ^(user|org)_\w{27}$ + seconds_until_expiration: + type: number + nullable: true + minimum: 0 + exclusiveMinimum: true + required: + - name + - subject + additionalProperties: false + responses: + '201': + description: 201 Created + content: + application/json: + schema: + type: object + properties: + object: + type: string + enum: + - api_key + id: + type: string + pattern: ^ak_[0-9A-Fa-f]{32}$ + example: ak_3beecc9c60adb5f9b850e91a8ee1e992 + type: + type: string + minLength: 3 + example: api_key + subject: + type: string + pattern: ^(user|org|mch)_\w{27}$ + example: user_2xhFjEI5X2qWRvtV13BzSj8H6Dk + name: + type: string + example: MY_SERVICE_API_KEY + description: + type: string + nullable: true + maxLength: 255 + example: This is my API Key + claims: + nullable: true + example: + foo: bar + scopes: + type: array + items: + type: string + example: + - read + - write + secret: + type: string + example: ak_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + revoked: + type: boolean + example: false + revocation_reason: + type: string + nullable: true + example: Revoked by user + expired: + type: boolean + example: false + expiration: + type: number + nullable: true + description: The timestamp for when the API key will expire, in milliseconds + example: 1716883200 + created_by: + type: string + nullable: true + pattern: ^user_\w{27}$ + example: user_2xhFjEI5X2qWRvtV13BzSj8H6Dk + last_used_at: + type: number + nullable: true + description: The timestamp for when the API key was last used, in milliseconds + example: 1716883200 + created_at: + type: number + description: The timestamp for when the API key was created, in milliseconds + example: 1716883200 + updated_at: + type: number + description: The timestamp for when the API key was last updated, in milliseconds + example: 1716883200 + required: + - object + - id + - type + - subject + - name + - claims + - scopes + - secret + - revoked + - revocation_reason + - expired + - expiration + - created_by + - last_used_at + - created_at + - updated_at + additionalProperties: false + '400': + description: 400 Bad Request + content: + application/json: + schema: + type: object + properties: + errors: + type: array + items: + type: object + properties: + message: + type: string + example: Bad Request + long_message: + type: string + example: "Invalid 'url_parameter.example': Failed regex check" + code: + type: string + example: bad_request + required: + - message + - long_message + - code + required: + - errors + '409': + description: 409 Conflict + content: + application/json: + schema: + type: object + properties: + errors: + type: array + items: + type: object + properties: + message: + type: string + example: some details about the error + long_message: + type: string + example: some details about the error + code: + type: string + example: some_error_code + required: + - message + - long_message + - code + required: + - errors + /api_keys/{apiKeyID}: + patch: + operationId: updateApiKey + summary: Update an API Key + tags: + - API Keys + parameters: + - schema: + type: string + pattern: ^(ak|api_key)_[0-9A-Fa-f]{32}$ + required: true + name: apiKeyID + in: path + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + description: + type: string + nullable: true + maxLength: 255 + additionalProperties: false + responses: + '200': + description: 200 OK + content: + application/json: + schema: + type: object + properties: + object: + type: string + enum: + - api_key + id: + type: string + pattern: ^ak_[0-9A-Fa-f]{32}$ + example: ak_3beecc9c60adb5f9b850e91a8ee1e992 + type: + type: string + minLength: 3 + example: api_key + subject: + type: string + pattern: ^(user|org|mch)_\w{27}$ + example: user_2xhFjEI5X2qWRvtV13BzSj8H6Dk + name: + type: string + example: MY_SERVICE_API_KEY + description: + type: string + nullable: true + maxLength: 255 + example: This is my API Key + claims: + nullable: true + example: + foo: bar + scopes: + type: array + items: + type: string + example: + - read + - write + revoked: + type: boolean + example: false + revocation_reason: + type: string + nullable: true + example: Revoked by user + expired: + type: boolean + example: false + expiration: + type: number + nullable: true + description: The timestamp for when the API key will expire, in milliseconds + example: 1716883200 + created_by: + type: string + nullable: true + pattern: ^user_\w{27}$ + example: user_2xhFjEI5X2qWRvtV13BzSj8H6Dk + last_used_at: + type: number + nullable: true + description: The timestamp for when the API key was last used, in milliseconds + example: 1716883200 + created_at: + type: number + description: The timestamp for when the API key was created, in milliseconds + example: 1716883200 + updated_at: + type: number + description: The timestamp for when the API key was last updated, in milliseconds + example: 1716883200 + required: + - object + - id + - type + - subject + - name + - claims + - scopes + - revoked + - revocation_reason + - expired + - expiration + - created_by + - last_used_at + - created_at + - updated_at + additionalProperties: false + '400': + description: 400 Bad Request + content: + application/json: + schema: + type: object + properties: + errors: + type: array + items: + type: object + properties: + message: + type: string + example: Bad Request + long_message: + type: string + example: "Invalid 'url_parameter.example': Failed regex check" + code: + type: string + example: bad_request + required: + - message + - long_message + - code + required: + - errors + '404': + description: 404 Not Found + content: + application/json: + schema: + type: object + properties: + errors: + type: array + items: + type: object + properties: + message: + type: string + example: some details about the error + long_message: + type: string + example: some details about the error + code: + type: string + example: some_error_code + required: + - message + - long_message + - code + required: + - errors + /api_keys/{apiKeyID}/revoke: + post: + operationId: revokeApiKey + summary: Revoke an API Key + tags: + - API Keys + parameters: + - schema: + type: string + pattern: ^(ak|api_key)_[0-9A-Fa-f]{32}$ + required: true + name: apiKeyID + in: path + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + revocation_reason: + type: string + nullable: true + additionalProperties: false + responses: + '200': + description: 200 OK + content: + application/json: + schema: + type: object + properties: + object: + type: string + enum: + - api_key + id: + type: string + pattern: ^ak_[0-9A-Fa-f]{32}$ + example: ak_3beecc9c60adb5f9b850e91a8ee1e992 + type: + type: string + minLength: 3 + example: api_key + subject: + type: string + pattern: ^(user|org|mch)_\w{27}$ + example: user_2xhFjEI5X2qWRvtV13BzSj8H6Dk + name: + type: string + example: MY_SERVICE_API_KEY + description: + type: string + nullable: true + maxLength: 255 + example: This is my API Key + claims: + nullable: true + example: + foo: bar + scopes: + type: array + items: + type: string + example: + - read + - write + revoked: + type: boolean + example: false + revocation_reason: + type: string + nullable: true + example: Revoked by user + expired: + type: boolean + example: false + expiration: + type: number + nullable: true + description: The timestamp for when the API key will expire, in milliseconds + example: 1716883200 + created_by: + type: string + nullable: true + pattern: ^user_\w{27}$ + example: user_2xhFjEI5X2qWRvtV13BzSj8H6Dk + last_used_at: + type: number + nullable: true + description: The timestamp for when the API key was last used, in milliseconds + example: 1716883200 + created_at: + type: number + description: The timestamp for when the API key was created, in milliseconds + example: 1716883200 + updated_at: + type: number + description: The timestamp for when the API key was last updated, in milliseconds + example: 1716883200 + required: + - object + - id + - type + - subject + - name + - claims + - scopes + - revoked + - revocation_reason + - expired + - expiration + - created_by + - last_used_at + - created_at + - updated_at + additionalProperties: false + '400': + description: 400 Bad Request + content: + application/json: + schema: + type: object + properties: + errors: + type: array + items: + type: object + properties: + message: + type: string + example: Bad Request + long_message: + type: string + example: "Invalid 'url_parameter.example': Failed regex check" + code: + type: string + example: bad_request + required: + - message + - long_message + - code + required: + - errors + '404': + description: 404 Not Found + content: + application/json: + schema: + type: object + properties: + errors: + type: array + items: + type: object + properties: + message: + type: string + example: some details about the error + long_message: + type: string + example: some details about the error + code: + type: string + example: some_error_code + required: + - message + - long_message + - code + required: + - errors +components: + securitySchemes: + ProductionBrowser: + type: apiKey + in: cookie + name: __client + description: |- + The Client API token sent in the form of a browser cookie. + + Used to authenticate production browser applications. + DevBrowser: + type: apiKey + in: query + name: __dev_session + description: |- + A Dev Browser API token sent as a query parameter. + + Used to authenticate only dev instances. + + To generate a dev instance API token you must first make a request to `/v1/dev_browser` + and copy it from the response. + ProductionNativeApp: + type: http + scheme: bearer + description: |- + The Client API token sent in the `Authorization` header. + + Used to authenticate native applications. + ProductionNativeFlag: + type: apiKey + in: query + name: _is_native + description: |- + If sent and the value is true, it instructs the server to parse the API token from the `Authorization` header. + + It should always be set to true when using the `Authorization` header authentication strategy. + ClientCredentials: + type: http + scheme: basic + description: HTTP Basic Authentication using Client ID as username and Client Secret as password. + schemas: + WellKnown.Assetlinks: + type: array + additionalProperties: false + items: + type: object + properties: + relation: + type: array + items: + type: string + enum: + - delegate_permission/common.handle_all_urls + - delegate_permission/common.get_login_creds + target: + type: object + properties: + namespace: + type: string + package_name: + type: string + sha256_cert_fingerprints: + type: array + items: + type: string + JWKS.ed25519.PublicKey: + type: object + properties: + kid: + type: string + alg: + type: string + use: + type: string + kty: + type: string + enum: + - OKP + crv: + type: string + enum: + - Ed25519 + x: + type: string + x5c: + type: array + items: + type: string + x5t: + type: string + x5t#S256: + type: string + x5u: + type: string + required: + - kid + - alg + - use + - kty + - crv + - x + JWKS.ecdsa.PublicKey: + type: object + properties: + kid: + type: string + alg: + type: string + use: + type: string + kty: + type: string + enum: + - EC + crv: + type: string + x: + type: string + 'y': + type: string + x5c: + type: array + items: + type: string + x5t: + type: string + x5t#S256: + type: string + x5u: + type: string + required: + - kid + - alg + - use + - kty + - crv + - x + - 'y' + JWKS.rsa.PublicKey: + type: object + properties: + kid: + type: string + alg: + type: string + use: + type: string + kty: + type: string + enum: + - RSA + 'n': + type: string + e: + type: string + x5c: + type: array + items: + type: string + x5t: + type: string + x5t#S256: + type: string + x5u: + type: string + required: + - kid + - alg + - use + - kty + - 'n' + - e + JWKS.ed25519.PrivateKey: + type: object + properties: + kid: + type: string + alg: + type: string + use: + type: string + kty: + type: string + enum: + - OKP + crv: + type: string + enum: + - Ed25519 + x: + type: string + d: + type: string + x5c: + type: array + items: + type: string + x5t: + type: string + x5t#S256: + type: string + x5u: + type: string + required: + - kid + - alg + - use + - kty + - crv + - x + - d + JWKS.ecdsa.PrivateKey: + type: object + properties: + kid: + type: string + alg: + type: string + use: + type: string + kty: + type: string + enum: + - EC + crv: + type: string + x: + type: string + 'y': + type: string + d: + type: string + x5c: + type: array + items: + type: string + x5t: + type: string + x5t#S256: + type: string + x5u: + type: string + required: + - kid + - alg + - use + - kty + - crv + - x + - 'y' + - d + JWKS.rsa.PrivateKey: + type: object + properties: + kid: + type: string + alg: + type: string + use: + type: string + kty: + type: string + enum: + - RSA + 'n': + type: string + e: + type: string + d: + type: string + p: + type: string + q: + type: string + dp: + type: string + dq: + type: string + qi: + type: string + x5c: + type: array + items: + type: string + x5t: + type: string + x5t#S256: + type: string + x5u: + type: string + required: + - kid + - alg + - use + - kty + - crv + - 'n' + - e + - d + - p + - q + JWKS.symmetric.Key: + type: object + properties: + kid: + type: string + alg: + type: string + use: + type: string + kty: + type: string + enum: + - oct + k: + type: string + x5c: + type: array + items: + type: string + x5t: + type: string + x5t#S256: + type: string + x5u: + type: string + required: + - kid + - alg + - use + - kty + - k + JWKS: + type: object + additionalProperties: false + properties: + keys: + type: array + items: + oneOf: + - $ref: '#/components/schemas/JWKS.ed25519.PublicKey' + - $ref: '#/components/schemas/JWKS.ecdsa.PublicKey' + - $ref: '#/components/schemas/JWKS.rsa.PublicKey' + - $ref: '#/components/schemas/JWKS.ed25519.PrivateKey' + - $ref: '#/components/schemas/JWKS.ecdsa.PrivateKey' + - $ref: '#/components/schemas/JWKS.rsa.PrivateKey' + - $ref: '#/components/schemas/JWKS.symmetric.Key' + ClerkError: + type: object + properties: + message: + type: string + long_message: + type: string + code: + type: string + meta: + type: object + additionalProperties: true + required: + - message + - long_message + - code + ClerkErrors: + type: object + properties: + errors: + type: array + items: + $ref: '#/components/schemas/ClerkError' + meta: + type: object + additionalProperties: true + clerk_trace_id: + type: string + required: + - errors + WellKnown.AppleAppSiteAssociation: + type: object + additionalProperties: false + properties: + webcredentials: + type: object + additionalProperties: false + properties: + apps: + type: array + items: + type: string + WellKnown.OpenIDConfiguration: + type: object + additionalProperties: false + properties: + issuer: + type: string + authorization_endpoint: + type: string + token_endpoint: + type: string + revocation_endpoint: + type: string + introspection_endpoint: + type: string + userinfo_endpoint: + type: string + device_authorization_endpoint: + type: string + jwks_uri: + type: string + scopes_supported: + type: array + items: + type: string + description: |- + Built-in scope values supported by the authorization server. On instances using + Clerk's current OAuth identity provider, this also includes the instance's custom + scopes that are marked as advertised. + response_types_supported: + type: array + items: + type: string + response_modes_supported: + type: array + items: + type: string + grant_types_supported: + type: array + items: + type: string + subject_types_supported: + type: array + items: + type: string + id_token_signing_alg_values_supported: + type: array + items: + type: string + token_endpoint_auth_methods_supported: + type: array + items: + type: string + claims_supported: + type: array + items: + type: string + code_challenge_methods_supported: + type: array + items: + type: string + backchannel_logout_supported: + type: boolean + frontchannel_logout_supported: + type: boolean + end_session_endpoint: + type: string + authorization_response_iss_parameter_supported: + type: boolean + required: + - issuer + - authorization_endpoint + - token_endpoint + - revocation_endpoint + - introspection_endpoint + - userinfo_endpoint + - jwks_uri + - scopes_supported + - response_types_supported + - response_modes_supported + - grant_types_supported + - subject_types_supported + - id_token_signing_alg_values_supported + - token_endpoint_auth_methods_supported + - claims_supported + - code_challenge_methods_supported + - backchannel_logout_supported + - frontchannel_logout_supported + WellKnown.OAuth2AuthorizationServerMetadata: + type: object + additionalProperties: false + properties: + issuer: + type: string + authorization_endpoint: + type: string + token_endpoint: + type: string + revocation_endpoint: + type: string + device_authorization_endpoint: + type: string + jwks_uri: + type: string + registration_endpoint: + type: string + response_types_supported: + type: array + items: + type: string + grant_types_supported: + type: array + items: + type: string + token_endpoint_auth_methods_supported: + type: array + items: + type: string + scopes_supported: + type: array + items: + type: string + description: |- + Built-in scope values supported by the authorization server. On instances using + Clerk's current OAuth identity provider, this also includes the instance's custom + scopes that are marked as advertised. + subject_types_supported: + type: array + items: + type: string + id_token_signing_alg_values_supported: + type: array + items: + type: string + claims_supported: + type: array + items: + type: string + service_documentation: + type: string + ui_locales_supported: + type: array + items: + type: string + op_tos_uri: + type: string + code_challenge_methods_supported: + type: array + items: + type: string + client_id_metadata_document_supported: + type: boolean + authorization_response_iss_parameter_supported: + type: boolean + required: + - issuer + - authorization_endpoint + - token_endpoint + - revocation_endpoint + - jwks_uri + - response_types_supported + - grant_types_supported + - token_endpoint_auth_methods_supported + - scopes_supported + - subject_types_supported + - id_token_signing_alg_values_supported + - claims_supported + - code_challenge_methods_supported + WellKnown.WebAuthn: + type: object + additionalProperties: false + properties: + origins: + type: array + items: + type: string + required: + - origins + OAuth.DynamicClientRegistrationRequest: + type: object + additionalProperties: false + description: |- + Client metadata used to register a new OAuth 2.0 client according to RFC 7591. + The authorization server assigns this client a unique client identifier. + properties: + redirect_uris: + type: array + items: + type: string + format: uri + description: |- + Array of redirection URI strings for use in redirect-based flows such as + the authorization code flow. As required by Section 2 of RFC 6749, clients + using flows with redirection must register their redirection URI values. + minItems: 1 + grant_types: + type: array + items: + type: string + description: |- + Grant types the client requests, as defined in RFC 7591 Section 2. + Accepted for compatibility; Clerk always registers dynamic clients with + `authorization_code` and `refresh_token`. + token_endpoint_auth_method: + type: string + enum: + - client_secret_basic + - client_secret_post + - none + default: client_secret_basic + description: |- + Indicator of the requested authentication method for the token endpoint. + - `client_secret_basic`: HTTP Basic authentication scheme + - `client_secret_post`: Client credentials in the request body + - `none`: Public client (no authentication) + + **Note:** In the current implementation, `client_secret_basic` and `client_secret_post` + are treated equivalently. Both methods can be used interchangeably at the token endpoint + regardless of which one was specified during registration. + client_name: + type: string + nullable: true + maxLength: 256 + description: |- + Human-readable string name of the client to be presented to the end-user + during authorization. + client_uri: + type: string + format: uri + nullable: true + maxLength: 1024 + description: URL string of a web page providing information about the client. + logo_uri: + type: string + format: uri + nullable: true + maxLength: 1024 + description: |- + URL string that references a logo for the client. If present, the + Clerk will display this image to the end-user during approval. + scope: + type: string + nullable: true + maxLength: 1024 + description: |- + Space-separated list of scope values that the client can use when requesting + access tokens. If omitted, the client will be registered with the default set of scopes. + + Accepts built-in scope values and, on instances using Clerk's current OAuth + identity provider, any custom scope defined for the instance. Instances still on the + legacy OAuth identity provider accept built-in scope values only. Scope values that + the instance does not define are rejected with `invalid_client_metadata`. + application_type: + type: string + description: |- + Kind of the application as defined by OpenID Connect Dynamic Client + Registration 1.0. Accepted values are `native` and `web`. Any other + non-empty value is rejected with `invalid_client_metadata`. An + omitted or empty value defaults to `web`. See + [OpenID Connect Dynamic Client Registration 1.0 §2](https://openid.net/specs/openid-connect-registration-1_0.html): + "The default, if omitted, is web." + + MCP clients should send this field (SEP-837) so native-style redirect + URIs are not judged against the OIDC default of `web`. Clerk cannot + identify an MCP client at this shared endpoint, so omitting the field + is still accepted and defaults to `web`. + + When `native`, custom URI schemes and claimed `https` redirect URIs + are allowed. See [RFC 8252 Section 7.2](https://datatracker.ietf.org/doc/html/rfc8252#section-7.2) + for claimed `https` redirect URIs. Plain `http` is allowed only for + loopback hosts (`localhost` and IPv4 or IPv6 loopback addresses); + other `http` hosts are rejected with `invalid_redirect_uri`. + + When `web`, the current redirect URI checks apply. Clerk does not + issue the implicit grant, so the OIDC `https` and non-`localhost` + rule for web clients is not enforced. + required: + - redirect_uris + OAuth.DynamicClientRegistrationResponse: + type: object + additionalProperties: false + description: |- + Client information response after successful registration according to RFC 7591. + Contains the client identifier, client secret (for confidential clients), and + other client metadata. + properties: + client_id: + type: string + description: |- + Unique client identifier string assigned by the authorization server. + The client identifier is not a secret; it is exposed to the resource owner + and must not be used alone for client authentication. + client_secret: + type: string + description: |- + Client secret string. This value is used by confidential clients to + authenticate to the token endpoint. The client secret is omitted for + public clients (when token_endpoint_auth_method is "none"). + client_id_issued_at: + type: integer + format: int64 + description: |- + Time at which the client identifier was issued. The time is represented + as the number of seconds from 1970-01-01T00:00:00Z as measured in UTC. + client_secret_expires_at: + type: integer + format: int64 + description: |- + Time at which the client secret will expire or 0 if it will not expire. + The time is represented as the number of seconds from 1970-01-01T00:00:00Z + as measured in UTC. + client_name: + type: string + description: |- + Human-readable string name of the client to be presented to the end-user + during authorization. + client_uri: + type: string + format: uri + description: URL string of a web page providing information about the client. + logo_uri: + type: string + format: uri + description: URL string that references a logo for the client. + redirect_uris: + type: array + items: + type: string + format: uri + description: Array of redirection URI strings for use in redirect-based flows. + grant_types: + type: array + items: + type: string + description: |- + Array of OAuth 2.0 grant type strings that the client can use at the + token endpoint. + response_types: + type: array + items: + type: string + description: |- + Array of the OAuth 2.0 response type strings that the client can use + at the authorization endpoint. + scope: + type: string + description: |- + Space-separated list of scope values that the client can use when + requesting access tokens. Contains the effective set of scopes registered + for the client, including both built-in and custom scope values. + token_endpoint_auth_method: + type: string + enum: + - client_secret_basic + - client_secret_post + - none + description: |- + Indicator of the requested authentication method for the token endpoint. + This value reflects what was specified during registration. + application_type: + type: string + description: |- + Kind of the application accepted during registration. `native` or + `web`. When the request omitted `application_type` or sent an empty + value, this is `web`. See + [OpenID Connect Dynamic Client Registration 1.0 §2](https://openid.net/specs/openid-connect-registration-1_0.html): + "The default, if omitted, is web." + required: + - client_id + - client_id_issued_at + - client_secret_expires_at + - client_name + - redirect_uris + - grant_types + - response_types + - scope + - token_endpoint_auth_method + OAuth.DynamicClientRegistrationError: + type: object + additionalProperties: false + description: |- + Error response for OAuth 2.0 Dynamic Client Registration according to RFC 7591. + The error response uses OAuth 2.0 error response format. + properties: + error: + type: string + enum: + - invalid_redirect_uri + - invalid_client_metadata + - invalid_software_statement + - unapproved_software_statement + description: |- + Single ASCII error code string indicating the error type. + - `invalid_redirect_uri`: The value of one or more redirect_uris is invalid + - `invalid_client_metadata`: One or more client metadata fields is invalid + - `invalid_software_statement`: The software statement presented is invalid + - `unapproved_software_statement`: The software statement is not approved + error_description: + type: string + description: |- + Human-readable ASCII text description of the error used for debugging. + This is not meant to be displayed to the end-user. + required: + - error + OAuth.Token: + type: object + additionalProperties: false + description: |- + OAuth 2.0 token response according to RFC 6749 Section 5.1. + When using OpenID Connect (with `openid` scope), also includes an `id_token` field. + properties: + access_token: + type: string + description: |- + The access token issued by the authorization server. + This token can be used to access protected resources on behalf of the user. + token_type: + type: string + description: The type of token issued. Always "Bearer" for OAuth 2.0. + example: Bearer + issued_token_type: + type: string + description: The type of token issued by an RFC 8693 token exchange. + example: urn:ietf:params:oauth:token-type:access_token + expires_in: + type: integer + description: |- + The lifetime in seconds of the access token. + For example, the value "86400" denotes that the access token will expire in 1 day from when the response was generated. + example: 86400 + refresh_token: + type: string + description: |- + The refresh token which can be used to obtain new access tokens using the same authorization grant. + Refresh tokens have a 10 year lifetime. + scope: + type: string + description: |- + Space-separated list of scopes that were granted for this access token. + May differ from requested scopes if the authorization server granted a subset. + example: openid email profile + id_token: + type: string + description: |- + JWT ID token containing user identity information (OpenID Connect). + Only present when the `openid` scope was granted. + This is a signed JWT that can be decoded and verified using the instance's public keys. + required: + - access_token + - token_type + OAuth.UserInfo: + type: object + additionalProperties: false + properties: + object: + type: string + instance_id: + type: string + user_id: + type: string + sub: + type: string + email: + type: string + nullable: true + email_verified: + type: boolean + nullable: true + family_name: + type: string + nullable: true + given_name: + type: string + nullable: true + name: + type: string + nullable: true + username: + type: string + nullable: true + deprecated: true + description: Deprecated. Use `preferred_username` instead. + preferred_username: + type: string + nullable: true + picture: + type: string + nullable: true + public_metadata: + type: object + nullable: true + additionalProperties: true + private_metadata: + type: object + nullable: true + additionalProperties: true + unsafe_metadata: + type: object + nullable: true + additionalProperties: true + org_id: + type: string + nullable: true + description: |- + The organization ID associated with the access token. + Present when the `user:org:read` scope was granted and the user + belongs to an organization. The value is determined at authorization + time from the explicit `organization_id` consent parameter, the + session's active organization, or the user's last active organization. + org_name: + type: string + nullable: true + description: The name of the user's organization associated with the access token. + org_slug: + type: string + nullable: true + description: The slug of the user's organization associated with the access token. + required: + - object + - instance_id + - user_id + - sub + OAuth.TokenActor: + type: object + additionalProperties: false + description: |- + The RFC 8693 section 4.1 actor chain of a token issued by the token + exchange: the client acting for the user, with the client the subject + token was issued to nested as the prior actor. Absent on tokens from + every other grant. + properties: + iss: + type: string + description: Issuer of the actor's identity; the token's own issuer. + sub: + type: string + description: The acting client's client_id. + act: + $ref: '#/components/schemas/OAuth.TokenActor' + required: + - iss + - sub + OAuth.TokenInfo: + type: object + additionalProperties: false + properties: + active: + type: boolean + description: Boolean indicator of whether or not the presented token is currently active. + client_id: + type: string + description: Client identifier for the OAuth 2.0 client that requested this token. + nullable: true + iat: + type: integer + description: Integer timestamp, measured in the number of seconds since January 1 1970 UTC, indicating when this token was originally issued, as defined in JWT [RFC7519]. + nullable: true + scope: + type: string + description: A JSON string containing a space-separated list of scopes associated with this token, in the format described in Section 3.3 of OAuth 2.0 [RFC6749]. + nullable: true + sub: + type: string + description: Subject of the token, as defined in JWT [RFC7519]. + nullable: true + aud: + type: array + items: + type: string + description: |- + Audience identifiers for this access token, as defined in JWT [RFC7519]. + When the authorization grant has an RFC 8707 resource, this array contains + the corresponding audience identifier. + nullable: true + org_id: + type: string + description: |- + The organization ID associated with the token. + Present when the `user:org:read` scope was granted and the user + belongs to an organization. + nullable: true + act: + $ref: '#/components/schemas/OAuth.TokenActor' + required: + - active + OAuth.ScopeWithDescription: + type: object + additionalProperties: false + properties: + scope: + type: string + description: + type: string + nullable: true + requires_consent: + type: boolean + description: Whether the scope requires user consent + required: + - scope + - requires_consent + OAuth.ConsentInfo: + type: object + additionalProperties: false + properties: + oauth_application_name: + type: string + oauth_application_logo_url: + type: string + nullable: true + description: URL to the OAuth application's logo image + oauth_application_url: + type: string + nullable: true + description: URL to the OAuth application's website or homepage + client_id: + type: string + redirect_domain: + type: string + nullable: true + description: | + The PSL-resolved registrable domain of the redirect URI (e.g. `example.co.uk` for + `https://app.example.co.uk/callback`). Null when no `redirect_uri` query parameter + was provided, when the URI is not registered for the OAuth application, or when the + URI points to an IP address or localhost. The frontend should fall back to client-side + display logic when this field is null. + scopes: + type: array + items: + $ref: '#/components/schemas/OAuth.ScopeWithDescription' + required: + - oauth_application_name + - client_id + - redirect_domain + - scopes + OAuth.DeviceVerification: + type: object + additionalProperties: false + properties: + object: + type: string + example: oauth_device_verification + status: + type: string + enum: + - approved + - denied + required: + - object + - status + OAuth.DeviceAuthorizationInfo: + type: object + additionalProperties: false + properties: + oauth_application_name: + type: string + oauth_application_logo_url: + type: string + nullable: true + description: URL to the OAuth application's logo image. + client_id: + type: string + scopes: + type: array + items: + $ref: '#/components/schemas/OAuth.ScopeWithDescription' + status: + type: string + enum: + - pending + - approved + - denied + - consumed + expires_at: + type: integer + format: int64 + description: Expiration time as Unix milliseconds. + required: + - oauth_application_name + - client_id + - scopes + - status + - expires_at + Token: + type: object + additionalProperties: false + properties: + object: + type: string + description: | + String representing the object's type. Objects of the same type share the same value. + enum: + - token + jwt: + type: string + description: | + String representing the encoded JWT value. + required: + - object + - jwt + Client.SessionBase: + type: object + properties: + id: + type: string + object: + type: string + description: | + String representing the object's type. Objects of the same type share the same value. + enum: + - session + status: + type: string + enum: + - active + - revoked + - ended + - expired + - removed + - abandoned + expire_at: + description: Unix timestamp in milliseconds. Zero means no maximum lifetime. + type: integer + format: int64 + abandon_at: + description: Unix timestamp in milliseconds. Zero means no abandonment deadline. + type: integer + format: int64 + last_active_at: + type: integer + format: int64 + last_active_token: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Token' + actor: + type: object + nullable: true + additionalProperties: true + required: + - id + - object + - status + - expire_at + - abandon_at + - last_active_at + Stubs.Verification.OTP: + type: object + properties: + object: + type: string + enum: + - verification_otp + status: + type: string + enum: + - unverified + - verified + - failed + - expired + strategy: + type: string + enum: + - phone_code + - email_code + - reset_password_email_code + - reset_password_phone_code + attempts: + type: integer + nullable: true + expire_at: + type: integer + required: + - status + - strategy + - expire_at + Stubs.Verification.Invitation: + type: object + additionalProperties: false + properties: + object: + type: string + enum: + - verification_invitation + status: + type: string + enum: + - verified + strategy: + type: string + enum: + - invitation + attempts: + type: integer + nullable: true + expire_at: + type: integer + nullable: true + required: + - status + - strategy + Stubs.Verification.Link: + type: object + properties: + object: + type: string + enum: + - verification_email_link + status: + type: string + enum: + - unverified + - verified + - failed + - expired + - transferable + strategy: + type: string + enum: + - email_link + attempts: + type: integer + nullable: true + expire_at: + type: integer + verified_at_client: + type: string + required: + - status + - strategy + - expire_at + Stubs.Verification.Ticket: + type: object + properties: + object: + type: string + enum: + - verification_ticket + status: + type: string + enum: + - unverified + - verified + - expired + strategy: + type: string + enum: + - ticket + attempts: + type: integer + nullable: true + expire_at: + type: integer + nullable: true + required: + - status + - strategy + Stubs.Verification.Admin: + type: object + additionalProperties: false + properties: + object: + type: string + enum: + - verification_admin + status: + type: string + enum: + - verified + - unverified + - failed + - expired + strategy: + type: string + enum: + - admin + attempts: + type: integer + nullable: true + expire_at: + type: integer + nullable: true + required: + - status + - strategy + Stubs.Verification.FromOauth: + type: object + properties: + object: + type: string + enum: + - verification_from_oauth + status: + type: string + enum: + - verified + - unverified + strategy: + type: string + pattern: ^from_oauth_[a-z0-9_]+$ + attempts: + type: integer + nullable: true + expire_at: + type: integer + nullable: true + required: + - status + - strategy + Stubs.Verification.SAML: + type: object + properties: + object: + type: string + enum: + - verification_saml + status: + type: string + enum: + - unverified + - verified + - failed + - expired + - transferable + strategy: + type: string + enum: + - saml + external_verification_redirect_url: + nullable: true + type: string + error: + allOf: + - $ref: '#/components/schemas/ClerkError' + - type: object + nullable: true + expire_at: + type: integer + nullable: true + attempts: + type: integer + nullable: true + required: + - status + - strategy + Stubs.Verification.SCIM: + type: object + properties: + object: + type: string + enum: + - verification_scim + status: + type: string + enum: + - verified + strategy: + type: string + enum: + - scim + attempts: + type: integer + nullable: true + expire_at: + type: integer + nullable: true + required: + - status + - strategy + Stubs.Identification.Link: + type: object + additionalProperties: false + properties: + type: + type: string + pattern: ^(?:oauth_[a-z0-9_]+|saml)$ + id: + type: string + required: + - type + - id + Client.EmailAddress: + type: object + additionalProperties: false + properties: + id: + type: string + object: + type: string + description: | + String representing the object's type. Objects of the same type share the same value. + enum: + - email_address + email_address: + type: string + reserved: + type: boolean + verification: + type: object + nullable: true + oneOf: + - $ref: '#/components/schemas/Stubs.Verification.OTP' + - $ref: '#/components/schemas/Stubs.Verification.Invitation' + - $ref: '#/components/schemas/Stubs.Verification.Link' + - $ref: '#/components/schemas/Stubs.Verification.Ticket' + - $ref: '#/components/schemas/Stubs.Verification.Admin' + - $ref: '#/components/schemas/Stubs.Verification.FromOauth' + - $ref: '#/components/schemas/Stubs.Verification.SAML' + - $ref: '#/components/schemas/Stubs.Verification.SCIM' + linked_to: + type: array + items: + $ref: '#/components/schemas/Stubs.Identification.Link' + matches_sso_connection: + description: | + Indicates whether this email address domain matches an active enterprise connection. + type: boolean + created_at: + type: integer + format: int64 + description: | + Unix timestamp of creation + updated_at: + type: integer + format: int64 + description: | + Unix timestamp of creation + required: + - id + - object + - email_address + - verification + - linked_to + - reserved + - created_at + - updated_at + Client.PhoneNumber: + type: object + additionalProperties: false + properties: + id: + type: string + object: + type: string + description: | + String representing the object's type. Objects of the same type share the same value. + enum: + - phone_number + phone_number: + type: string + reserved_for_second_factor: + type: boolean + default_second_factor: + type: boolean + reserved: + type: boolean + verification: + nullable: true + type: object + oneOf: + - $ref: '#/components/schemas/Stubs.Verification.OTP' + - $ref: '#/components/schemas/Stubs.Verification.Admin' + linked_to: + type: array + items: + $ref: '#/components/schemas/Stubs.Identification.Link' + backup_codes: + type: array + items: + type: string + nullable: true + created_at: + type: integer + format: int64 + description: | + Unix timestamp of creation + updated_at: + type: integer + format: int64 + description: | + Unix timestamp of creation + required: + - id + - object + - phone_number + - verification + - linked_to + - reserved + - created_at + - updated_at + Stubs.Verification.Web3Signature: + type: object + properties: + object: + type: string + enum: + - verification_web3 + status: + type: string + enum: + - unverified + - verified + - failed + - expired + strategy: + type: string + enum: + - web3_metamask_signature + - web3_base_signature + - web3_coinbase_wallet_signature + - web3_okx_wallet_signature + - web3_solana_signature + attempts: + type: integer + nullable: true + expire_at: + type: integer + nullable: true + nonce: + type: string + nullable: true + message: + type: string + nullable: true + required: + - status + - strategy + Client.Web3Wallet: + type: object + additionalProperties: false + properties: + id: + type: string + object: + type: string + description: | + String representing the object's type. Objects of the same type share the same value. + enum: + - web3_wallet + web3_wallet: + type: string + verification: + nullable: true + type: object + oneOf: + - $ref: '#/components/schemas/Stubs.Verification.Web3Signature' + - $ref: '#/components/schemas/Stubs.Verification.Admin' + created_at: + type: integer + format: int64 + description: | + Unix timestamp of creation + updated_at: + type: integer + format: int64 + description: | + Unix timestamp of creation + required: + - id + - object + - web3_wallet + - verification + - created_at + - updated_at + Stubs.Verification.Passkey: + type: object + additionalProperties: false + properties: + object: + type: string + enum: + - verification_passkey + status: + type: string + enum: + - unverified + - verified + - failed + - expired + strategy: + type: string + enum: + - passkey + attempts: + type: integer + nullable: true + expire_at: + type: integer + nonce: + type: string + required: + - status + - strategy + - expire_at + Client.Passkey: + type: object + additionalProperties: false + properties: + id: + type: string + object: + type: string + description: | + String representing the object's type. Objects of the same type share the same value. + enum: + - passkey + name: + type: string + last_used_at: + type: integer + format: int64 + description: | + Unix timestamp of when the passkey was last used. + nullable: true + verification: + nullable: true + type: object + oneOf: + - $ref: '#/components/schemas/Stubs.Verification.Passkey' + created_at: + type: integer + format: int64 + description: | + Unix timestamp of creation + updated_at: + type: integer + format: int64 + description: | + Unix timestamp of update + required: + - id + - object + - name + - verification + Client.Organization: + type: object + additionalProperties: false + properties: + id: + type: string + object: + type: string + description: | + String representing the object's type. Objects of the same type share the same value. + enum: + - organization + name: + type: string + slug: + type: string + image_url: + type: string + has_image: + type: boolean + members_count: + type: integer + pending_invitations_count: + type: integer + max_allowed_memberships: + type: integer + admin_delete_enabled: + type: boolean + self_serve_sso_enabled: + type: boolean + description: | + Whether this organization can configure self-serve enterprise SSO. + self_serve_directory_sync_enabled: + type: boolean + description: | + Whether this organization can configure self-serve Directory Sync (SCIM). + public_metadata: + type: object + additionalProperties: true + created_at: + type: integer + format: int64 + description: | + Unix timestamp of creation. + updated_at: + type: integer + format: int64 + description: | + Unix timestamp of last update. + logo_url: + type: string + nullable: true + deprecated: true + description: Deprecated. Use `image_url` instead. + required: + - object + - id + - name + - slug + - has_image + - max_allowed_memberships + - admin_delete_enabled + - public_metadata + - created_at + - updated_at + Client.PublicUserData: + type: object + additionalProperties: false + properties: + first_name: + type: string + nullable: true + last_name: + type: string + nullable: true + image_url: + type: string + nullable: true + has_image: + type: boolean + identifier: + type: string + profile_image_url: + type: string + nullable: true + deprecated: true + description: Use `image_url` instead. + user_id: + type: string + nullable: true + username: + type: string + nullable: true + banned: + type: boolean + deprovisioned: + type: boolean + description: | + Flag to denote whether the user has been deprovisioned via SCIM and is restricted from signing in. + required: + - first_name + - last_name + - identifier + - has_image + Client.OrganizationMembership: + type: object + properties: + id: + type: string + object: + type: string + description: | + String representing the object's type. Objects of the same type share the same value. + enum: + - organization_membership + public_metadata: + type: object + additionalProperties: true + role: + type: string + role_name: + type: string + permissions: + type: array + nullable: true + items: + type: string + created_at: + type: integer + format: int64 + description: Unix timestamp of creation. + updated_at: + type: integer + format: int64 + description: Unix timestamp of last update. + organization: + $ref: '#/components/schemas/Client.Organization' + public_user_data: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Client.PublicUserData' + required: + - object + - id + - public_metadata + - role + - role_name + - permissions + - created_at + - updated_at + - organization + verification_oauth: + x-speakeasy-name-override: Oauth + type: object + additionalProperties: false + properties: + object: + type: string + enum: + - verification_oauth + status: + type: string + x-speakeasy-unknown-values: allow + enum: + - unverified + - verified + - failed + - expired + - transferable + strategy: + type: string + x-speakeasy-unknown-values: allow + pattern: ^oauth_(?:(?:token_)|(?:custom_))?[a-z]+$ + external_verification_redirect_url: + type: string + error: + type: object + nullable: true + oneOf: + - $ref: '#/components/schemas/ClerkError' + expire_at: + type: integer + attempts: + type: integer + nullable: true + verified_at_client: + type: string + nullable: true + required: + - status + - strategy + - attempts + - expire_at + verification_google_one_tap: + x-speakeasy-name-override: GoogleOneTap + type: object + additionalProperties: false + properties: + object: + type: string + enum: + - verification_google_one_tap + status: + type: string + enum: + - unverified + - verified + strategy: + type: string + enum: + - google_one_tap + expire_at: + type: integer + nullable: true + attempts: + type: integer + nullable: true + verified_at_client: + type: string + nullable: true + error: + type: object + nullable: true + oneOf: + - $ref: '#/components/schemas/ClerkError' + required: + - status + - strategy + - attempts + - expire_at + ExternalAccountWithVerification: + type: object + additionalProperties: true + properties: + object: + type: string + description: String representing the object's type. Objects of the same type share the same value. + enum: + - external_account + - facebook_account + - google_account + id: + type: string + provider: + type: string + identification_id: + type: string + provider_user_id: + description: The unique ID of the user in the external provider's system + type: string + approved_scopes: + type: string + email_address: + type: string + email_address_verified: + type: boolean + nullable: true + description: | + Whether the email was verified by the OAuth provider at creation time. null = unknown (pre-migration data or custom OAuth providers), true = provider confirmed email was verified, false = provider confirmed email was NOT verified + first_name: + type: string + last_name: + type: string + avatar_url: + type: string + deprecated: true + description: Please use `image_url` instead + image_url: + type: string + nullable: true + username: + type: string + nullable: true + phone_number: + type: string + nullable: true + public_metadata: + type: object + additionalProperties: true + label: + type: string + nullable: true + created_at: + type: integer + format: int64 + description: | + Unix timestamp of creation + updated_at: + type: integer + format: int64 + description: | + Unix timestamp of creation + verification: + type: object + nullable: true + oneOf: + - $ref: '#/components/schemas/verification_oauth' + - $ref: '#/components/schemas/verification_google_one_tap' + discriminator: + propertyName: object + required: + - object + - id + - provider + - identification_id + - provider_user_id + - approved_scopes + - email_address + - first_name + - last_name + - public_metadata + - created_at + - updated_at + - verification + Stubs.SAMLConnection.SAMLAccount: + type: object + additionalProperties: false + properties: + id: + type: string + name: + type: string + domain: + type: string + deprecated: true + domains: + type: array + items: + type: string + active: + type: boolean + provider: + type: string + sync_user_attributes: + type: boolean + allow_subdomains: + type: boolean + allow_idp_initiated: + type: boolean + disable_additional_identifications: + type: boolean + allow_organization_account_linking: + type: boolean + created_at: + type: integer + format: int64 + description: | + Unix timestamp of creation. + updated_at: + type: integer + format: int64 + description: | + Unix timestamp of last update. + required: + - id + - name + - active + - provider + - sync_user_attributes + - created_at + - updated_at + anyOf: + - required: + - domain + - required: + - domains + Client.SAMLAccount: + type: object + additionalProperties: false + properties: + id: + type: string + object: + type: string + description: | + String representing the object's type. Objects of the same type share the same value. + enum: + - saml_account + provider: + type: string + active: + type: boolean + email_address: + type: string + first_name: + type: string + nullable: true + last_name: + type: string + nullable: true + provider_user_id: + description: The unique ID of the user in the external provider's system + type: string + nullable: true + enterprise_connection_id: + type: string + nullable: true + last_authenticated_at: + type: integer + format: int64 + nullable: true + description: | + Unix timestamp of last authentication. + public_metadata: + type: object + additionalProperties: true + verification: + type: object + nullable: true + oneOf: + - $ref: '#/components/schemas/Stubs.Verification.SAML' + - $ref: '#/components/schemas/Stubs.Verification.Ticket' + saml_connection: + type: object + nullable: true + oneOf: + - $ref: '#/components/schemas/Stubs.SAMLConnection.SAMLAccount' + required: + - id + - object + - provider + - active + - email_address + - first_name + - last_name + - provider_user_id + - public_metadata + - saml_connection + - verification + Client.User: + type: object + properties: + id: + type: string + object: + type: string + description: | + String representing the object's type. Objects of the same type share the same value. + enum: + - user + username: + nullable: true + type: string + first_name: + nullable: true + type: string + last_name: + nullable: true + type: string + image_url: + type: string + has_image: + type: boolean + primary_email_address_id: + nullable: true + type: string + primary_phone_number_id: + nullable: true + type: string + primary_web3_wallet_id: + nullable: true + type: string + password_enabled: + type: boolean + two_factor_enabled: + type: boolean + totp_enabled: + type: boolean + backup_code_enabled: + type: boolean + email_addresses: + type: array + items: + $ref: '#/components/schemas/Client.EmailAddress' + phone_numbers: + type: array + items: + $ref: '#/components/schemas/Client.PhoneNumber' + web3_wallets: + type: array + items: + $ref: '#/components/schemas/Client.Web3Wallet' + passkeys: + type: array + items: + $ref: '#/components/schemas/Client.Passkey' + organization_memberships: + type: array + items: + $ref: '#/components/schemas/Client.OrganizationMembership' + external_accounts: + type: array + items: + $ref: '#/components/schemas/ExternalAccountWithVerification' + saml_accounts: + type: array + items: + $ref: '#/components/schemas/Client.SAMLAccount' + password_last_updated_at: + nullable: true + type: integer + format: int64 + description: Unix timestamp of last update. + example: 1700690400000 + public_metadata: + type: object + additionalProperties: true + private_metadata: + type: object + additionalProperties: true + unsafe_metadata: + type: object + additionalProperties: true + external_id: + nullable: true + type: string + last_sign_in_at: + type: integer + format: int64 + nullable: true + description: Unix timestamp of last sign-in. + example: 1700690400000 + banned: + type: boolean + description: Flag to denote whether user is banned or not. + locked: + type: boolean + description: | + Flag to denote whether user is currently locked, i.e. restricted from signing in or not. + deprovisioned: + type: boolean + description: | + Flag to denote whether the user has been deprovisioned via SCIM and is restricted from signing in. Only present on instances with SCIM enabled. + lockout_expires_in_seconds: + type: integer + format: int64 + nullable: true + description: | + The number of seconds remaining until the lockout period expires for a locked user. A null value for a locked user indicates that lockout never expires. + example: 300 + verification_attempts_remaining: + type: integer + format: int64 + nullable: true + description: | + The number of verification attempts remaining until the user is locked. Null if account lockout is not enabled. Note: if a user is locked explicitly via the Backend API, they may still have verification attempts remaining. + created_at: + type: integer + format: int64 + description: Unix timestamp of creation. + example: 1700690400000 + updated_at: + type: integer + format: int64 + description: Unix timestamp of last update. + example: 1700690400000 + delete_self_enabled: + type: boolean + description: If enabled, user can delete themselves via FAPI. + create_organization_enabled: + type: boolean + description: If enabled, user can create organizations via FAPI. + create_organizations_limit: + type: integer + description: The maximum number of organizations the user can create. 0 means unlimited. + last_active_at: + type: integer + format: int64 + nullable: true + description: Unix timestamp of the latest session activity, with day precision. + example: 1700690400000 + mfa_enabled_at: + type: integer + format: int64 + nullable: true + description: Unix timestamp at which the user enabled MFA. + example: 1700690400000 + mfa_disabled_at: + type: integer + format: int64 + nullable: true + description: Unix timestamp at which the user disabled MFA. + example: 1700690400000 + legal_accepted_at: + type: integer + format: int64 + nullable: true + description: Unix timestamp at which the user accepted the legal requirements. + example: 1700690400000 + profile_image_url: + type: string + deprecated: true + description: Deprecated. Use `image_url` instead. + required: + - id + - object + - username + - first_name + - last_name + - has_image + - primary_email_address_id + - primary_phone_number_id + - primary_web3_wallet_id + - password_enabled + - two_factor_enabled + - totp_enabled + - backup_code_enabled + - email_addresses + - phone_numbers + - web3_wallets + - passkeys + - external_accounts + - saml_accounts + - public_metadata + - external_id + - last_sign_in_at + - banned + - locked + - lockout_expires_in_seconds + - verification_attempts_remaining + - created_at + - updated_at + - delete_self_enabled + - create_organization_enabled + - last_active_at + - mfa_enabled_at + - mfa_disabled_at + - legal_accepted_at + Client.Session: + allOf: + - $ref: '#/components/schemas/Client.SessionBase' + - type: object + properties: + last_active_organization_id: + type: string + nullable: true + user: + $ref: '#/components/schemas/Client.User' + public_user_data: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Client.PublicUserData' + factor_verification_age: + type: array + description: Each item represents the minutes that have passed since the last time a first or second factor were verified. + items: + type: integer + created_at: + type: integer + format: int64 + description: Unix timestamp of creation. + example: 1700690400000 + updated_at: + type: integer + format: int64 + description: Unix timestamp of last update. + example: 1700690400000 + required: + - last_active_organization_id + - public_user_data + - factor_verification_age + - created_at + - updated_at + Stubs.SignInFactor: + type: object + additionalProperties: false + properties: + strategy: + type: string + enum: + - ticket + - password + - email_code + - email_link + - phone_code + - web3_metamask_signature + - web3_base_signature + - web3_coinbase_wallet_signature + - web3_okx_wallet_signature + - web3_solana_signature + - totp + - backup_code + - oauth_apple + - oauth_google + - oauth_facebook + - oauth_hubspot + - oauth_github + - oauth_microsoft + - oauth_mock + - oauth_custom_mock + - oauth_token_mock + - saml + - enterprise_sso + - reset_password_email_code + - reset_password_phone_code + - passkey + - trusted_device + - google_one_tap + safe_identifier: + type: string + enterprise_connection_id: + type: string + enterprise_connection_name: + type: string + enterprise_connection_logo_public_url: + type: string + enterprise_connection_provider: + type: string + email_address_id: + type: string + description: |- + The `email_address_id` to pass back to `prepare_first_factor` for this factor, unchanged. + + Treat this as an opaque value scoped to this sign-in. It is stable for the life of the sign-in, but + it is not guaranteed to be an identification ID, to be the same on a later sign-in, or to be usable + anywhere else, including the Backend API. + phone_number_id: + type: string + description: |- + The `phone_number_id` to pass back to `prepare_first_factor` for this factor, unchanged. + + Treat this as an opaque value scoped to this sign-in. It is stable for the life of the sign-in, but + it is not guaranteed to be an identification ID, to be the same on a later sign-in, or to be usable + anywhere else, including the Backend API. + web3_wallet_id: + type: string + description: |- + The `web3_wallet_id` to pass back to `prepare_first_factor` for this factor, unchanged. + + Treat this as an opaque value scoped to this sign-in. It is stable for the life of the sign-in, but + it is not guaranteed to be an identification ID, to be the same on a later sign-in, or to be usable + anywhere else, including the Backend API. + passkey_id: + type: string + trusted_device_id: + type: string + primary: + type: boolean + nullable: true + external_verification_redirect_url: + nullable: true + type: string + default: + type: boolean + required: + - strategy + Stubs.Verification.Password: + type: object + additionalProperties: false + properties: + object: + type: string + enum: + - verification_password + status: + type: string + enum: + - unverified + - verified + strategy: + type: string + enum: + - password + attempts: + type: integer + nullable: true + expire_at: + type: integer + nullable: true + required: + - status + - strategy + Stubs.Verification.Oauth: + type: object + additionalProperties: false + properties: + object: + type: string + enum: + - verification_oauth + status: + type: string + enum: + - unverified + - verified + - failed + - expired + - transferable + strategy: + type: string + pattern: ^oauth_(?:(?:token_)|(?:custom_))?[a-z0-9_]+$ + external_verification_redirect_url: + nullable: true + type: string + error: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/ClerkError' + expire_at: + type: integer + attempts: + type: integer + nullable: true + required: + - status + - strategy + - expire_at + Stubs.Verification.TrustedDevice: + type: object + additionalProperties: false + properties: + object: + type: string + enum: + - verification_trusted_device + status: + type: string + enum: + - unverified + - verified + - failed + - expired + strategy: + type: string + enum: + - trusted_device + attempts: + type: integer + nullable: true + expire_at: + type: integer + nullable: true + trusted_device_challenge: + type: object + nullable: true + additionalProperties: false + properties: + object: + type: string + enum: + - trusted_device_challenge + challenge: + type: string + challenge_id: + type: string + trusted_device_id: + type: string + client_data: + type: string + expires_at: + type: integer + format: int64 + algorithm: + type: string + enum: + - ES256 + required: + - object + - challenge + - challenge_id + - trusted_device_id + - client_data + - expires_at + - algorithm + required: + - status + - strategy + - expire_at + Stubs.Verification.GoogleOneTap: + type: object + additionalProperties: false + properties: + object: + type: string + enum: + - verification_google_one_tap + status: + type: string + enum: + - unverified + - verified + strategy: + type: string + enum: + - google_one_tap + expire_at: + type: integer + nullable: true + attempts: + type: integer + nullable: true + required: + - status + - strategy + Stubs.Verification.TOTP: + type: object + additionalProperties: false + properties: + object: + type: string + enum: + - verification_totp + status: + type: string + enum: + - unverified + - verified + strategy: + type: string + enum: + - totp + attempts: + type: integer + nullable: true + expire_at: + type: integer + nullable: true + required: + - status + - strategy + Stubs.Verification.BackupCode: + type: object + additionalProperties: false + properties: + object: + type: string + enum: + - verification_backup_code + status: + type: string + enum: + - unverified + - verified + strategy: + type: string + enum: + - backup_code + attempts: + type: integer + nullable: true + expire_at: + type: integer + nullable: true + required: + - status + - strategy + Client.SignIn: + type: object + additionalProperties: false + properties: + object: + type: string + description: String representing the object's type. Objects of the same type share the same value. + enum: + - sign_in_attempt + id: + type: string + status: + type: string + enum: + - abandoned + - needs_identifier + - needs_first_factor + - needs_second_factor + - needs_client_trust + - needs_new_password + - needs_protect_check + - complete + supported_identifiers: + type: array + description: List of supported identifiers that can be used to sign in. + items: + type: string + enum: + - email_address + - phone_number + - username + - web3_wallet + - passkey + supported_first_factors: + type: array + nullable: true + items: + $ref: '#/components/schemas/Stubs.SignInFactor' + sso_bypass_first_factors: + type: array + description: | + Factors a user routed to an enterprise connection may use when they cannot reach their identity provider. Absent unless the instance allows SSO bypass and the address belongs to a user on the bypass allowlist of a connection serving it. Pass the factor's `email_address_id` unchanged to `prepare_first_factor`. Deliberately separate from `supported_first_factors`, which callers rely on containing only enterprise factors before redirecting to the provider. + items: + $ref: '#/components/schemas/Stubs.SignInFactor' + supported_second_factors: + type: array + nullable: true + items: + $ref: '#/components/schemas/Stubs.SignInFactor' + first_factor_verification: + type: object + nullable: true + oneOf: + - $ref: '#/components/schemas/Stubs.Verification.Password' + - $ref: '#/components/schemas/Stubs.Verification.Oauth' + - $ref: '#/components/schemas/Stubs.Verification.OTP' + - $ref: '#/components/schemas/Stubs.Verification.Link' + - $ref: '#/components/schemas/Stubs.Verification.Web3Signature' + - $ref: '#/components/schemas/Stubs.Verification.Ticket' + - $ref: '#/components/schemas/Stubs.Verification.SAML' + - $ref: '#/components/schemas/Stubs.Verification.Passkey' + - $ref: '#/components/schemas/Stubs.Verification.TrustedDevice' + - $ref: '#/components/schemas/Stubs.Verification.GoogleOneTap' + second_factor_verification: + type: object + nullable: true + oneOf: + - $ref: '#/components/schemas/Stubs.Verification.OTP' + - $ref: '#/components/schemas/Stubs.Verification.TOTP' + - $ref: '#/components/schemas/Stubs.Verification.Ticket' + - $ref: '#/components/schemas/Stubs.Verification.BackupCode' + - $ref: '#/components/schemas/Stubs.Verification.Passkey' + identifier: + nullable: true + type: string + user_data: + type: object + additionalProperties: false + nullable: true + properties: + first_name: + type: string + nullable: true + last_name: + type: string + nullable: true + image_url: + type: string + has_image: + type: boolean + profile_image_url: + type: string + nullable: true + deprecated: true + description: Use `image_url` instead. + required: + - first_name + - last_name + - has_image + created_session_id: + nullable: true + type: string + abandon_at: + type: integer + format: int64 + description: Unix timestamp at which the sign in will be abandoned. + example: 1700690400000 + timezone: + type: string + nullable: true + description: The IANA timezone recorded when the sign-in attempt was created. + example: America/Los_Angeles + client_trust_state: + type: string + nullable: true + description: | + The Device Trust state of this sign-in attempt. - `pending`: The identifier has not been set yet. - `new`: The user has not had a session on this device before. - `known`: The user has had a session on this device before. + enum: + - pending + - new + - known + required: + - object + - id + - status + - supported_identifiers + - supported_first_factors + - supported_second_factors + - first_factor_verification + - second_factor_verification + - identifier + - user_data + - created_session_id + - abandon_at + Stubs.SignUpVerification.AdditionalFields: + type: object + properties: + next_action: + type: string + enum: + - needs_prepare + - needs_attempt + - '' + supported_strategies: + type: array + items: + type: string + required: + - next_action + - supported_strategies + Client.SignUp.Verifications: + type: object + properties: + email_address: + type: object + nullable: true + oneOf: + - allOf: + - $ref: '#/components/schemas/Stubs.Verification.OTP' + - $ref: '#/components/schemas/Stubs.SignUpVerification.AdditionalFields' + - allOf: + - $ref: '#/components/schemas/Stubs.Verification.Link' + - $ref: '#/components/schemas/Stubs.SignUpVerification.AdditionalFields' + - allOf: + - $ref: '#/components/schemas/Stubs.Verification.Ticket' + - $ref: '#/components/schemas/Stubs.SignUpVerification.AdditionalFields' + - allOf: + - $ref: '#/components/schemas/Stubs.Verification.FromOauth' + - $ref: '#/components/schemas/Stubs.SignUpVerification.AdditionalFields' + - allOf: + - $ref: '#/components/schemas/Stubs.Verification.SAML' + - $ref: '#/components/schemas/Stubs.SignUpVerification.AdditionalFields' + phone_number: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Stubs.Verification.OTP' + - $ref: '#/components/schemas/Stubs.SignUpVerification.AdditionalFields' + web3_wallet: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Stubs.Verification.Web3Signature' + - $ref: '#/components/schemas/Stubs.SignUpVerification.AdditionalFields' + external_account: + type: object + nullable: true + oneOf: + - $ref: '#/components/schemas/Stubs.Verification.Oauth' + - $ref: '#/components/schemas/Stubs.Verification.SAML' + - $ref: '#/components/schemas/Stubs.Verification.Ticket' + - $ref: '#/components/schemas/Stubs.Verification.GoogleOneTap' + required: + - email_address + - phone_number + - web3_wallet + - external_account + Client.SignUp: + type: object + properties: + object: + type: string + description: | + String representing the object's type. Objects of the same type share the same value. + enum: + - sign_up_attempt + id: + type: string + description: Unique identifier for this sign up. + status: + type: string + enum: + - abandoned + - missing_requirements + - complete + required_fields: + type: array + items: + type: string + description: | + List of required fields which need to be supplied to the current sign-up. These fields are mandatory in order for the sign-up to satisfy the attached registration policy and be marked as complete. + optional_fields: + type: array + items: + type: string + description: | + List of optional fields which can be supplied to the current sign-up. These fields are not required and their absence does not prevent the sign-up to be marked as complete. + missing_fields: + type: array + items: + type: string + description: | + List of the missing fields which still need to be supplied to the current sign-up. These fields are mandatory in order for the sign-up to satisfy the attached registration policy and be marked as complete. + unverified_fields: + type: array + items: + type: string + description: | + List of fields which are already supplied to the current sign-up but they need to be verified. Example of such fields are email addresses and phone numbers. + verifications: + description: | + Group for all available verifications. + allOf: + - $ref: '#/components/schemas/Client.SignUp.Verifications' + username: + type: string + nullable: true + email_address: + type: string + nullable: true + phone_number: + type: string + nullable: true + web3_wallet: + type: string + nullable: true + password_enabled: + type: boolean + first_name: + type: string + nullable: true + last_name: + type: string + nullable: true + unsafe_metadata: + description: | + Custom JSON that callers can use to store arbitrary values that make sense in the context of the current sign up. + type: object + additionalProperties: true + public_metadata: + description: | + Custom JSON that can be used to store arbitrary values which will end up in the user's public metadata. This field can only be populated from the application's BE. At this point, this can be done via invitations. + type: object + additionalProperties: true + custom_action: + type: boolean + external_id: + type: string + nullable: true + created_session_id: + type: string + nullable: true + created_user_id: + type: string + nullable: true + abandon_at: + type: integer + format: int64 + description: Unix timestamp at which the sign up will be abandoned. + example: 1700690400000 + legal_accepted_at: + type: integer + format: int64 + nullable: true + description: Unix timestamp at which the user accepted the legal requirements. + example: 1700690400000 + timezone: + type: string + nullable: true + description: The IANA timezone recorded when the sign-up attempt was created. + example: America/Los_Angeles + required: + - object + - id + - status + - required_fields + - optional_fields + - missing_fields + - unverified_fields + - verifications + - username + - email_address + - phone_number + - web3_wallet + - password_enabled + - first_name + - last_name + - custom_action + - external_id + - created_session_id + - created_user_id + - abandon_at + - legal_accepted_at + Client.Client: + type: object + nullable: true + properties: + object: + type: string + description: String representing the object's type. Objects of the same type share the same value. + enum: + - client + id: + type: string + description: String representing the identifier of the session. + sessions: + type: array + items: + $ref: '#/components/schemas/Client.Session' + sign_in: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Client.SignIn' + sign_up: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Client.SignUp' + last_active_session_id: + nullable: true + type: string + description: Last active session_id. + last_authentication_strategy: + nullable: true + type: string + description: | + The authentication strategy that was last used to authenticate the user on this client. + cookie_expires_at: + nullable: true + type: integer + format: int64 + description: Unix timestamp of the cookie expiration. + captcha_bypass: + type: boolean + description: Whether the client can bypass CAPTCHA. + created_at: + type: integer + format: int64 + description: Unix timestamp of creation. + updated_at: + type: integer + format: int64 + description: Unix timestamp of last update. + required: + - object + - id + - sessions + - sign_in + - sign_up + - last_active_session_id + - last_authentication_strategy + - cookie_expires_at + - captcha_bypass + - created_at + - updated_at + Client.AccountPortal: + type: object + additionalProperties: false + properties: + object: + type: string + enum: + - account_portal + allowed: + type: boolean + enabled: + type: boolean + internal_linking: + type: boolean + after_sign_in_url: + type: string + after_sign_up_url: + type: string + after_join_waitlist_url: + type: string + after_create_organization_url: + type: string + after_leave_organization_url: + type: string + logo_link_url: + type: string + customization: + type: object + properties: + appearance: + type: string + enum: + - auto + - dark + - light + nullable: true + background_color_dark: + type: string + nullable: true + background_color_light: + type: string + nullable: true + primary_color_dark: + type: string + nullable: true + primary_color_light: + type: string + nullable: true + required: + - appearance + - background_color_dark + - background_color_light + - primary_color_dark + - primary_color_light + required: + - object + - allowed + - enabled + - internal_linking + - after_sign_in_url + - after_sign_up_url + - after_join_waitlist_url + - after_create_organization_url + - after_leave_organization_url + - logo_link_url + - customization + Client.NativeSettings: + type: object + additionalProperties: false + properties: + object: + type: string + enum: + - native_settings + api_enabled: + type: boolean + trusted_device_sign_in_enabled: + type: boolean + trusted_device_enrollment_prompt_after_sign_in_enabled: + type: boolean + trusted_device_enrollment_prompt_after_sign_up_enabled: + type: boolean + required: + - object + - api_enabled + - trusted_device_sign_in_enabled + - trusted_device_enrollment_prompt_after_sign_in_enabled + - trusted_device_enrollment_prompt_after_sign_up_enabled + Client.AuthConfig: + type: object + additionalProperties: false + properties: + object: + type: string + enum: + - auth_config + id: + type: string + first_name: + type: string + enum: + - 'on' + - 'off' + - required + last_name: + type: string + enum: + - 'on' + - 'off' + - required + email_address: + type: string + enum: + - 'on' + - 'off' + - required + phone_number: + type: string + enum: + - 'on' + - 'off' + - required + username: + type: string + enum: + - 'on' + - 'off' + - required + password: + type: string + enum: + - 'on' + - 'off' + - required + identification_requirements: + type: array + items: + type: array + items: + type: string + identification_strategies: + type: array + items: + type: string + first_factors: + type: array + items: + type: string + second_factors: + type: array + items: + type: string + email_address_verification_strategies: + type: array + items: + type: string + single_session_mode: + type: boolean + enhanced_email_deliverability: + type: boolean + test_mode: + type: boolean + cookieless_dev: + type: boolean + description: Please use `url_based_session_syncing` instead + deprecated: true + url_based_session_syncing: + type: boolean + claimed_at: + type: integer + format: int64 + nullable: true + reverification: + type: boolean + native_settings: + $ref: '#/components/schemas/Client.NativeSettings' + required: + - object + - id + - first_name + - last_name + - email_address + - phone_number + - username + - password + - identification_requirements + - identification_strategies + - first_factors + - second_factors + - email_address_verification_strategies + - single_session_mode + - enhanced_email_deliverability + - test_mode + - url_based_session_syncing + - claimed_at + - reverification + - native_settings + Image: + type: object + properties: + object: + type: string + description: The object type. Always "image". + id: + type: string + description: Unique identifier for the image. + name: + type: string + description: The file name of the uploaded image. + public_url: + type: string + description: The publicly accessible URL for the image. + Client.DisplayConfig: + type: object + additionalProperties: false + properties: + object: + type: string + enum: + - display_config + id: + type: string + instance_environment_type: + type: string + enum: + - production + - development + - staging + application_name: + type: string + theme: + type: object + preferred_sign_in_strategy: + type: string + enum: + - password + - otp + logo_image_url: + nullable: true + type: string + favicon_image_url: + nullable: true + type: string + home_url: + type: string + sign_in_url: + type: string + sign_up_url: + type: string + user_profile_url: + type: string + waitlist_url: + type: string + oauth_consent_url: + type: string + description: URL of the OAuth consent screen for flows that require human consent. + oauth_device_verification_url: + type: string + after_sign_in_url: + type: string + after_sign_up_url: + type: string + after_sign_out_one_url: + type: string + after_sign_out_all_url: + type: string + after_switch_session_url: + type: string + after_join_waitlist_url: + type: string + organization_profile_url: + type: string + create_organization_url: + type: string + after_leave_organization_url: + type: string + after_create_organization_url: + type: string + logo_link_url: + type: string + support_email: + type: string + nullable: true + branded: + type: boolean + experimental_force_oauth_first: + type: boolean + clerk_js_version: + type: string + nullable: true + show_devmode_warning: + type: boolean + google_one_tap_client_id: + type: string + nullable: true + help_url: + type: string + nullable: true + privacy_policy_url: + type: string + nullable: true + terms_url: + type: string + nullable: true + logo_url: + nullable: true + type: string + deprecated: true + description: Use `logo_image_url` instead + favicon_url: + nullable: true + type: string + deprecated: true + description: Use `favicon_image_url` instead + logo_image: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Image' + deprecated: true + description: Use `logo_image_url` instead + favicon_image: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Image' + deprecated: true + description: Use `favicon_image_url` instead + captcha_public_key: + type: string + nullable: true + captcha_widget_type: + type: string + nullable: true + enum: + - smart + - invisible + captcha_public_key_invisible: + type: string + nullable: true + captcha_provider: + type: string + nullable: true + enum: + - turnstile + captcha_oauth_bypass: + type: array + items: + type: string + captcha_heartbeat: + type: boolean + nullable: true + required: + - object + - id + - instance_environment_type + - application_name + - theme + - preferred_sign_in_strategy + - home_url + - sign_in_url + - sign_up_url + - user_profile_url + - waitlist_url + - oauth_consent_url + - after_sign_in_url + - after_sign_up_url + - after_sign_out_one_url + - after_sign_out_all_url + - after_switch_session_url + - after_join_waitlist_url + - organization_profile_url + - create_organization_url + - after_leave_organization_url + - after_create_organization_url + - logo_link_url + - support_email + - branded + - experimental_force_oauth_first + - clerk_js_version + - show_devmode_warning + - google_one_tap_client_id + - help_url + - privacy_policy_url + - terms_url + - logo_url + - favicon_url + - logo_image + - favicon_image + - captcha_public_key + - captcha_widget_type + - captcha_public_key_invisible + - captcha_provider + - captcha_oauth_bypass + UserSettings.Attribute: + type: object + additionalProperties: false + properties: + enabled: + type: boolean + required: + type: boolean + used_for_first_factor: + type: boolean + first_factors: + type: array + items: + type: string + used_for_second_factor: + type: boolean + second_factors: + type: array + items: + type: string + verifications: + type: array + items: + type: string + verify_at_sign_up: + type: boolean + immutable: + type: boolean + required: + - enabled + - required + - used_for_first_factor + - first_factors + - used_for_second_factor + - second_factors + - verifications + - verify_at_sign_up + - immutable + UserSettings.Attributes: + type: object + additionalProperties: false + properties: + email_address: + $ref: '#/components/schemas/UserSettings.Attribute' + phone_number: + $ref: '#/components/schemas/UserSettings.Attribute' + username: + $ref: '#/components/schemas/UserSettings.Attribute' + web3_wallet: + $ref: '#/components/schemas/UserSettings.Attribute' + first_name: + $ref: '#/components/schemas/UserSettings.Attribute' + last_name: + $ref: '#/components/schemas/UserSettings.Attribute' + password: + $ref: '#/components/schemas/UserSettings.Attribute' + authenticator_app: + $ref: '#/components/schemas/UserSettings.Attribute' + ticket: + $ref: '#/components/schemas/UserSettings.Attribute' + backup_code: + $ref: '#/components/schemas/UserSettings.Attribute' + passkey: + $ref: '#/components/schemas/UserSettings.Attribute' + required: + - email_address + - phone_number + - username + - web3_wallet + - first_name + - last_name + - password + - authenticator_app + - ticket + - backup_code + - passkey + UserSettings.Social: + type: object + additionalProperties: false + properties: + enabled: + type: boolean + required: + type: boolean + authenticatable: + type: boolean + block_email_subaddresses: + type: boolean + strategy: + type: string + not_selectable: + type: boolean + deprecated: + type: boolean + name: + type: string + description: A human readable name for the social provider. + example: Google + logo_url: + type: string + nullable: true + description: A URL pointing to the logo of the social provider. + example: https://img.clerk.com/static/google.png + required: + - enabled + - required + - authenticatable + - block_email_subaddresses + - strategy + - not_selectable + - deprecated + UserSettings.Socials: + type: object + additionalProperties: false + properties: + oauth_agentid: + $ref: '#/components/schemas/UserSettings.Social' + oauth_apple: + $ref: '#/components/schemas/UserSettings.Social' + oauth_atlassian: + $ref: '#/components/schemas/UserSettings.Social' + oauth_bitbucket: + $ref: '#/components/schemas/UserSettings.Social' + oauth_box: + $ref: '#/components/schemas/UserSettings.Social' + oauth_coinbase: + $ref: '#/components/schemas/UserSettings.Social' + oauth_custom_mock: + $ref: '#/components/schemas/UserSettings.Social' + oauth_discord: + $ref: '#/components/schemas/UserSettings.Social' + oauth_dropbox: + $ref: '#/components/schemas/UserSettings.Social' + oauth_enstall: + $ref: '#/components/schemas/UserSettings.Social' + oauth_expressen: + $ref: '#/components/schemas/UserSettings.Social' + oauth_facebook: + $ref: '#/components/schemas/UserSettings.Social' + oauth_github: + $ref: '#/components/schemas/UserSettings.Social' + oauth_gitlab: + $ref: '#/components/schemas/UserSettings.Social' + oauth_google: + $ref: '#/components/schemas/UserSettings.Social' + oauth_hubspot: + $ref: '#/components/schemas/UserSettings.Social' + oauth_huggingface: + $ref: '#/components/schemas/UserSettings.Social' + oauth_instagram: + $ref: '#/components/schemas/UserSettings.Social' + oauth_line: + $ref: '#/components/schemas/UserSettings.Social' + oauth_linear: + $ref: '#/components/schemas/UserSettings.Social' + oauth_linkedin: + $ref: '#/components/schemas/UserSettings.Social' + oauth_linkedin_oidc: + $ref: '#/components/schemas/UserSettings.Social' + oauth_microsoft: + $ref: '#/components/schemas/UserSettings.Social' + oauth_mock: + $ref: '#/components/schemas/UserSettings.Social' + oauth_notion: + $ref: '#/components/schemas/UserSettings.Social' + oauth_slack: + $ref: '#/components/schemas/UserSettings.Social' + oauth_spotify: + $ref: '#/components/schemas/UserSettings.Social' + oauth_tiktok: + $ref: '#/components/schemas/UserSettings.Social' + oauth_twitch: + $ref: '#/components/schemas/UserSettings.Social' + oauth_twitter: + $ref: '#/components/schemas/UserSettings.Social' + oauth_x: + $ref: '#/components/schemas/UserSettings.Social' + oauth_xero: + $ref: '#/components/schemas/UserSettings.Social' + UserSettings.EnterpriseSSO: + type: object + additionalProperties: false + properties: + enabled: + type: boolean + self_serve_sso: + type: boolean + description: When true, end-users may manage enterprise connections for this environment. + self_serve_directory_sync: + type: boolean + description: When true, end-users may manage Directory Sync (SCIM) for their enterprise connections. Treat an absent value as false. + required: + - enabled + UserSettings.SignIn: + type: object + additionalProperties: false + properties: + second_factor: + type: object + additionalProperties: false + properties: + required: + type: boolean + required: + - required + required: + - second_factor + UserSettings.SignUp: + type: object + additionalProperties: false + properties: + captcha_enabled: + type: boolean + captcha_widget_type: + type: string + enum: + - smart + - invisible + custom_action_required: + type: boolean + progressive: + type: boolean + mode: + type: string + enum: + - public + - restricted + - waitlist + legal_consent_enabled: + type: boolean + required: + - captcha_enabled + - captcha_widget_type + - custom_action_required + - progressive + - mode + - legal_consent_enabled + UserSettings.Restrictions.Enabled: + type: object + additionalProperties: false + properties: + enabled: + type: boolean + required: + - enabled + UserSettings.Restrictions: + type: object + additionalProperties: false + properties: + allowlist: + $ref: '#/components/schemas/UserSettings.Restrictions.Enabled' + blocklist: + $ref: '#/components/schemas/UserSettings.Restrictions.Enabled' + allowlist_blocklist_disabled_on_sign_in: + $ref: '#/components/schemas/UserSettings.Restrictions.Enabled' + block_email_subaddresses: + $ref: '#/components/schemas/UserSettings.Restrictions.Enabled' + block_disposable_email_domains: + $ref: '#/components/schemas/UserSettings.Restrictions.Enabled' + required: + - allowlist + - blocklist + - allowlist_blocklist_disabled_on_sign_in + - block_email_subaddresses + - block_disposable_email_domains + UserSettings.PasswordSettings: + type: object + additionalProperties: false + properties: + disable_hibp: + type: boolean + disable_password_reverification: + type: boolean + min_length: + type: integer + max_length: + type: integer + require_special_char: + type: boolean + description: | + Denotes whether given passwords should contain at least one of the following special characters: #%$&_! + require_numbers: + type: boolean + require_uppercase: + type: boolean + require_lowercase: + type: boolean + show_zxcvbn: + type: boolean + min_zxcvbn_strength: + type: integer + minimum: 0 + maximum: 4 + enforce_hibp_on_sign_in: + type: boolean + allowed_special_characters: + type: string + required: + - disable_hibp + - disable_password_reverification + - min_length + - max_length + - require_special_char + - require_numbers + - require_uppercase + - require_lowercase + - show_zxcvbn + - min_zxcvbn_strength + - enforce_hibp_on_sign_in + - allowed_special_characters + UserSettings.UsernameSettings: + type: object + additionalProperties: false + properties: + min_length: + type: integer + nullable: true + max_length: + type: integer + nullable: true + allow_extended_special_characters: + type: boolean + allow_numeric_usernames: + description: Whether usernames may contain only digits. When enabled, phone-number inputs must be provided in explicit E.164 format. + type: boolean + required: + - min_length + - max_length + - allow_extended_special_characters + - allow_numeric_usernames + UserSettings.ActionsSettings: + type: object + additionalProperties: false + properties: + delete_self: + type: boolean + create_organization: + type: boolean + create_organizations_limit: + type: boolean + nullable: true + required: + - delete_self + - create_organization + - create_organizations_limit + UserSettings.AttackProtectionSettings.UserLockout: + type: object + additionalProperties: false + properties: + enabled: + type: boolean + max_attempts: + type: integer + duration_in_minutes: + type: integer + required: + - enabled + - max_attempts + - duration_in_minutes + UserSettings.AttackProtectionSettings.PII: + type: object + additionalProperties: false + properties: + enabled: + type: boolean + required: + - enabled + UserSettings.AttackProtectionSettings.EmailLink: + type: object + additionalProperties: false + properties: + require_same_client: + type: boolean + required: + - require_same_client + UserSettings.AttackProtectionSettings.EnumerationProtection: + type: object + additionalProperties: false + properties: + enabled: + type: boolean + required: + - enabled + UserSettings.AttackProtectionSettings: + type: object + additionalProperties: false + properties: + user_lockout: + $ref: '#/components/schemas/UserSettings.AttackProtectionSettings.UserLockout' + pii: + $ref: '#/components/schemas/UserSettings.AttackProtectionSettings.PII' + email_link: + $ref: '#/components/schemas/UserSettings.AttackProtectionSettings.EmailLink' + enumeration_protection: + $ref: '#/components/schemas/UserSettings.AttackProtectionSettings.EnumerationProtection' + required: + - user_lockout + - pii + - email_link + - enumeration_protection + UserSettings.PasskeySettings: + type: object + additionalProperties: false + properties: + allow_autofill: + type: boolean + description: Passkey UI will be shown when user interacts with the text input. + default: true + show_sign_in_button: + type: boolean + description: Passkey UI will be shown when user clicks on a button. + default: true + satisfies_second_factor: + type: boolean + description: Whether signing in with a passkey satisfies an existing second-factor requirement (skipping the second-factor prompt). + default: true + required: + - allow_autofill + - show_sign_in_button + - satisfies_second_factor + Client.UserSettings: + type: object + additionalProperties: false + properties: + attributes: + $ref: '#/components/schemas/UserSettings.Attributes' + social: + $ref: '#/components/schemas/UserSettings.Socials' + saml: + $ref: '#/components/schemas/UserSettings.EnterpriseSSO' + enterprise_sso: + $ref: '#/components/schemas/UserSettings.EnterpriseSSO' + sign_in: + $ref: '#/components/schemas/UserSettings.SignIn' + sign_up: + $ref: '#/components/schemas/UserSettings.SignUp' + restrictions: + $ref: '#/components/schemas/UserSettings.Restrictions' + password_settings: + $ref: '#/components/schemas/UserSettings.PasswordSettings' + username_settings: + $ref: '#/components/schemas/UserSettings.UsernameSettings' + actions: + $ref: '#/components/schemas/UserSettings.ActionsSettings' + attack_protection: + $ref: '#/components/schemas/UserSettings.AttackProtectionSettings' + passkey_settings: + $ref: '#/components/schemas/UserSettings.PasskeySettings' + required: + - attributes + - social + - saml + - enterprise_sso + - sign_in + - sign_up + - restrictions + - password_settings + - username_settings + - actions + - attack_protection + - passkey_settings + OrganizationSettings.ActionsSettings: + type: object + additionalProperties: false + properties: + admin_delete: + type: boolean + required: + - admin_delete + OrganizationSettings.DomainsSettings: + type: object + additionalProperties: false + properties: + enabled: + type: boolean + enrollment_modes: + type: array + items: + type: string + enum: + - manual_invitation + - automatic_invitation + - automatic_suggestion + default_role: + type: string + description: The role key that it will be used in order to create an organization invitation or suggestion. + required: + - enabled + - enrollment_modes + - default_role + OrganizationSettings.SlugSettings: + type: object + additionalProperties: false + properties: + disabled: + type: boolean + required: + - disabled + Client.OrganizationSettings: + type: object + additionalProperties: false + properties: + enabled: + type: boolean + max_allowed_memberships: + type: integer + actions: + $ref: '#/components/schemas/OrganizationSettings.ActionsSettings' + domains: + $ref: '#/components/schemas/OrganizationSettings.DomainsSettings' + slug: + $ref: '#/components/schemas/OrganizationSettings.SlugSettings' + creator_role: + type: string + description: The role key that a user will be assigned after creating an organization. + required: + - enabled + - max_allowed_memberships + - actions + - domains + - creator_role + FraudSettings.NativeSettings: + type: object + additionalProperties: false + properties: + device_attestation_mode: + type: string + enum: + - disabled + - onboarding + - enforced + required: + - device_attestation_mode + Client.FraudSettings: + type: object + additionalProperties: false + properties: + object: + type: string + description: String representing the object's type. Objects of the same type share the same value. + enum: + - fraud_settings + native: + $ref: '#/components/schemas/FraudSettings.NativeSettings' + required: + - native + Client.CommerceSettings: + type: object + additionalProperties: false + properties: + billing: + type: object + additionalProperties: false + properties: + stripe_publishable_key: + type: string + nullable: true + free_trial_requires_payment_method: + type: boolean + user: + type: object + additionalProperties: false + properties: + enabled: + type: boolean + has_paid_plans: + type: boolean + organization: + type: object + additionalProperties: false + properties: + enabled: + type: boolean + has_paid_plans: + type: boolean + required: + - billing + Client.ApiKeysSettings: + type: object + additionalProperties: false + properties: + enabled: + type: boolean + description: Indicates whether the API Keys feature is enabled. + user_api_keys_enabled: + type: boolean + description: Indicates whether User API Keys are enabled. + orgs_api_keys_enabled: + type: boolean + description: Indicates whether Organization API Keys are enabled. + required: + - enabled + - user_api_keys_enabled + - orgs_api_keys_enabled + Client.ProtectConfig.Loader: + type: object + additionalProperties: false + description: Configuration for the Clerk Protect loader. + properties: + rollout: + type: number + format: float + description: rollout percentage (between 0.0 and 1.0) + minimum: 0 + maximum: 1 + type: + type: string + target: + type: string + attributes: + type: object + additionalProperties: + type: string + textContent: + type: string + Client.ProtectConfig: + type: object + additionalProperties: false + properties: + loaders: + type: array + items: + $ref: '#/components/schemas/Client.ProtectConfig.Loader' + Client.Environment: + type: object + additionalProperties: false + properties: + auth_config: + $ref: '#/components/schemas/Client.AuthConfig' + display_config: + $ref: '#/components/schemas/Client.DisplayConfig' + user_settings: + $ref: '#/components/schemas/Client.UserSettings' + organization_settings: + $ref: '#/components/schemas/Client.OrganizationSettings' + fraud_settings: + $ref: '#/components/schemas/Client.FraudSettings' + commerce_settings: + $ref: '#/components/schemas/Client.CommerceSettings' + api_keys_settings: + $ref: '#/components/schemas/Client.ApiKeysSettings' + protect_config: + $ref: '#/components/schemas/Client.ProtectConfig' + client_debug_mode: + type: boolean + partitioned_cookies: + type: boolean + maintenance_mode: + type: boolean + Client.SessionTask: + type: object + properties: + key: + type: string + required: + - key + Client.SessionBase-2: + type: object + properties: + id: + type: string + object: + type: string + description: | + String representing the object's type. Objects of the same type share the same value. + enum: + - session + status: + type: string + enum: + - active + - revoked + - ended + - expired + - removed + - abandoned + - pending + expire_at: + description: Unix timestamp in milliseconds. Zero means no maximum lifetime. + type: integer + format: int64 + abandon_at: + description: Unix timestamp in milliseconds. Zero means no abandonment deadline. + type: integer + format: int64 + last_active_at: + type: integer + format: int64 + last_active_token: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Token' + actor: + type: object + nullable: true + additionalProperties: true + tasks: + type: array + nullable: true + items: + $ref: '#/components/schemas/Client.SessionTask' + required: + - id + - object + - status + - expire_at + - abandon_at + - last_active_at + Client.Session-2: + allOf: + - $ref: '#/components/schemas/Client.SessionBase-2' + - type: object + properties: + last_active_organization_id: + type: string + nullable: true + user: + $ref: '#/components/schemas/Client.User' + public_user_data: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Client.PublicUserData' + factor_verification_age: + type: array + description: Each item represents the minutes that have passed since the last time a first or second factor were verified. + items: + type: integer + created_at: + type: integer + format: int64 + description: Unix timestamp of creation. + example: 1700690400000 + updated_at: + type: integer + format: int64 + description: Unix timestamp of last update. + example: 1700690400000 + required: + - last_active_organization_id + - public_user_data + - factor_verification_age + - created_at + - updated_at + Client.SignIn-2: + type: object + additionalProperties: false + properties: + object: + type: string + description: String representing the object's type. Objects of the same type share the same value. + enum: + - sign_in_attempt + id: + type: string + status: + type: string + enum: + - abandoned + - needs_identifier + - needs_first_factor + - needs_second_factor + - needs_client_trust + - needs_new_password + - needs_protect_check + - complete + supported_identifiers: + type: array + description: List of supported identifiers that can be used to sign in. + items: + type: string + enum: + - email_address + - phone_number + - username + - web3_wallet + - passkey + supported_first_factors: + type: array + nullable: true + items: + $ref: '#/components/schemas/Stubs.SignInFactor' + sso_bypass_first_factors: + type: array + description: | + Factors a user routed to an enterprise connection may use when they cannot reach their identity provider. Absent unless the instance allows SSO bypass and the address belongs to a user on the bypass allowlist of a connection serving it. Pass the factor's `email_address_id` unchanged to `prepare_first_factor`. Deliberately separate from `supported_first_factors`, which callers rely on containing only enterprise factors before redirecting to the provider. + items: + $ref: '#/components/schemas/Stubs.SignInFactor' + supported_second_factors: + type: array + nullable: true + items: + $ref: '#/components/schemas/Stubs.SignInFactor' + first_factor_verification: + type: object + nullable: true + oneOf: + - $ref: '#/components/schemas/Stubs.Verification.Password' + - $ref: '#/components/schemas/Stubs.Verification.Oauth' + - $ref: '#/components/schemas/Stubs.Verification.OTP' + - $ref: '#/components/schemas/Stubs.Verification.Link' + - $ref: '#/components/schemas/Stubs.Verification.Web3Signature' + - $ref: '#/components/schemas/Stubs.Verification.Ticket' + - $ref: '#/components/schemas/Stubs.Verification.SAML' + - $ref: '#/components/schemas/Stubs.Verification.Passkey' + - $ref: '#/components/schemas/Stubs.Verification.TrustedDevice' + - $ref: '#/components/schemas/Stubs.Verification.GoogleOneTap' + second_factor_verification: + type: object + nullable: true + oneOf: + - $ref: '#/components/schemas/Stubs.Verification.OTP' + - $ref: '#/components/schemas/Stubs.Verification.Link' + - $ref: '#/components/schemas/Stubs.Verification.TOTP' + - $ref: '#/components/schemas/Stubs.Verification.Ticket' + - $ref: '#/components/schemas/Stubs.Verification.BackupCode' + - $ref: '#/components/schemas/Stubs.Verification.Passkey' + identifier: + nullable: true + type: string + user_data: + type: object + additionalProperties: false + nullable: true + properties: + first_name: + type: string + nullable: true + last_name: + type: string + nullable: true + image_url: + type: string + has_image: + type: boolean + profile_image_url: + type: string + nullable: true + deprecated: true + description: Use `image_url` instead. + required: + - first_name + - last_name + - has_image + created_session_id: + nullable: true + type: string + abandon_at: + type: integer + format: int64 + description: Unix timestamp at which the sign in will be abandoned. + example: 1700690400000 + timezone: + type: string + nullable: true + description: The IANA timezone recorded when the sign-in attempt was created. + example: America/Los_Angeles + client_trust_state: + type: string + nullable: true + description: | + The Device Trust state of this sign-in attempt. - `pending`: The identifier has not been set yet. - `new`: The user has not had a session on this device before. - `known`: The user has had a session on this device before. + enum: + - pending + - new + - known + required: + - object + - id + - status + - supported_identifiers + - supported_first_factors + - supported_second_factors + - first_factor_verification + - second_factor_verification + - identifier + - user_data + - created_session_id + - abandon_at + Client.Client-2: + type: object + nullable: true + properties: + object: + type: string + description: String representing the object's type. Objects of the same type share the same value. + enum: + - client + id: + type: string + description: String representing the identifier of the session. + sessions: + type: array + items: + $ref: '#/components/schemas/Client.Session-2' + sign_in: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Client.SignIn-2' + sign_up: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Client.SignUp' + last_active_session_id: + nullable: true + type: string + description: Last active session_id. + last_authentication_strategy: + nullable: true + type: string + description: | + The authentication strategy that was last used to authenticate the user on this client. + cookie_expires_at: + nullable: true + type: integer + format: int64 + description: Unix timestamp of the cookie expiration. + captcha_bypass: + type: boolean + description: Whether the client can bypass CAPTCHA. + created_at: + type: integer + format: int64 + description: Unix timestamp of creation. + updated_at: + type: integer + format: int64 + description: Unix timestamp of last update. + required: + - object + - id + - sessions + - sign_in + - sign_up + - last_active_session_id + - last_authentication_strategy + - cookie_expires_at + - captcha_bypass + - created_at + - updated_at + Client.ClientWrappedClient: + type: object + additionalProperties: false + properties: + response: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Client.Client-2' + client: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.DeleteSession: + type: object + additionalProperties: false + properties: + response: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Client.Client-2' + client: + type: object + nullable: true + required: + - response + - client + Client.ClientWrappedSignIn: + type: object + additionalProperties: false + properties: + response: + $ref: '#/components/schemas/Client.SignIn-2' + client: + $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.ClientWrappedSignIn-2: + type: object + additionalProperties: false + properties: + response: + $ref: '#/components/schemas/Client.SignIn' + client: + $ref: '#/components/schemas/Client.Client' + required: + - response + - client + Client.ClientWrappedSignUp: + type: object + additionalProperties: false + properties: + response: + $ref: '#/components/schemas/Client.SignUp' + client: + $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.TrustedDeviceValidation: + type: object + additionalProperties: false + properties: + object: + type: string + enum: + - trusted_device_validation + valid: + type: boolean + required: + - object + - valid + Client.ClientWrappedTrustedDeviceValidation: + type: object + additionalProperties: false + properties: + response: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.TrustedDeviceValidation' + client: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.BiometricCredentialValidation: + type: object + additionalProperties: false + properties: + object: + type: string + enum: + - trusted_device_validation + valid: + type: boolean + required: + - object + - valid + Client.ClientWrappedBiometricCredentialValidation: + type: object + additionalProperties: false + properties: + response: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.BiometricCredentialValidation' + client: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.ClientWrappedSession: + type: object + additionalProperties: false + properties: + response: + $ref: '#/components/schemas/Client.Session-2' + client: + $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.TouchWrappedSession: + type: object + additionalProperties: false + properties: + response: + $ref: '#/components/schemas/Client.Session-2' + client: + allOf: + - $ref: '#/components/schemas/Client.Client-2' + nullable: true + required: + - response + - client + Client.SessionActivity: + type: object + properties: + object: + type: string + id: + type: string + device_type: + type: string + is_mobile: + type: boolean + browser_name: + type: string + browser_version: + type: string + ip_address: + type: string + city: + type: string + country: + type: string + required: + - id + - object + - is_mobile + Client.ActiveSession: + allOf: + - $ref: '#/components/schemas/Client.SessionBase-2' + - type: object + properties: + latest_activity: + $ref: '#/components/schemas/Client.SessionActivity' + Client.ActiveSessions: + type: array + items: + $ref: '#/components/schemas/Client.ActiveSession' + Client.SessionReverification: + type: object + properties: + object: + type: string + description: String representing the object's type. Objects of the same type share the same value. + enum: + - session_reverification + id: + type: string + description: The id of this reverification, for tying a specific action to it + level: + type: string + description: The level used for the session reverification + status: + type: string + enum: + - needs_first_factor + - needs_second_factor + - complete + supported_first_factors: + type: array + nullable: true + items: + $ref: '#/components/schemas/Stubs.SignInFactor' + supported_second_factors: + type: array + nullable: true + items: + $ref: '#/components/schemas/Stubs.SignInFactor' + first_factor_verification: + type: object + nullable: true + oneOf: + - $ref: '#/components/schemas/Stubs.Verification.Password' + - $ref: '#/components/schemas/Stubs.Verification.OTP' + - $ref: '#/components/schemas/Stubs.Verification.Passkey' + - $ref: '#/components/schemas/Stubs.Verification.TrustedDevice' + - $ref: '#/components/schemas/Stubs.Verification.SAML' + second_factor_verification: + type: object + nullable: true + oneOf: + - $ref: '#/components/schemas/Stubs.Verification.TrustedDevice' + - $ref: '#/components/schemas/Stubs.Verification.OTP' + - $ref: '#/components/schemas/Stubs.Verification.TOTP' + - $ref: '#/components/schemas/Stubs.Verification.BackupCode' + - $ref: '#/components/schemas/Stubs.Verification.Passkey' + session: + type: object + oneOf: + - $ref: '#/components/schemas/Client.SessionBase-2' + required: + - object + - id + - level + - status + - supported_first_factors + - supported_second_factors + - first_factor_verification + - second_factor_verification + - session + Client.ClientWrappedSessionReverification: + type: object + properties: + response: + $ref: '#/components/schemas/Client.SessionReverification' + client: + $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Server.Sessions: + type: array + items: + $ref: '#/components/schemas/Client.Session-2' + EnterpriseConnectionResponse: + type: object + required: + - id + - name + - provider + - active + - domains + - sync_user_attributes + - disable_additional_identifications + - allow_organization_account_linking + - created_at + - updated_at + properties: + id: + type: string + description: The enterprise connection ID + name: + type: string + description: The display name of the connection + provider: + type: string + logo_public_url: + type: string + nullable: true + active: + type: boolean + description: Whether the enterprise connection is active + domains: + type: array + items: + type: string + description: Domains associated with the enterprise connection + organization_id: + type: string + description: Organization ID when the connection is linked to an organization + sync_user_attributes: + type: boolean + description: Whether user attributes are synced from the identity provider on each sign-in + disable_additional_identifications: + type: boolean + description: Whether additional identification methods are disabled for users signing in through this connection + allow_organization_account_linking: + type: boolean + description: Whether this connection supports account linking via organization membership + saml_connection: + type: object + description: Present when the enterprise connection uses SAML + properties: + id: + type: string + description: SAML connection ID + name: + type: string + description: SAML connection display name + idp_entity_id: + type: string + description: IdP entity ID (optional, when connection details are loaded) + idp_sso_url: + type: string + description: IdP SSO URL (optional, when connection details are loaded) + idp_certificate: + type: string + description: IdP X.509 signing certificate (optional, when connection details are loaded) + idp_certificate_issued_at: + type: integer + format: int64 + description: Unix timestamp (milliseconds) of the start of the IdP certificate validity window (X.509 NotBefore). Present when a certificate is configured. + idp_certificate_expires_at: + type: integer + format: int64 + description: Unix timestamp (milliseconds) of the end of the IdP certificate validity window (X.509 NotAfter). Present when a certificate is configured. + idp_certificates: + type: array + description: Every IdP signing certificate the connection trusts, primary first (optional, when connection details are loaded) + items: + type: object + properties: + certificate: + type: string + description: The X.509 certificate, base64 DER without PEM armor + issued_at: + type: integer + format: int64 + nullable: true + description: Unix timestamp (milliseconds) of the X.509 NotBefore + expires_at: + type: integer + format: int64 + nullable: true + description: Unix timestamp (milliseconds) of the X.509 NotAfter + required: + - certificate + - issued_at + - expires_at + idp_metadata_url: + type: string + description: IdP metadata URL (optional, when connection details are loaded) + acs_url: + type: string + description: Assertion Consumer Service URL + sp_entity_id: + type: string + description: Service Provider entity ID + sp_metadata_url: + type: string + description: Service Provider metadata URL + active: + type: boolean + description: Whether the SAML connection is active + allow_idp_initiated: + type: boolean + description: Whether IdP-initiated SSO is allowed + allow_subdomains: + type: boolean + description: Whether subdomains are allowed for domain matching + force_authn: + type: boolean + description: Whether to force re-authentication + login_hint: + type: object + description: Configuration for the login_hint sent to the IdP on SSO sign-in + properties: + mode: + type: string + enum: + - email_address + - custom_attribute + - 'off' + description: Controls the login_hint sent to the IdP on SSO sign-in + source: + type: string + description: The user public_metadata key whose value is sent as the login_hint when mode is custom_attribute + required: + - mode + oauth_config: + type: object + description: Present when the enterprise connection uses OIDC + properties: + id: + type: string + description: OAuth config ID + name: + type: string + description: Custom OIDC provider display name + provider_key: + type: string + description: OAuth provider key (e.g. oidc_custom, oidc_ghe_*, oidc_gitlab_ent_*) + client_id: + type: string + description: OAuth client ID + discovery_url: + type: string + description: OIDC discovery URL + auth_url: + type: string + description: OAuth authorization endpoint URL (present when configured or resolved from discovery) + token_url: + type: string + description: OAuth token endpoint URL (present when configured or resolved from discovery) + user_info_url: + type: string + description: OIDC userinfo endpoint URL (present when configured or resolved from discovery) + redirect_uri: + type: string + description: Authorized redirect URI for this OIDC connection + requires_pkce: + type: boolean + description: Whether PKCE is required for this OAuth client + logo_public_url: + type: string + description: Logo URL for the provider + created_at: + type: integer + format: int64 + description: Unix timestamp in milliseconds when the config was created + updated_at: + type: integer + format: int64 + description: Unix timestamp in milliseconds when the config was last updated + created_at: + type: integer + format: int64 + description: Unix timestamp in milliseconds when the connection was created + updated_at: + type: integer + format: int64 + description: Unix timestamp in milliseconds when the connection was last updated + Client.ClientWrappedEnterpriseConnections: + type: object + additionalProperties: false + properties: + response: + type: array + items: + $ref: '#/components/schemas/EnterpriseConnectionResponse' + description: List of active enterprise connections for the current user's primary email domain + client: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.ClientWrappedEnterpriseConnection: + type: object + additionalProperties: false + properties: + response: + $ref: '#/components/schemas/EnterpriseConnectionResponse' + client: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.DeletedObject: + type: object + additionalProperties: false + properties: + id: + type: string + slug: + type: string + object: + type: string + deleted: + type: boolean + required: + - object + - deleted + Client.ClientWrappedDeletedObject: + type: object + additionalProperties: false + properties: + response: + $ref: '#/components/schemas/Client.DeletedObject' + client: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + EnterpriseConnectionTestRunListItem: + type: object + required: + - id + - status + - connection_type + - created_at + properties: + id: + type: string + status: + type: string + description: Test run status (e.g. pending, success, failed) + connection_type: + type: string + enum: + - saml + - oauth + parsed_user_info: + type: object + nullable: true + additionalProperties: false + properties: + email_address: + type: string + first_name: + type: string + last_name: + type: string + user_id: + type: string + logs: + type: array + items: + type: object + additionalProperties: false + properties: + level: + type: string + code: + type: string + short_message: + type: string + message: + type: string + saml: + type: object + nullable: true + additionalProperties: false + properties: + saml_request: + type: string + saml_response: + type: string + relay_state: + type: string + oauth: + type: object + nullable: true + additionalProperties: false + properties: + id_token: + type: string + access_token: + type: string + user_info: + type: string + created_at: + type: integer + format: int64 + description: Unix timestamp in milliseconds when the test run was created + EnterpriseConnectionTestRunsPaginatedResponse: + type: object + additionalProperties: false + required: + - data + - total_count + properties: + data: + type: array + items: + $ref: '#/components/schemas/EnterpriseConnectionTestRunListItem' + total_count: + type: integer + format: int64 + Client.ClientWrappedEnterpriseConnectionTestRunsList: + type: object + additionalProperties: false + properties: + response: + $ref: '#/components/schemas/EnterpriseConnectionTestRunsPaginatedResponse' + client: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + EnterpriseConnectionTestRunResponse: + type: object + required: + - url + properties: + url: + type: string + description: URL to open in the browser to start the IdP test flow. + Client.ClientWrappedEnterpriseConnectionTestRun: + type: object + additionalProperties: false + properties: + response: + $ref: '#/components/schemas/EnterpriseConnectionTestRunResponse' + client: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + SSOBypassAllowlistPublicUserData: + description: The allowlisted user's public data. + type: object + additionalProperties: false + properties: + first_name: + type: string + nullable: true + last_name: + type: string + nullable: true + image_url: + type: string + has_image: + type: boolean + identifier: + type: string + username: + type: string + nullable: true + profile_image_url: + type: string + nullable: true + deprecated: true + description: Use `image_url` instead. + required: + - first_name + - last_name + - identifier + - has_image + - username + - profile_image_url + SSOBypassAllowlistUser: + description: | + A member who may verify an email code instead of reaching their identity provider when enterprise SSO is unreachable. + type: object + additionalProperties: false + properties: + object: + type: string + enum: + - sso_bypass_allowlist_user + user_id: + type: string + description: The allowlisted user, and the identifier the delete endpoint takes. + public_user_data: + $ref: '#/components/schemas/SSOBypassAllowlistPublicUserData' + created_at: + type: integer + format: int64 + description: Unix timestamp of creation. + updated_at: + type: integer + format: int64 + description: Unix timestamp of last update. + required: + - object + - user_id + - public_user_data + - created_at + - updated_at + Client.ClientWrappedSSOBypassAllowlistUsers: + type: object + additionalProperties: false + properties: + response: + type: array + items: + $ref: '#/components/schemas/SSOBypassAllowlistUser' + description: The organization's SSO bypass allowlist, deduplicated by user. + client: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.ClientWrappedSSOBypassAllowlistUser: + type: object + additionalProperties: false + properties: + response: + $ref: '#/components/schemas/SSOBypassAllowlistUser' + client: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + SSOBypassAllowlistBulkCreateError: + description: A user the bulk add could not put on the allowlist. + type: object + additionalProperties: false + properties: + user_id: + type: string + description: The user this error applies to. + code: + type: string + description: | + The API error code the single-user endpoint returns for this user (e.g. `sso_bypass_domain_not_served`, `resource_not_found`). + required: + - user_id + - code + SSOBypassAllowlistBulkCreate: + description: The partial-success payload of a bulk add to the SSO bypass allowlist. + type: object + additionalProperties: false + properties: + data: + type: array + items: + $ref: '#/components/schemas/SSOBypassAllowlistUser' + description: The members who were added, or were already on the list. + errors: + type: array + items: + $ref: '#/components/schemas/SSOBypassAllowlistBulkCreateError' + description: The users who could not be added, with the reason each. + required: + - data + - errors + Client.ClientWrappedSSOBypassAllowlistBulkCreate: + type: object + additionalProperties: false + properties: + response: + $ref: '#/components/schemas/SSOBypassAllowlistBulkCreate' + client: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Server.UserEmailAddresses: + type: array + items: + $ref: '#/components/schemas/Client.EmailAddress' + Client.ClientWrappedEmailAddress: + type: object + additionalProperties: false + properties: + response: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.EmailAddress' + client: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Server.UserPhoneNumbers: + type: array + items: + $ref: '#/components/schemas/Client.PhoneNumber' + Client.ClientWrappedPhoneNumber: + type: object + additionalProperties: false + properties: + response: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.PhoneNumber' + client: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Server.UserWeb3Wallets: + type: array + items: + $ref: '#/components/schemas/Client.Web3Wallet' + Client.ClientWrappedWeb3Wallet: + type: object + additionalProperties: false + properties: + response: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Web3Wallet' + client: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.ClientWrappedPasskey: + type: object + additionalProperties: false + properties: + response: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Passkey' + client: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.TrustedDevice: + type: object + additionalProperties: false + properties: + id: + type: string + object: + type: string + enum: + - trusted_device + platform: + type: string + enum: + - ios + - android + app_identifier: + type: string + description: Bundle identifier or package name for the native application that owns the credential. + name: + type: string + nullable: true + description: Optional user-facing device name supplied by the SDK. + algorithm: + type: string + enum: + - ES256 + status: + type: string + enum: + - active + - revoked + created_at: + type: integer + format: int64 + updated_at: + type: integer + format: int64 + last_used_at: + type: integer + format: int64 + nullable: true + revoked_at: + type: integer + format: int64 + nullable: true + required: + - id + - object + - platform + - app_identifier + - name + - algorithm + - status + - created_at + - updated_at + - last_used_at + - revoked_at + Client.ClientWrappedTrustedDevices: + type: object + additionalProperties: false + properties: + response: + type: array + items: + $ref: '#/components/schemas/Client.TrustedDevice' + client: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.TrustedDeviceChallenge: + type: object + additionalProperties: false + properties: + object: + type: string + enum: + - trusted_device_challenge + challenge: + type: string + challenge_id: + type: string + trusted_device_id: + type: string + description: Present for sign-in challenges and omitted for enrollment challenges. + client_data: + type: string + description: Canonical JSON challenge payload that the native SDK must sign. + expires_at: + type: integer + format: int64 + algorithm: + type: string + enum: + - ES256 + required: + - object + - challenge + - challenge_id + - client_data + - expires_at + - algorithm + Client.ClientWrappedTrustedDeviceChallenge: + type: object + additionalProperties: false + properties: + response: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.TrustedDeviceChallenge' + client: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.ClientWrappedTrustedDevice: + type: object + additionalProperties: false + properties: + response: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.TrustedDevice' + client: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.BiometricCredential: + type: object + additionalProperties: false + properties: + id: + type: string + object: + type: string + enum: + - trusted_device + platform: + type: string + enum: + - ios + - android + app_identifier: + type: string + description: Bundle identifier or package name for the native application that owns the credential. + name: + type: string + nullable: true + description: Optional user-facing credential name supplied by the SDK. + algorithm: + type: string + enum: + - ES256 + status: + type: string + enum: + - active + - revoked + created_at: + type: integer + format: int64 + updated_at: + type: integer + format: int64 + last_used_at: + type: integer + format: int64 + nullable: true + revoked_at: + type: integer + format: int64 + nullable: true + required: + - id + - object + - platform + - app_identifier + - name + - algorithm + - status + - created_at + - updated_at + - last_used_at + - revoked_at + Client.ClientWrappedBiometricCredentials: + type: object + additionalProperties: false + properties: + response: + type: array + items: + $ref: '#/components/schemas/Client.BiometricCredential' + client: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.BiometricCredentialChallenge: + type: object + additionalProperties: false + properties: + object: + type: string + enum: + - trusted_device_challenge + challenge: + type: string + challenge_id: + type: string + trusted_device_id: + type: string + description: Present for sign-in challenges and omitted for enrollment challenges. + client_data: + type: string + description: Canonical JSON challenge payload that the native SDK must sign. + expires_at: + type: integer + format: int64 + algorithm: + type: string + enum: + - ES256 + required: + - object + - challenge + - challenge_id + - client_data + - expires_at + - algorithm + Client.ClientWrappedBiometricCredentialChallenge: + type: object + additionalProperties: false + properties: + response: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.BiometricCredentialChallenge' + client: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.ClientWrappedBiometricCredential: + type: object + additionalProperties: false + properties: + response: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.BiometricCredential' + client: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.ClientWrappedExternalAccount: + type: object + additionalProperties: false + properties: + response: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/ExternalAccountWithVerification' + client: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.ClientWrappedUser: + type: object + additionalProperties: false + properties: + response: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.User' + client: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + TOTP: + type: object + additionalProperties: true + properties: + object: + type: string + enum: + - totp + id: + type: string + secret: + type: string + nullable: true + uri: + type: string + nullable: true + verified: + type: boolean + backup_codes: + type: array + items: + type: string + nullable: true + created_at: + type: integer + format: int64 + updated_at: + type: integer + format: int64 + required: + - object + - id + - secret + - uri + - verified + - created_at + - updated_at + Client.ClientWrappedTOTP: + type: object + additionalProperties: false + properties: + response: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/TOTP' + client: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + BackupCodes: + type: object + properties: + object: + type: string + enum: + - backup_code + id: + type: string + codes: + type: array + items: + type: string + description: A list of backup codes + created_at: + type: integer + format: int64 + updated_at: + type: integer + format: int64 + required: + - object + - id + - codes + - created_at + - updated_at + Client.ClientWrappedBackupCodes: + type: object + additionalProperties: false + properties: + response: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/BackupCodes' + client: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.ClientWrappedImage: + type: object + properties: + response: + $ref: '#/components/schemas/Image' + client: + $ref: '#/components/schemas/Client.Client' + required: + - response + - client + Client.ClientWrappedUser-2: + type: object + additionalProperties: false + properties: + response: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.User' + client: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Client' + required: + - response + - client + BillingMoneyResponse: + type: object + additionalProperties: false + properties: + amount: + type: integer + format: int64 + description: Amount in cents + amount_formatted: + type: string + description: Formatted amount as a string (e.g., "$10.00") + currency: + type: string + description: Currency code (e.g., "USD") + currency_symbol: + type: string + description: Currency symbol (e.g., "$") + required: + - amount + - amount_formatted + - currency + - currency_symbol + BillingSubscriptionCreditResponse: + type: object + additionalProperties: false + properties: + amount: + $ref: '#/components/schemas/BillingMoneyResponse' + description: Credit amount with formatting + cycle_days_remaining: + type: integer + format: int64 + description: Number of days remaining in the billing cycle + cycle_days_total: + type: integer + format: int64 + description: Total number of days in the billing cycle + cycle_remaining_percent: + type: number + format: double + description: Percentage of the billing cycle remaining + required: + - amount + - cycle_days_remaining + - cycle_days_total + - cycle_remaining_percent + BillingProrationCreditDetail: + type: object + additionalProperties: false + properties: + amount: + $ref: '#/components/schemas/BillingMoneyResponse' + description: Credit amount with formatting + cycle_days_remaining: + type: integer + format: int64 + description: Number of days remaining in the billing cycle + cycle_days_total: + type: integer + format: int64 + description: Total number of days in the billing cycle + cycle_remaining_percent: + type: number + format: double + description: Percentage of the billing cycle remaining + required: + - amount + - cycle_days_remaining + - cycle_days_total + - cycle_remaining_percent + BillingPayerCreditResponse: + type: object + additionalProperties: false + properties: + remaining_balance: + $ref: '#/components/schemas/BillingMoneyResponse' + description: The payer's remaining credit balance after credit is applied for this checkout (in cents) + applied_amount: + $ref: '#/components/schemas/BillingMoneyResponse' + description: The amount of payer credit applied to this checkout (in cents) + required: + - remaining_balance + - applied_amount + BillingCreditsResponse: + type: object + additionalProperties: false + properties: + proration: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/BillingProrationCreditDetail' + description: Proration credit details from unused subscription time + payer: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/BillingPayerCreditResponse' + description: Payer credit details from the payer's credit balance + total: + $ref: '#/components/schemas/BillingMoneyResponse' + description: Total credits applied + required: + - proration + - payer + - total + BillingFee: + type: object + additionalProperties: false + properties: + amount: + type: integer + format: int64 + description: Amount in the smallest currency unit (e.g., cents) + minimum: 0 + amount_formatted: + type: string + description: The formatted amount as a string (e.g., "$49.99") + currency: + type: string + description: The currency code (e.g., "USD") + currency_symbol: + type: string + description: The currency symbol (e.g., "$") + required: + - amount + - amount_formatted + - currency + - currency_symbol + BillingPlanUnitPriceTier: + type: object + additionalProperties: false + properties: + starts_at_block: + type: integer + format: int64 + description: Start block (inclusive) for this tier + ends_after_block: + type: integer + format: int64 + nullable: true + description: End block (inclusive) for this tier; null means unlimited + fee_per_block: + $ref: '#/components/schemas/BillingMoneyResponse' + description: Price charged per block in this tier + required: + - starts_at_block + - fee_per_block + BillingPlanUnitPrice: + type: object + additionalProperties: false + properties: + name: + type: string + description: Name of the billable unit (for example, seats) + block_size: + type: integer + format: int64 + description: Number of units included in each pricing block + tiers: + type: array + items: + $ref: '#/components/schemas/BillingPlanUnitPriceTier' + description: Tiered pricing configuration for this unit + required: + - name + - block_size + - tiers + BillingPlan: + type: object + additionalProperties: false + properties: + object: + type: string + description: String representing the object's type + enum: + - commerce_plan + id: + type: string + description: Unique identifier for the plan + name: + type: string + description: The name of the plan + fee: + allOf: + - $ref: '#/components/schemas/BillingFee' + nullable: true + description: The monthly fee for this plan + annual_monthly_fee: + allOf: + - $ref: '#/components/schemas/BillingFee' + nullable: true + description: The monthly equivalent when billed annually + annual_fee: + allOf: + - $ref: '#/components/schemas/BillingFee' + nullable: true + description: The annual fee for this plan + description: + type: string + description: The description of the plan + nullable: true + product_id: + type: string + description: The ID of the product this plan belongs to + deprecated: true + is_default: + type: boolean + description: Whether this is the default plan + is_recurring: + type: boolean + description: Whether this is a recurring plan + publicly_visible: + type: boolean + description: Whether this plan is publicly visible + has_base_fee: + type: boolean + description: Whether this plan has a base fee + for_payer_type: + type: string + description: The payer type this plan is designed for + slug: + type: string + description: The URL-friendly slug for the plan + avatar_url: + type: string + description: The URL of the plan's avatar image + nullable: true + free_trial_enabled: + type: boolean + description: Whether free trial is enabled for this plan + free_trial_days: + type: integer + nullable: true + description: Number of free trial days for this plan + unit_prices: + type: array + items: + $ref: '#/components/schemas/BillingPlanUnitPrice' + description: Per-unit pricing tiers for this plan (for example, seats) + features: + type: array + description: The features included in this plan + items: + type: object + properties: + object: + type: string + enum: + - feature + description: String representing the object's type + id: + type: string + description: Unique identifier for the feature + name: + type: string + description: Name of the feature + description: + type: string + nullable: true + description: Description of the feature + slug: + type: string + description: URL-friendly representation of the feature's name + avatar_url: + type: string + nullable: true + description: URL for an image to represent the feature + required: + - object + - id + - name + - description + - slug + - avatar_url + required: + - object + - id + - name + - fee + - annual_monthly_fee + - annual_fee + - description + - product_id + - is_default + - is_recurring + - publicly_visible + - has_base_fee + - for_payer_type + - slug + - avatar_url + - free_trial_enabled + - free_trial_days + BillingPaymentMethod: + type: object + additionalProperties: false + properties: + object: + type: string + description: String representing the object's type + enum: + - commerce_payment_method + id: + type: string + description: Unique identifier for the payment method + payer_id: + type: string + description: Unique identifier for the payer + payment_type: + type: string + description: The payment method type + enum: + - card + - link + - payer-credits + is_default: + type: boolean + description: Whether this is the default payment method for the payer + gateway: + type: string + description: The payment gateway + gateway_external_id: + type: string + description: External ID in the payment gateway + gateway_external_account_id: + type: string + nullable: true + description: External account ID in the payment gateway + last4: + type: string + description: Last 4 digits of the card (for card payment methods) + nullable: true + status: + type: string + description: Status of the payment method + enum: + - active + - disconnected + wallet_type: + type: string + description: Type of wallet (if applicable) + nullable: true + card_type: + type: string + description: Type of card (if applicable) + nullable: true + expiry_year: + type: integer + description: Card expiration year (for card payment methods) + nullable: true + expiry_month: + type: integer + description: Card expiration month (for card payment methods) + nullable: true + created_at: + type: integer + format: int64 + description: Unix timestamp (in milliseconds) when the payment method was created + updated_at: + type: integer + format: int64 + description: Unix timestamp (in milliseconds) when the payment method was last updated + is_removable: + type: boolean + description: Whether this payment method can be removed + required: + - object + - id + - payer_id + - payment_type + - gateway + - gateway_external_id + - gateway_external_account_id + - last4 + - status + - card_type + BillingPerUnitTotalTier: + type: object + additionalProperties: false + properties: + quantity: + type: integer + format: int64 + nullable: true + description: Units billed in this tier; null means unlimited + fee_per_block: + $ref: '#/components/schemas/BillingMoneyResponse' + description: Price charged per block in this tier + total: + $ref: '#/components/schemas/BillingMoneyResponse' + description: Total charge for this tier + required: + - fee_per_block + - total + BillingPerUnitTotal: + type: object + additionalProperties: false + properties: + name: + type: string + description: Name of the billable unit (for example, seats) + block_size: + type: integer + format: int64 + description: Number of units included in each pricing block + tiers: + type: array + items: + $ref: '#/components/schemas/BillingPerUnitTotalTier' + description: Computed totals for each pricing tier + required: + - name + - block_size + - tiers + BillingProrationDiscountDetail: + type: object + additionalProperties: false + properties: + amount: + $ref: '#/components/schemas/BillingMoneyResponse' + description: Credit amount with formatting + cycle_days_passed: + type: integer + format: int64 + description: Number of days that have passed in the billing cycle + cycle_days_total: + type: integer + format: int64 + description: Total number of days in the billing cycle + cycle_passed_percent: + type: number + format: double + description: Percentage of the billing cycle that has passed + required: + - amount + - cycle_days_passed + - cycle_days_total + - cycle_passed_percent + BillingAppliedDiscount: + type: object + additionalProperties: false + properties: + amount: + $ref: '#/components/schemas/BillingMoneyResponse' + description: How much this discount takes off the transaction + discount_id: + type: string + description: Unique identifier for the discount + name: + type: string + description: The display name of the discount + effect: + type: string + description: The snapshotted discount effect + enum: + - percentage + - fixed_amount + percent_off: + type: number + format: double + description: Percent off when the effect is percentage + amount_off: + allOf: + - $ref: '#/components/schemas/BillingMoneyResponse' + description: Configured fixed amount off when the effect is fixed_amount + promo_code: + type: string + description: The promo code applied at checkout, when applicable + cycles_remaining: + type: integer + format: int32 + nullable: true + description: Remaining billing cycles the discount applies to. Null means the discount lasts forever + duration_in_cycles: + type: integer + format: int32 + nullable: true + description: Original configured duration in billing cycles. Null means the discount lasts forever + cycles_applied: + type: integer + format: int32 + nullable: true + description: Number of billing cycles the discount has already been applied to + required: + - amount + - discount_id + - name + - effect + - cycles_remaining + - duration_in_cycles + - cycles_applied + BillingDiscountsResponse: + type: object + additionalProperties: false + properties: + proration: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/BillingProrationDiscountDetail' + description: Proration details from passed subscription time + discount: + allOf: + - $ref: '#/components/schemas/BillingAppliedDiscount' + nullable: true + description: Discount applied to this transaction, if any + total: + $ref: '#/components/schemas/BillingMoneyResponse' + description: Total discounts applied + required: + - proration + - total + BillingTotalsResponse: + type: object + additionalProperties: false + properties: + subtotal: + $ref: '#/components/schemas/BillingMoneyResponse' + description: Subtotal before adjustments + base_fee: + $ref: '#/components/schemas/BillingMoneyResponse' + description: Base fee component before per-unit charges and adjustments + tax_total: + $ref: '#/components/schemas/BillingMoneyResponse' + description: Total tax amount + grand_total: + $ref: '#/components/schemas/BillingMoneyResponse' + description: Grand total amount + total_due_after_free_trial: + allOf: + - $ref: '#/components/schemas/BillingMoneyResponse' + nullable: true + description: Total amount due after free trial ends + credit: + allOf: + - $ref: '#/components/schemas/BillingMoneyResponse' + nullable: true + description: Credit amount + past_due: + allOf: + - $ref: '#/components/schemas/BillingMoneyResponse' + nullable: true + description: Past due amount + total_due_now: + allOf: + - $ref: '#/components/schemas/BillingMoneyResponse' + nullable: true + description: Total amount due now + per_unit_totals: + type: array + items: + $ref: '#/components/schemas/BillingPerUnitTotal' + description: Per-unit total breakdown (for example, seats) + credits: + allOf: + - $ref: '#/components/schemas/BillingCreditsResponse' + nullable: true + description: Unified credits breakdown + discounts: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/BillingDiscountsResponse' + description: Information about the discounts applied to the payment + proration: + type: object + nullable: true + additionalProperties: false + properties: + credit: + $ref: '#/components/schemas/BillingMoneyResponse' + description: Proration credit amount + required: + - subtotal + - base_fee + - tax_total + - grand_total + BillingSubscriptionItemNextPayment: + type: object + additionalProperties: false + properties: + amount: + $ref: '#/components/schemas/BillingFee' + description: Base plan fee for the next payment. + date: + type: integer + format: int64 + description: Unix timestamp (in milliseconds) for the next payment date + per_unit_totals: + type: array + items: + $ref: '#/components/schemas/BillingPerUnitTotal' + description: Per-unit total breakdown for the next payment + totals: + allOf: + - $ref: '#/components/schemas/BillingTotalsResponse' + nullable: true + description: Breakdown of the recurring amount that will be billed at renewal (base fee + per-unit charges). Tax and credits are not included. + BillingPayer: + type: object + additionalProperties: false + properties: + object: + type: string + description: String representing the object's type + enum: + - commerce_payer + id: + type: string + description: Unique identifier for the payer + instance_id: + type: string + description: Unique identifier for the Clerk instance + user_id: + type: string + description: User ID for user-type payers + nullable: true + first_name: + type: string + description: First name of the payer (only set when user_id isn't null) + nullable: true + last_name: + type: string + description: Last name of the payer (only set when user_id isn't null) + nullable: true + email: + type: string + format: email + description: Email address of the payer (only set when user_id isn't null) + nullable: true + organization_id: + type: string + description: Organization ID for org-type payers + nullable: true + organization_name: + type: string + description: Organization name for org-type payers (only set when organization_id isn't null) + nullable: true + image_url: + type: string + format: uri + description: URL of the payer's image/avatar + credits_balance: + $ref: '#/components/schemas/BillingMoneyResponse' + description: Current credits balance for the payer + created_at: + type: integer + format: int64 + description: Unix timestamp (in milliseconds) when the payer was created + updated_at: + type: integer + format: int64 + description: Unix timestamp (in milliseconds) when the payer was last updated + required: + - object + - id + - instance_id + - user_id + - organization_id + BillingSubscriptionItemSeats: + type: object + additionalProperties: false + properties: + quantity: + type: integer + format: int64 + nullable: true + description: Seat quantity being billed; null means unlimited + tiers: + type: array + items: + $ref: '#/components/schemas/BillingPerUnitTotalTier' + description: Per-unit cost breakdown by pricing tier + required: + - quantity + BillingDiscountRedemption: + type: object + additionalProperties: false + properties: + object: + type: string + description: String representing the object's type. Objects of the same type share the same value. + enum: + - commerce_discount_redemption + id: + type: string + description: Unique identifier for the discount redemption + subscription_item_id: + type: string + description: Unique identifier for the subscription item the discount was applied to + discount_id: + type: string + description: Unique identifier for the discount that was applied + name: + type: string + description: The display name of the discount + source: + type: string + description: How the discount was applied to the subscription item + enum: + - manual + - promo_code + promo_code: + type: string + description: The promo code used to redeem the discount, when applicable + effect: + type: string + description: The snapshotted discount effect + enum: + - percentage + - fixed_amount + percent_off: + type: number + format: double + nullable: true + description: Percent off when the effect is percentage + amount_off: + allOf: + - $ref: '#/components/schemas/BillingMoneyResponse' + nullable: true + description: Fixed amount off when the effect is fixed_amount + amount: + allOf: + - $ref: '#/components/schemas/BillingMoneyResponse' + nullable: true + description: |- + How much this discount takes off the subscription item's next renewal charge. + Present for either effect when next-payment data is available. + cycles_remaining: + type: integer + format: int32 + nullable: true + description: Remaining billing cycles the discount applies to. Null means the discount lasts forever + cycles_applied: + type: integer + format: int32 + description: Number of billing cycles the discount has already been applied to + status: + type: string + description: Current status of the discount redemption + enum: + - active + - exhausted + - removed + redeemed_at: + type: integer + format: int64 + description: Unix timestamp (in milliseconds) when the discount was redeemed + redeemed_by: + type: string + nullable: true + description: Identifier of the actor that redeemed the discount, when available + required: + - object + - id + - subscription_item_id + - discount_id + - source + - cycles_applied + - cycles_remaining + - redeemed_at + - redeemed_by + BillingSubscriptionItem: + type: object + additionalProperties: false + properties: + object: + type: string + description: String representing the object's type + enum: + - commerce_subscription_item + id: + type: string + description: Unique identifier for the subscription item + instance_id: + type: string + description: Unique identifier for the Clerk instance + status: + type: string + description: Current status of the subscription item + enum: + - active + - canceled + - ended + - past_due + - upcoming + - incomplete + - abandoned + credit: + $ref: '#/components/schemas/BillingSubscriptionCreditResponse' + description: Credit information for this subscription (available in payment attempt events) + credits: + allOf: + - $ref: '#/components/schemas/BillingCreditsResponse' + nullable: true + description: Unified credits breakdown for this subscription item + plan_id: + type: string + description: Unique identifier for the associated plan + price_id: + type: string + description: Unique identifier for the associated price + plan: + $ref: '#/components/schemas/BillingPlan' + description: The associated plan + plan_period: + type: string + description: The billing period for this subscription + enum: + - month + - annual + payment_method_id: + type: string + description: Unique identifier for the payment method + payment_method: + $ref: '#/components/schemas/BillingPaymentMethod' + description: The payment method associated with this subscription + lifetime_paid: + $ref: '#/components/schemas/BillingFee' + description: Total amount paid over the lifetime of this subscription + amount: + $ref: '#/components/schemas/BillingFee' + description: Current amount due every period for this subscription + next_payment: + $ref: '#/components/schemas/BillingSubscriptionItemNextPayment' + nullable: true + description: Information about the next payment + payer_id: + type: string + description: Unique identifier for the payer + payer: + $ref: '#/components/schemas/BillingPayer' + description: The payer associated with this subscription + is_free_trial: + type: boolean + description: Whether this subscription is currently on a free trial + period_start: + type: integer + format: int64 + description: Unix timestamp (in milliseconds) when the current period started + period_end: + type: integer + format: int64 + nullable: true + description: Unix timestamp (in milliseconds) when the current period ends + proration_date: + type: string + description: Date used for proration calculations + canceled_at: + type: integer + format: int64 + nullable: true + description: Unix timestamp (in milliseconds) when the subscription was canceled + past_due_at: + type: integer + format: int64 + nullable: true + description: Unix timestamp (in milliseconds) when the subscription became past due + ended_at: + type: integer + format: int64 + nullable: true + description: Unix timestamp (in milliseconds) when the subscription ended + created_at: + type: integer + format: int64 + description: Unix timestamp (in milliseconds) when the subscription was created + updated_at: + type: integer + format: int64 + description: Unix timestamp (in milliseconds) when the subscription was last updated + seats: + allOf: + - $ref: '#/components/schemas/BillingSubscriptionItemSeats' + nullable: true + description: Seat quantity for seat-based billing + totals: + allOf: + - $ref: '#/components/schemas/BillingTotalsResponse' + nullable: true + description: Totals for this subscription item + applied_discount: + allOf: + - $ref: '#/components/schemas/BillingDiscountRedemption' + nullable: true + description: The currently active discount applied to this subscription item, if any + required: + - object + - id + - instance_id + - status + - plan_id + - plan_period + - payer_id + - is_free_trial + - period_start + - period_end + - canceled_at + - past_due_at + - ended_at + BillingSubscriptionNextPayment: + type: object + additionalProperties: false + properties: + date: + type: integer + format: int64 + description: Unix timestamp (milliseconds) of the next payment date + amount: + $ref: '#/components/schemas/BillingFee' + description: The base plan fee for the next payment. + totals: + allOf: + - $ref: '#/components/schemas/BillingTotalsResponse' + nullable: true + description: Breakdown of the recurring amount that will be billed at renewal (base fee + per-unit charges). Tax and credits are not included. + required: + - date + - amount + BillingSubscription: + type: object + additionalProperties: false + properties: + object: + type: string + description: String representing the object's type + enum: + - commerce_subscription + id: + type: string + description: Unique identifier for the commerce subscription + instance_id: + type: string + description: The ID of the instance this subscription belongs to + status: + type: string + description: The current status of the subscription + enum: + - active + - past_due + - canceled + - ended + - abandoned + - incomplete + payer_id: + type: string + description: The ID of the payer for this subscription + created_at: + type: integer + format: int64 + description: Unix timestamp (milliseconds) of creation + updated_at: + type: integer + format: int64 + description: Unix timestamp (milliseconds) of last update + active_at: + type: integer + format: int64 + nullable: true + description: Unix timestamp (milliseconds) when the subscription became active + past_due_at: + type: integer + format: int64 + nullable: true + description: Unix timestamp (milliseconds) when the subscription became past due + subscription_items: + type: array + items: + $ref: '#/components/schemas/BillingSubscriptionItem' + description: Array of subscription items in this subscription + next_payment: + allOf: + - $ref: '#/components/schemas/BillingSubscriptionNextPayment' + nullable: true + description: Information about the next scheduled payment + eligible_for_free_trial: + type: boolean + description: Whether the payer is eligible for a free trial + required: + - object + - id + - instance_id + - status + - payer_id + - created_at + - updated_at + - active_at + - past_due_at + - subscription_items + Client.BillingSubscription: + type: object + additionalProperties: false + properties: + response: + $ref: '#/components/schemas/BillingSubscription' + client: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.BillingSubscriptionItemList: + type: object + additionalProperties: false + properties: + response: + type: object + additionalProperties: false + properties: + data: + type: array + items: + $ref: '#/components/schemas/BillingSubscriptionItem' + total_count: + type: integer + format: int64 + description: Total number of subscription items + client: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.BillingSubscriptionItem: + type: object + additionalProperties: false + properties: + response: + $ref: '#/components/schemas/BillingSubscriptionItem' + client: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.BillingPaymentMethodList: + type: object + additionalProperties: false + properties: + response: + type: object + additionalProperties: false + properties: + data: + type: array + items: + $ref: '#/components/schemas/BillingPaymentMethod' + total_count: + type: integer + format: int64 + description: Total number of payment methods + client: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.BillingPaymentMethod: + type: object + additionalProperties: false + properties: + response: + allOf: + - $ref: '#/components/schemas/BillingPaymentMethod' + nullable: true + client: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + BillingPaymentMethodInitialize: + type: object + additionalProperties: false + properties: + object: + type: string + description: String representing the object's type + enum: + - commerce_payment_method_initialize + external_client_secret: + type: string + description: Client secret from the payment gateway for setting up payment method + external_gateway_id: + type: string + description: External gateway identifier + payment_method_order: + type: array + items: + type: string + description: Ordered list of supported payment methods + required: + - object + - payment_method_order + Client.BillingPaymentMethodInitialize: + type: object + additionalProperties: false + properties: + response: + $ref: '#/components/schemas/BillingPaymentMethodInitialize' + client: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + BillingPerPeriodTotals: + type: object + additionalProperties: false + properties: + subtotal: + $ref: '#/components/schemas/BillingMoneyResponse' + description: The total, pre-tax, that will be due + base_fee: + $ref: '#/components/schemas/BillingMoneyResponse' + description: Base fee component before per-unit charges and adjustments + tax_total: + $ref: '#/components/schemas/BillingMoneyResponse' + description: The total amount of tax that will be due + grand_total: + $ref: '#/components/schemas/BillingMoneyResponse' + description: The total, including tax, that will be due + per_unit_totals: + type: array + items: + $ref: '#/components/schemas/BillingPerUnitTotal' + description: Per-unit total breakdown (for example, seats) + required: + - subtotal + - base_fee + - tax_total + - grand_total + BillingCheckoutTotals: + type: object + additionalProperties: false + properties: + subtotal: + $ref: '#/components/schemas/BillingMoneyResponse' + description: The total, pre-tax, that will be due + base_fee: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/BillingMoneyResponse' + description: Base fee component before per-unit charges and adjustments. + tax_total: + $ref: '#/components/schemas/BillingMoneyResponse' + description: The total amount of tax that will be due + grand_total: + $ref: '#/components/schemas/BillingMoneyResponse' + description: The total, including tax, that will be due + total_due_after_free_trial: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/BillingMoneyResponse' + description: The total, including tax, that will be charged after the free trial ends + total_due_now: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/BillingMoneyResponse' + description: The total, including tax, that will be charged upon checkout + total_due_per_period: + $ref: '#/components/schemas/BillingMoneyResponse' + description: The expected total payment for each future billing period. + totals_due_per_period: + type: object + $ref: '#/components/schemas/BillingPerPeriodTotals' + description: The amounts that will be due per billing period on a recurring basis. + past_due: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/BillingMoneyResponse' + description: The amount that is past due for the subscription + credit: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/BillingMoneyResponse' + description: The amount of credit that will be consumed before the payment method is charged + credits: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/BillingCreditsResponse' + description: Unified credits breakdown + discounts: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/BillingDiscountsResponse' + description: Information about the discounts applied to the checkout + per_unit_totals: + type: array + items: + $ref: '#/components/schemas/BillingPerUnitTotal' + description: Per-unit total breakdown (for example, seats) + required: + - subtotal + - base_fee + - tax_total + - grand_total + - total_due_after_free_trial + - credit + - credits + - total_due_now + - total_due_per_period + BillingCheckout: + type: object + additionalProperties: false + properties: + object: + type: string + description: String representing the object's type + enum: + - commerce_checkout + id: + type: string + description: Unique identifier for the checkout + plan: + $ref: '#/components/schemas/BillingPlan' + description: The plan that is being purchased. + plan_period: + type: string + description: How frequently the plan being purchased will be renewed + enum: + - month + - annual + payer: + $ref: '#/components/schemas/BillingPayer' + description: The payer that will be charged + payment_method: + $ref: '#/components/schemas/BillingPaymentMethod' + description: The payment method that will be charged + external_gateway_id: + type: string + description: The external ID of the object in the payment processor that will receive the funds + status: + type: string + description: Current status of the subscription item + enum: + - completed + - needs_confirmation + totals: + $ref: '#/components/schemas/BillingCheckoutTotals' + description: The various subtotals and totals owed + subscription_item: + type: object + $ref: '#/components/schemas/BillingSubscriptionItem' + description: The subscription item created as a result of the checkout + plan_period_start: + type: integer + format: int64 + description: When the first billing period of the plan will start, as milliseconds since the Unix epoch + is_immediate_plan_change: + type: boolean + description: Whether the checkout will take effect upon completion or at the start of the next billing period + free_trial_ends_at: + type: integer + format: int64 + description: When the free trial will end, as milliseconds since the Unix epoch + needs_payment_method: + type: boolean + description: Whether this checkout requires an active payment method or not + required: + - object + - id + - plan + - plan_period + - payer + - external_gateway_id + - status + - totals + - is_immediate_plan_change + - needs_payment_method + Client.BillingCheckout: + type: object + additionalProperties: false + properties: + response: + $ref: '#/components/schemas/BillingCheckout' + client: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + BillingPayee: + type: object + additionalProperties: false + properties: + object: + type: string + description: String representing the object's type + enum: + - commerce_payee + id: + type: string + description: Unique identifier for the payee + gateway_type: + type: string + description: Type of payment gateway + gateway_external_id: + type: string + description: External ID in the payment gateway + gateway_status: + type: string + description: Status of the gateway connection + required: + - object + - id + - gateway_type + - gateway_external_id + - gateway_status + BillingPaymentAttempt: + type: object + additionalProperties: false + properties: + object: + type: string + description: String representing the object's type + enum: + - commerce_payment + id: + type: string + description: Unique identifier for the payment attempt + payment_id: + type: string + description: Unique identifier for the payment + instance_id: + type: string + description: Unique identifier for the instance + charge_type: + type: string + description: Type of charge + payee_id: + type: string + description: Unique identifier for the payee + payee: + $ref: '#/components/schemas/BillingPayee' + description: The payee associated with this payment + payer_id: + type: string + description: Unique identifier for the payer + payer: + $ref: '#/components/schemas/BillingPayer' + description: The payer associated with this payment + subscription_item_id: + type: string + description: Unique identifier for the subscription item (only present for recurring payments) + subscription_item: + $ref: '#/components/schemas/BillingSubscriptionItem' + description: The subscription item associated with this payment + amount: + $ref: '#/components/schemas/BillingMoneyResponse' + description: Payment amount with formatting + totals: + allOf: + - $ref: '#/components/schemas/BillingTotalsResponse' + nullable: true + description: Totals breakdown for this payment attempt + payment_method_id: + type: string + description: Unique identifier for the payment method used + payment_method: + $ref: '#/components/schemas/BillingPaymentMethod' + nullable: true + description: The payment method used for this payment + statement_id: + type: string + description: Unique identifier for the statement + gateway_external_id: + type: string + nullable: true + description: External ID from the payment gateway + gateway_external_url: + type: string + nullable: true + description: URL to view this payment in the gateway dashboard + status: + type: string + description: Status of the payment attempt + paid_at: + type: integer + format: int64 + nullable: true + description: Unix timestamp (in milliseconds) when the payment was completed + failed_at: + type: integer + format: int64 + nullable: true + description: Unix timestamp (in milliseconds) when the payment failed + failed_reason: + type: object + description: Reason for payment failure + created_at: + type: integer + format: int64 + description: Unix timestamp (in milliseconds) when the payment attempt was created + updated_at: + type: integer + format: int64 + description: Unix timestamp (in milliseconds) when the payment attempt was last updated + required: + - object + - id + - payment_id + - instance_id + - charge_type + - payee_id + - payee + - payer_id + - payer + - subscription_item_id + - subscription_item + - amount + - payment_method_id + - payment_method + - statement_id + - gateway_external_id + - gateway_external_url + - status + - paid_at + - failed_at + - created_at + - updated_at + Client.BillingPaymentAttemptList: + type: object + additionalProperties: false + properties: + response: + type: object + additionalProperties: false + properties: + data: + type: array + items: + $ref: '#/components/schemas/BillingPaymentAttempt' + total_count: + type: integer + format: int64 + description: Total number of payment attempts + client: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.BillingPaymentAttempt: + type: object + additionalProperties: false + properties: + response: + $ref: '#/components/schemas/BillingPaymentAttempt' + client: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + BillingStatementTotalsResponse: + type: object + additionalProperties: false + properties: + subtotal: + $ref: '#/components/schemas/BillingMoneyResponse' + description: Subtotal amount (base fee + unit price totals) + base_fee: + $ref: '#/components/schemas/BillingMoneyResponse' + description: Base plan fee before unit pricing + tax_total: + $ref: '#/components/schemas/BillingMoneyResponse' + description: Total tax amount + grand_total: + $ref: '#/components/schemas/BillingMoneyResponse' + description: Grand total amount + required: + - subtotal + - base_fee + - tax_total + - grand_total + BillingStatementResponseGroup: + type: object + additionalProperties: false + properties: + object: + type: string + description: String representing the object's type + timestamp: + type: integer + format: int64 + description: Unix timestamp (in milliseconds) for the group + items: + type: array + items: + $ref: '#/components/schemas/BillingPaymentAttempt' + description: Payment items in this group + required: + - object + - timestamp + - items + BillingStatement: + type: object + additionalProperties: false + properties: + object: + type: string + description: String representing the object's type + enum: + - commerce_statement + instance_id: + type: string + description: Unique identifier for the instance + id: + type: string + description: Unique identifier for the statement + timestamp: + type: integer + format: int64 + description: Unix timestamp (in milliseconds) for the statement + payer: + $ref: '#/components/schemas/BillingPayer' + description: The payer associated with this statement + status: + type: string + description: Status of the statement + totals: + $ref: '#/components/schemas/BillingStatementTotalsResponse' + description: Totals breakdown for the statement + groups: + type: array + items: + $ref: '#/components/schemas/BillingStatementResponseGroup' + description: Groups of payment items in the statement + required: + - object + - instance_id + - id + - timestamp + - payer + - status + - totals + - groups + Client.BillingStatementList: + type: object + additionalProperties: false + properties: + response: + type: object + additionalProperties: false + properties: + data: + type: array + items: + $ref: '#/components/schemas/BillingStatement' + total_count: + type: integer + format: int64 + description: Total number of statements + client: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.BillingStatement: + type: object + additionalProperties: false + properties: + response: + $ref: '#/components/schemas/BillingStatement' + client: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.BillingGenericObject: + type: object + additionalProperties: false + properties: + response: + type: object + nullable: true + client: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.BillingGenericArray: + type: object + additionalProperties: false + properties: + response: + type: object + properties: + data: + type: array + items: + type: object + total_count: + type: integer + format: int64 + required: + - data + - total_count + client: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.ClientWrappedOrganizationMemberships: + type: object + additionalProperties: false + properties: + response: + oneOf: + - type: object + nullable: false + properties: + data: + type: array + items: + $ref: '#/components/schemas/Client.OrganizationMembership' + total_count: + type: integer + format: int64 + - type: array + items: + $ref: '#/components/schemas/Client.OrganizationMembership' + client: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.PublicOrganizationData: + type: object + additionalProperties: false + properties: + id: + type: string + name: + type: string + slug: + type: string + image_url: + type: string + has_image: + type: boolean + required: + - id + - name + - slug + - has_image + Client.OrganizationInvitation: + type: object + properties: + id: + type: string + object: + type: string + description: String representing the object's type. Objects of the same type share the same value. + enum: + - organization_invitation + email_address: + type: string + role: + type: string + role_name: + type: string + organization_id: + type: string + public_organization_data: + type: object + allOf: + - $ref: '#/components/schemas/Client.PublicOrganizationData' + status: + type: string + description: String representing the object's type. Objects of the same type share the same value. + enum: + - pending + - revoked + - accepted + - expired + - completed + public_metadata: + type: object + additionalProperties: true + url: + type: string + nullable: true + expires_at: + type: integer + nullable: true + format: int64 + created_at: + type: integer + format: int64 + description: Unix timestamp of creation. + updated_at: + type: integer + format: int64 + description: Unix timestamp of last update. + required: + - object + - id + - email_address + - role + - role_name + - public_metadata + - url + - expires_at + - created_at + - updated_at + Client.OrganizationInvitationUserContext: + type: object + properties: + public_organization_data: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.PublicOrganizationData' + allOf: + - $ref: '#/components/schemas/Client.OrganizationInvitation' + Client.ClientWrappedOrganizationInvitationsUserContext: + type: object + additionalProperties: false + properties: + response: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Client.OrganizationInvitationUserContext' + total_count: + type: integer + format: int64 + client: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.ClientWrappedOrganizationInvitationUserContext: + type: object + additionalProperties: false + properties: + response: + allOf: + - $ref: '#/components/schemas/Client.OrganizationInvitationUserContext' + type: object + nullable: false + client: + allOf: + - $ref: '#/components/schemas/Client.Client-2' + type: object + nullable: false + required: + - response + - client + Client.OrganizationSuggestion: + type: object + additionalProperties: false + properties: + object: + type: string + enum: + - organization_suggestion + id: + type: string + public_organization_data: + type: object + allOf: + - $ref: '#/components/schemas/Client.PublicOrganizationData' + status: + type: string + created_at: + type: integer + format: int64 + description: Unix timestamp of creation. + updated_at: + type: integer + format: int64 + description: Unix timestamp of last update. + required: + - object + - id + - status + - created_at + - updated_at + Client.ClientWrappedOrganizationSuggestions: + type: object + additionalProperties: false + properties: + response: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Client.OrganizationSuggestion' + total_count: + type: integer + format: int64 + client: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.ClientWrappedOrganizationSuggestion: + type: object + additionalProperties: false + properties: + response: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.OrganizationSuggestion' + client: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.ClientWrappedOrganization: + type: object + additionalProperties: false + properties: + response: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Organization' + client: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.ClientWrappedOrganization-2: + type: object + additionalProperties: false + properties: + response: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Organization' + client: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Client' + required: + - response + - client + Client.ClientWrappedOrganizationInvitations: + type: object + additionalProperties: false + properties: + response: + oneOf: + - type: array + items: + $ref: '#/components/schemas/Client.OrganizationInvitation' + - type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Client.OrganizationInvitation' + total_count: + type: integer + format: int64 + client: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.ClientWrappedOrganizationInvitation: + type: object + additionalProperties: false + properties: + response: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.OrganizationInvitation' + client: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.ClientWrappedOrganizationMembership: + type: object + additionalProperties: false + properties: + response: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.OrganizationMembership' + client: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.OrganizationDomainVerification: + type: object + additionalProperties: false + properties: + status: + type: string + strategy: + type: string + attempts: + type: integer + nullable: true + expire_at: + nullable: true + type: integer + format: int64 + verified_at: + nullable: true + type: integer + format: int64 + required: + - status + - strategy + - attempts + - expire_at + - verified_at + Client.OrganizationDomainOwnershipVerification: + type: object + additionalProperties: false + properties: + status: + type: string + strategy: + type: string + attempts: + type: integer + nullable: true + expire_at: + nullable: true + type: integer + format: int64 + verified_at: + nullable: true + type: integer + format: int64 + txt_record_name: + nullable: true + type: string + description: | + The fully qualified DNS name the org admin must publish the TXT record under. Present while the domain has an outstanding TXT ownership challenge (status `unverified`): on the `prepare_ownership_verification` response and re-surfaced on domain read paths. Null for verified domains and once the challenge has expired. + txt_record_value: + nullable: true + type: string + description: | + The exact value the org admin must publish in the TXT record. Present on the `prepare_ownership_verification` response and re-surfaced on domain read paths while the challenge is outstanding and recoverable — but only for requesters holding `org:sys_domains:manage`. Null for verified domains, expired challenges, legacy challenges issued before the value was stored recoverably, and members without the manage permission. A read never regenerates or rotates the challenge. + required: + - status + - strategy + - attempts + - expire_at + - verified_at + Client.OrganizationDomain: + type: object + additionalProperties: false + properties: + object: + type: string + enum: + - organization_domain + id: + type: string + organization_id: + type: string + name: + type: string + enrollment_mode: + type: string + affiliation_email_address: + nullable: true + type: string + affiliation_verification: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Client.OrganizationDomainVerification' + ownership_verification: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Client.OrganizationDomainOwnershipVerification' + verification: + type: object + nullable: true + deprecated: true + allOf: + - $ref: '#/components/schemas/Client.OrganizationDomainVerification' + description: | + Deprecated alias for `affiliation_verification`. Kept for backward compatibility on the current API version; will be removed in the next API version. Prefer `affiliation_verification`. + total_pending_invitations: + type: integer + total_pending_suggestions: + type: integer + public_organization_data: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Client.PublicOrganizationData' + created_at: + type: integer + format: int64 + description: Unix timestamp of creation. + updated_at: + type: integer + format: int64 + description: Unix timestamp of last update. + required: + - object + - id + - organization_id + - name + - enrollment_mode + - affiliation_email_address + - affiliation_verification + - ownership_verification + - verification + - total_pending_invitations + - total_pending_suggestions + - created_at + - updated_at + Client.ClientWrappedOrganizationDomains: + type: object + additionalProperties: false + properties: + response: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Client.OrganizationDomain' + total_count: + type: integer + format: int64 + client: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.ClientWrappedOrganizationDomain: + type: object + additionalProperties: false + properties: + response: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.OrganizationDomain' + client: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.OrganizationDomainBulkCreateError: + type: object + additionalProperties: false + properties: + name: + type: string + description: The domain name this error applies to. + code: + type: string + description: | + The API error code for this domain name (e.g. `form_param_format_invalid`), so the SDK can decide how to surface the per-name failure. + required: + - name + - code + Client.OrganizationDomainBulkCreate: + type: object + additionalProperties: false + description: | + Partial-success payload of the bulk organization domain creation endpoint. Each requested domain name lands in exactly one of `data` (the created organization domain) or `errors` (a per-name failure). + properties: + data: + type: array + items: + $ref: '#/components/schemas/Client.OrganizationDomain' + errors: + type: array + items: + $ref: '#/components/schemas/Client.OrganizationDomainBulkCreateError' + required: + - data + - errors + Client.ClientWrappedOrganizationDomainBulkCreate: + type: object + additionalProperties: false + properties: + response: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.OrganizationDomainBulkCreate' + client: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.OrganizationDomainBulkOwnershipVerificationError: + type: object + additionalProperties: false + properties: + id: + type: string + description: The organization domain ID this error applies to. + code: + type: string + description: | + The API error code for this id (e.g. `resource_not_found`), so the SDK can decide how to surface the per-id failure. + required: + - id + - code + Client.OrganizationDomainBulkOwnershipVerification: + type: object + additionalProperties: false + description: | + Partial-success payload of the bulk organization domain ownership verification endpoints (`prepare_ownership_verification` and `attempt_ownership_verification`). Each requested id lands in exactly one of `data` (its current/updated ownership state) or `errors` (a per-id failure). + properties: + data: + type: array + items: + $ref: '#/components/schemas/Client.OrganizationDomain' + errors: + type: array + items: + $ref: '#/components/schemas/Client.OrganizationDomainBulkOwnershipVerificationError' + required: + - data + - errors + Client.ClientWrappedOrganizationDomainBulkOwnershipVerification: + type: object + additionalProperties: false + properties: + response: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.OrganizationDomainBulkOwnershipVerification' + client: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.OrganizationMembershipRequest: + type: object + additionalProperties: false + properties: + object: + type: string + enum: + - organization_membership_request + id: + type: string + organization_id: + type: string + status: + type: string + public_user_data: + type: object + nullable: true + allOf: + - $ref: '#/components/schemas/Client.PublicUserData' + created_at: + type: integer + format: int64 + description: Unix timestamp of creation. + updated_at: + type: integer + format: int64 + description: Unix timestamp of last update. + required: + - object + - id + - organization_id + - status + - public_user_data + - created_at + - updated_at + Client.ClientWrappedOrganizationMembershipRequests: + type: object + additionalProperties: false + properties: + response: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Client.OrganizationMembershipRequest' + total_count: + type: integer + format: int64 + client: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.ClientWrappedOrganizationMembershipRequest: + type: object + additionalProperties: false + properties: + response: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.OrganizationMembershipRequest' + client: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.Permission: + type: object + additionalProperties: false + properties: + object: + type: string + enum: + - permission + id: + type: string + name: + type: string + key: + type: string + description: + type: string + type: + type: string + enum: + - system + - user + created_at: + type: integer + format: int64 + description: Unix timestamp of creation. + updated_at: + type: integer + format: int64 + description: Unix timestamp of last update. + required: + - object + - id + - name + - key + - description + - type + - created_at + - updated_at + Client.Role: + type: object + additionalProperties: false + properties: + object: + type: string + enum: + - role + id: + type: string + name: + type: string + key: + type: string + description: + type: string + is_creator_eligible: + type: boolean + permissions: + type: array + items: + $ref: '#/components/schemas/Client.Permission' + created_at: + type: integer + format: int64 + description: Unix timestamp of creation. + updated_at: + type: integer + format: int64 + description: Unix timestamp of last update. + required: + - object + - id + - name + - key + - description + - is_creator_eligible + - permissions + - created_at + - updated_at + Client.ClientWrappedRoles: + type: object + additionalProperties: false + properties: + response: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/Client.Role' + total_count: + type: integer + format: int64 + has_role_set_migration: + type: boolean + description: Indicates whether there is an active role set migration for this organization. + client: + type: object + nullable: false + allOf: + - $ref: '#/components/schemas/Client.Client-2' + required: + - response + - client + Client.WaitlistEntry: + type: object + additionalProperties: false + properties: + object: + type: string + enum: + - waitlist_entry + id: + type: string + required: + - object + - id + responses: + WellKnown.Assetlinks: + description: Get Android asset links for universal/deep linking + content: + application/json: + schema: + $ref: '#/components/schemas/WellKnown.Assetlinks' + WellKnown.JWKS: + description: Get the JSON Web Key Set + content: + application/json: + schema: + $ref: '#/components/schemas/JWKS' + ClerkErrors: + description: Request was not successful + content: + application/json: + schema: + $ref: '#/components/schemas/ClerkErrors' + WellKnown.AppleAppSiteAssociation: + description: Get the Apple App Site Association file + content: + application/json: + schema: + $ref: '#/components/schemas/WellKnown.AppleAppSiteAssociation' + WellKnown.OpenIDConfiguration: + description: Get a basic OpenID configuration payload + content: + application/json: + schema: + $ref: '#/components/schemas/WellKnown.OpenIDConfiguration' + WellKnown.OAuth2AuthorizationServerMetadata: + description: Get the OAuth 2.0 Authorization Server Metadata + content: + application/json: + schema: + $ref: '#/components/schemas/WellKnown.OAuth2AuthorizationServerMetadata' + WellKnown.WebAuthn: + description: Get the WebAuthn Related Origin Requests document + content: + application/json: + schema: + $ref: '#/components/schemas/WellKnown.WebAuthn' + OAuth.DynamicClientRegistration: + description: |- + Client information response after successful OAuth 2.0 dynamic client registration. + Returns the client identifier, client secret (for confidential clients), and client metadata + according to RFC 7591. + content: + application/json: + schema: + $ref: '#/components/schemas/OAuth.DynamicClientRegistrationResponse' + OAuth.DynamicClientRegistrationError: + description: |- + OAuth 2.0 dynamic client registration error response according to RFC 7591. + Indicates that the client registration request was invalid. + content: + application/json: + schema: + $ref: '#/components/schemas/OAuth.DynamicClientRegistrationError' + OAuth.DeviceAuthorization: + description: Device authorization request created. + content: + application/json: + schema: + type: object + additionalProperties: false + required: + - device_code + - user_code + - verification_uri + - expires_in + properties: + device_code: + type: string + description: The code the device polls the token endpoint with. Never shown to the user. + user_code: + type: string + description: The code the user types into the verification page, displayed in groups of four characters. + verification_uri: + type: string + description: The URL of the page where the user enters the user code. + verification_uri_complete: + type: string + description: The verification URI with the user code pre-filled, suitable for a QR code. + expires_in: + type: integer + description: Seconds until both codes expire. + interval: + type: integer + description: Minimum seconds the device must wait between polls of the token endpoint. + example: + device_code: 5FZ4RVGZ8QW3HJ7KM2NP9TXC6BD1YS0AE5RGH4KJVQW + user_code: BCDF-GHJK + verification_uri: https://accounts.example.com/device + verification_uri_complete: https://accounts.example.com/device?user_code=BCDF-GHJK + expires_in: 600 + interval: 5 + OAuth.DeviceAuthorizationError400: + description: Bad request. + content: + application/json: + schema: + type: object + properties: + error: + type: string + enum: + - invalid_request + - invalid_grant + - invalid_scope + - insufficient_entropy + example: invalid_grant + error_description: + type: string + example: The provided authorization grant is invalid + OAuth.TokenError401: + description: |- + Unauthorized error. Common causes: + - Invalid client credentials (`client_id` or `client_secret`) + - Invalid or expired authorization code + - Invalid or expired refresh token + - Invalid `code_verifier` for PKCE flow + content: + application/json: + schema: + type: object + properties: + error: + type: string + enum: + - invalid_client + - invalid_grant + - unsupported_grant_type + example: invalid_client + error_description: + type: string + example: Client authentication failed + TooManyRequests: + description: Too many requests + content: + application/json: + schema: + $ref: '#/components/schemas/ClerkErrors' + OAuth.Token: + description: |- + Successfully issued OAuth2 tokens. + + The response includes an `access_token` and `refresh_token`. If the `openid` scope was + granted, an `id_token` (JWT) will also be included in the response. + content: + application/json: + schema: + $ref: '#/components/schemas/OAuth.Token' + examples: + withoutIdToken: + summary: Token Response without ID Token + description: Response when `openid` scope is not included + value: + access_token: oat_abc123xyz... + token_type: Bearer + expires_in: 86400 + refresh_token: clk_refresh_abc123xyz... + scope: email profile + withIdToken: + summary: Token Response with ID Token (OpenID Connect) + description: Response when `openid` scope is included, includes JWT ID token + value: + access_token: oat_abc123xyz... + token_type: Bearer + expires_in: 86400 + refresh_token: clk_refresh_abc123xyz... + scope: openid email profile + id_token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9... + OAuth.TokenError400: + description: |- + Bad request error. Common causes: + - Missing required parameters + - Invalid `grant_type` value + - Malformed request + - Expired authorization code + - Invalid `redirect_uri` (doesn't match authorization request) + - Unsupported token type for revocation (e.g., JWT access tokens) + - A granted scope is no longer allowed (for example after a custom scope is deleted) + - The device authorization request has not been approved yet, has been polled too + quickly, or has expired (RFC 8628 §3.5) + - Invalid, repeated, or mismatched `resource` value (RFC 8707) + content: + application/json: + schema: + type: object + properties: + error: + type: string + enum: + - invalid_request + - invalid_grant + - invalid_scope + - invalid_target + - unsupported_grant_type + - unauthorized_client + - unsupported_token_type + - authorization_pending + - slow_down + - expired_token + - invalid_target + example: invalid_request + error_description: + type: string + example: The request is missing a required parameter + OAuth.TokenError403: + description: |- + Forbidden. The response may contain `access_denied` when a device authorization + request was denied. + content: + application/json: + schema: + type: object + properties: + error: + type: string + enum: + - access_denied + example: access_denied + error_description: + type: string + example: The resource owner denied the request + OAuth.UserInfo: + description: Get user info after successful OAuth2 flow + content: + application/json: + schema: + $ref: '#/components/schemas/OAuth.UserInfo' + OAuth.TokenInfo: + description: Get information for an access or refresh token + content: + application/json: + schema: + $ref: '#/components/schemas/OAuth.TokenInfo' + OAuth.ConsentInfo: + description: OAuth2 consent information (scopes with descriptions) for a given OAuth Application Client ID + content: + application/json: + schema: + $ref: '#/components/schemas/OAuth.ConsentInfo' + OAuth.DeviceVerification: + description: Completed OAuth2 device authorization decision. + content: + application/json: + schema: + $ref: '#/components/schemas/OAuth.DeviceVerification' + OAuth.DeviceAuthorizationInfo: + description: OAuth2 device authorization information for the verification screen. + content: + application/json: + schema: + $ref: '#/components/schemas/OAuth.DeviceAuthorizationInfo' + AuthenticationInvalid: + description: Authentication invalid + content: + application/json: + schema: + $ref: '#/components/schemas/ClerkErrors' + AuthorizationInvalid: + description: Authorization invalid + content: + application/json: + schema: + $ref: '#/components/schemas/ClerkErrors' + Client.AccountPortal: + description: Returns the account portal. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.AccountPortal' + Client.Environment: + description: Returns the environment. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.Environment' + Client.Client: + description: Returns the current session object. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedClient' + Client.DeleteSession: + description: Returns the response for DELETE session object. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.DeleteSession' + Client.SignIn: + description: Returns the sign in object, as well as the updated session object. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedSignIn' + Client.SignIn-2: + description: Returns the sign in object, as well as the updated session object. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedSignIn-2' + Client.SignUp: + description: Returns the sign up object, as well as the updated session object. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedSignUp' + Client.Session: + description: Returns a Session object. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedSession' + Client.TouchSession: + description: Returns a touched Session object. The `client` field may be null for intents that skip piggybacking. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.TouchWrappedSession' + Client.ActiveSessions: + description: Returns an Active Sessions array. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ActiveSessions' + Client.SessionReverification: + description: Returns the session reverification object, as well as the session object. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedSessionReverification' + Server.Sessions: + description: Returns Sessions array. + content: + application/json: + schema: + $ref: '#/components/schemas/Server.Sessions' + Client.ClientWrappedEnterpriseConnections: + description: Returns active enterprise connections for the current user's primary email domain. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedEnterpriseConnections' + Client.ClientWrappedEnterpriseConnection: + description: Returns the enterprise connection for the current user's primary email domain. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedEnterpriseConnection' + PaymentRequired: + description: Payment required + content: + application/json: + schema: + $ref: '#/components/schemas/ClerkErrors' + ResourceNotFound: + description: Resource not found + content: + application/json: + schema: + $ref: '#/components/schemas/ClerkErrors' + UnprocessableEntity: + description: Invalid request parameters + content: + application/json: + schema: + $ref: '#/components/schemas/ClerkErrors' + Client.ClientWrappedDeletedObject: + description: Returns a deleted generic object. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedDeletedObject' + Client.ClientWrappedEnterpriseConnectionTestRunsList: + description: Returns a paginated list of enterprise connection test runs. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedEnterpriseConnectionTestRunsList' + Client.ClientWrappedEnterpriseConnectionTestRun: + description: Returns a URL to initiate an enterprise connection test run. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedEnterpriseConnectionTestRun' + Client.ClientWrappedSSOBypassAllowlistUsers: + description: Returns the organization's SSO bypass allowlist, deduplicated by user. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedSSOBypassAllowlistUsers' + Client.ClientWrappedSSOBypassAllowlistUser: + description: Returns an SSO bypass allowlist entry. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedSSOBypassAllowlistUser' + Client.ClientWrappedSSOBypassAllowlistBulkCreate: + description: Returns the per-user partial-success payload for a bulk add to the SSO bypass allowlist. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedSSOBypassAllowlistBulkCreate' + Server.UserEmailAddresses: + description: Returns UserEmailAddresses array. + content: + application/json: + schema: + $ref: '#/components/schemas/Server.UserEmailAddresses' + Client.ClientWrappedEmailAddress: + description: Returns the response for Session wrapped Email object. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedEmailAddress' + Server.UserPhoneNumbers: + description: Returns UserPhoneNumbers array. + content: + application/json: + schema: + $ref: '#/components/schemas/Server.UserPhoneNumbers' + Client.ClientWrappedPhoneNumber: + description: Returns the response for Session wrapped Email object. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedPhoneNumber' + Client.DeletedPhoneNumber: + description: Returns single DeletedPhoneNumber. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedDeletedObject' + Server.UserWeb3Wallets: + description: Returns UserWeb3Wallets array. + content: + application/json: + schema: + $ref: '#/components/schemas/Server.UserWeb3Wallets' + Client.ClientWrappedWeb3Wallet: + description: Returns the response for Session wrapped Web3 wallet object. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedWeb3Wallet' + Client.ClientWrappedPasskey: + description: Returns the response for Session wrapped Passkey object. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedPasskey' + Client.DeletedPasskey: + description: Returns a deleted Passkey object. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedDeletedObject' + Client.ClientWrappedExternalAccount: + description: Returns the response for Session wrapped Verification object. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedExternalAccount' + Client.DeletedExternalAccount: + description: Returns a deleted external account. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedDeletedObject' + Client.ClientWrappedUser: + description: Returns the response for Session wrapped User object. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedUser' + Client.ClientWrappedTOTP: + description: Returns the response for Session wrapped TOTP object. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedTOTP' + Client.DeletedTOTP: + description: Returns a deleted TOTP. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedDeletedObject' + Client.ClientWrappedBackupCodes: + description: Returns the response for Session wrapped Backup code object. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedBackupCodes' + Token: + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/Token' + Client.ClientWrappedImage: + description: Returns an Image object. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedImage' + Client.ClientWrappedUser-2: + description: Returns the response for Session wrapped User object. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedUser-2' + Client.BillingSubscription: + description: Returns a commerce subscription. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.BillingSubscription' + Client.BillingSubscriptionItemList: + description: Returns a list of commerce subscription items. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.BillingSubscriptionItemList' + Client.BillingSubscriptionItem: + description: Returns a commerce subscription item. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.BillingSubscriptionItem' + Client.BillingPaymentMethodList: + description: Returns a list of commerce payment methods. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.BillingPaymentMethodList' + Client.BillingPaymentMethod: + description: Returns a commerce payment method. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.BillingPaymentMethod' + Client.BillingPaymentMethodInitialize: + description: Returns payment method initialization data. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.BillingPaymentMethodInitialize' + Client.BillingCheckout: + description: Returns a commerce checkout. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.BillingCheckout' + Client.BillingPaymentAttemptList: + description: Returns a list of commerce payment attempts. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.BillingPaymentAttemptList' + Client.BillingPaymentAttempt: + description: Returns a commerce payment attempt. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.BillingPaymentAttempt' + Client.BillingStatementList: + description: Returns a list of commerce statements. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.BillingStatementList' + Client.BillingStatement: + description: Returns a commerce statement. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.BillingStatement' + Client.BillingGenericObject: + description: Returns a generic commerce object. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.BillingGenericObject' + Client.BillingGenericArray: + description: Returns a generic commerce array. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.BillingGenericArray' + BillingPlanList: + description: Returns a list of commerce plans. + content: + application/json: + schema: + type: object + additionalProperties: false + required: + - data + - total_count + properties: + data: + type: array + items: + $ref: '#/components/schemas/BillingPlan' + total_count: + type: integer + format: int64 + BillingPlan: + description: Returns a commerce plan. + content: + application/json: + schema: + $ref: '#/components/schemas/BillingPlan' + Client.ClientWrappedOrganizationMemberships: + description: Returns the response for Client wrapped array of OrganizationMembership objects. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedOrganizationMemberships' + Client.ClientWrappedOrganizationInvitationsUserContext: + description: Returns the response for Client wrapped array of OrganizationInvitation objects with user context. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedOrganizationInvitationsUserContext' + Client.ClientWrappedOrganizationInvitationUserContext: + description: Returns the response for Client wrapped OrganizationInvitation object with user context. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedOrganizationInvitationUserContext' + Client.ClientWrappedOrganizationSuggestions: + description: Returns the response for Client wrapped array of OrganizationSuggestion objects with user context. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedOrganizationSuggestions' + Client.ClientWrappedOrganizationSuggestion: + description: Returns the response for Client wrapped OrganizationSuggestion object with user context. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedOrganizationSuggestion' + Client.ClientWrappedOrganization: + description: Returns the response for Client wrapped Organization object. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedOrganization' + Client.ClientWrappedOrganization-2: + description: Returns the response for Client wrapped Organization object. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedOrganization-2' + Client.ClientWrappedOrganizationInvitations: + description: Returns the response for Client wrapped array of OrganizationInvitation objects. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedOrganizationInvitations' + Client.ClientWrappedOrganizationInvitation: + description: Returns the response for Client wrapped OrganizationInvitation object. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedOrganizationInvitation' + Client.ClientWrappedOrganizationMembership: + description: Returns the response for Client wrapped OrganizationMembership object. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedOrganizationMembership' + Client.ClientWrappedOrganizationDomains: + description: Returns the response for Client wrapped array of OrganizationDomain objects. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedOrganizationDomains' + Client.ClientWrappedOrganizationDomain: + description: Returns the response for Client wrapped OrganizationDomain object. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedOrganizationDomain' + Client.ClientWrappedOrganizationDomainBulkCreate: + description: Returns the per-name partial-success payload for bulk organization domain creation. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedOrganizationDomainBulkCreate' + Client.ClientWrappedOrganizationDomainBulkOwnershipVerification: + description: Returns the per-domain partial-success payload for the bulk organization domain ownership verification endpoints. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedOrganizationDomainBulkOwnershipVerification' + Client.DeletedOrganizationDomain: + description: Returns a deleted organization domain. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedDeletedObject' + Client.ClientWrappedOrganizationMembershipRequests: + description: Returns the response for Client wrapped OrganizationMembershipRequest objects. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedOrganizationMembershipRequests' + Client.ClientWrappedOrganizationMembershipRequest: + description: Returns the response for Client wrapped OrganizationMembershipRequest object. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedOrganizationMembershipRequest' + Client.ClientWrappedRoles: + description: Returns the response for Client wrapped Roles objects. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.ClientWrappedRoles' + Client.WaitlistEntry: + description: Returns the waitlist object. + content: + application/json: + schema: + $ref: '#/components/schemas/Client.WaitlistEntry' + parameters: + LimitParameter: + name: limit + in: query + description: |- + Applies a limit to the number of results returned. + Can be used for paginating the results together with `offset`. + required: false + schema: + type: integer + default: 10 + minimum: 1 + maximum: 500 + OffsetParameter: + name: offset + in: query + description: |- + Skip the first `offset` results when paginating. + Needs to be an integer greater or equal to zero. + To be used in conjunction with `limit`. + required: false + schema: + type: integer + default: 0 + minimum: 0 + Paginated: + name: paginated + in: query + description: |- + Whether to paginate the results. + If true, the results will be paginated. + If false, the results will not be paginated. + required: false + schema: + type: boolean +x-tagGroups: + - name: Environment + tags: + - Environment + - name: Client + tags: + - Client + - Sessions + - Sign Ins + - Sign Ups + - name: User + tags: + - User + - Active Sessions + - Email Addresses + - Phone Numbers + - Web3 Wallets + - Passkeys + - Trusted Devices + - Biometric Credentials + - External Accounts + - TOTP + - Backup Codes + - Organizations Memberships + - API Keys + - name: Organizations + tags: + - Organization + - Organization Creation Defaults + - Invitations + - Membership Requests + - Members + - Domains + - Roles + - SSO Bypass + - name: Billing + tags: + - Plans + - Subscriptions + - Subscription Items + - Checkouts + - Payment Methods + - Statements + - Payment Attempts + - name: Other + tags: + - Well Known + - DevBrowser + - DevTools + - Health + - OAuth2 Identity Provider + - OAuth2 Callbacks + - SAML + - Waitlist + - Enterprise Connections diff --git a/packages/mosaic/src/__tests__/feature/fapi.ts b/packages/mosaic/src/__tests__/feature/fapi.ts index f53462b5f2e..8fe33d08d84 100644 --- a/packages/mosaic/src/__tests__/feature/fapi.ts +++ b/packages/mosaic/src/__tests__/feature/fapi.ts @@ -1,28 +1,34 @@ import type { ApiKeyJSON, - APIKeysSettingsJSON, AttributeDataJSON, AttributesJSON, - AuthConfigJSON, ClientJSON, DisplayConfigJSON, EmailAddressJSON, EnterpriseAccountConnectionJSON, EnterpriseAccountJSON, EnterpriseConnectionJSON, - EnvironmentJSON, ExternalAccountJSON, - OAuthProviders, + NativeSettingsJSON, + OAuthProviderSettings, + OAuthStrategy, + OrganizationInvitationJSON, OrganizationJSON, OrganizationMembershipJSON, - OrganizationSettingsJSON, OrganizationSuggestionJSON, PasskeyJSON, + PasskeySettingsData, + PasswordSettingsData, + PermissionJSON, PhoneNumberJSON, PublicKeyCredentialRequestOptionsJSON, PublicOrganizationDataJSON, + RoleJSON, + SessionActivityJSON, SessionJSON, + SessionTask, SessionVerificationJSON, + SignUpData, TokenJSON, UserJSON, UserOrganizationInvitationJSON, @@ -32,30 +38,220 @@ import type { Web3WalletJSON, } from '@clerk/shared/types'; -type Settings = Omit; +interface Timestamps { + created_at: number; + updated_at: number; +} + +interface Enabled { + enabled: boolean; +} + +export type FapiToken = Omit; + +export type FapiEmailAddress = EmailAddressJSON & Timestamps & { reserved: boolean }; + +export type FapiPhoneNumber = PhoneNumberJSON & Timestamps & { reserved: boolean }; + +export type FapiWeb3Wallet = Web3WalletJSON & Timestamps; + +export type FapiExternalAccount = ExternalAccountJSON & Timestamps; + +export type FapiUser = Omit< + UserJSON, + | 'profile_image_id' + | 'create_organizations_limit' + | 'timezone' + | 'email_addresses' + | 'phone_numbers' + | 'web3_wallets' + | 'external_accounts' +> & { + email_addresses: FapiEmailAddress[]; + phone_numbers: FapiPhoneNumber[]; + web3_wallets: FapiWeb3Wallet[]; + external_accounts: FapiExternalAccount[]; + saml_accounts: unknown[]; + create_organizations_limit?: number; + banned: boolean; + locked: boolean; + lockout_expires_in_seconds: number | null; + verification_attempts_remaining: number | null; + last_active_at: number | null; + mfa_enabled_at: number | null; + mfa_disabled_at: number | null; +}; + +export type FapiSession = Omit & { + user: FapiUser; + last_active_token: FapiToken | null; + factor_verification_age: [firstFactorAge: number, secondFactorAge: number]; + tasks?: SessionTask[]; +}; + +export type FapiSessionActivity = SessionActivityJSON & { is_mobile: boolean }; -type FapiWeb3Attribute = Omit & { +export type FapiActiveSession = Omit & { + user: null; + latest_activity: FapiSessionActivity; +}; + +export type FapiClient = Omit & { + sessions: FapiSession[]; + captcha_bypass: boolean; +}; + +export type FapiSessionReverification = Omit & { + object: 'session_reverification'; + session: FapiSession; +}; + +export type FapiRole = RoleJSON & { is_creator_eligible: boolean }; + +export type FapiOrganizationInvitation = OrganizationInvitationJSON & { + url: string | null; + expires_at: number | null; +}; + +export type FapiUserInvitation = UserOrganizationInvitationJSON & { + role_name: string; + url: string | null; + expires_at: number | null; +}; + +type FapiAttributeName = keyof AttributesJSON | 'ticket'; + +type FapiAttribute = Omit & { first_factors: Array; + immutable: boolean; }; -export type FapiUserSettings = Omit, 'social' | 'attributes'> & { - social: Partial; - attributes: Omit & { web3_wallet: FapiWeb3Attribute }; +export type FapiSocial = OAuthProviderSettings & { + block_email_subaddresses: boolean; + not_selectable: boolean; + deprecated: boolean; }; -export type FapiEnvironment = Omit & { - user_settings: FapiUserSettings; - organization_settings: Settings; +export interface FapiUserSettings { + attributes: Record; + social: Partial>; + saml: Enabled; + enterprise_sso: UserSettingsJSON['enterprise_sso']; + sign_in: { second_factor: { required: boolean } }; + sign_up: Omit & { + captcha_widget_type: 'smart' | 'invisible'; + custom_action_required: boolean; + }; + restrictions: Record< + | 'allowlist' + | 'blocklist' + | 'allowlist_blocklist_disabled_on_sign_in' + | 'block_email_subaddresses' + | 'block_disposable_email_domains', + Enabled + >; + password_settings: PasswordSettingsData & { + disable_password_reverification: boolean; + enforce_hibp_on_sign_in: boolean; + }; + username_settings: { + min_length: number; + max_length: number; + allow_extended_special_characters: boolean; + allow_numeric_usernames: boolean; + }; + actions: UserSettingsJSON['actions'] & { create_organizations_limit: boolean | null }; + attack_protection: { + user_lockout: Enabled & { max_attempts: number; duration_in_minutes: number }; + pii: Enabled; + email_link: { require_same_client: boolean }; + enumeration_protection: Enabled; + }; + passkey_settings: PasskeySettingsData & { satisfies_second_factor: boolean }; +} + +type FieldSetting = 'on' | 'off' | 'required'; + +export interface FapiAuthConfig { + object: 'auth_config'; + id: string; + first_name: FieldSetting; + last_name: FieldSetting; + email_address: FieldSetting; + phone_number: FieldSetting; + username: FieldSetting; + password: FieldSetting; + identification_requirements: string[][]; + identification_strategies: string[]; + first_factors: string[]; + second_factors: string[]; + email_address_verification_strategies: string[]; + single_session_mode: boolean; + enhanced_email_deliverability: boolean; + test_mode: boolean; + url_based_session_syncing: boolean; + claimed_at: number | null; + reverification: boolean; + native_settings: NativeSettingsJSON; +} + +export type FapiDisplayConfig = Omit< + DisplayConfigJSON, + 'captcha_oauth_bypass' | 'clerk_js_version' | 'google_one_tap_client_id' +> & { + captcha_oauth_bypass: OAuthStrategy[]; + oauth_consent_url: string; + logo_link_url: string; + experimental_force_oauth_first: boolean; + clerk_js_version: string | null; + google_one_tap_client_id: string | null; + help_url: string | null; + logo_url: string | null; + favicon_url: string | null; + logo_image: object | null; + favicon_image: object | null; }; -export type FapiAttributeOverrides = Partial>>; +export interface FapiOrganizationSettings { + enabled: boolean; + max_allowed_memberships: number; + force_organization_selection: boolean; + actions: { admin_delete: boolean }; + domains: { enabled: boolean; enrollment_modes: string[]; default_role: string }; + slug: { disabled: boolean }; + creator_role: string; +} + +export interface FapiApiKeysSettings { + enabled: boolean; + user_api_keys_enabled: boolean; + orgs_api_keys_enabled: boolean; +} + +export interface FapiEnvironment { + maintenance_mode: boolean; + api_keys_settings: FapiApiKeysSettings; + auth_config: FapiAuthConfig; + commerce_settings: { + billing: { + stripe_publishable_key: string | null; + organization: { enabled: boolean; has_paid_plans: boolean }; + user: { enabled: boolean; has_paid_plans: boolean }; + }; + }; + display_config: FapiDisplayConfig; + organization_settings: FapiOrganizationSettings; + user_settings: FapiUserSettings; +} + +export type FapiAttributeOverrides = Partial>>; export interface FapiEnvironmentOverrides { - api_keys_settings?: Partial>; + api_keys_settings?: Partial; attributes?: FapiAttributeOverrides; - auth_config?: Partial; - display_config?: Partial; - organization_settings?: Partial>; + auth_config?: Partial; + display_config?: Partial; + organization_settings?: Partial; user_settings?: Partial; } @@ -66,16 +262,16 @@ export interface FapiPage { const createdAt = Date.UTC(2026, 0, 1); const farFuture = Date.UTC(2100, 0, 1); +const unsetFactorAge: [number, number] = [99999, -1]; function base64Url(value: object): string { return btoa(JSON.stringify(value)).replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_'); } -export function fapiToken(claims: Record = {}): TokenJSON { +export function fapiToken(claims: Record = {}): FapiToken { const iat = Math.floor(Date.now() / 1000); return { object: 'token', - id: '', jwt: [ base64Url({ alg: 'RS256', typ: 'JWT', kid: 'ins_test' }), base64Url({ iat, nbf: iat, exp: iat + 3600, ...claims }), @@ -84,10 +280,11 @@ export function fapiToken(claims: Record = {}): TokenJSON { }; } -function attribute(overrides: Partial = {}): AttributeDataJSON { +function attribute(overrides: Partial = {}): FapiAttribute { return { enabled: true, required: false, + immutable: false, verifications: [], used_for_first_factor: false, first_factors: [], @@ -98,7 +295,7 @@ function attribute(overrides: Partial = {}): AttributeDataJSO }; } -function attributes(overrides: FapiAttributeOverrides = {}): AttributesJSON { +function attributes(overrides: FapiAttributeOverrides = {}): Record { return { email_address: attribute({ verifications: ['email_code'], @@ -114,19 +311,19 @@ function attributes(overrides: FapiAttributeOverrides = {}): AttributesJSON { password: attribute(overrides.password), web3_wallet: attribute({ enabled: false, ...overrides.web3_wallet }), authenticator_app: attribute({ enabled: false, ...overrides.authenticator_app }), + ticket: attribute({ enabled: false, ...overrides.ticket }), backup_code: attribute({ enabled: false, ...overrides.backup_code }), passkey: attribute({ enabled: false, ...overrides.passkey }), }; } +const disabled = { enabled: false }; + export function fapiEnvironment(overrides: FapiEnvironmentOverrides = {}): FapiEnvironment { return { - object: 'environment', - id: 'env_1', maintenance_mode: false, api_keys_settings: { - object: 'api_keys_settings', - id: 'api_keys_settings_1', + enabled: false, user_api_keys_enabled: false, orgs_api_keys_enabled: false, ...overrides.api_keys_settings, @@ -134,14 +331,33 @@ export function fapiEnvironment(overrides: FapiEnvironmentOverrides = {}): FapiE auth_config: { object: 'auth_config', id: 'aac_1', + first_name: 'off', + last_name: 'off', + email_address: 'on', + phone_number: 'off', + username: 'off', + password: 'on', + identification_requirements: [], + identification_strategies: [], + first_factors: [], + second_factors: [], + email_address_verification_strategies: [], single_session_mode: false, + enhanced_email_deliverability: false, + test_mode: false, + url_based_session_syncing: false, claimed_at: null, reverification: false, + native_settings: { + object: 'native_settings', + api_enabled: false, + trusted_device_sign_in_enabled: false, + trusted_device_enrollment_prompt_after_sign_in_enabled: false, + trusted_device_enrollment_prompt_after_sign_up_enabled: false, + }, ...overrides.auth_config, }, commerce_settings: { - object: 'commerce_settings', - id: 'commerce_settings_1', billing: { stripe_publishable_key: null, organization: { enabled: false, has_paid_plans: false }, @@ -162,11 +378,21 @@ export function fapiEnvironment(overrides: FapiEnvironmentOverrides = {}): FapiE captcha_widget_type: null, captcha_public_key_invisible: null, captcha_provider: 'turnstile', - captcha_oauth_bypass: null, + captcha_oauth_bypass: [], + clerk_js_version: null, + experimental_force_oauth_first: false, + favicon_image: null, + favicon_url: null, + google_one_tap_client_id: null, + help_url: null, home_url: '', instance_environment_type: 'production', + logo_image: null, + logo_link_url: '', + logo_url: null, logo_image_url: '', favicon_image_url: '', + oauth_consent_url: '', preferred_sign_in_strategy: 'password', sign_in_url: '/sign-in', sign_up_url: '/sign-up', @@ -202,27 +428,38 @@ export function fapiEnvironment(overrides: FapiEnvironmentOverrides = {}): FapiE max_allowed_memberships: 5, force_organization_selection: false, actions: { admin_delete: true }, - domains: { enabled: false, enrollment_modes: [], default_role: null }, + domains: { enabled: false, enrollment_modes: [], default_role: 'org:member' }, slug: { disabled: false }, - organization_creation_defaults: { enabled: false }, + creator_role: 'org:admin', ...overrides.organization_settings, }, user_settings: { attributes: attributes(overrides.attributes), - actions: { delete_self: true, create_organization: true }, + actions: { delete_self: true, create_organization: true, create_organizations_limit: null }, social: {}, + saml: disabled, enterprise_sso: { enabled: false, self_serve_sso: false, self_serve_directory_sync: false }, - sign_in: { second_factor: { required: false, enabled: false } }, + sign_in: { second_factor: { required: false } }, sign_up: { - allowlist_only: false, progressive: true, captcha_enabled: false, + captcha_widget_type: 'smart', + custom_action_required: false, mode: 'public', legal_consent_enabled: false, }, + restrictions: { + allowlist: disabled, + blocklist: disabled, + allowlist_blocklist_disabled_on_sign_in: disabled, + block_email_subaddresses: disabled, + block_disposable_email_domains: disabled, + }, password_settings: { allowed_special_characters: '', disable_hibp: false, + disable_password_reverification: false, + enforce_hibp_on_sign_in: false, min_length: 8, max_length: 72, require_special_char: false, @@ -232,30 +469,58 @@ export function fapiEnvironment(overrides: FapiEnvironmentOverrides = {}): FapiE show_zxcvbn: false, min_zxcvbn_strength: 0, }, - passkey_settings: { allow_autofill: false, show_sign_in_button: false }, - username_settings: { min_length: 4, max_length: 64 }, + passkey_settings: { allow_autofill: false, show_sign_in_button: false, satisfies_second_factor: false }, + username_settings: { + min_length: 4, + max_length: 64, + allow_extended_special_characters: false, + allow_numeric_usernames: false, + }, + attack_protection: { + user_lockout: { enabled: false, max_attempts: 100, duration_in_minutes: 60 }, + pii: disabled, + email_link: { require_same_client: false }, + enumeration_protection: disabled, + }, ...overrides.user_settings, }, - protect_config: { object: 'protect_config', id: 'protect_config_1' }, + }; +} + +export function fapiSocial(strategy: OAuthStrategy, name: string, overrides: Partial = {}): FapiSocial { + return { + enabled: true, + required: false, + authenticatable: true, + block_email_subaddresses: false, + not_selectable: false, + deprecated: false, + strategy, + name, + logo_url: null, + ...overrides, }; } export function fapiEmailAddress( - overrides: Partial & Pick, -): EmailAddressJSON { + overrides: Partial & Pick, +): FapiEmailAddress { return { object: 'email_address', email_address: `${overrides.id}@example.com`, + reserved: false, verification: null, linked_to: [], matches_sso_connection: false, + created_at: createdAt, + updated_at: createdAt, ...overrides, }; } export function fapiExternalAccount( - overrides: Partial & Pick, -): ExternalAccountJSON { + overrides: Partial & Pick, +): FapiExternalAccount { return { object: 'external_account', identification_id: overrides.id, @@ -270,19 +535,47 @@ export function fapiExternalAccount( public_metadata: {}, label: '', verification: fapiVerification(`oauth_${overrides.provider}`, { status: 'verified' }), + created_at: createdAt, + updated_at: createdAt, ...overrides, }; } +export function fapiSamlLinking(verification: VerificationJSON) { + return { + object: 'external_account', + id: '', + provider: '', + identification_id: '', + provider_user_id: '', + approved_scopes: '', + email_address: '', + email_address_verified: null, + first_name: '', + last_name: '', + avatar_url: '', + username: null, + phone_number: null, + public_metadata: null, + label: null, + created_at: 0, + updated_at: 0, + verification, + }; +} + export function fapiPhoneNumber( - overrides: Partial & Pick, -): PhoneNumberJSON { + overrides: Partial & Pick, +): FapiPhoneNumber { return { object: 'phone_number', + reserved: false, reserved_for_second_factor: false, default_second_factor: false, linked_to: [], verification: null, + created_at: createdAt, + updated_at: createdAt, ...overrides, }; } @@ -297,7 +590,6 @@ export function fapiEnterpriseConnection( provider: 'saml_okta', logo_public_url: null, domains: [], - organization_id: null, sync_user_attributes: false, disable_additional_identifications: false, allow_organization_account_linking: true, @@ -307,7 +599,7 @@ export function fapiEnterpriseConnection( }; } -export function fapiUser(overrides: Partial & Pick): UserJSON { +export function fapiUser(overrides: Partial & Pick): FapiUser { return { object: 'user', external_id: null, @@ -322,10 +614,10 @@ export function fapiUser(overrides: Partial & Pick): U web3_wallets: [], external_accounts: [], enterprise_accounts: [], + saml_accounts: [], passkeys: [], organization_memberships: [], password_enabled: true, - profile_image_id: '', first_name: null, last_name: null, totp_enabled: false, @@ -333,9 +625,15 @@ export function fapiUser(overrides: Partial & Pick): U two_factor_enabled: false, public_metadata: {}, unsafe_metadata: {}, + banned: false, + locked: false, + lockout_expires_in_seconds: null, + verification_attempts_remaining: null, last_sign_in_at: null, + last_active_at: null, + mfa_enabled_at: null, + mfa_disabled_at: null, create_organization_enabled: true, - create_organizations_limit: null, delete_self_enabled: true, legal_accepted_at: null, created_at: createdAt, @@ -385,34 +683,30 @@ export function fapiEnterpriseAccount( } export function fapiWeb3Wallet( - overrides: Partial & Pick, -): Web3WalletJSON { + overrides: Partial & Pick, +): FapiWeb3Wallet { return { object: 'web3_wallet', - verification: fapiVerification('web3_metamask_signature', { - status: 'verified', - verified_at_client: '', - attempts: 1, - expire_at: 0, - }), + verification: fapiVerification('web3_metamask_signature', { status: 'verified', attempts: 1, expire_at: 0 }), + created_at: createdAt, + updated_at: createdAt, ...overrides, }; } -export function fapiSession(overrides: Partial & Pick): SessionJSON { +export function fapiSession(overrides: Partial & Pick): FapiSession { const { user } = overrides; const primaryEmail = user.email_addresses.find(email => email.id === user.primary_email_address_id); return { object: 'session', status: 'active', - factor_verification_age: null, + factor_verification_age: unsetFactorAge, expire_at: farFuture, abandon_at: farFuture, last_active_at: createdAt, last_active_token: fapiToken({ sid: overrides.id, sub: user.id }), last_active_organization_id: null, actor: null, - tasks: null, public_user_data: { first_name: user.first_name, last_name: user.last_name, @@ -427,43 +721,66 @@ export function fapiSession(overrides: Partial & Pick = { password: 'verification_password', email_code: 'verification_otp', phone_code: 'verification_otp', + reset_password_email_code: 'verification_otp', + reset_password_phone_code: 'verification_otp', + email_link: 'verification_email_link', totp: 'verification_totp', backup_code: 'verification_backup_code', passkey: 'verification_passkey', + admin: 'verification_admin', + ticket: 'verification_ticket', + saml: 'verification_saml', + google_one_tap: 'verification_google_one_tap', }; -export function fapiPasskeyRequestOptions(): PublicKeyCredentialRequestOptionsJSON { - return { - allowCredentials: [], - challenge: 'Y2hhbGxlbmdl', - rpId: 'clerk.abcef.12345.prod.lclclerk.com', - timeout: 60000, - userVerification: 'preferred', - }; +const verificationObjectPrefixes: ReadonlyArray<[prefix: string, object: string]> = [ + ['from_oauth_', 'verification_from_oauth'], + ['oauth_', 'verification_oauth'], + ['web3_', 'verification_web3'], +]; + +const verificationExtras: Record Partial> = { + passkey: () => ({ nonce: JSON.stringify(fapiPasskeyRequestOptions()) }), + email_link: () => ({ verified_at_client: '' }), +}; + +function verificationObject(strategy: string): string { + const prefixed = verificationObjectPrefixes.find(([prefix]) => strategy.startsWith(prefix)); + return verificationObjects[strategy] ?? prefixed?.[1] ?? 'verification'; } export function fapiVerification(strategy: string, overrides: Partial = {}): VerificationJSON { return { - object: verificationObjects[strategy] ?? 'verification', + object: verificationObject(strategy), status: 'unverified', strategy, attempts: 0, expire_at: farFuture, - ...(strategy === 'passkey' ? { nonce: JSON.stringify(fapiPasskeyRequestOptions()) } : {}), + ...verificationExtras[strategy]?.(), ...overrides, } as VerificationJSON; } export function fapiSessionVerification( - session: SessionJSON, - overrides: Partial = {}, -): SessionVerificationJSON { + session: FapiSession, + overrides: Partial = {}, +): FapiSessionReverification { return { - object: 'session_verification', + object: 'session_reverification', id: `sessverif_${session.id}`, status: 'needs_first_factor', level: 'first_factor', @@ -476,7 +793,7 @@ export function fapiSessionVerification( }; } -export function fapiClient(sessions: SessionJSON[] = []): ClientJSON { +export function fapiClient(sessions: FapiSession[] = []): FapiClient { return { object: 'client', id: 'client_1', @@ -486,6 +803,7 @@ export function fapiClient(sessions: SessionJSON[] = []): ClientJSON { last_active_session_id: sessions[0]?.id ?? null, last_authentication_strategy: null, cookie_expires_at: null, + captcha_bypass: false, created_at: createdAt, updated_at: createdAt, }; @@ -528,6 +846,20 @@ export function fapiMembership( }; } +export function fapiRole(key: string, name: string, permissions: PermissionJSON[] = []): FapiRole { + return { + object: 'role', + id: `role_${key.replace('org:', '')}`, + key, + name, + description: '', + is_creator_eligible: true, + permissions, + created_at: createdAt, + updated_at: createdAt, + }; +} + function publicOrganizationData(organization: OrganizationJSON): PublicOrganizationDataJSON { return { id: organization.id, @@ -541,8 +873,8 @@ function publicOrganizationData(organization: OrganizationJSON): PublicOrganizat export function fapiInvitation( id: string, organization: OrganizationJSON, - overrides: Partial = {}, -): UserOrganizationInvitationJSON { + overrides: Partial = {}, +): FapiUserInvitation { return { object: 'organization_invitation', id, @@ -551,6 +883,9 @@ export function fapiInvitation( public_metadata: {}, status: 'pending', role: 'org:member', + role_name: 'Member', + url: null, + expires_at: null, created_at: createdAt, updated_at: createdAt, ...overrides, @@ -592,6 +927,10 @@ export function fapiApiKey(overrides: Partial & Pick(data: T[], totalCount = data.length): FapiPage { return { data, total_count: totalCount }; } diff --git a/packages/mosaic/src/__tests__/feature/raw.d.ts b/packages/mosaic/src/__tests__/feature/raw.d.ts new file mode 100644 index 00000000000..331f2668fc7 --- /dev/null +++ b/packages/mosaic/src/__tests__/feature/raw.d.ts @@ -0,0 +1,4 @@ +declare module '*.yml?raw' { + const source: string; + export default source; +} diff --git a/packages/mosaic/src/features/api-keys/__tests__/api-keys-table.feature.test.tsx b/packages/mosaic/src/features/api-keys/__tests__/api-keys-table.feature.test.tsx index 9da43587fb7..8445adad308 100644 --- a/packages/mosaic/src/features/api-keys/__tests__/api-keys-table.feature.test.tsx +++ b/packages/mosaic/src/features/api-keys/__tests__/api-keys-table.feature.test.tsx @@ -5,9 +5,17 @@ import { http, HttpResponse } from 'msw'; import { describe, expect, it, vi } from 'vitest'; import { openDialog } from '../../../__tests__/feature/dialog'; -import { type FakeFapiSeed, fapiUrl, holdRequests, serveFapi, worker } from '../../../__tests__/feature/fake-fapi'; +import { + allowSpecViolation, + type FakeFapiSeed, + fapiUrl, + holdRequests, + serveFapi, + worker, +} from '../../../__tests__/feature/fake-fapi'; import { fapiApiKey, + fapiApiKeyId, fapiClient, fapiEmailAddress, fapiEnvironment, @@ -24,10 +32,10 @@ import { APIKeysTable } from '../api-keys-table'; const DAY = 24 * 60 * 60 * 1000; -const acme = fapiOrganization({ id: 'org_1', name: 'Acme' }); +const acme = fapiOrganization({ id: 'org_acme00000000000000000000000', name: 'Acme' }); const alice = fapiUser({ - id: 'user_1', + id: 'user_alice0000000000000000000000', first_name: 'Alice', email_addresses: [fapiEmailAddress({ id: 'idn_alice', email_address: 'alice@example.com' })], organization_memberships: [fapiMembership(acme)], @@ -36,10 +44,10 @@ const alice = fapiUser({ const userKey = (id: string, name: string, overrides: Partial = {}) => fapiApiKey({ id, name, subject: alice.id, created_at: Date.UTC(2026, 0, 5, 12), ...overrides }); -const webApp = userKey('ak_1234567890FKWO', 'Web app', { expiration: Date.UTC(2027, 11, 31, 12) }); -const ciPipeline = userKey('ak_2', 'CI pipeline', { last_used_at: Date.now() - 2 * 60_000 }); +const webApp = userKey(fapiApiKeyId(0xbeef), 'Web app', { expiration: Date.UTC(2027, 11, 31, 12) }); +const ciPipeline = userKey(fapiApiKeyId(2), 'CI pipeline', { last_used_at: Date.now() - 2 * 60_000 }); -const manyKeys = Array.from({ length: 11 }, (_, index) => userKey(`ak_${index + 1}`, `Key ${index + 1}`)); +const manyKeys = Array.from({ length: 11 }, (_, index) => userKey(fapiApiKeyId(index + 1), `Key ${index + 1}`)); function signedIn(apiKeys: ApiKeyJSON[] = [webApp, ciPipeline], overrides: FakeFapiSeed = {}): FakeFapiSeed { return { @@ -143,7 +151,7 @@ describe('APIKeysTable', () => { describe('listing keys', () => { it("shows the user's own keys, not the active organization's", async () => { await renderTable( - signedIn([webApp, ciPipeline, fapiApiKey({ id: 'ak_org', name: 'Org key', subject: acme.id })]), + signedIn([webApp, ciPipeline, fapiApiKey({ id: fapiApiKeyId(0x0a), name: 'Org key', subject: acme.id })]), ); expect(await within(await loadedTable()).findByText('Web app')).toBeVisible(); @@ -160,11 +168,11 @@ describe('APIKeysTable', () => { expect(within(table()).getByText('Never expires')).toBeVisible(); expect(within(table()).getByRole('cell', { name: '2 minutes ago' })).toBeVisible(); expect(within(table()).getByRole('cell', { name: '-' })).toBeVisible(); - expect(within(table()).getByText('ak_...FKWO', { exact: false })).toBeVisible(); + expect(within(table()).getByText('ak_...beef', { exact: false })).toBeVisible(); }); it('describes a key last used after the table read the time as used now', async () => { - await renderTable(signedIn([userKey('ak_3', 'Worker', { last_used_at: Date.now() + 5 * 60_000 })])); + await renderTable(signedIn([userKey(fapiApiKeyId(3), 'Worker', { last_used_at: Date.now() + 5 * 60_000 })])); expect(await within(await loadedTable()).findByRole('cell', { name: 'now' })).toBeVisible(); }); @@ -272,7 +280,10 @@ describe('APIKeysTable', () => { worker.use( http.get(fapiUrl('/api_keys'), () => failing - ? HttpResponse.json({ errors: [{ code: 'internal_error', message: 'internal_error' }] }, { status: 400 }) + ? HttpResponse.json( + { errors: [{ code: 'internal_error', message: 'internal_error', long_message: 'internal_error' }] }, + { status: 400 }, + ) : undefined, ), ); @@ -612,6 +623,7 @@ describe('APIKeysTable', () => { }); it('explains a created key that came back without its secret and lists it', async () => { + allowSpecViolation(`POST /api_keys 201: / must have required property 'secret' {"missingProperty":"secret"}`); const consoleError = vi.spyOn(console, 'error'); const { fapi, user } = await renderTable(); const dialog = await openCreate(user); @@ -620,9 +632,9 @@ describe('APIKeysTable', () => { http.post( fapiUrl('/api_keys'), () => { - const created = userKey('ak_new', 'Deploy'); + const created = userKey(fapiApiKeyId(0x0e), 'Deploy'); fapi.apiKeys = [created, ...fapi.apiKeys]; - return HttpResponse.json(created); + return HttpResponse.json(created, { status: 201 }); }, { once: true }, ), @@ -808,7 +820,7 @@ describe('APIKeysTable', () => { const readKeys = 'org:sys_api_keys:read'; const manageKeys = 'org:sys_api_keys:manage'; const orgKey = fapiApiKey({ - id: 'ak_org', + id: fapiApiKeyId(0x0a), name: 'Org key', subject: acme.id, created_at: Date.UTC(2026, 0, 5, 12), @@ -890,7 +902,7 @@ describe('APIKeysTable', () => { it('starts over on the first page with no search when the subject changes', async () => { const orgKeys = Array.from({ length: 11 }, (_, index) => - fapiApiKey({ id: `ak_org_${index + 1}`, name: `Org key ${index + 1}`, subject: acme.id }), + fapiApiKey({ id: fapiApiKeyId(0x100 + index), name: `Org key ${index + 1}`, subject: acme.id }), ); const { user, rerender } = await renderTable(memberOfAcme([readKeys], [...orgKeys, ...manyKeys]), { subject: acme.id, diff --git a/packages/mosaic/src/features/organization-profile/__tests__/organization-profile-api-keys-panel.feature.test.tsx b/packages/mosaic/src/features/organization-profile/__tests__/organization-profile-api-keys-panel.feature.test.tsx index 987bfd8dd7b..732bf172f31 100644 --- a/packages/mosaic/src/features/organization-profile/__tests__/organization-profile-api-keys-panel.feature.test.tsx +++ b/packages/mosaic/src/features/organization-profile/__tests__/organization-profile-api-keys-panel.feature.test.tsx @@ -4,6 +4,7 @@ import { describe, expect, it } from 'vitest'; import { serveFapi } from '../../../__tests__/feature/fake-fapi'; import { fapiApiKey, + fapiApiKeyId, fapiClient, fapiEnvironment, fapiMembership, @@ -14,10 +15,10 @@ import { import { renderWithClerk } from '../../../__tests__/feature/render'; import { OrganizationProfileApiKeysPanel } from '../organization-profile-api-keys-panel'; -const acme = fapiOrganization({ id: 'org_1', name: 'Acme' }); +const acme = fapiOrganization({ id: 'org_acme00000000000000000000000', name: 'Acme' }); const alice = fapiUser({ - id: 'user_1', + id: 'user_alice0000000000000000000000', first_name: 'Alice', organization_memberships: [fapiMembership(acme, { permissions: ['org:sys_api_keys:read'] })], }); @@ -26,7 +27,7 @@ function serve(apiKeysSettings: { user_api_keys_enabled: boolean; orgs_api_keys_ serveFapi({ environment: fapiEnvironment({ api_keys_settings: apiKeysSettings }), client: fapiClient([fapiSession({ id: 'sess_1', user: alice, last_active_organization_id: acme.id })]), - apiKeys: [fapiApiKey({ id: 'ak_org', name: 'Org key', subject: acme.id })], + apiKeys: [fapiApiKey({ id: fapiApiKeyId(0x0a), name: 'Org key', subject: acme.id })], }); } diff --git a/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/__tests__/organization-profile-invitations.feature.test.tsx b/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/__tests__/organization-profile-invitations.feature.test.tsx index 1183c6a75cf..337ff943230 100644 --- a/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/__tests__/organization-profile-invitations.feature.test.tsx +++ b/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/__tests__/organization-profile-invitations.feature.test.tsx @@ -1,4 +1,3 @@ -import type { OrganizationInvitationJSON, RoleJSON } from '@clerk/shared/types'; import { act, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { http, HttpResponse } from 'msw'; @@ -11,6 +10,9 @@ import { fapiEnvironment, fapiMembership, fapiOrganization, + type FapiOrganizationInvitation, + type FapiRole, + fapiRole, fapiSession, fapiUser, } from '../../../../__tests__/feature/fapi'; @@ -20,7 +22,7 @@ import { MosaicLocalizationProvider, resolveLocalization } from '../../../../loc import { OrganizationProfileMembersPanel } from '../organization-profile-members-panel'; const organization = fapiOrganization({ id: 'org_invitations', name: 'Acme' }); -const invitation: OrganizationInvitationJSON = { +const invitation: FapiOrganizationInvitation = { object: 'organization_invitation', id: 'orginv_ada', email_address: 'ada@example.com', @@ -29,6 +31,8 @@ const invitation: OrganizationInvitationJSON = { status: 'pending', role: 'org:member', role_name: 'Member', + url: null, + expires_at: null, created_at: 1_785_000_000_000, updated_at: 1_785_000_000_000, }; @@ -386,17 +390,8 @@ describe('connected organization invitations', () => { }); }); -const adminRole: RoleJSON = { - object: 'role', - id: 'role_admin', - key: 'org:admin', - name: 'Admin', - description: '', - permissions: [], - created_at: 0, - updated_at: 0, -}; -const memberRole: RoleJSON = { ...adminRole, id: 'role_member', key: 'org:member', name: 'Member' }; +const adminRole = fapiRole('org:admin', 'Admin'); +const memberRole = fapiRole('org:member', 'Member'); const bob = fapiMembership(organization, { id: 'orgmem_bob', public_user_data: { @@ -413,12 +408,12 @@ const MANAGE = ['org:sys_memberships:read', 'org:sys_memberships:manage']; function serveInvite({ permissions = MANAGE, roles = [adminRole, memberRole], - defaultRole = null, + defaultRole = '', maxAllowedMemberships = 0, }: { permissions?: string[]; - roles?: RoleJSON[]; - defaultRole?: string | null; + roles?: FapiRole[]; + defaultRole?: string; maxAllowedMemberships?: number; } = {}) { const org = { ...organization, max_allowed_memberships: maxAllowedMemberships }; @@ -553,7 +548,7 @@ describe('inviting members', () => { fapi.environment = fapiEnvironment({ organization_settings: { ...fapiEnvironment().organization_settings, - domains: { enabled: true, enrollment_modes: [], default_role: null }, + domains: { enabled: true, enrollment_modes: [], default_role: '' }, }, }); await renderWithClerk(); diff --git a/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/__tests__/organization-profile-members-panel.feature.test.tsx b/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/__tests__/organization-profile-members-panel.feature.test.tsx index e911eeb2b7b..bf729c72f32 100644 --- a/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/__tests__/organization-profile-members-panel.feature.test.tsx +++ b/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/__tests__/organization-profile-members-panel.feature.test.tsx @@ -1,4 +1,3 @@ -import type { RoleJSON } from '@clerk/shared/types'; import { act, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { http, HttpResponse } from 'msw'; @@ -9,6 +8,7 @@ import { fapiClient, fapiMembership, fapiOrganization, + fapiRole, fapiSession, fapiUser, } from '../../../../__tests__/feature/fapi'; @@ -29,17 +29,8 @@ const bob = fapiMembership(organization, { has_image: false, }, }); -const adminRole: RoleJSON = { - object: 'role', - id: 'role_admin', - key: 'org:admin', - name: 'Admin', - description: '', - permissions: [], - created_at: 0, - updated_at: 0, -}; -const memberRole: RoleJSON = { ...adminRole, id: 'role_member', key: 'org:member', name: 'Member' }; +const adminRole = fapiRole('org:admin', 'Admin'); +const memberRole = fapiRole('org:member', 'Member'); const manage = ['org:sys_memberships:read', 'org:sys_memberships:manage']; function serve(permissions: string[] = ['org:sys_memberships:read']) { @@ -502,7 +493,11 @@ describe('OrganizationProfileMembersPanel', () => { fapiUrl('/v1/organizations/:organizationId/roles'), () => HttpResponse.json( - { errors: [{ code: 'roles_unavailable', message: 'roles_unavailable' }] }, + { + errors: [ + { code: 'roles_unavailable', message: 'roles_unavailable', long_message: 'roles_unavailable' }, + ], + }, { status: 400 }, ), { once: true }, diff --git a/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/__tests__/organization-profile-requests.feature.test.tsx b/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/__tests__/organization-profile-requests.feature.test.tsx index 7ff813b6ca5..121220510c8 100644 --- a/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/__tests__/organization-profile-requests.feature.test.tsx +++ b/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/__tests__/organization-profile-requests.feature.test.tsx @@ -38,7 +38,7 @@ function serve(permissions: string[], domainsEnabled = true) { const user = fapiUser({ id: 'user_manager', organization_memberships: [membership] }); return serveFapi({ environment: fapiEnvironment({ - organization_settings: { domains: { enabled: domainsEnabled, enrollment_modes: [], default_role: null } }, + organization_settings: { domains: { enabled: domainsEnabled, enrollment_modes: [], default_role: '' } }, }), client: fapiClient([fapiSession({ id: 'sess_requests', user, last_active_organization_id: organization.id })]), memberships: [membership], @@ -282,7 +282,7 @@ describe('connected organization requests', () => { const user = fapiUser({ id: 'user_manager', organization_memberships: [first, second] }); const fapi = serveFapi({ environment: fapiEnvironment({ - organization_settings: { domains: { enabled: true, enrollment_modes: [], default_role: null } }, + organization_settings: { domains: { enabled: true, enrollment_modes: [], default_role: '' } }, }), client: fapiClient([fapiSession({ id: 'sess_requests', user, last_active_organization_id: organization.id })]), memberships: [first, second], diff --git a/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/organization-profile-members-panel.model.ts b/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/organization-profile-members-panel.model.ts index f7024b61bef..86126c40846 100644 --- a/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/organization-profile-members-panel.model.ts +++ b/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/organization-profile-members-panel.model.ts @@ -34,7 +34,7 @@ export function useMembersPanelRolesModel(canReadRoles: boolean): MembersPanelRo const environment = useMosaicEnvironment(); const { data: result, isLoading } = useOrganizationRolesQuery({ enabled: canReadRoles }); return { - defaultRole: environment?.organizationSettings.domains.defaultRole ?? null, + defaultRole: environment?.organizationSettings.domains.defaultRole || null, roles: result ? { roles: result.data.map(({ key, name }) => ({ key, name })), diff --git a/packages/mosaic/src/features/reverification/__tests__/reverification.feature.test.tsx b/packages/mosaic/src/features/reverification/__tests__/reverification.feature.test.tsx index 6aa155725c9..745ecdf9aad 100644 --- a/packages/mosaic/src/features/reverification/__tests__/reverification.feature.test.tsx +++ b/packages/mosaic/src/features/reverification/__tests__/reverification.feature.test.tsx @@ -373,7 +373,10 @@ describe('Reverification', () => { await renderReverification(guardedAction(), verificationSeed({ firstFactors: [passwordFactor] })); worker.use( http.post(fapiUrl(VERIFY), () => - HttpResponse.json({ errors: [{ code: 'some_new_error', message: 'Nope' }] }, { status: 400 }), + HttpResponse.json( + { errors: [{ code: 'some_new_error', message: 'Nope', long_message: 'Nope' }] }, + { status: 400 }, + ), ), ); @@ -1651,7 +1654,10 @@ describe('Reverification', () => { await renderReverification(guardedAction(), verificationSeed({ firstFactors: [passwordFactor] })); worker.use( http.post(fapiUrl(ATTEMPT_FIRST), () => - HttpResponse.json({ errors: [{ code: 'some_new_error', message: 'Short message' }] }, { status: 422 }), + HttpResponse.json( + { errors: [{ code: 'some_new_error', message: 'Short message', long_message: 'Short message' }] }, + { status: 422 }, + ), ), ); const user = await startVerification(); diff --git a/packages/mosaic/src/features/user-button/__tests__/user-button.feature.test.tsx b/packages/mosaic/src/features/user-button/__tests__/user-button.feature.test.tsx index 27b2f9dc87a..94817708b0f 100644 --- a/packages/mosaic/src/features/user-button/__tests__/user-button.feature.test.tsx +++ b/packages/mosaic/src/features/user-button/__tests__/user-button.feature.test.tsx @@ -1,4 +1,4 @@ -import type { CustomPage, UserJSON, Web3WalletJSON } from '@clerk/shared/types'; +import type { CustomPage } from '@clerk/shared/types'; import { act, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { describe, expect, it, onTestFinished, vi } from 'vitest'; @@ -15,7 +15,9 @@ import { fapiPhoneNumber, fapiSession, fapiSuggestion, + type FapiUser, fapiUser, + fapiWeb3Wallet, } from '../../../__tests__/feature/fapi'; import { openMenu } from '../../../__tests__/feature/menu'; import { renderWithClerk } from '../../../__tests__/feature/render'; @@ -990,15 +992,10 @@ describe('UserButton', () => { describe('naming the account', () => { const phone = fapiPhoneNumber({ id: 'idn_phone', phone_number: '+15550100' }); - const wallet: Web3WalletJSON = { - object: 'web3_wallet', - id: 'idn_wallet', - web3_wallet: '0xabc', - verification: null, - }; + const wallet = fapiWeb3Wallet({ id: 'idn_wallet', web3_wallet: '0xabc', verification: null }); const noEmail = { username: null, email_addresses: [], primary_email_address_id: null }; - const signedInAs = (overrides: Partial) => + const signedInAs = (overrides: Partial) => signedIn({ client: fapiClient([fapiSession({ id: 'sess_1', user: fapiUser({ ...alice, ...overrides }) })]) }); it.each([ diff --git a/packages/mosaic/src/features/user-profile/__tests__/user-profile-api-keys-panel.feature.test.tsx b/packages/mosaic/src/features/user-profile/__tests__/user-profile-api-keys-panel.feature.test.tsx index a80a8732f13..65e34f56912 100644 --- a/packages/mosaic/src/features/user-profile/__tests__/user-profile-api-keys-panel.feature.test.tsx +++ b/packages/mosaic/src/features/user-profile/__tests__/user-profile-api-keys-panel.feature.test.tsx @@ -2,17 +2,24 @@ import { screen } from '@testing-library/react'; import { describe, expect, it } from 'vitest'; import { serveFapi } from '../../../__tests__/feature/fake-fapi'; -import { fapiApiKey, fapiClient, fapiEnvironment, fapiSession, fapiUser } from '../../../__tests__/feature/fapi'; +import { + fapiApiKey, + fapiApiKeyId, + fapiClient, + fapiEnvironment, + fapiSession, + fapiUser, +} from '../../../__tests__/feature/fapi'; import { renderWithClerk } from '../../../__tests__/feature/render'; import { UserProfileApiKeysPanel } from '../user-profile-api-keys-panel'; -const alice = fapiUser({ id: 'user_1', first_name: 'Alice' }); +const alice = fapiUser({ id: 'user_alice0000000000000000000000', first_name: 'Alice' }); function serve(userAPIKeysEnabled: boolean) { serveFapi({ environment: fapiEnvironment({ api_keys_settings: { user_api_keys_enabled: userAPIKeysEnabled } }), client: fapiClient([fapiSession({ id: 'sess_1', user: alice })]), - apiKeys: [fapiApiKey({ id: 'ak_1', name: 'Web app', subject: alice.id })], + apiKeys: [fapiApiKey({ id: fapiApiKeyId(1), name: 'Web app', subject: alice.id })], }); } diff --git a/packages/mosaic/src/features/user-profile/user-profile-active-devices-section/__tests__/user-profile-active-devices-section.feature.test.tsx b/packages/mosaic/src/features/user-profile/user-profile-active-devices-section/__tests__/user-profile-active-devices-section.feature.test.tsx index fd42ecf450d..223615756d2 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-active-devices-section/__tests__/user-profile-active-devices-section.feature.test.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-active-devices-section/__tests__/user-profile-active-devices-section.feature.test.tsx @@ -1,5 +1,5 @@ import { getClerkQueryClient } from '@clerk/shared/react'; -import type { ActClaim, SessionWithActivitiesJSON } from '@clerk/shared/types'; +import type { ActClaim } from '@clerk/shared/types'; import { createDeferredPromise } from '@clerk/shared/utils'; import { act, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; @@ -13,7 +13,13 @@ import { serveFapi, worker, } from '../../../../__tests__/feature/fake-fapi'; -import { fapiClient, fapiSession, fapiUser } from '../../../../__tests__/feature/fapi'; +import { + type FapiActiveSession, + fapiClient, + fapiSession, + type FapiSessionActivity, + fapiUser, +} from '../../../../__tests__/feature/fapi'; import { openMenu } from '../../../../__tests__/feature/menu'; import { renderWithClerk } from '../../../../__tests__/feature/render'; import { MosaicNowProvider } from '../../../../hooks/use-now'; @@ -25,14 +31,21 @@ const alice = fapiUser({ id: 'user_1' }); function device( id: string, - status: SessionWithActivitiesJSON['status'], - activity: Partial = {}, + status: ActiveDeviceRecord['status'], + activity: Partial = {}, ): ActiveDeviceRecord { + return { ...activeSession(id, status, activity), ownerUserId: alice.id }; +} + +function activeSession( + id: string, + status: FapiActiveSession['status'], + activity: Partial = {}, +): FapiActiveSession { const { user: _user, ...session } = fapiSession({ id, user: alice }); return { ...session, user: null, - ownerUserId: alice.id, status, latest_activity: { object: 'session_activity', @@ -40,12 +53,13 @@ function device( browser_name: 'Safari', browser_version: '18', device_type: 'MacBook Pro', + is_mobile: false, city: 'Paris', country: 'France', ip_address: '192.0.2.1', ...activity, }, - } satisfies ActiveDeviceRecord; + }; } function serveDevices( @@ -70,14 +84,26 @@ function serveDevices( if (params.id === options.failOnceId && !failed) { failed = true; return HttpResponse.json( - { errors: [{ code: 'revoke_failed', message: 'Could not revoke device' }] }, + { + errors: [ + { code: 'revoke_failed', message: 'Could not revoke device', long_message: 'Could not revoke device' }, + ], + }, { status: 400 }, ); } if (params.id === options.reverifyOnceId && !failed) { failed = true; return HttpResponse.json( - { errors: [{ code: 'session_reverification_required', message: 'Verification required' }] }, + { + errors: [ + { + code: 'session_reverification_required', + message: 'Verification required', + long_message: 'Verification required', + }, + ], + }, { status: 400 }, ); } @@ -276,10 +302,12 @@ describe('Active devices', () => { }); it('filters an expired row even if a stale list response includes it', async () => { - const expired = device('sess_expired', 'expired', { device_type: 'Expired laptop' }); + const expired = activeSession('sess_expired', 'expired', { device_type: 'Expired laptop' }); serveDevices([device('sess_current', 'active')]); worker.use( - http.get(fapiUrl('/v1/me/sessions/active'), () => HttpResponse.json([device('sess_current', 'active'), expired])), + http.get(fapiUrl('/v1/me/sessions/active'), () => + HttpResponse.json([activeSession('sess_current', 'active'), expired]), + ), ); await renderWithClerk(); @@ -292,7 +320,10 @@ describe('Active devices', () => { serveDevices([device('sess_current', 'active')]); worker.use( http.get(fapiUrl('/v1/me/sessions/active'), () => - HttpResponse.json({ errors: [{ code: 'internal_clerk_error', message: 'Unavailable' }] }, { status: 500 }), + HttpResponse.json( + { errors: [{ code: 'internal_clerk_error', message: 'Unavailable', long_message: 'Unavailable' }] }, + { status: 500 }, + ), ), ); @@ -451,8 +482,8 @@ describe('Active devices', () => { oldRequests += 1; await pending.promise; return HttpResponse.json([ - device('sess_current', 'active'), - device('sess_other', 'active', { device_type: 'Old phone' }), + activeSession('sess_current', 'active'), + activeSession('sess_other', 'active', { device_type: 'Old phone' }), ]); }), ); @@ -490,7 +521,7 @@ describe('Active devices', () => { http.get(fapiUrl('/v1/me/sessions/active'), async () => { requests += 1; await pending.promise; - return HttpResponse.json([device('sess_current', 'active')]); + return HttpResponse.json([activeSession('sess_current', 'active')]); }), ); const view = await renderWithClerk(
); @@ -539,8 +570,11 @@ describe('Active devices', () => { http.get(fapiUrl('/v1/me/sessions/active'), () => HttpResponse.json( fapi.client.last_active_session_id === 'sess_next' - ? [device('sess_next', 'active', { device_type: 'Next laptop' })] - : [device('sess_current', 'active'), device('sess_other', 'active', { device_type: 'Old phone' })], + ? [activeSession('sess_next', 'active', { device_type: 'Next laptop' })] + : [ + activeSession('sess_current', 'active'), + activeSession('sess_other', 'active', { device_type: 'Old phone' }), + ], ), ), http.post(fapiUrl('/v1/me/sessions/sess_other/revoke'), async () => { @@ -714,7 +748,14 @@ describe('Active devices', () => { serveDevices([device('sess_current', 'active'), device('sess_other', 'active', { device_type: 'iPhone' })]); worker.use( http.post(fapiUrl('/v1/me/sessions/sess_other/revoke'), () => - HttpResponse.json({ errors: [{ code: 'revoke_failed', message: 'Could not revoke device' }] }, { status: 400 }), + HttpResponse.json( + { + errors: [ + { code: 'revoke_failed', message: 'Could not revoke device', long_message: 'Could not revoke device' }, + ], + }, + { status: 400 }, + ), ), ); await renderWithClerk(); diff --git a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/__tests__/user-profile-connected-accounts.connect.feature.test.tsx b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/__tests__/user-profile-connected-accounts.connect.feature.test.tsx index 2595e112eaf..11eaa07c9a1 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/__tests__/user-profile-connected-accounts.connect.feature.test.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/__tests__/user-profile-connected-accounts.connect.feature.test.tsx @@ -195,7 +195,7 @@ describe('connected accounts', () => { }); it('shows the OAuth error when FAPI returns no verification URL', async () => { - serveFapi(signedIn([])); + const fapi = serveFapi(signedIn([])); worker.use( http.post(fapiUrl('/v1/me/external_accounts'), () => HttpResponse.json({ @@ -204,7 +204,7 @@ describe('connected accounts', () => { provider: 'github', verification: fapiVerification('oauth_github', { status: 'unverified' }), }), - client: null, + client: fapi.client, }), ), ); @@ -325,7 +325,7 @@ describe('connected accounts', () => { }); it('does not open an OAuth response after the active user changes', async () => { - serveFapi( + const fapi = serveFapi( signedIn([], { client: fapiClient([ fapiSession({ id: 'sess_1', user: fapiUser({ id: 'user_1', external_accounts: [] }) }), @@ -362,7 +362,7 @@ describe('connected accounts', () => { }), }); await act(async () => { - response.resolve(HttpResponse.json({ response: pending, client: null })); + response.resolve(HttpResponse.json({ response: pending, client: fapi.client })); await create.mock.results[0]?.value; }); diff --git a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/__tests__/user-profile-connected-accounts.fixtures.tsx b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/__tests__/user-profile-connected-accounts.fixtures.tsx index 929bf808624..f9a850d1755 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/__tests__/user-profile-connected-accounts.fixtures.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/__tests__/user-profile-connected-accounts.fixtures.tsx @@ -7,6 +7,7 @@ import { fapiEnvironment, fapiExternalAccount, fapiSession, + fapiSocial, fapiUser, fapiVerification, } from '../../../../__tests__/feature/fapi'; @@ -32,22 +33,8 @@ export function signedIn(accounts = [google], overrides: FakeFapiSeed = {}) { environment: fapiEnvironment({ user_settings: { social: { - oauth_google: { - enabled: true, - required: false, - authenticatable: true, - strategy: 'oauth_google', - name: 'Google', - logo_url: null, - }, - oauth_github: { - enabled: true, - required: false, - authenticatable: true, - strategy: 'oauth_github', - name: 'GitHub', - logo_url: null, - }, + oauth_google: fapiSocial('oauth_google', 'Google'), + oauth_github: fapiSocial('oauth_github', 'GitHub'), }, }, }), diff --git a/packages/mosaic/src/features/user-profile/user-profile-email-section/__tests__/user-profile-email-section.feature.test.tsx b/packages/mosaic/src/features/user-profile/user-profile-email-section/__tests__/user-profile-email-section.feature.test.tsx index 5086ae72501..2baaa2d33d0 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-email-section/__tests__/user-profile-email-section.feature.test.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-email-section/__tests__/user-profile-email-section.feature.test.tsx @@ -1,4 +1,4 @@ -import type { EmailAddressJSON, EnterpriseAccountConnectionJSON } from '@clerk/shared/types'; +import type { EnterpriseAccountConnectionJSON } from '@clerk/shared/types'; import { act, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { describe, expect, it, vi } from 'vitest'; @@ -13,6 +13,7 @@ import { } from '../../../../__tests__/feature/fake-fapi'; import { fapiClient, + type FapiEmailAddress, fapiEmailAddress, fapiEnterpriseAccount, fapiEnvironment, @@ -27,7 +28,7 @@ import { UserProfileEmailSection } from '../user-profile-email-section'; const VERIFIED = fapiVerification('email_code', { status: 'verified' }); const PRIMARY = fapiEmailAddress({ id: 'idn_primary', email_address: 'alice@example.com', verification: VERIFIED }); -function signedInWithEmails(emails: EmailAddressJSON[], environment = fapiEnvironment()): FakeFapiSeed { +function signedInWithEmails(emails: FapiEmailAddress[], environment = fapiEnvironment()): FakeFapiSeed { const user = fapiUser({ id: 'user_1', first_name: 'Alice', diff --git a/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/__tests__/user-profile-enterprise-accounts-contract.feature.test.tsx b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/__tests__/user-profile-enterprise-accounts-contract.feature.test.tsx index 4b75e14bf30..94c82fc8475 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/__tests__/user-profile-enterprise-accounts-contract.feature.test.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/__tests__/user-profile-enterprise-accounts-contract.feature.test.tsx @@ -26,7 +26,7 @@ describe('enterprise linking server contract', () => { serveMember([ okta, fapiEnterpriseConnection({ id: 'inactive', active: false, organization_id: 'org_acme' }), - fapiEnterpriseConnection({ id: 'unscoped', organization_id: null }), + fapiEnterpriseConnection({ id: 'unscoped' }), fapiEnterpriseConnection({ id: 'nonmember', organization_id: 'org_other' }), fapiEnterpriseConnection({ id: 'disabled', @@ -49,7 +49,8 @@ describe('enterprise linking server contract', () => { if (change === 'inactive') { fapi.enterpriseConnections = [{ ...okta, active: false }]; } else if (change === 'unscoped') { - fapi.enterpriseConnections = [{ ...okta, organization_id: null }]; + const { organization_id: _organizationId, ...unscoped } = okta; + fapi.enterpriseConnections = [unscoped]; } else if (change === 'linking disabled') { fapi.enterpriseConnections = [{ ...okta, allow_organization_account_linking: false }]; } else if (change === 'nonmember') { @@ -109,7 +110,7 @@ describe('enterprise linking server contract', () => { const account = await create.mock.results[0]?.value; expect(account?.verification?.strategy).toBe('saml'); expect(account?.provider).toBe(''); - expect(account?.id).toBeUndefined(); + expect(account?.id).toBe(''); expect(fapi.client.sessions[0]?.user.external_accounts).toEqual([]); expect(fapi.client.sessions[0]?.user.enterprise_accounts).toEqual([]); }); diff --git a/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/__tests__/user-profile-enterprise-accounts-identity.feature.test.tsx b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/__tests__/user-profile-enterprise-accounts-identity.feature.test.tsx index d4d7186a982..aaa0c659b8b 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/__tests__/user-profile-enterprise-accounts-identity.feature.test.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/__tests__/user-profile-enterprise-accounts-identity.feature.test.tsx @@ -4,7 +4,13 @@ import { http, HttpResponse } from 'msw'; import { describe, expect, it, vi } from 'vitest'; import { fapiUrl, serveFapi, worker } from '../../../../__tests__/feature/fake-fapi'; -import { fapiClient, fapiSession, fapiUser, fapiVerification } from '../../../../__tests__/feature/fapi'; +import { + fapiClient, + fapiSamlLinking, + fapiSession, + fapiUser, + fapiVerification, +} from '../../../../__tests__/feature/fapi'; import { renderWithClerk } from '../../../../__tests__/feature/render'; import { UserProfileEnterpriseAccountsSection } from '../user-profile-enterprise-accounts-section'; import { enterpriseAccountSeed, enterpriseMember, okta } from './enterprise-accounts.fixtures'; @@ -31,7 +37,7 @@ function deferred() { describe('enterprise account identity', () => { it.each(['switch', 'sign out'] as const)('does not navigate an old linking response after %s', async change => { - serveUsers(); + const fapi = serveUsers(); const response = deferred(); let received = false; worker.use( @@ -54,14 +60,13 @@ describe('enterprise account identity', () => { await act(async () => { response.resolve( HttpResponse.json({ - response: { - object: 'external_account', - verification: fapiVerification('saml', { + response: fapiSamlLinking( + fapiVerification('saml', { status: 'unverified', external_verification_redirect_url: 'https://idp.example/old-user-link', }), - }, - client: null, + ), + client: fapi.client, }), ); await create.mock.results[0]?.value; @@ -79,7 +84,7 @@ describe('enterprise account identity', () => { nextReceived = true; return nextQuery.promise; } - return HttpResponse.json({ response: [okta], client: null }); + return HttpResponse.json({ response: [okta], client: fapi.client }); }), ); const { clerk } = await renderWithClerk(); @@ -91,7 +96,7 @@ describe('enterprise account identity', () => { expect(screen.queryByRole('button', { name: 'Connect Acme Okta' })).toBeNull(); } finally { await act(async () => { - nextQuery.resolve(HttpResponse.json({ response: [], client: null })); + nextQuery.resolve(HttpResponse.json({ response: [], client: fapi.client })); await nextQuery.promise; }); } @@ -104,7 +109,9 @@ describe('enterprise account identity', () => { http.post(fapiUrl('/v1/me/external_accounts'), () => { requests++; return HttpResponse.json( - { errors: [{ code: 'unexpected_request', message: 'Unexpected request' }] }, + { + errors: [{ code: 'unexpected_request', message: 'Unexpected request', long_message: 'Unexpected request' }], + }, { status: 400 }, ); }), diff --git a/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/__tests__/user-profile-enterprise-accounts.feature.test.tsx b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/__tests__/user-profile-enterprise-accounts.feature.test.tsx index c439ee6409c..5b6a9460706 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/__tests__/user-profile-enterprise-accounts.feature.test.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/__tests__/user-profile-enterprise-accounts.feature.test.tsx @@ -16,6 +16,7 @@ import { fapiEnterpriseAccount, fapiEnterpriseConnection, fapiEnvironment, + fapiSamlLinking, fapiSession, fapiUser, fapiVerification, @@ -204,15 +205,12 @@ describe('enterprise accounts', () => { }); it('shows a missing redirect error and allows another attempt', async () => { - serveFapi(signedIn()); + const fapi = serveFapi(signedIn()); worker.use( http.post(fapiUrl('/v1/me/external_accounts'), () => HttpResponse.json({ - response: { - object: 'external_account', - verification: fapiVerification('saml', { status: 'unverified' }), - }, - client: null, + response: fapiSamlLinking(fapiVerification('saml', { status: 'unverified' })), + client: fapi.client, }), ), ); @@ -310,7 +308,9 @@ describe('enterprise accounts', () => { worker.use( http.post(fapiUrl('/v1/me/external_accounts'), () => HttpResponse.json( - { errors: [{ code: 'form_param_invalid', message: 'Server fallback', meta }] }, + { + errors: [{ code: 'form_param_invalid', message: 'Server fallback', long_message: 'Server fallback', meta }], + }, { status: 422 }, ), ), @@ -366,12 +366,12 @@ describe('enterprise accounts', () => { }); it('localizes a client-defined missing redirect error', async () => { - serveFapi(signedIn()); + const fapi = serveFapi(signedIn()); worker.use( http.post(fapiUrl('/v1/me/external_accounts'), () => HttpResponse.json({ - response: { object: 'external_account', verification: fapiVerification('saml', { status: 'unverified' }) }, - client: null, + response: fapiSamlLinking(fapiVerification('saml', { status: 'unverified' })), + client: fapi.client, }), ), ); diff --git a/packages/mosaic/src/features/user-profile/user-profile-mfa-section/__tests__/mfa-feature-setup.tsx b/packages/mosaic/src/features/user-profile/user-profile-mfa-section/__tests__/mfa-feature-setup.tsx index a7929d2f344..572452c3a15 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-mfa-section/__tests__/mfa-feature-setup.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-mfa-section/__tests__/mfa-feature-setup.tsx @@ -27,7 +27,6 @@ export function mfaEnvironment() { environment.user_settings.attributes.backup_code.enabled = true; environment.user_settings.attributes.backup_code.used_for_second_factor = true; environment.user_settings.attributes.backup_code.second_factors = ['backup_code']; - environment.user_settings.sign_in.second_factor.enabled = true; return environment; } diff --git a/packages/mosaic/src/features/user-profile/user-profile-mfa-section/__tests__/user-profile-mfa-interactions.feature.test.tsx b/packages/mosaic/src/features/user-profile/user-profile-mfa-section/__tests__/user-profile-mfa-interactions.feature.test.tsx index e2f2ecfa02b..391f4016180 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-mfa-section/__tests__/user-profile-mfa-interactions.feature.test.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-mfa-section/__tests__/user-profile-mfa-interactions.feature.test.tsx @@ -35,7 +35,11 @@ describe('User profile MFA interaction boundaries', () => { requests += 1; return requests === 1 ? HttpResponse.json( - { errors: [{ code: 'service_unavailable', message: 'Please try again.' }] }, + { + errors: [ + { code: 'service_unavailable', message: 'Please try again.', long_message: 'Please try again.' }, + ], + }, { status: 503 }, ) : undefined; @@ -193,7 +197,11 @@ describe('User profile MFA interaction boundaries', () => { requests += 1; return requests === 1 ? HttpResponse.json( - { errors: [{ code: 'service_unavailable', message: 'Please try again.' }] }, + { + errors: [ + { code: 'service_unavailable', message: 'Please try again.', long_message: 'Please try again.' }, + ], + }, { status: 503 }, ) : undefined; diff --git a/packages/mosaic/src/features/user-profile/user-profile-mfa-section/__tests__/user-profile-mfa-management.feature.test.tsx b/packages/mosaic/src/features/user-profile/user-profile-mfa-section/__tests__/user-profile-mfa-management.feature.test.tsx index 99640f3e314..ce65c76ec54 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-mfa-section/__tests__/user-profile-mfa-management.feature.test.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-mfa-section/__tests__/user-profile-mfa-management.feature.test.tsx @@ -33,7 +33,10 @@ describe('User profile MFA management', () => { const { rerender } = await renderWithClerk(section('First localized error')); worker.use( http.post(fapiUrl('/v1/me/totp'), () => - HttpResponse.json({ errors: [{ code: 'action_blocked', message: 'Server detail' }] }, { status: 403 }), + HttpResponse.json( + { errors: [{ code: 'action_blocked', message: 'Server detail', long_message: 'Server detail' }] }, + { status: 403 }, + ), ), ); const user = userEvent.setup(); @@ -201,7 +204,10 @@ describe('User profile MFA management', () => { worker.use( http.post(fapiUrl('/v1/me/totp'), ({ request }) => new URL(request.url).searchParams.get('_method') === 'DELETE' - ? HttpResponse.json({ errors: [{ code: 'action_blocked', message: 'Action blocked' }] }, { status: 403 }) + ? HttpResponse.json( + { errors: [{ code: 'action_blocked', message: 'Action blocked', long_message: 'Action blocked' }] }, + { status: 403 }, + ) : undefined, ), ); @@ -764,7 +770,10 @@ describe('User profile MFA management', () => { await screen.findByRole('textbox', { name: 'Verification code' }); worker.use( http.get(fapiUrl('/v1/me'), () => - HttpResponse.json({ errors: [{ code: 'unexpected_error', message: 'Reload failed' }] }, { status: 503 }), + HttpResponse.json( + { errors: [{ code: 'unexpected_error', message: 'Reload failed', long_message: 'Reload failed' }] }, + { status: 503 }, + ), ), ); await user.type(screen.getByRole('textbox', { name: 'Verification code' }), '123456'); @@ -778,7 +787,10 @@ describe('User profile MFA management', () => { ); worker.use( http.get(fapiUrl('/v1/me'), () => - HttpResponse.json({ errors: [{ code: 'unexpected_error', message: 'Reload failed' }] }, { status: 503 }), + HttpResponse.json( + { errors: [{ code: 'unexpected_error', message: 'Reload failed', long_message: 'Reload failed' }] }, + { status: 503 }, + ), ), ); const user = userEvent.setup(); @@ -844,7 +856,15 @@ describe('User profile MFA management', () => { attempts += 1; return attempts === 1 ? HttpResponse.json( - { errors: [{ code: 'unexpected_error', message: 'Unable to update default method.' }] }, + { + errors: [ + { + code: 'unexpected_error', + message: 'Unable to update default method.', + long_message: 'Unable to update default method.', + }, + ], + }, { status: 503 }, ) : undefined; @@ -928,7 +948,10 @@ describe('User profile MFA management', () => { await user.click(screen.getByRole('menuitem', { name: 'Remove method' })); worker.use( http.get(fapiUrl('/v1/me'), () => - HttpResponse.json({ errors: [{ code: 'unexpected_error', message: 'Reload failed' }] }, { status: 503 }), + HttpResponse.json( + { errors: [{ code: 'unexpected_error', message: 'Reload failed', long_message: 'Reload failed' }] }, + { status: 503 }, + ), ), ); await user.click(within(screen.getByRole('alertdialog')).getByRole('button', { name: 'Remove' })); diff --git a/packages/mosaic/src/features/user-profile/user-profile-passkeys-section/__tests__/user-profile-passkeys-section.feature.test.tsx b/packages/mosaic/src/features/user-profile/user-profile-passkeys-section/__tests__/user-profile-passkeys-section.feature.test.tsx index 46008130173..49bb0ccdf83 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-passkeys-section/__tests__/user-profile-passkeys-section.feature.test.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-passkeys-section/__tests__/user-profile-passkeys-section.feature.test.tsx @@ -407,7 +407,7 @@ describe('Renaming a passkey', () => { http.post(fapiUrl('/v1/me/passkeys/pk_1'), () => HttpResponse.json( { - errors: [{ code: 'form_param_invalid', message: '', meta: { param_name: 'name' } }], + errors: [{ code: 'form_param_invalid', message: '', long_message: '', meta: { param_name: 'name' } }], }, { status: 400 }, ), diff --git a/packages/mosaic/src/features/user-profile/user-profile-passkeys-section/__tests__/user-profile-passkeys-validation.feature.test.tsx b/packages/mosaic/src/features/user-profile/user-profile-passkeys-section/__tests__/user-profile-passkeys-validation.feature.test.tsx index 92a14621ffa..5c5a67bbb20 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-passkeys-section/__tests__/user-profile-passkeys-validation.feature.test.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-passkeys-section/__tests__/user-profile-passkeys-validation.feature.test.tsx @@ -53,7 +53,7 @@ describe('Validating a passkey name', () => { worker.use( http.post(fapiUrl('/v1/me/passkeys/pk_1'), () => HttpResponse.json( - { errors: [{ code: 'form_param_invalid', message: '', meta: { param_name: 'name' } }] }, + { errors: [{ code: 'form_param_invalid', message: '', long_message: '', meta: { param_name: 'name' } }] }, { status: 400 }, ), ), diff --git a/packages/mosaic/src/features/user-profile/user-profile-phone-section/__tests__/user-profile-phone-section.feature.test.tsx b/packages/mosaic/src/features/user-profile/user-profile-phone-section/__tests__/user-profile-phone-section.feature.test.tsx index 1fd442dabb0..ac3fd3ce3df 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-phone-section/__tests__/user-profile-phone-section.feature.test.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-phone-section/__tests__/user-profile-phone-section.feature.test.tsx @@ -1,4 +1,4 @@ -import type { EnterpriseAccountConnectionJSON, PhoneNumberJSON } from '@clerk/shared/types'; +import type { EnterpriseAccountConnectionJSON } from '@clerk/shared/types'; import { act, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { describe, expect, it } from 'vitest'; @@ -10,7 +10,7 @@ import { serveFapi, VERIFICATION_CODE, } from '../../../../__tests__/feature/fake-fapi'; -import type { FapiAttributeOverrides } from '../../../../__tests__/feature/fapi'; +import type { FapiAttributeOverrides, FapiPhoneNumber } from '../../../../__tests__/feature/fapi'; import { fapiClient, fapiEmailAddress, @@ -38,7 +38,7 @@ const WORK = fapiPhoneNumber({ const HOME_LABEL = '+1 (801) 555-0100'; const WORK_LABEL = '+1 (801) 555-0199'; -function signedIn(phones: PhoneNumberJSON[] = [], overrides: Partial = {}): FakeFapiSeed { +function signedIn(phones: FapiPhoneNumber[] = [], overrides: Partial = {}): FakeFapiSeed { const user = fapiUser({ id: 'user_1', first_name: 'Alice', diff --git a/packages/mosaic/src/features/user-profile/user-profile-profile-section/__tests__/user-profile-profile-section.feature.test.tsx b/packages/mosaic/src/features/user-profile/user-profile-profile-section/__tests__/user-profile-profile-section.feature.test.tsx index 1c57b8bc19c..193f8110764 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-profile-section/__tests__/user-profile-profile-section.feature.test.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-profile-section/__tests__/user-profile-profile-section.feature.test.tsx @@ -108,7 +108,11 @@ describe('the user profile profile section', () => { it('says why an upload was refused', async () => { const { actor, container } = await renderSection(); - failsWith('/v1/me/profile_image', { code: 'avatar_file_size_exceeded', message: 'Too large' }, 413); + failsWith( + '/v1/me/profile_image', + { code: 'avatar_file_size_exceeded', message: 'Too large', long_message: 'Too large' }, + 413, + ); await actor.upload(fileInput(container), new File(['x'], 'me.png', { type: 'image/png' })); @@ -149,7 +153,7 @@ describe('the user profile profile section', () => { await actor.upload(fileInput(container), oversized()); expect(screen.getByRole('alert')).toHaveTextContent('File size exceeds the maximum limit of 10MB.'); - failsWith('/v1/me/profile_image', { code: 'action_blocked', message: 'Blocked' }, 403); + failsWith('/v1/me/profile_image', { code: 'action_blocked', message: 'Blocked', long_message: 'Blocked' }, 403); await openMenu(actor, screen.getByRole('button', { name: 'Manage profile picture' })); await actor.click(screen.getByRole('menuitem', { name: 'Remove avatar' })); diff --git a/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/__tests__/user-profile-web3-wallets-races.feature.test.tsx b/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/__tests__/user-profile-web3-wallets-races.feature.test.tsx index 73c1830183b..168e7e29dd6 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/__tests__/user-profile-web3-wallets-races.feature.test.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/__tests__/user-profile-web3-wallets-races.feature.test.tsx @@ -1,17 +1,17 @@ -import type { UserJSON, Web3Strategy } from '@clerk/shared/types'; +import type { Web3Strategy } from '@clerk/shared/types'; import { act, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { describe, expect, it, vi } from 'vitest'; import { deferred } from '../../../../__tests__/async'; import { demandReverificationOnce, holdRequests, serveFapi } from '../../../../__tests__/feature/fake-fapi'; -import { fapiVerification, fapiWeb3Wallet } from '../../../../__tests__/feature/fapi'; +import { fapiVerification, type FapiWeb3Wallet, fapiWeb3Wallet } from '../../../../__tests__/feature/fapi'; import { openMenu } from '../../../../__tests__/feature/menu'; import { renderWithClerk } from '../../../../__tests__/feature/render'; import { UserProfileWeb3WalletsSection } from '../user-profile-web3-wallets-section'; import { signedInWallets } from './user-profile-web3-wallets.fixtures'; -async function renderWallets(wallets: UserJSON['web3_wallets'] = []) { +async function renderWallets(wallets: FapiWeb3Wallet[] = []) { const fapi = serveFapi( signedInWallets( ['web3_metamask_signature', 'web3_coinbase_wallet_signature'], @@ -31,7 +31,7 @@ function savedWallet(id: string, address: string, strategy: Web3Strategy = 'web3 } describe('Web3 wallet selection races', () => { - it.each(['matching', 'null strategy', 'unrelated'] as const)( + it.each(['matching', 'no verification', 'unrelated'] as const)( 'handles a %s saved wallet published during account selection', async kind => { const selection = deferred(); @@ -48,11 +48,11 @@ describe('Web3 wallet selection races', () => { throw new Error('Missing user'); } session.user.web3_wallets = [savedWallet('published_wallet', kind === 'unrelated' ? '0x1234' : '0xAbCd')]; - if (kind === 'null strategy' && session.user.web3_wallets[0]?.verification) { - session.user.web3_wallets[0].verification.strategy = null; + if (kind === 'no verification' && session.user.web3_wallets[0]) { + session.user.web3_wallets[0].verification = null; } await act(() => clerk.user?.reload()); - if (kind !== 'null strategy') { + if (kind !== 'no verification') { expect(screen.queryByRole('button', { name: 'Connect MetaMask' })).toBeNull(); } selection.resolve(['0xabcd']); diff --git a/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/__tests__/user-profile-web3-wallets-section.feature.test.tsx b/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/__tests__/user-profile-web3-wallets-section.feature.test.tsx index cb9fc8857d2..ae8f278662f 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/__tests__/user-profile-web3-wallets-section.feature.test.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/__tests__/user-profile-web3-wallets-section.feature.test.tsx @@ -1,4 +1,3 @@ -import type { UserJSON } from '@clerk/shared/types'; import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import type { WindowAppReadyEventAPI } from '@wallet-standard/core'; @@ -15,6 +14,7 @@ import { fapiEnterpriseAccount, fapiEnvironment, fapiSession, + type FapiUser, fapiUser, fapiVerification, fapiWeb3Wallet, @@ -30,7 +30,7 @@ import { registerWallets, signedInWallets, web3Environment } from './user-profil describe('Web3 wallets', () => { const walletFactors = ['web3_metamask_signature', 'web3_solana_signature'] as const; - async function renderWeb3(overrides: Partial = {}, seed: FakeFapiSeed = {}) { + async function renderWeb3(overrides: Partial = {}, seed: FakeFapiSeed = {}) { const fapi = serveFapi(signedInWallets(walletFactors, overrides, seed)); await renderWithClerk(); return fapi; diff --git a/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/__tests__/user-profile-web3-wallets.fixtures.tsx b/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/__tests__/user-profile-web3-wallets.fixtures.tsx index 02b983049e2..54660a131a9 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/__tests__/user-profile-web3-wallets.fixtures.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/__tests__/user-profile-web3-wallets.fixtures.tsx @@ -1,8 +1,8 @@ -import type { UserJSON, Web3Strategy } from '@clerk/shared/types'; +import type { Web3Strategy } from '@clerk/shared/types'; import type { WindowAppReadyEventAPI } from '@wallet-standard/core'; import type { FakeFapiSeed } from '../../../../__tests__/feature/fake-fapi'; -import { fapiClient, fapiEnvironment, fapiSession, fapiUser } from '../../../../__tests__/feature/fapi'; +import { fapiClient, fapiEnvironment, fapiSession, type FapiUser, fapiUser } from '../../../../__tests__/feature/fapi'; export function web3Environment(firstFactors: readonly Web3Strategy[]) { const base = fapiEnvironment(); @@ -23,7 +23,7 @@ export function web3Environment(firstFactors: readonly Web3Strategy[]) { export function signedInWallets( firstFactors: readonly Web3Strategy[], - user: Partial = {}, + user: Partial = {}, overrides: FakeFapiSeed = {}, ): FakeFapiSeed { return { diff --git a/packages/mosaic/vitest.config.mts b/packages/mosaic/vitest.config.mts index 4d9b79a4415..8269c7bc20f 100644 --- a/packages/mosaic/vitest.config.mts +++ b/packages/mosaic/vitest.config.mts @@ -69,6 +69,8 @@ export default defineConfig({ '@testing-library/user-event', 'msw', 'msw/browser', + 'ajv/dist/2019', + 'yaml', ], }, test: { diff --git a/packages/mosaic/vitest.setup.browser.mts b/packages/mosaic/vitest.setup.browser.mts index 9688e433d04..5ef4372d180 100644 --- a/packages/mosaic/vitest.setup.browser.mts +++ b/packages/mosaic/vitest.setup.browser.mts @@ -5,6 +5,7 @@ import { afterAll, afterEach, beforeAll, beforeEach, expect } from 'vitest'; import { startWorker, + takeContractViolations, takeUnhandledRequests, takeUnsettledHolds, switchFapiHost, @@ -64,7 +65,7 @@ beforeEach(() => { window.focus(); }); -afterEach(() => { +afterEach(async () => { cleanup(); __resetClerkQueryClientForTest(); for (const channel of [...openChannels]) { @@ -78,8 +79,10 @@ afterEach(() => { worker.resetHandlers(); const unhandled = takeUnhandledRequests(); const unsettled = takeUnsettledHolds(); + const violations = await takeContractViolations(); expect(unhandled, 'Frontend API requests without a handler').toEqual([]); expect(unsettled, 'Held Frontend API requests never released or failed').toEqual([]); + expect(violations, 'Fake Frontend API responses that do not match the spec').toEqual([]); }); afterAll(() => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0725a9079d6..2c04a7022a6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -872,6 +872,9 @@ importers: '@vitest/eslint-plugin': specifier: 1.6.28 version: 1.6.28(@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.31.0(jiti@2.7.0))(typescript@6.0.3))(eslint@9.31.0(jiti@2.7.0))(typescript@6.0.3))(eslint@9.31.0(jiti@2.7.0))(typescript@6.0.3)(vitest@5.0.0) + ajv: + specifier: ^8.20.0 + version: 8.20.0 axe-core: specifier: ^4.11.3 version: 4.11.4 @@ -908,6 +911,9 @@ importers: vitest-axe: specifier: ^0.1.0 version: 0.1.0(vitest@5.0.0) + yaml: + specifier: ^2.9.0 + version: 2.9.0 packages/msw: dependencies: @@ -21738,9 +21744,9 @@ snapshots: obug: 2.1.4 std-env: 4.2.0 tinyrainbow: 3.1.1 - vitest: 5.0.0(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.0)(@types/node@22.20.1)(@vitest/browser-playwright@5.0.0)(@vitest/coverage-v8@5.0.0)(@vitest/ui@5.0.0)(happy-dom@20.10.2(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@27.0.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(msw@2.15.0(@types/node@22.20.1)(typescript@6.0.3))(vite@8.2.2(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.46.1)(tsx@4.23.14)(yaml@2.9.0)) + vitest: 5.0.0(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.0)(@types/node@25.6.0)(@vitest/browser-playwright@5.0.0)(@vitest/coverage-v8@5.0.0)(@vitest/ui@5.0.0)(happy-dom@20.10.2(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@27.0.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(msw@2.15.0(@types/node@25.6.0)(typescript@6.0.3))(vite@8.2.2(@types/node@25.6.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.46.1)(tsx@4.23.14)(yaml@2.9.0)) optionalDependencies: - '@vitest/browser': 5.0.0(bufferutil@4.1.0)(msw@2.15.0(@types/node@22.20.1)(typescript@6.0.3))(utf-8-validate@5.0.10)(vite@8.2.2(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.46.1)(tsx@4.23.14)(yaml@2.9.0))(vitest@5.0.0) + '@vitest/browser': 5.0.0(bufferutil@4.1.0)(msw@2.15.0(@types/node@25.6.0)(typescript@6.0.3))(utf-8-validate@5.0.10)(vite@8.2.2(@types/node@25.6.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.46.1)(tsx@4.23.14)(yaml@2.9.0))(vitest@5.0.0) '@vitest/eslint-plugin@1.6.28(@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.31.0(jiti@2.7.0))(typescript@6.0.3))(eslint@9.31.0(jiti@2.7.0))(typescript@6.0.3))(eslint@9.31.0(jiti@2.7.0))(typescript@6.0.3)(vitest@5.0.0)': dependencies: @@ -21804,7 +21810,7 @@ snapshots: pathe: 2.0.3 sirv: 3.0.2 tinyrainbow: 3.1.1 - vitest: 5.0.0(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.0)(@types/node@22.20.1)(@vitest/browser-playwright@5.0.0)(@vitest/coverage-v8@5.0.0)(@vitest/ui@5.0.0)(happy-dom@20.10.2(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@27.0.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(msw@2.15.0(@types/node@22.20.1)(typescript@6.0.3))(vite@8.2.2(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.46.1)(tsx@4.23.14)(yaml@2.9.0)) + vitest: 5.0.0(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.0)(@types/node@25.6.0)(@vitest/browser-playwright@5.0.0)(@vitest/coverage-v8@5.0.0)(@vitest/ui@5.0.0)(happy-dom@20.10.2(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@27.0.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(msw@2.15.0(@types/node@25.6.0)(typescript@6.0.3))(vite@8.2.2(@types/node@25.6.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.46.1)(tsx@4.23.14)(yaml@2.9.0)) '@vitest/utils@5.0.0': dependencies: From 66ddf714e8e53f3b3ae96365d9bc1c2084217fff Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Tue, 27 Oct 2026 22:31:30 -0400 Subject: [PATCH 2/4] test(mosaic): wait for late contract checks and dedupe lockfile --- .../mosaic/src/__tests__/feature/fake-fapi.ts | 32 +++++++++++++++++-- pnpm-lock.yaml | 6 ++-- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/packages/mosaic/src/__tests__/feature/fake-fapi.ts b/packages/mosaic/src/__tests__/feature/fake-fapi.ts index 71a36a540b9..8b4bda01e17 100644 --- a/packages/mosaic/src/__tests__/feature/fake-fapi.ts +++ b/packages/mosaic/src/__tests__/feature/fake-fapi.ts @@ -121,12 +121,25 @@ export function startWorker() { }); } -worker.events.on('response:mocked', ({ request, response }) => { +const CONTRACT_CHECK_GRACE_MS = 1000; +const pendingChecks = new Map) => void>(); + +worker.events.on('request:match', ({ request, requestId }) => { if (new URL(request.url).host === fapiHost) { - contractChecks.push(fapiContractViolations(request, response)); + contractChecks.push(new Promise(resolve => pendingChecks.set(requestId, resolve))); } }); +worker.events.on('response:mocked', ({ request, requestId, response }) => { + pendingChecks.get(requestId)?.(fapiContractViolations(request, response)); + pendingChecks.delete(requestId); +}); + +worker.events.on('response:bypass', ({ requestId }) => { + pendingChecks.get(requestId)?.(Promise.resolve([])); + pendingChecks.delete(requestId); +}); + export function takeUnhandledRequests(): string[] { return unhandled.splice(0); } @@ -138,7 +151,8 @@ export function allowSpecViolation(violation: string) { export async function takeContractViolations(): Promise { const checks = contractChecks.splice(0); const allowed = allowedViolations.splice(0); - const violations = (await Promise.all(checks)).flat(); + const violations = (await settledWithin(CONTRACT_CHECK_GRACE_MS, checks)).flat(); + pendingChecks.clear(); return [ ...violations.filter(violation => !allowed.includes(violation)), ...allowed @@ -147,6 +161,18 @@ export async function takeContractViolations(): Promise { ]; } +async function settledWithin(ms: number, checks: Promise[]): Promise { + let timer: ReturnType | undefined; + const deadline = new Promise(resolve => { + timer = setTimeout(() => resolve([]), ms); + }); + try { + return await Promise.all(checks.map(check => Promise.race([check, deadline]))); + } finally { + clearTimeout(timer); + } +} + function page(items: T[], url: URL) { const offset = Number(url.searchParams.get('offset') ?? 0); const limit = Number(url.searchParams.get('limit') ?? items.length); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c04a7022a6..bcef2bf17ae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21744,9 +21744,9 @@ snapshots: obug: 2.1.4 std-env: 4.2.0 tinyrainbow: 3.1.1 - vitest: 5.0.0(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.0)(@types/node@25.6.0)(@vitest/browser-playwright@5.0.0)(@vitest/coverage-v8@5.0.0)(@vitest/ui@5.0.0)(happy-dom@20.10.2(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@27.0.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(msw@2.15.0(@types/node@25.6.0)(typescript@6.0.3))(vite@8.2.2(@types/node@25.6.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.46.1)(tsx@4.23.14)(yaml@2.9.0)) + vitest: 5.0.0(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.0)(@types/node@22.20.1)(@vitest/browser-playwright@5.0.0)(@vitest/coverage-v8@5.0.0)(@vitest/ui@5.0.0)(happy-dom@20.10.2(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@27.0.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(msw@2.15.0(@types/node@22.20.1)(typescript@6.0.3))(vite@8.2.2(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.46.1)(tsx@4.23.14)(yaml@2.9.0)) optionalDependencies: - '@vitest/browser': 5.0.0(bufferutil@4.1.0)(msw@2.15.0(@types/node@25.6.0)(typescript@6.0.3))(utf-8-validate@5.0.10)(vite@8.2.2(@types/node@25.6.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.46.1)(tsx@4.23.14)(yaml@2.9.0))(vitest@5.0.0) + '@vitest/browser': 5.0.0(bufferutil@4.1.0)(msw@2.15.0(@types/node@22.20.1)(typescript@6.0.3))(utf-8-validate@5.0.10)(vite@8.2.2(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.46.1)(tsx@4.23.14)(yaml@2.9.0))(vitest@5.0.0) '@vitest/eslint-plugin@1.6.28(@typescript-eslint/eslint-plugin@8.58.0(@typescript-eslint/parser@8.58.0(eslint@9.31.0(jiti@2.7.0))(typescript@6.0.3))(eslint@9.31.0(jiti@2.7.0))(typescript@6.0.3))(eslint@9.31.0(jiti@2.7.0))(typescript@6.0.3)(vitest@5.0.0)': dependencies: @@ -21810,7 +21810,7 @@ snapshots: pathe: 2.0.3 sirv: 3.0.2 tinyrainbow: 3.1.1 - vitest: 5.0.0(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.0)(@types/node@25.6.0)(@vitest/browser-playwright@5.0.0)(@vitest/coverage-v8@5.0.0)(@vitest/ui@5.0.0)(happy-dom@20.10.2(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@27.0.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(msw@2.15.0(@types/node@25.6.0)(typescript@6.0.3))(vite@8.2.2(@types/node@25.6.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.46.1)(tsx@4.23.14)(yaml@2.9.0)) + vitest: 5.0.0(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.0)(@types/node@22.20.1)(@vitest/browser-playwright@5.0.0)(@vitest/coverage-v8@5.0.0)(@vitest/ui@5.0.0)(happy-dom@20.10.2(bufferutil@4.1.0)(utf-8-validate@5.0.10))(jsdom@27.0.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(msw@2.15.0(@types/node@22.20.1)(typescript@6.0.3))(vite@8.2.2(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.46.1)(tsx@4.23.14)(yaml@2.9.0)) '@vitest/utils@5.0.0': dependencies: From 5eb65718d6c7364683756d0e8b8ebdabda252090 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 28 Oct 2026 08:49:02 -0400 Subject: [PATCH 3/4] test(mosaic): check FAPI responses as the page receives them --- .../mosaic/src/__tests__/feature/fake-fapi.ts | 44 +++++-------------- packages/mosaic/vitest.setup.browser.mts | 4 ++ 2 files changed, 16 insertions(+), 32 deletions(-) diff --git a/packages/mosaic/src/__tests__/feature/fake-fapi.ts b/packages/mosaic/src/__tests__/feature/fake-fapi.ts index 8b4bda01e17..bc406f9a023 100644 --- a/packages/mosaic/src/__tests__/feature/fake-fapi.ts +++ b/packages/mosaic/src/__tests__/feature/fake-fapi.ts @@ -121,24 +121,17 @@ export function startWorker() { }); } -const CONTRACT_CHECK_GRACE_MS = 1000; -const pendingChecks = new Map) => void>(); - -worker.events.on('request:match', ({ request, requestId }) => { - if (new URL(request.url).host === fapiHost) { - contractChecks.push(new Promise(resolve => pendingChecks.set(requestId, resolve))); - } -}); - -worker.events.on('response:mocked', ({ request, requestId, response }) => { - pendingChecks.get(requestId)?.(fapiContractViolations(request, response)); - pendingChecks.delete(requestId); -}); - -worker.events.on('response:bypass', ({ requestId }) => { - pendingChecks.get(requestId)?.(Promise.resolve([])); - pendingChecks.delete(requestId); -}); +export function checkingContract(fetch: typeof window.fetch): typeof window.fetch { + return async (input, init) => { + const response = await fetch(input, init); + const url = input instanceof Request ? input.url : String(input); + if (new URL(url).host === fapiHost) { + const method = init?.method ?? (input instanceof Request ? input.method : 'GET'); + contractChecks.push(fapiContractViolations(new Request(url, { method }), response.clone())); + } + return response; + }; +} export function takeUnhandledRequests(): string[] { return unhandled.splice(0); @@ -151,8 +144,7 @@ export function allowSpecViolation(violation: string) { export async function takeContractViolations(): Promise { const checks = contractChecks.splice(0); const allowed = allowedViolations.splice(0); - const violations = (await settledWithin(CONTRACT_CHECK_GRACE_MS, checks)).flat(); - pendingChecks.clear(); + const violations = (await Promise.all(checks)).flat(); return [ ...violations.filter(violation => !allowed.includes(violation)), ...allowed @@ -161,18 +153,6 @@ export async function takeContractViolations(): Promise { ]; } -async function settledWithin(ms: number, checks: Promise[]): Promise { - let timer: ReturnType | undefined; - const deadline = new Promise(resolve => { - timer = setTimeout(() => resolve([]), ms); - }); - try { - return await Promise.all(checks.map(check => Promise.race([check, deadline]))); - } finally { - clearTimeout(timer); - } -} - function page(items: T[], url: URL) { const offset = Number(url.searchParams.get('offset') ?? 0); const limit = Number(url.searchParams.get('limit') ?? items.length); diff --git a/packages/mosaic/vitest.setup.browser.mts b/packages/mosaic/vitest.setup.browser.mts index 5ef4372d180..46aff59a673 100644 --- a/packages/mosaic/vitest.setup.browser.mts +++ b/packages/mosaic/vitest.setup.browser.mts @@ -4,6 +4,7 @@ import { cleanup, configure } from '@testing-library/react'; import { afterAll, afterEach, beforeAll, beforeEach, expect } from 'vitest'; import { + checkingContract, startWorker, takeContractViolations, takeUnhandledRequests, @@ -30,6 +31,7 @@ disableMotion.textContent = ` `; document.head.append(disableMotion); +const nativeFetch = window.fetch; const NativeBroadcastChannel = window.BroadcastChannel; const channelNamespace = crypto.randomUUID(); const openChannels = new Set(); @@ -57,6 +59,7 @@ class IsolatedBroadcastChannel extends NativeBroadcastChannel { beforeAll(async () => { window.BroadcastChannel = IsolatedBroadcastChannel; + window.fetch = checkingContract(nativeFetch); await startWorker(); }); @@ -87,5 +90,6 @@ afterEach(async () => { afterAll(() => { window.BroadcastChannel = NativeBroadcastChannel; + window.fetch = nativeFetch; worker.stop(); }); From 4b3392a96bcecfa07544666520b106061b8884a9 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 28 Oct 2026 09:12:27 -0400 Subject: [PATCH 4/4] test(mosaic): check FAPI requests against the OpenAPI spec Check the query and body clerk-js sends against the operation's parameters and requestBody, reading form values as the declared types. Record the request-side spec gaps the check found (tab_state on token requests, status as a list on organization suggestions, with_organization_account_linking on enterprise connections), give two tests IDs in the format the api_keys subject pattern expects, and cap local feature runs at two workers. --- packages/mosaic/docs/testing.md | 32 ++- .../mosaic/src/__tests__/feature/fake-fapi.ts | 7 +- .../feature/fapi-contract.feature.test.tsx | 176 +++++++++++++++++ .../src/__tests__/feature/fapi-contract.ts | 186 +++++++++++++++--- .../src/__tests__/feature/fapi-spec-gaps.ts | 23 +++ .../organization-profile.feature.test.tsx | 2 +- .../__tests__/user-profile.feature.test.tsx | 2 +- packages/mosaic/vitest.config.mts | 1 + packages/mosaic/vitest.setup.browser.mts | 2 +- 9 files changed, 391 insertions(+), 40 deletions(-) diff --git a/packages/mosaic/docs/testing.md b/packages/mosaic/docs/testing.md index c8055d558ed..96d9f07d978 100644 --- a/packages/mosaic/docs/testing.md +++ b/packages/mosaic/docs/testing.md @@ -100,7 +100,8 @@ pnpm --filter @clerk/mosaic exec vitest run # every project ``` CI runs feature tests in their own `Mosaic Feature Tests` job, so the repo-wide -unit test job never needs a browser. +unit test job never needs a browser. Locally the feature project runs two test +files at a time, since each one drives a browser. jsdom and happy-dom have no layout engine, so every rect is 0 and a test that asserts on position, size or scrolling always passes there. Name such a test @@ -190,25 +191,36 @@ FAPI returns. ### Matching the backend -Every mocked response is checked against the FAPI OpenAPI spec, vendored as -`fapi-spec.yml`. The check is strict: a field the spec doesn't list fails, and -so does a missing required field or a wrong type. It reads the status and the -`_method` override, and checks a 4xx or 5xx the spec doesn't document against -`ClerkErrors`. A violation fails the test from the `afterEach`, with the -request and the JSON pointer of the bad field. +Every FAPI request clerk-js sends, and every mocked response, is checked +against the FAPI OpenAPI spec, vendored as `fapi-spec.yml`. The check is +strict: a field the spec doesn't list fails, and so does a missing required +field or a wrong type. It reads the status and the `_method` override, and +checks a 4xx or 5xx the spec doesn't document against `ClerkErrors`. A +violation fails the test from the `afterEach`, with the request and the JSON +pointer of the bad field. + +On the request, the query and the body are checked. Form and multipart values +are read as the type the spec declares, so `true` passes as a boolean and a +repeated key as an array. The query params clerk-js adds to every request, +such as `_clerk_js_version`, pass everywhere. Path parameters and headers are +not checked. When a test fails here, fix the builder or the handler so it sends what FAPI -sends. A builder can make a backend quirk part of its name, for example +sends, or the test data so it looks like real data, such as an ID in the +format a spec `pattern` expects. A builder can make a backend quirk part of its name, for example `fapiSamlLinking(verification)` for the bare account that SAML linking returns. When FAPI sends something the spec gets wrong, add an entry to `fapi-spec-gaps.ts`: -- `at` and `property` name the spec schema and the field. +- `at` and `property` name the spec schema and the field. A request body is + the operation's `requestBody` schema. A query is the operation's + `x-query-schema`, which the check builds from its parameters. - `schema` is what FAPI really sends. `replaces` is the spec's current schema, when the gap widens a field the spec already has. -- `sentBy` names the `clerk_go` code that sends it. +- `sentBy` names the `clerk_go` code that sends it, or that reads it for a + request field. The list prunes itself. Once the spec adds the field, or changes the schema that `replaces` records, the gap's test fails and the entry should go. diff --git a/packages/mosaic/src/__tests__/feature/fake-fapi.ts b/packages/mosaic/src/__tests__/feature/fake-fapi.ts index bc406f9a023..d982b853039 100644 --- a/packages/mosaic/src/__tests__/feature/fake-fapi.ts +++ b/packages/mosaic/src/__tests__/feature/fake-fapi.ts @@ -123,11 +123,10 @@ export function startWorker() { export function checkingContract(fetch: typeof window.fetch): typeof window.fetch { return async (input, init) => { + const request = new Request(input instanceof Request ? input.clone() : input, init); const response = await fetch(input, init); - const url = input instanceof Request ? input.url : String(input); - if (new URL(url).host === fapiHost) { - const method = init?.method ?? (input instanceof Request ? input.method : 'GET'); - contractChecks.push(fapiContractViolations(new Request(url, { method }), response.clone())); + if (new URL(request.url).host === fapiHost) { + contractChecks.push(fapiContractViolations(request, response.clone())); } return response; }; diff --git a/packages/mosaic/src/__tests__/feature/fapi-contract.feature.test.tsx b/packages/mosaic/src/__tests__/feature/fapi-contract.feature.test.tsx index 141efe388c8..5be6ab3c262 100644 --- a/packages/mosaic/src/__tests__/feature/fapi-contract.feature.test.tsx +++ b/packages/mosaic/src/__tests__/feature/fapi-contract.feature.test.tsx @@ -11,6 +11,34 @@ function request(method: string, path: string) { return new Request(`${FAPI}${path}`, { method }); } +function formRequest(method: string, path: string, body: string) { + return new Request(`${FAPI}${path}`, { + method, + headers: { 'content-type': 'application/x-www-form-urlencoded' }, + body, + }); +} + +function jsonRequest(method: string, path: string, body: unknown) { + return new Request(`${FAPI}${path}`, { + method, + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(body), + }); +} + +function multipartRequest(method: string, path: string, fields: Record) { + const body = new FormData(); + for (const [name, value] of Object.entries(fields)) { + body.append(name, value); + } + return new Request(`${FAPI}${path}`, { method, body }); +} + +function rejected() { + return HttpResponse.json({ errors: [{ code: 'invalid', message: 'no', long_message: 'no' }] }, { status: 422 }); +} + const user = fapiUser({ id: 'user_1', email_addresses: [fapiEmailAddress({ id: 'idn_1' })] }); const client = fapiClient([fapiSession({ id: 'sess_1', user })]); @@ -109,6 +137,144 @@ describe('fapiContractViolations', () => { }); }); +describe('fapiContractViolations on the request', () => { + const enterpriseConnection = '/v1/me/enterprise_connections/ec_1'; + + it('accepts a form body that matches the spec', async () => { + expect(await fapiContractViolations(formRequest('PATCH', '/v1/me', 'first_name=Ada'), envelope(user))).toEqual([]); + }); + + it('rejects a body field the spec does not declare', async () => { + const violations = await fapiContractViolations( + formRequest('PATCH', '/v1/me', 'first_name=Ada&invented_field=1'), + envelope(user), + ); + expect(violations).toEqual([expect.stringContaining('invented_field')]); + }); + + it('rejects a body missing a required field', async () => { + const violations = await fapiContractViolations( + formRequest('POST', '/v1/me/email_addresses', 'invented_field=1'), + rejected(), + ); + expect(violations).toEqual([expect.stringContaining('email_address')]); + }); + + it('reads form values as the types the spec declares', async () => { + const body = 'active=true&domains=a.example&domains=b.example'; + expect(await fapiContractViolations(formRequest('PATCH', enterpriseConnection, body), rejected())).toEqual([]); + }); + + it('reads a single form value as a one-item array where the spec declares an array', async () => { + const body = 'domains=a.example'; + expect(await fapiContractViolations(formRequest('PATCH', enterpriseConnection, body), rejected())).toEqual([]); + }); + + it('rejects a form value that is not the declared type', async () => { + const violations = await fapiContractViolations( + formRequest('PATCH', enterpriseConnection, 'active=maybe'), + rejected(), + ); + expect(violations).toEqual([expect.stringContaining('/active')]); + }); + + it('checks the body of a POST sent with _method against the operation it stands for', async () => { + const violations = await fapiContractViolations( + formRequest('POST', '/v1/me?_method=PATCH', 'first_name=Ada'), + envelope(user), + ); + expect(violations).toEqual([]); + }); + + it('rejects a body the operation does not take', async () => { + const deleted = HttpResponse.json({ response: { object: 'user', id: 'user_1', deleted: true }, client }); + const violations = await fapiContractViolations(formRequest('POST', '/v1/me?_method=DELETE', 'x=1'), deleted); + expect(violations).toEqual([expect.stringContaining('body')]); + }); + + it('accepts an empty body on an operation that takes none', async () => { + const deleted = HttpResponse.json({ response: { object: 'user', id: 'user_1', deleted: true }, client }); + expect(await fapiContractViolations(formRequest('POST', '/v1/me?_method=DELETE', ''), deleted)).toEqual([]); + }); + + it('rejects a missing body the operation requires', async () => { + const violations = await fapiContractViolations(request('POST', '/v1/me/profile_image'), rejected()); + expect(violations).toEqual([expect.stringContaining('body')]); + }); + + it('rejects a content type the operation does not take', async () => { + const violations = await fapiContractViolations( + jsonRequest('PATCH', '/v1/me', { first_name: 'Ada' }), + envelope(user), + ); + expect(violations).toEqual([expect.stringContaining('application/json')]); + }); + + it('checks a multipart body', async () => { + const file = new File(['image'], 'avatar.png', { type: 'image/png' }); + const path = '/v1/me/profile_image'; + expect(await fapiContractViolations(multipartRequest('POST', path, { file }), rejected())).toEqual([]); + expect( + await fapiContractViolations(multipartRequest('POST', path, { file, invented_field: 'x' }), rejected()), + ).toEqual([expect.stringContaining('invented_field')]); + }); + + it('checks a JSON body without reading strings as other types', async () => { + const path = '/api_keys/ak_1'; + expect(await fapiContractViolations(jsonRequest('PATCH', path, { description: 'ci' }), rejected())).toEqual([]); + expect(await fapiContractViolations(jsonRequest('PATCH', path, { description: 5 }), rejected())).toEqual([ + expect.stringContaining('/description'), + ]); + }); + + it('reads an OpenAPI 3.0 exclusive minimum', async () => { + const subject = `user_${'a'.repeat(27)}`; + const body = (seconds: number) => ({ name: 'ci key', subject, seconds_until_expiration: seconds }); + expect(await fapiContractViolations(jsonRequest('POST', '/api_keys', body(1)), rejected())).toEqual([]); + expect(await fapiContractViolations(jsonRequest('POST', '/api_keys', body(0)), rejected())).toEqual([ + expect.stringContaining('/seconds_until_expiration'), + ]); + }); + + it('reads query values as the types the spec declares', async () => { + const path = '/v1/me/organization_memberships?limit=10&offset=0&paginated=true'; + expect(await fapiContractViolations(request('GET', path), rejected())).toEqual([]); + }); + + it('rejects a query value that is not the declared type', async () => { + const path = '/v1/me/organization_memberships?limit=ten'; + expect(await fapiContractViolations(request('GET', path), rejected())).toEqual([expect.stringContaining('/limit')]); + }); + + it('rejects a query parameter the spec does not declare', async () => { + const violations = await fapiContractViolations(request('GET', '/v1/me?invented_param=1'), envelope(user)); + expect(violations).toEqual([expect.stringContaining('invented_param')]); + }); + + it('accepts a body field the spec is known to be missing', async () => { + const token = HttpResponse.json({ object: 'token', jwt: 'header.payload.signature' }); + const tokens = formRequest('POST', '/v1/client/sessions/sess_1/tokens', 'tab_state=active'); + expect(await fapiContractViolations(tokens, token)).toEqual([]); + }); + + it('accepts a query parameter the spec is known to type too narrowly', async () => { + const path = '/v1/me/organization_suggestions?status=pending&status=accepted'; + expect(await fapiContractViolations(request('GET', path), rejected())).toEqual([]); + }); + + it('accepts the transport params clerk-js adds to any request', async () => { + const transport = [ + '__clerk_api_version=2025-11-10', + '_clerk_js_version=6.0.0', + '_clerk_session_id=sess_1', + '_clerk_retry_attempt=1', + 'rotating_token_nonce=nonce', + '__domain=satellite.example', + ].join('&'); + expect(await fapiContractViolations(request('GET', `/v1/me?${transport}`), envelope(user))).toEqual([]); + }); +}); + describe('staleSpecGaps', () => { it('has no known gap that the spec has since closed', () => { expect(staleSpecGaps()).toEqual([]); @@ -149,6 +315,16 @@ describe('takeContractViolations', () => { expect(await takeContractViolations()).toEqual([invalidMe]); }); + it('reports a request body that does not match the spec', async () => { + worker.use(http.post(fapiUrl('/v1/me'), () => HttpResponse.json({ response: user, client }))); + await fetch(fapiUrl('/v1/me?_method=PATCH'), { + method: 'POST', + headers: { 'content-type': 'application/x-www-form-urlencoded' }, + body: 'invented_field=1', + }); + expect(await takeContractViolations()).toEqual([expect.stringContaining('PATCH /v1/me body')]); + }); + it('drops a violation the test allows', async () => { allowSpecViolation(invalidMe); await fetchInvalidMe(); diff --git a/packages/mosaic/src/__tests__/feature/fapi-contract.ts b/packages/mosaic/src/__tests__/feature/fapi-contract.ts index 028bb5a3f64..5060e2f20eb 100644 --- a/packages/mosaic/src/__tests__/feature/fapi-contract.ts +++ b/packages/mosaic/src/__tests__/feature/fapi-contract.ts @@ -14,6 +14,45 @@ export async function fapiContractViolations(request: Request, response: Respons if (!operation) { return [`${endpoint} is not in the Frontend API spec`]; } + return [ + ...queryViolations(endpoint, operation, url.searchParams), + ...(await bodyViolations(endpoint, operation, request)), + ...(await responseViolations(endpoint, operation, response)), + ]; +} + +function queryViolations(endpoint: string, operation: Operation, query: URLSearchParams): string[] { + return violations(`${endpoint} query`, formValidator(`${operation.pointer}/${QUERY_SCHEMA}`), formFields(query)); +} + +async function bodyViolations(endpoint: string, operation: Operation, request: Request): Promise { + const body = await request.clone().text(); + const requestBody = operation.definition.requestBody; + if (!body) { + return requestBody?.required ? [`${endpoint} sent no body, which the spec requires`] : []; + } + if (!requestBody?.content) { + return [`${endpoint} sent a body, which the spec does not document`]; + } + const contentType = request.headers.get('content-type')?.split(';')[0] ?? ''; + if (!(contentType in requestBody.content)) { + return [`${endpoint} sent ${contentType || 'a body without a content type'}, which the spec does not document`]; + } + const schema = `${operation.pointer}/requestBody/content/${pointerSegment(contentType)}/schema`; + const label = `${endpoint} body`; + switch (contentType) { + case 'application/json': + return violations(label, validator(schema), JSON.parse(body)); + case 'application/x-www-form-urlencoded': + return violations(label, formValidator(schema), formFields(new URLSearchParams(body))); + case 'multipart/form-data': + return violations(label, formValidator(schema), formFields(await request.clone().formData())); + default: + return []; + } +} + +async function responseViolations(endpoint: string, operation: Operation, response: Response): Promise { const schema = responseSchemaPointer(operation, response.status); if (schema === undefined) { return [`${endpoint} answered ${response.status}, which the spec does not document`]; @@ -21,11 +60,24 @@ export async function fapiContractViolations(request: Request, response: Respons if (!schema || !response.headers.get('content-type')?.includes('json')) { return []; } - const validate = validator(schema); - if (validate(await response.clone().json())) { + return violations(`${endpoint} ${response.status}`, validator(schema), await response.clone().json()); +} + +function violations(label: string, validate: ValidateFunction, data: unknown): string[] { + if (validate(data)) { return []; } - return [`${endpoint} ${response.status}: ${[...new Set((validate.errors ?? []).map(describe))].join('; ')}`]; + return [`${label}: ${[...new Set((validate.errors ?? []).map(describe))].join('; ')}`]; +} + +function formFields(entries: URLSearchParams | FormData): Record { + const fields: Record = {}; + entries.forEach((entry, key) => { + const value = typeof entry === 'string' ? entry : entry.name; + const existing = fields[key]; + fields[key] = existing === undefined ? value : [...(Array.isArray(existing) ? existing : [existing]), value]; + }); + return fields; } type Method = 'get' | 'post' | 'put' | 'patch' | 'delete'; @@ -36,8 +88,19 @@ interface ResponseObject { content?: Record; } +interface ParameterObject { + $ref?: string; + name?: string; + in?: string; + required?: boolean; + schema?: unknown; +} + interface OperationObject { + parameters?: ParameterObject[]; + requestBody?: { required?: boolean; content?: Record }; responses: Record; + [QUERY_SCHEMA]?: unknown; } interface Spec { @@ -49,42 +112,63 @@ interface Operation { method: Method; template: string; pattern: RegExp; - responses: Record; + pointer: string; + definition: OperationObject; } const METHODS: readonly Method[] = ['get', 'post', 'put', 'patch', 'delete']; const JSON_SCHEMA = 'content/application~1json/schema'; +const QUERY_SCHEMA = 'x-query-schema'; const CLERK_ERRORS = '#/components/schemas/ClerkErrors'; +const EXCLUSIVE_BOUNDS = [ + ['exclusiveMinimum', 'minimum'], + ['exclusiveMaximum', 'maximum'], +] as const; +const TRANSPORT_PARAMS = [ + '__clerk_api_version', + '_clerk_js_version', + '_clerk_session_id', + '_clerk_retry_attempt', + '_method', + '__domain', + 'rotating_token_nonce', +]; const spec: Spec = parse(specSource); const operations: Operation[] = Object.entries(spec.paths) .flatMap(([template, item]) => METHODS.flatMap(method => { - const operation = item[method]; - return operation ? [{ method, template, pattern: pathPattern(template), responses: operation.responses }] : []; + const definition = item[method]; + const pointer = `#/paths/${pointerSegment(template)}/${method}`; + return definition ? [{ method, template, pattern: pathPattern(template), pointer, definition }] : []; }), ) .sort((a, b) => parameterCount(a.template) - parameterCount(b.template)); -const ajv = new Ajv2019({ strict: false, allErrors: true, validateFormats: false, validateSchema: false }); -ajv.addSchema(closeObjects(withSpecGaps(spec)), 'fapi'); - -const validators = new Map(); +const specWithQueries = withQuerySchemas(spec); +const contract = closeObjects(withSpecGaps(specWithQueries)); +const ajvOptions = { strict: false, allErrors: true, validateFormats: false, validateSchema: false }; +const validator = compiler(new Ajv2019(ajvOptions).addSchema(contract, 'fapi')); +const formValidator = compiler(new Ajv2019({ ...ajvOptions, coerceTypes: 'array' }).addSchema(contract, 'fapi')); -function validator(pointer: string): ValidateFunction { - const cached = validators.get(pointer); - if (cached) { - return cached; - } - const validate = ajv.compile({ $ref: `fapi${pointer}` }); - validators.set(pointer, validate); - return validate; +function compiler(ajv: Ajv2019): (pointer: string) => ValidateFunction { + const validators = new Map(); + return pointer => { + const cached = validators.get(pointer); + if (cached) { + return cached; + } + const validate = ajv.compile({ $ref: `fapi${pointer}` }); + validators.set(pointer, validate); + return validate; + }; } function responseSchemaPointer(operation: Operation, status: number): string | null | undefined { - const key = [String(status), `${String(status)[0]}XX`, 'default'].find(k => k in operation.responses); - const response = key === undefined ? undefined : operation.responses[key]; + const { responses } = operation.definition; + const key = [String(status), `${String(status)[0]}XX`, 'default'].find(k => k in responses); + const response = key === undefined ? undefined : responses[key]; if (!response || key === undefined) { return status >= 400 ? CLERK_ERRORS : undefined; } @@ -94,12 +178,12 @@ function responseSchemaPointer(operation: Operation, status: number): string | n if (!response.content?.['application/json']) { return null; } - return `#/paths/${pointerSegment(operation.template)}/${operation.method}/responses/${key}/${JSON_SCHEMA}`; + return `${operation.pointer}/responses/${key}/${JSON_SCHEMA}`; } export function staleSpecGaps(gaps: readonly SpecGap[] = SPEC_GAPS): string[] { return gaps.flatMap(({ at, property, replaces }) => { - const schema = resolvePointer(spec, at); + const schema = resolvePointer(specWithQueries, at); if (!isObject(schema)) { return [`${at} is no longer in the spec`]; } @@ -122,6 +206,37 @@ function withSpecGaps(source: Spec): Spec { return patched; } +function withQuerySchemas(source: Spec): Spec { + const paths = Object.fromEntries( + Object.entries(source.paths).map(([template, item]) => [ + template, + Object.fromEntries( + Object.entries(item).map(([method, operation]) => [ + method, + { ...operation, [QUERY_SCHEMA]: querySchema(source, operation.parameters ?? []) }, + ]), + ), + ]), + ); + return { ...source, paths }; +} + +function querySchema(source: Spec, parameters: ParameterObject[]): unknown { + const query = parameters + .map(parameter => (parameter.$ref ? resolvePointer(source, parameter.$ref) : parameter)) + .filter(isObject) + .filter(parameter => parameter.in === 'query' && typeof parameter.name === 'string'); + return { + type: 'object', + properties: { + ...Object.fromEntries(TRANSPORT_PARAMS.map(name => [name, { type: 'string' }])), + ...Object.fromEntries(query.map(parameter => [parameter.name, parameter.schema ?? {}])), + }, + required: query.filter(parameter => parameter.required === true).map(parameter => parameter.name), + additionalProperties: false, + }; +} + function resolvePointer(root: unknown, pointer: string): unknown { return pointer .replace(/^#\//, '') @@ -163,7 +278,11 @@ function closeObjects(source: Spec): Spec { Object.fromEntries( Object.entries(item).map(([method, operation]) => [ method, - { ...operation, responses: closeResponses(operation.responses) }, + { + ...operation, + requestBody: closeRequestBody(operation.requestBody), + responses: closeResponses(operation.responses), + }, ]), ), ]), @@ -171,6 +290,19 @@ function closeObjects(source: Spec): Spec { return { ...source, paths, components: { ...source.components, schemas } }; } +function closeRequestBody(requestBody: OperationObject['requestBody']): OperationObject['requestBody'] { + if (!requestBody?.content) { + return requestBody; + } + const content = Object.fromEntries( + Object.entries(requestBody.content).map(([type, media]) => [ + type, + isObject(media) ? { ...media, schema: closeSchema(media.schema) } : media, + ]), + ); + return { ...requestBody, content }; +} + function closeResponses(responses: Record): Record { return Object.fromEntries( Object.entries(responses).map(([status, response]) => { @@ -214,6 +346,14 @@ function closeSchema(node: unknown, isAllOfMember = false): unknown { if (declaresFields && !isAllOfMember && schema.additionalProperties === undefined) { schema.unevaluatedProperties = false; } + for (const [exclusive, bound] of EXCLUSIVE_BOUNDS) { + if (schema[exclusive] === true) { + schema[exclusive] = schema[bound]; + delete schema[bound]; + } else if (schema[exclusive] === false) { + delete schema[exclusive]; + } + } if (schema.nullable === true) { const { nullable: _nullable, ...nonNull } = schema; return { anyOf: [{ type: 'null' }, nonNull] }; diff --git a/packages/mosaic/src/__tests__/feature/fapi-spec-gaps.ts b/packages/mosaic/src/__tests__/feature/fapi-spec-gaps.ts index 31ff91a0f24..a936b1b36cd 100644 --- a/packages/mosaic/src/__tests__/feature/fapi-spec-gaps.ts +++ b/packages/mosaic/src/__tests__/feature/fapi-spec-gaps.ts @@ -73,6 +73,25 @@ export const SPEC_GAPS: readonly SpecGap[] = [ schema: { type: 'string', enum: ['token'] }, sentBy: 'api/serialize/token.go Token', }, + { + at: `${operation('/v1/client/sessions/{session_id}/tokens', 'post')}/requestBody/content/application~1x-www-form-urlencoded/schema`, + property: 'tab_state', + schema: { type: 'string' }, + sentBy: 'api/fapi/v1/tokens/http.go paramTabState', + }, + { + at: `${operation('/v1/me/organization_suggestions', 'get')}/x-query-schema`, + property: 'status', + schema: { type: 'array', items: { type: 'string' } }, + replaces: { type: 'string' }, + sentBy: 'api/fapi/v1/users/http.go ListOrganizationSuggestions', + }, + { + at: `${operation('/v1/me/enterprise_connections', 'get')}/x-query-schema`, + property: 'with_organization_account_linking', + schema: { type: 'boolean' }, + sentBy: 'api/fapi/v1/enterprise_connections/deprecated_http.go ListEnterpriseConnections', + }, { at: externalAccount, property: 'public_metadata', @@ -101,3 +120,7 @@ export const SPEC_GAPS: readonly SpecGap[] = [ function component(name: string): string { return `#/components/schemas/${name}`; } + +function operation(path: string, method: string): string { + return `#/paths/${path.replace(/\//g, '~1')}/${method}`; +} diff --git a/packages/mosaic/src/features/organization-profile/__tests__/organization-profile.feature.test.tsx b/packages/mosaic/src/features/organization-profile/__tests__/organization-profile.feature.test.tsx index afc43fae780..b22eb159fd8 100644 --- a/packages/mosaic/src/features/organization-profile/__tests__/organization-profile.feature.test.tsx +++ b/packages/mosaic/src/features/organization-profile/__tests__/organization-profile.feature.test.tsx @@ -16,7 +16,7 @@ import { import { renderWithClerk } from '../../../__tests__/feature/render'; import { OrganizationProfile } from '../organization-profile'; -const acme = fapiOrganization({ id: 'org_1', name: 'Acme', slug: 'acme' }); +const acme = fapiOrganization({ id: 'org_acme00000000000000000000000', name: 'Acme', slug: 'acme' }); function serve({ permissions = ['org:sys_memberships:read'], diff --git a/packages/mosaic/src/features/user-profile/__tests__/user-profile.feature.test.tsx b/packages/mosaic/src/features/user-profile/__tests__/user-profile.feature.test.tsx index bec8fb3ac97..2f52dda5e16 100644 --- a/packages/mosaic/src/features/user-profile/__tests__/user-profile.feature.test.tsx +++ b/packages/mosaic/src/features/user-profile/__tests__/user-profile.feature.test.tsx @@ -14,7 +14,7 @@ import { signedIn } from '../user-profile-connected-accounts-section/__tests__/u function serve({ userAPIKeysEnabled = false } = {}) { return serveFapi({ environment: fapiEnvironment({ api_keys_settings: { user_api_keys_enabled: userAPIKeysEnabled } }), - client: fapiClient([fapiSession({ id: 'sess_1', user: fapiUser({ id: 'user_1' }) })]), + client: fapiClient([fapiSession({ id: 'sess_1', user: fapiUser({ id: 'user_alice0000000000000000000000' }) })]), }); } diff --git a/packages/mosaic/vitest.config.mts b/packages/mosaic/vitest.config.mts index 8269c7bc20f..38e5bcbc466 100644 --- a/packages/mosaic/vitest.config.mts +++ b/packages/mosaic/vitest.config.mts @@ -76,6 +76,7 @@ export default defineConfig({ test: { name: 'feature', include: [featureTests], + maxWorkers: process.env.CI ? undefined : 2, setupFiles: [resolve(import.meta.dirname, 'vitest.setup.browser.mts')], browser: { enabled: true, diff --git a/packages/mosaic/vitest.setup.browser.mts b/packages/mosaic/vitest.setup.browser.mts index 46aff59a673..ce8c98d51f0 100644 --- a/packages/mosaic/vitest.setup.browser.mts +++ b/packages/mosaic/vitest.setup.browser.mts @@ -85,7 +85,7 @@ afterEach(async () => { const violations = await takeContractViolations(); expect(unhandled, 'Frontend API requests without a handler').toEqual([]); expect(unsettled, 'Held Frontend API requests never released or failed').toEqual([]); - expect(violations, 'Fake Frontend API responses that do not match the spec').toEqual([]); + expect(violations, 'Frontend API traffic that does not match the spec').toEqual([]); }); afterAll(() => {