# Lovelio API reference

Recruitment automation API. Create jobs, manage candidates, run ad-hoc actions, integrate with your tools.

Generated from the OpenAPI 3.1 document at https://lovelio.ai/api/v1/openapi.json.
API version 7.2.0. 157 paths, 234 operations.

This is the plain-markdown copy of the reference rendered at https://lovelio.ai/docs/api.

## Fetch less than all of this

| URL | What you get |
| --- | --- |
| `https://lovelio.ai/docs/api.md` | Everything: this header plus every operation in full. |
| `https://lovelio.ai/docs/api.md?index=1` | This header plus the endpoint index. No per-operation detail. |
| `https://lovelio.ai/docs/api.md?tag=Jobs` | This header plus one section in full. Comma-separate for several: `?tag=Jobs,Applications`. |
| `https://lovelio.ai/api/v1/openapi.json` | The machine-readable source this file is generated from. |

Tags: System, Accounts, Jobs, Job Ads, Candidates, Candidate Imports, Applications, Interviews, Submissions, Placements, Quotas, Marketplace, Webhooks, Batch, Activities, Documents, Tasks, Outreach, Talent Pools, Review Queue, Forms, Referees, Integrations, Chat Integrations, Analytics, Calendar, Scheduled Emails, Workflow Rules, Email Templates, Stages, Distribution, Clients, Specs, Search, Business Development.

## Base URL

Docs live on lovelio.ai. The API does not. Every workspace belongs to a region and an API key only works on its own region's host. Calling `lovelio.ai/api/v1/...` returns a JSON error naming all three hosts rather than data.

| Region | Base URL |
| --- | --- |
| Production (US (N. California)) | `https://us.lovelio.ai/api/v1` |
| Production (EU (London)) | `https://eu.lovelio.ai/api/v1` |
| Production (ANZ (Sydney)) | `https://anz.lovelio.ai/api/v1` |

A workspace's exact host is shown in Settings > API keys.

## Authentication

```http
Authorization: Bearer sk_live_...
```

API key, sent as Authorization: Bearer <key>. Three key types: sk_live_ (production), sk_test_ (development - same workspace, same data, marked as a test key; use a separate workspace if you need isolated test data), and sk_trial_ (issued at signup, expires 7 days later - swap to a live key from Settings > API keys). Keys are server-generated, shown once at creation, and scoped per permission (or admin for everything). Lovelio Connect apps authenticate with the OAuth access token from the connect flow instead (Authorization: Bearer lc_at_...) - it carries exactly the scopes the agency approved, enforced on every request. Money is gated at FIELD level, not route level: the three scopes placements:financials:read, clients:financials:read and marketplace:financials:read unlock salary, fee, contract rates, expected GP, commission percents, the client fee schedule and split-fee deal amounts. Without them those fields return null and everything else on the record still comes through - a read never 403s for want of a money scope. admin satisfies all three. Requesting any of them puts a Connect app in the elevated review tier. See /docs/agents/build-an-integration.

## Pagination and shared parameters

Every list endpoint takes these. Cursor-based: read `meta.next_cursor` from a response and pass it back as `after`.

| Name | In | Type | Description |
| --- | --- | --- | --- |
| `limit` | query | integer, 1-100, default 25 | Rows per page. Clamped to the 1-100 range rather than rejected, so asking for 500 returns 100 - read meta.has_more and page with the cursor. Unparseable values fall back to 25. |
| `after` | query | string | Cursor for forward pagination |
| `before` | query | string | Cursor for backward pagination |
| `created_after` | query | string (date-time) | Only rows created at or after this ISO 8601 date or datetime (inclusive). Malformed values are a 422, never silently ignored. |
| `created_before` | query | string (date-time) | Only rows created at or before this ISO 8601 date or datetime (inclusive). Malformed values are a 422, never silently ignored. |

## Rate limit headers

| Header | Type | Description |
| --- | --- | --- |
| `X-RateLimit-Limit` | integer | Requests allowed per minute |
| `X-RateLimit-Remaining` | integer | Requests remaining in window |
| `X-RateLimit-Reset` | integer | Unix timestamp when window resets |

## Guides

- https://lovelio.ai/docs/agents - what an agent can drive end to end
- https://lovelio.ai/docs/agents/top-10-tasks - runnable examples for the ten most common calls
- https://lovelio.ai/docs/agents/build-an-integration - sandbox agency, test API key, live webhook testing
- https://lovelio.ai/docs/agents/error-handling - error envelope, request IDs, retries, idempotency keys
- https://lovelio.ai/docs/agents/webhooks - subscribing and verifying HMAC signatures
- https://lovelio.ai/docs/agents/sdks - single-file TypeScript and Python clients
- https://lovelio.ai/docs/api/changelog - every version, including breaking changes
- https://lovelio.ai/llms-full.txt - all of the above inlined in one fetch

## Endpoint index

Filtered to: Accounts.

### Accounts

Account and API key management

- `POST /accounts/signup` - Create account
- `POST /accounts/verify-email` - Verify account email
- `GET /accounts/me` - Get current account
- `GET /accounts/{id}` - Get account
- `PATCH /accounts/{id}` - Update account
- `GET /accounts/{id}/users` - List users
- `POST /accounts/{id}/users` - Invite a user
- `GET /accounts/{id}/users/{userId}` - Get user
- `PATCH /accounts/{id}/users/{userId}` - Update user
- `DELETE /accounts/{id}/users/{userId}` - Delete user
- `GET /accounts/{id}/teams` - List teams
- `POST /accounts/{id}/teams` - Create team
- `GET /accounts/{id}/teams/{teamId}` - Get team
- `PATCH /accounts/{id}/teams/{teamId}` - Update team
- `DELETE /accounts/{id}/teams/{teamId}` - Delete team
- `GET /accounts/{id}/api-keys` - List API keys
- `POST /accounts/{id}/api-keys` - Create API key
- `DELETE /accounts/{id}/api-keys/{keyId}` - Revoke API key
- `GET /accounts/{id}/sso` - List SSO connections
- `POST /accounts/{id}/sso` - Create SSO connection
- `DELETE /accounts/{id}/sso/{connectionId}` - Remove SSO connection
- `GET /accounts/{id}/transcription-usage` - Get video transcription usage
- `POST /accounts/{id}/upgrade` - Activate account outside Stripe Checkout
- `GET /accounts/{id}/intake-slo` - Get intake pipeline SLO
- `GET /accounts/{id}/intelligence` - Get company intelligence profile
- `POST /accounts/{id}/intelligence/refresh` - Refresh company intelligence

---

## Accounts

Account and API key management

### POST /accounts/signup

**Create account**

Public, unauthenticated signup. Takes a work email; the company name and website are derived from the email domain (free-email domains are refused). Returns 202 with a 7-day trial API key (key_type "trial", admin scopes) that can READ immediately - writes stay blocked with 403 EMAIL_NOT_VERIFIED until the customer clicks the verification email. The trial_api_key is shown exactly once. Async enrichment (branding, logo, timezone) runs behind enrichment_task_id; an account.enriched webhook fires when it completes. IP rate limit: 3 signups/hour, 5/day.

Operation ID: `createAccount`

**Request body** (`application/json`)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `email` | string | yes | Work email address. Free/personal domains (gmail etc.) are refused. |
| `source` | string | no | Where the signup came from (e.g. "claude-agent"). Defaults to "api". |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `202` | Account created (or, for an existing unverified account with the same email, re-acknowledged WITHOUT a new key - duplicate signups are a no-op). data: { account_id, status: "pending_verification", trial_api_key (new accounts only, shown once), email_verified: false, message, enrichment_task_id }. | { success, data: object } |
| `409` | Another account already claims this email or its domain (error.code CONFLICT). |  |
| `422` | Missing/invalid email, or a free-email domain. |  |
| `429` | Rate limit exceeded (3/hour, 5/day per IP). Retry-After header set. |  |

### POST /accounts/verify-email

**Verify account email**

Completes signup by redeeming the token from the verification email. No authentication required - the token is the credential. Tokens are single-use and expire; a used or expired token returns 409 / 422.

Operation ID: `verifyAccountEmail`

**Request body** (`application/json`, required)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `token` | string | yes | Verification token from the email link. |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Email verified, account active | { success, data: object } |
| `409` | Token already used |  |
| `422` | Token missing, invalid, or expired |  |

### GET /accounts/me

**Get current account**

Operation ID: `getMyAccount`

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Account details | { data: object } |

### GET /accounts/{id}

**Get account**

Operation ID: `getAccount`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Account details | { success, data: Account } |

### PATCH /accounts/{id}

**Update account**

Update mutable fields on the account. Partners can also mark onboarding as complete to bypass the in-app wizard.

Operation ID: `updateAccount`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

**Request body** (`application/json`)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `company_name` | string | no |  |
| `tone_of_voice` | string \| null | no |  |
| `logo_url` | string \| null | no |  |
| `website_url` | string \| null | no |  |
| `location` | string \| null | no |  |
| `onboarding_completed_at` | boolean \| string (date-time) \| null | no | Pass true to mark onboarding complete (server sets NOW()), false/null to clear it, or an ISO 8601 timestamp to set it exactly. |
| `timezone` | string | no | IANA timezone (e.g. "Europe/London"). Setting this marks timezone_source as "manual" so automated enrichment will not overwrite it. |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Updated account | { success, data: Account } |

### GET /accounts/{id}/users

**List users**

Operation ID: `listUsers`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Array of users | { success, data: array of User } |

### POST /accounts/{id}/users

**Invite a user**

Operation ID: `createUser`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

**Request body** (`application/json`)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes | Full name. Alternatively pass first_name + last_name. |
| `first_name` | string | no | Given name. Preferred over name. |
| `last_name` | string | no | Family name. |
| `email` | string (email) | yes | Must be on the account's email_domain when one is set, else the invite is rejected. |
| `phone` | string | no |  |
| `role` | string (`admin`, `team_leader`, `recruiter`) | no |  |
| `team_id` | string | no | Team (tem_ id) the user joins when they accept the invite. |
| `role_title` | string | no |  |
| `linkedin_url` | string | no |  |
| `slack_user_id` | string | no |  |
| `whatsapp_number` | string | no |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `201` | User invited | { success, data: User } |

### GET /accounts/{id}/users/{userId}

**Get user**

Operation ID: `getUser`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `userId` | string | yes |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | User details | { success, data: User } |

### PATCH /accounts/{id}/users/{userId}

**Update user**

Operation ID: `updateUser`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `userId` | string | yes |  |

**Request body** (`application/json`)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | no | Full name. Alternatively pass first_name + last_name; either way first/last and the display name stay in step. |
| `first_name` | string | no |  |
| `last_name` | string | no |  |
| `email` | string (email) | no | Must be on the account's email_domain when one is set. |
| `phone` | string | no |  |
| `role` | string (`admin`, `team_leader`, `recruiter`) | no |  |
| `role_title` | string | no |  |
| `team_id` | string \| null | no | Assign the member to a team (tem_ id), or null to clear. Requires the user to have accepted their invite. |
| `linkedin_url` | string | no |  |
| `slack_user_id` | string | no |  |
| `whatsapp_number` | string | no |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Updated user | { success, data: User } |

### DELETE /accounts/{id}/users/{userId}

**Delete user**

Operation ID: `deleteUser`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `userId` | string | yes |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `204` | User deleted |  |

### GET /accounts/{id}/teams

**List teams**

Operation ID: `listTeams`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Array of teams | { success, data: array of Team } |

### POST /accounts/{id}/teams

**Create team**

Operation ID: `createTeam`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

**Request body** (`application/json`)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes |  |
| `parent_team_id` | string \| null | no | Parent team this team nests under, or null/omitted for top level. |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `201` | Team created | { success, data: Team } |

### GET /accounts/{id}/teams/{teamId}

**Get team**

Operation ID: `getTeam`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `teamId` | string | yes |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Team details | { success, data: Team } |

### PATCH /accounts/{id}/teams/{teamId}

**Update team**

Operation ID: `updateTeam`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `teamId` | string | yes |  |

**Request body** (`application/json`)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | no |  |
| `parent_team_id` | string \| null | no | Re-parent the team. Send null to move it back to the top level. Must not create a cycle. |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Updated team | { success, data: Team } |

### DELETE /accounts/{id}/teams/{teamId}

**Delete team**

Operation ID: `deleteTeam`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `teamId` | string | yes |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `204` | Team deleted |  |

### GET /accounts/{id}/api-keys

**List API keys**

Operation ID: `listApiKeys`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Array of API keys | { success, data: array of ApiKey } |

### POST /accounts/{id}/api-keys

**Create API key**

Returns the full API key once - store it securely.

Operation ID: `createApiKey`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

**Request body** (`application/json`)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes |  |
| `scopes` | array of string | yes |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `201` | API key created with full key value | { success, data: ApiKey, key } |

### DELETE /accounts/{id}/api-keys/{keyId}

**Revoke API key**

Operation ID: `revokeApiKey`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `keyId` | string | yes |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `204` | API key revoked |  |

### GET /accounts/{id}/sso

**List SSO connections**

Returns all SSO connections configured for this account.

Operation ID: `listSsoConnections`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Array of SSO connections | { success, data: array of SsoConnection } |

### POST /accounts/{id}/sso

**Create SSO connection**

Creates a pending SSO connection for the account. Requires an active subscription. The admin must finish IdP configuration through the WorkOS Admin Portal before the connection becomes active.

Operation ID: `createSsoConnection`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

**Request body** (`application/json`, required)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `domain` | string | yes |  |
| `provider` | string (`google`, `microsoft`, `saml`) | yes |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `201` | Connection created | { success, data: SsoConnection } |
| `402` | Plan upgrade required |  |
| `409` | Connection already exists for this domain |  |
| `503` | SSO provider not configured on server |  |

### DELETE /accounts/{id}/sso/{connectionId}

**Remove SSO connection**

Operation ID: `deleteSsoConnection`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |
| `connectionId` | string | yes |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `204` | Connection deleted |  |

### GET /accounts/{id}/transcription-usage

**Get video transcription usage**

Returns this calendar month's video-transcription usage and the plan cap. Lovelio covers the underlying vendor cost. Active (paying) accounts get a larger monthly cap than trialing accounts.

Operation ID: `getTranscriptionUsage`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Usage snapshot | { success, data: object } |

### POST /accounts/{id}/upgrade

**Activate account outside Stripe Checkout**

Sets the account to active at the given seat count and promotes its trial API key to a live one. For partner-comped accounts; normal billing goes through Stripe Checkout in the dashboard. Lovelio has one plan with everything included, so there is no tier to choose.

Operation ID: `upgradeAccount`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

**Request body** (`application/json`)

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `seats` | integer, min 1, default 1 | no |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Account upgraded | { success, data: object } |

### GET /accounts/{id}/intake-slo

**Get intake pipeline SLO**

Returns p50/p95/p99 total pipeline latency for the intake agent loop (CV parse + AI assessment + auto-decision) over the last 30 days, plus the decision-path breakdown and parse success rate. Use this to surface "first response to candidate: 30 seconds" style claims with real numbers.

Operation ID: `getIntakeSlo`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Intake SLO reading | { success, data: object } |

### GET /accounts/{id}/intelligence

**Get company intelligence profile**

Returns the synthesised seven-section Company Intelligence profile used by every AI prompt builder (job ads, outreach, assessments). Includes freshness fields (is_stale, last_synthesised_at, tier) so callers can render a stale banner and decide when to POST /intelligence/refresh.

Operation ID: `getCompanyIntelligence`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `200` | Company intelligence profile | { success, data: object } |

### POST /accounts/{id}/intelligence/refresh

**Refresh company intelligence**

Queues a fresh deep-enrich + seven-pass synthesis run. Returns 202 immediately with a task_id and poll_url. Rate-limited by a 10-minute per-account cool-down. Requires an Idempotency-Key header.

Operation ID: `refreshCompanyIntelligence`

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes |  |

**Header parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | string | yes |  |

**Responses**

| Code | Description | Body |
| --- | --- | --- |
| `202` | Refresh queued | { success, data: object } |
| `409` | Cool-down active (refreshed in the last 10 minutes). |  |

## Schemas

Objects referenced by the operations above.

### Account

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | no |  |
| `company_name` | string | no |  |
| `slug` | string | no |  |
| `domain` | string \| null | no |  |
| `logo_url` | string \| null | no |  |
| `brand_colours` | array of string | no |  |
| `location` | string \| null | no |  |
| `tone_of_voice` | string \| null | no |  |
| `seats` | integer \| null | no | Paid seat count. Null while trialing (before first checkout). |
| `plan_tier` | string (`premium`) | no | Legacy field. Lovelio has one plan with everything included, so this always reads premium. |
| `plan_status` | string (`trialing`, `active`, `past_due`, `cancelled`) | no |  |
| `email_verified` | boolean | no |  |
| `status` | string | no |  |
| `website_url` | string \| null | no |  |
| `created_at` | string (date-time) | no |  |
| `enriched_at` | string (date-time) \| null | no |  |
| `brand_voice_prompt` | string \| null | no |  |
| `mission` | string \| null | no | Company mission - 1-2 sentences. Auto-extracted from the company website on signup; editable in the dashboard. |
| `what_we_do` | string \| null | no | What problems the company solves and for whom - 1-2 sentences. Auto-extracted; editable. |
| `why_join` | string \| null | no | Bullet list of reasons to work at the company. Auto-extracted; editable. |
| `benefits` | string \| null | no | Bullet list of tangible benefits and perks. Auto-extracted; editable. Job ad generation never invents items not in this block. |
| `company_values` | string \| null | no | Bullet list of cultural anchors / values that define how the team works. |
| `role_intelligence` | object \| null | no | Operational role data. Known keys: rolesWePlaceInput (string), rolesWePlace (object mapping specialisation/group name to bullet list of role titles), roleIntelligence (object mapping role title to { salaryRange, keySkills[], seniority[] }), roleTemplates (object mapping role title to JD template text). |
| `onboarding_completed_at` | string (date-time) \| null | no | Set when the account holder finishes the in-app onboarding wizard. Null means the next dashboard visit will force the wizard. |
| `onboarding_answers` | object | no | Raw answers captured by the onboarding wizard. Keys: logoConfirmed (boolean). |
| `timezone` | string | no | IANA timezone used for company-wide scheduled work like the weekly digest. Defaults to UTC; automatically populated from Brandfetch / Firecrawl / the first admin browser on signup. |
| `timezone_source` | string (`default`, `manual`, `brandfetch`, `firecrawl`, `browser`) | no | How the timezone was set. "manual" means an admin or API partner set it explicitly and no automated process will overwrite it. |
| `is_demo` | boolean | no | True for throwaway demo accounts created by the MagicHire team. Always false for real customer accounts created via /v1/accounts/signup. |

### ApiKey

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | no |  |
| `account_id` | string | no |  |
| `name` | string | no |  |
| `prefix` | string | no |  |
| `scopes` | array of string | no |  |
| `status` | string | no |  |
| `key_type` | string | no |  |
| `last_used_at` | string (date-time) \| null | no |  |
| `created_at` | string (date-time) | no |  |

### SsoConnection

Single sign-on connection for an account. Each connection is bound to a single email domain; users signing in with that domain are routed through the associated identity provider.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | no |  |
| `account_id` | string | no |  |
| `provider` | string (`google`, `microsoft`, `saml`) | no | google = Google Workspace, microsoft = Microsoft Entra / Azure AD, saml = any generic SAML 2.0 IdP (Okta, JumpCloud, OneLogin, etc). |
| `domain` | string | no |  |
| `status` | string (`pending`, `active`, `disabled`) | no |  |
| `workos_organization_id` | string \| null | no | WorkOS organization ID. Used to generate Admin Portal setup URLs. |
| `created_at` | string (date-time) | no |  |
| `last_verified_at` | string (date-time) \| null | no |  |

### Team

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | no |  |
| `account_id` | string | no |  |
| `name` | string | no |  |
| `parent_team_id` | string \| null | no | Parent team this team nests under in the org chart, or null at top level. |
| `created_at` | string (date-time) | no |  |

### User

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | no |  |
| `account_id` | string | no |  |
| `name` | string | no |  |
| `first_name` | string \| null | no |  |
| `last_name` | string \| null | no |  |
| `email` | string (email) | no |  |
| `role` | string (`admin`, `team_leader`, `recruiter`) | no |  |
| `team_ids` | array of string | no | The team the accepted member sits on. Always [] while their invite is pending. |
| `status` | string | no |  |
| `slack_user_id` | string \| null | no |  |
| `whatsapp_number` | string \| null | no |  |
| `phone` | string \| null | no |  |
| `linkedin_url` | string \| null | no |  |
| `created_at` | string (date-time) | no |  |
