---
title: Erika API reference
url: https://erikawork.com/api-reference
spec: https://erikawork.com/openapi.json
---

# Erika API reference

Erika is an AI-first applicant tracking system. This is the REST API for external integrations — authenticate with an API key from Settings → API Keys.

- Base URL: `https://app.erikawork.com/api` (paths below are absolute)
- OpenAPI spec: https://erikawork.com/openapi.json (live copy: https://app.erikawork.com/api/openapi.json)
- MCP server for AI clients: https://erikawork.com/mcp.md
- Integration guide: https://erikawork.com/for-builders.md

## Authentication

Send an API key from Settings → API Keys (https://app.erikawork.com/settings/api-keys) in the `X-Api-Key` header on every request. `Authorization: Bearer` is not supported. Keys look like `ek_live_…`; permissions are bound to the key when it is created (Auto Campaign API for campaign builds and sourcing reads, Jobs and Candidates for the ATS, each at View or Manage level). Every request is scoped to the key's organization.

## Responses and errors

Success: `{ "success": true, "data": … }`. Lists are cursor-paginated: pass `nextCursor` back as `cursor` while `hasMore` is true. Errors: `{ "success": false, "error": "…", "code": "…", "requestId": "…" }`; quote the `requestId` to support.

## Campaign Builds

### Start an autonomous campaign build

`POST https://app.erikawork.com/api/v1/campaign-builds`

Create a Job, generate creatives + videos, and launch a Meta campaign end to end from one call. Requires the MANAGE_CAMPAIGN_API scope. Returns 202 immediately; poll the status URL or receive a signed webhook on completion.

Request body (JSON, schema `CreateCampaignBuild`):

| Field | Type | Description |
| --- | --- | --- |
| `jobDescription` | string | Description Erika builds a NEW job from. Provide exactly one of jobDescription or jobId. · 50–20000 chars |
| `jobId` | string (uuid) | Reuse an EXISTING Erika job (no new job is created); targeting is seeded from its details. Provide exactly one of jobDescription or jobId. |
| `budget` | number | Total credit envelope. Production (videos + AI) is spent from it; the remainder funds Meta. · required, > 0 |
| `budgetCurrency` | "CREDITS" \| "USD" \| "EUR" | default "CREDITS" |
| `duration` | integer | Campaign run length in days · required, > 0 |
| `city` | string | Work-location city. Inferred from the job description when omitted. · 1–120 chars |
| `countryCode` | string | ISO-3166 alpha-2 (e.g. CZ). Inferred from the job description when omitted. · 2–2 chars |
| `language` | string | Ad + screening language (e.g. cs). Inferred from the job description when omitted. · 2–5 chars |
| `startDate` | string | Scheduled launch day, ISO YYYY-MM-DD. Omitted launches today. A past date is clamped to now. · pattern ^\d{4}-\d{2}-\d{2}$ |
| `callbackUrl` | string (uri) | HTTPS endpoint POSTed a signed result on completion/failure |
| `metadata` | map of string | Caller-defined tracking, echoed back verbatim on the status endpoint and the completion webhook (your job id, client, brand, campaign ref). Opaque to Erika. Up to 20 keys; key <= 40 chars, value <= 500 chars. |
| `idempotencyKey` | string | Required. A retry with the same key returns the same build (no double spend). · required, 8–255 chars |

Response: **202** Build accepted and queued. `data`: CampaignBuildAccepted

- **400** Validation error, or INSUFFICIENT_JD_CONTEXT when the job description lacks enough detail to determine a location
- **401** Missing or invalid API key
- **402** Insufficient budget or credits (code INSUFFICIENT_BUDGET / INSUFFICIENT_CREDITS)
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **404** JOB_NOT_FOUND — the jobId does not match a job in this organization
- **409** Idempotency key reused with a different request body
- **429** Too many builds in progress for this organization
- **500** Internal server error
- **503** Could not analyze the job description right now (transient). Retry shortly.

### Get campaign build status

`GET https://app.erikawork.com/api/v1/campaign-builds/{id}`

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Campaign build ID · path, required |

Response: **200** Current build status. `data`: CampaignBuildStatus

- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **404** Build not found
- **500** Internal server error

## Sourcing

### List sourcing campaigns

`GET https://app.erikawork.com/api/sourcing/campaigns`

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `jobId` | string | query |

Response: **200** Campaigns. `data`: object

- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

### Get sourcing campaign

`GET https://app.erikawork.com/api/sourcing/campaigns/{id}`

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Resource ID (UUID) · path, required |

Response: **200** Campaign. `data`: SourcingCampaign

- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **404** Resource not found
- **500** Internal server error

### List creatives

`GET https://app.erikawork.com/api/sourcing/creatives`

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `campaignId` | string | query |

Response: **200** Creatives. `data`: object

- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

### Get creative

`GET https://app.erikawork.com/api/sourcing/creatives/{id}`

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Resource ID (UUID) · path, required |

Response: **200** Creative. `data`: SourcingCreative

- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **404** Resource not found
- **500** Internal server error

### List adverts

`GET https://app.erikawork.com/api/sourcing/adverts`

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `campaignId` | string | query |

Response: **200** Adverts. `data`: object

- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

## Jobs

### List jobs

`GET https://app.erikawork.com/api/jobs`

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `status` | "draft" \| "open" \| "paused" \| "closed" \| "archived" | query |
| `limit` | integer | query, ≥ 1, ≤ 100 |
| `cursor` | string | query |

Response: **200** Jobs page. `data`: Job[], plus `nextCursor`, `hasMore`

- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

### Create job

`POST https://app.erikawork.com/api/jobs`

Request body (JSON):

| Field | Type | Description |
| --- | --- | --- |
| `title` | string | required, 1–200 chars |
| `externalRef` | string | Your own reference for this position, typically the job ID from your ATS. Free text, up to 100 characters. Every application on this job returns it as `jobExternalRef`, so you can match applications without a lookup. · max 100 chars |
| `description` | string | max 20000 chars |
| `pipelineTemplateId` | string | required |
| `hiringManagerId` | string |  |
| `jobDetails` | JobDetails |  |
| `screeningConfig` | map of any \| null |  |

Response: **201** Created. Fetch GET /jobs/{id} for the full job.. `data`: object

- **400** Validation error or business-rule violation
- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

### Get job by ID

`GET https://app.erikawork.com/api/jobs/{id}`

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Resource ID (UUID) · path, required |

Response: **200** Job. `data`: Job

- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **404** Resource not found
- **500** Internal server error

### Update job

`PATCH https://app.erikawork.com/api/jobs/{id}`

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Resource ID (UUID) · path, required |

Request body (JSON):

| Field | Type | Description |
| --- | --- | --- |
| `title` | string | 1–200 chars |
| `externalRef` | string | Send an empty string to clear it. · max 100 chars |
| `description` | string | max 20000 chars |
| `hiringManagerId` | string \| null |  |
| `jobDetails` | JobDetails |  |
| `screeningConfig` | map of any \| null |  |

Response: **200** Updated. Fetch GET /jobs/{id} for the full job.. `data`: object

- **400** Validation error or business-rule violation
- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

### Delete job (soft delete)

`DELETE https://app.erikawork.com/api/jobs/{id}`

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Resource ID (UUID) · path, required |

Response: **200** Deleted. `data`: object

- **400** Validation error or business-rule violation
- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

### Change job status

`PATCH https://app.erikawork.com/api/jobs/{id}/status`

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Resource ID (UUID) · path, required |

Request body (JSON):

| Field | Type | Description |
| --- | --- | --- |
| `status` | "draft" \| "open" \| "paused" \| "closed" \| "archived" | required |

Response: **200** Status changed. `data`: Job

- **400** Validation error or business-rule violation
- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

### Replace job pipeline stages

`PUT https://app.erikawork.com/api/jobs/{id}/stages`

Replaces all stages on the job. Existing applications are kept (mapped onto stages by ID when possible). Use with care.

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Resource ID (UUID) · path, required |

Request body (JSON):

| Field | Type | Description |
| --- | --- | --- |
| `stages` | object[] | required, min 1 items |

Response: **200** Stages replaced. `data`: object

- **400** Validation error or business-rule violation
- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

### Get kanban board view

`GET https://app.erikawork.com/api/jobs/{jobId}/board`

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `jobId` | string | path, required |
| `includeRejected` | string | query |

Response: **200** Board (stages + applications). `data`: object

- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **404** Resource not found
- **500** Internal server error

### Get the unified kanban board across all jobs

`GET https://app.erikawork.com/api/board/all`

Applications across every in-scope job, grouped into columns keyed by stage category. Defaults to open + paused jobs.

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `includeRejected` | string | query |
| `includeClosed` | string | query |
| `jobIds` | string | query |

Response: **200** Unified board (jobs + category columns). `data`: object

- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **404** Resource not found
- **500** Internal server error

### Get the AI qualifier setting of a position

`GET https://app.erikawork.com/api/jobs/{jobId}/qualifier`

The AI qualifier checks each sourced candidate against the position's must-haves and, in mode `move`, moves the qualified ones to `targetStage` (the next stage unless the position picked another one from `targetOptions`). It never rejects. `requirements` is the position's own list when one was set, else null (Erika derives the must-haves from the job and the application form).

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `jobId` | string | path, required |

Response: **200** Qualifier setting. `data`: object

- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **404** Resource not found
- **500** Internal server error

### Change the AI qualifier of a position

`PUT https://app.erikawork.com/api/jobs/{jobId}/qualifier`

Set the mode, where qualified candidates go (`targetStageId`, one of `targetOptions`; `null` = the next stage after the one candidates land in, the default) and/or the position's own must-haves (`requirements: null` goes back to Erika's own list). Switching a position off also makes off the company default for new campaigns.

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `jobId` | string | path, required |

Request body (JSON):

| Field | Type | Description |
| --- | --- | --- |
| `mode` | "move" \| "rank_only" \| "off" | move = Erika moves qualified candidates to the target stage; rank_only = only marks them, without moving (the value keeps its name for compatibility); off. |
| `targetStageId` | string \| null |  |
| `requirements` | object[] \| null |  |

Response: **200** Saved. `data`: object

- **400** Validation error or business-rule violation
- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

## Pipeline Templates

### List pipeline templates

`GET https://app.erikawork.com/api/pipeline-templates`

Response: **200** Templates. `data`: PipelineTemplate[]

- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

### Create pipeline template

`POST https://app.erikawork.com/api/pipeline-templates`

Request body (JSON):

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | required, 1–100 chars |
| `stages` | object[] | required, min 1 items |

Response: **201** Created. `data`: PipelineTemplate

- **400** Validation error or business-rule violation
- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

### Update pipeline template

`PATCH https://app.erikawork.com/api/pipeline-templates/{id}`

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Resource ID (UUID) · path, required |

Request body (JSON):

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | 1–100 chars |
| `isDefault` | boolean |  |
| `stages` | object[] | min 1 items |

Response: **200** Updated. `data`: PipelineTemplate

- **400** Validation error or business-rule violation
- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

### Delete pipeline template

`DELETE https://app.erikawork.com/api/pipeline-templates/{id}`

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Resource ID (UUID) · path, required |

Response: **200** Deleted. `data`: object

- **400** Validation error or business-rule violation
- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

## Candidates

### List candidates

`GET https://app.erikawork.com/api/candidates`

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `source` | string | query |
| `tag` | string | query |
| `search` | string | query |
| `limit` | integer | query, ≥ 1, ≤ 100 |
| `cursor` | string | query |

Response: **200** Candidates page. `data`: Candidate[], plus `nextCursor`, `hasMore`

- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

### Create candidate

`POST https://app.erikawork.com/api/candidates`

Request body (JSON):

| Field | Type | Description |
| --- | --- | --- |
| `firstName` | string | required, 1–100 chars |
| `lastName` | string | required, 1–100 chars |
| `email` | string (email) |  |
| `phonePrefix` | string | max 10 chars |
| `phoneNumber` | string | max 40 chars |
| `linkedinUrl` | string (uri) |  |
| `source` | string | max 60 chars |
| `tags` | string[] |  |

Response: **201** Created. `data`: object

- **400** Validation error or business-rule violation
- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

### Bulk create candidates

`POST https://app.erikawork.com/api/candidates/bulk`

Up to 1000 rows per request. Duplicates by email are skipped and reported in the response.

Request body (JSON):

| Field | Type | Description |
| --- | --- | --- |
| `rows` | object[] | required, min 1 items |

Response: **200** Bulk result. `data`: object

- **400** Validation error or business-rule violation
- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

### Get candidate by ID

`GET https://app.erikawork.com/api/candidates/{id}`

Full candidate detail including every application, its form answers, and the candidate's stated situation facts (`candidateProfile`: start availability, salary expectation, job-search status, best time to call). `candidateProfile` requires the API key to hold VIEW_SCREENING.

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Resource ID (UUID) · path, required |

Response: **200** Candidate. `data`: CandidateDetail

- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **404** Resource not found
- **500** Internal server error

### Update candidate

`PATCH https://app.erikawork.com/api/candidates/{id}`

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Resource ID (UUID) · path, required |

Request body (JSON):

| Field | Type | Description |
| --- | --- | --- |
| `firstName` | string | 1–100 chars |
| `lastName` | string | 1–100 chars |
| `email` | string (email) |  |
| `phonePrefix` | string | max 10 chars |
| `phoneNumber` | string | max 40 chars |
| `linkedinUrl` | string (uri) |  |
| `source` | string | max 60 chars |
| `tags` | string[] |  |

Response: **200** Updated. `data`: object

- **400** Validation error or business-rule violation
- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

### Delete candidate (soft delete)

`DELETE https://app.erikawork.com/api/candidates/{id}`

Soft-deletes the candidate and cascades to their applications + notes (the platform talent profile is left intact). Returns the bulk-delete result (deletedCount is 1 for this single-id path; deletedApplications is the cascade count).

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Resource ID (UUID) · path, required |

Response: **200** Deleted. `data`: object

- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **404** Resource not found
- **500** Internal server error

### Add note to candidate

`POST https://app.erikawork.com/api/candidates/{id}/notes`

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Resource ID (UUID) · path, required |

Request body (JSON):

| Field | Type | Description |
| --- | --- | --- |
| `content` | string | required, 1–10000 chars |

Response: **201** Note added. `data`: object

- **400** Validation error or business-rule violation
- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

## Applications

### List all applications (flat)

`GET https://app.erikawork.com/api/applications`

Flat list across all jobs. For paginated per-job results use GET /jobs/{jobId}/applications.

Response: **200** Applications. `data`: ApplicationSummary[]

- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

### List applications for a job

`GET https://app.erikawork.com/api/jobs/{jobId}/applications`

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `jobId` | string | path, required |
| `limit` | integer | query, ≥ 1, ≤ 100 |
| `cursor` | string | query |

Response: **200** Page of applications. `data`: ApplicationListItem[], plus `nextCursor`, `hasMore`

- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

### Create application (assign candidate to job)

`POST https://app.erikawork.com/api/jobs/{jobId}/applications`

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `jobId` | string | path, required |

Request body (JSON):

| Field | Type | Description |
| --- | --- | --- |
| `candidateId` | string | required |
| `source` | string | max 60 chars |
| `stageId` | string |  |

Response: **201** Created. `data`: object

- **400** Validation error or business-rule violation
- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

### Get application

`GET https://app.erikawork.com/api/applications/{id}`

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Resource ID (UUID) · path, required |

Response: **200** Application. `data`: Application

- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **404** Resource not found
- **500** Internal server error

### Delete application (soft delete)

`DELETE https://app.erikawork.com/api/applications/{id}`

Removes the application from boards, lists, exports and search. Destructive — distinct from rejecting (which keeps the record). Returns the bulk-delete result (deletedCount is 1 for this single-id path).

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Resource ID (UUID) · path, required |

Response: **200** Deleted. `data`: ApplicationBulkDeleteResult

- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **404** Resource not found
- **500** Internal server error

### Move application to a different stage

`PATCH https://app.erikawork.com/api/applications/{id}/stage`

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Resource ID (UUID) · path, required |

Request body (JSON):

| Field | Type | Description |
| --- | --- | --- |
| `stageId` | string | required |

Response: **200** Moved. `data`: object

- **400** Validation error or business-rule violation
- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

### Change application status

`PATCH https://app.erikawork.com/api/applications/{id}/status`

Set to rejected, withdrawn, or hired. Reject requires either reason text or rejectionReasonId.

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Resource ID (UUID) · path, required |

Request body (JSON):

| Field | Type | Description |
| --- | --- | --- |
| `status` | "rejected" \| "withdrawn" \| "hired" | required |
| `reason` | string |  |
| `rejectionReasonId` | string |  |
| `note` | string |  |

Response: **200** Status changed. `data`: object

- **400** Validation error or business-rule violation
- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

### Revert hired status back to active

`PATCH https://app.erikawork.com/api/applications/{id}/unhire`

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Resource ID (UUID) · path, required |

Response: **200** Reverted. `data`: object

- **400** Validation error or business-rule violation
- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

### Add note to application timeline

`POST https://app.erikawork.com/api/applications/{id}/note`

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Resource ID (UUID) · path, required |

Request body (JSON):

| Field | Type | Description |
| --- | --- | --- |
| `content` | string | required, 1–10000 chars |

Response: **201** Note added. `data`: object

- **400** Validation error or business-rule violation
- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

### Add a candidate to a position the AI qualifier suggested

`POST https://app.erikawork.com/api/applications/{id}/qualifier/add-to-position`

For a candidate the AI qualifier judged not qualified, add them to one of the other open positions it suggested (`qualifier.suggestions` on the application). Creates a new application with the same source; the original application stays. Both timelines note where the candidate came from. Requires MANAGE_CANDIDATES.

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | path, required |

Request body (JSON):

| Field | Type | Description |
| --- | --- | --- |
| `jobId` | string (uuid) | required |

Response: **201** Added. `data`: object

- **400** Validation error or business-rule violation
- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

## Ratings

### Create or update current worker's rating

`PUT https://app.erikawork.com/api/applications/{id}/rating`

Idempotent — one rating per (worker, application). Subsequent PUTs replace the prior value.

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Resource ID (UUID) · path, required |

Request body (JSON):

| Field | Type | Description |
| --- | --- | --- |
| `rating` | integer | required, ≥ 1, ≤ 5 |
| `commentKeys` | string[] |  |
| `comment` | string | max 2000 chars |

Response: **200** Saved. `data`: Rating

- **400** Validation error or business-rule violation
- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

### Delete current worker's rating

`DELETE https://app.erikawork.com/api/applications/{id}/rating`

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Resource ID (UUID) · path, required |

Response: **200** Deleted. `data`: object

- **400** Validation error or business-rule violation
- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

### Get all ratings for an application

`GET https://app.erikawork.com/api/applications/{id}/ratings`

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Resource ID (UUID) · path, required |

Response: **200** Ratings (one per worker who has rated). `data`: object

- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

## Attachments

### List attachments on an application

`GET https://app.erikawork.com/api/applications/{id}/attachments`

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Resource ID (UUID) · path, required |

Response: **200** Attachments. `data`: Attachment[]

- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

### Upload attachment to an application

`POST https://app.erikawork.com/api/applications/{id}/attachments`

multipart/form-data with `file` field. Max ~50MB.

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Resource ID (UUID) · path, required |

Response: **201** Uploaded. `data`: Attachment

- **400** Validation error or business-rule violation
- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

### Get a signed download URL

`GET https://app.erikawork.com/api/attachments/{id}/url`

Returns a short-lived signed URL pointing at GCS. Use for direct browser downloads.

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Resource ID (UUID) · path, required |

Response: **200** Signed URL. `data`: object

- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **404** Resource not found
- **500** Internal server error

### Download attachment (proxied stream)

`GET https://app.erikawork.com/api/attachments/{id}/download`

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Resource ID (UUID) · path, required |
| `inline` | string | query |

Response: **200** Binary file stream.

- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **404** Resource not found
- **500** Internal server error

### Delete attachment

`DELETE https://app.erikawork.com/api/attachments/{id}`

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Resource ID (UUID) · path, required |

Response: **200** Deleted. `data`: object

- **400** Validation error or business-rule violation
- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

## Rejection Reasons

### List rejection reasons

`GET https://app.erikawork.com/api/rejection-reasons`

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `includeArchived` | string | query |

Response: **200** Reasons. `data`: RejectionReason[]

- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

### Create rejection reason

`POST https://app.erikawork.com/api/rejection-reasons`

Request body (JSON):

| Field | Type | Description |
| --- | --- | --- |
| `label` | string | required, 1–120 chars |
| `description` | string | max 500 chars |

Response: **201** Created. `data`: RejectionReason

- **400** Validation error or business-rule violation
- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

### Update rejection reason

`PATCH https://app.erikawork.com/api/rejection-reasons/{id}`

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Resource ID (UUID) · path, required |

Request body (JSON):

| Field | Type | Description |
| --- | --- | --- |
| `label` | string | 1–120 chars |
| `description` | string | max 500 chars |
| `isArchived` | boolean |  |

Response: **200** Updated. `data`: RejectionReason

- **400** Validation error or business-rule violation
- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

### Delete rejection reason (soft delete)

`DELETE https://app.erikawork.com/api/rejection-reasons/{id}`

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Resource ID (UUID) · path, required |

Response: **200** Deleted. `data`: object

- **400** Validation error or business-rule violation
- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

### Reorder rejection reasons

`POST https://app.erikawork.com/api/rejection-reasons/reorder`

Request body (JSON):

| Field | Type | Description |
| --- | --- | --- |
| `orderedIds` | string[] | required, min 1 items |

Response: **200** Reordered. `data`: object

- **400** Validation error or business-rule violation
- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

## Webhooks

### List webhook registrations

`GET https://app.erikawork.com/api/webhook-registrations`

Response: **200** Registrations. `data`: WebhookRegistration[]

- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

### Create webhook registration

`POST https://app.erikawork.com/api/webhook-registrations`

The response includes a one-time secret used to sign deliveries — save it now, it can't be retrieved later.

Request body (JSON):

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | required, 1–100 chars |
| `endpointUrl` | string (uri) | required |
| `description` | string | max 500 chars |
| `events` | "candidate.created" \| "candidate.updated" \| "application.created" \| "application.stage_changed" \| "application.qualified" \| "application.unqualified" \| "application.status_changed" \| "screening.completed" \| "screening.failed"[] | required, min 1 items |

Response: **201** Created. `data`: object

- **400** Validation error or business-rule violation
- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

### Update webhook registration

`PATCH https://app.erikawork.com/api/webhook-registrations/{id}`

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Resource ID (UUID) · path, required |

Request body (JSON):

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | 1–100 chars |
| `endpointUrl` | string (uri) |  |
| `description` | string | max 500 chars |
| `events` | "candidate.created" \| "candidate.updated" \| "application.created" \| "application.stage_changed" \| "application.qualified" \| "application.unqualified" \| "application.status_changed" \| "screening.completed" \| "screening.failed"[] | min 1 items |
| `isActive` | boolean |  |

Response: **200** Updated. `data`: WebhookRegistration

- **400** Validation error or business-rule violation
- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

### Delete webhook registration

`DELETE https://app.erikawork.com/api/webhook-registrations/{id}`

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Resource ID (UUID) · path, required |

Response: **200** Deleted. `data`: object

- **400** Validation error or business-rule violation
- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

### List recent deliveries for a webhook

`GET https://app.erikawork.com/api/webhook-registrations/{id}/events`

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `id` | string | Resource ID (UUID) · path, required |
| `limit` | integer | query, ≥ 1, ≤ 200 |

Response: **200** Recent deliveries. `data`: WebhookEvent[]

- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

## Organization

### Get current organization

`GET https://app.erikawork.com/api/organizations/me`

Returns the organization associated with the API key. Note: PATCH /organizations/me is web-only.

Response: **200** Organization details. `data`: Organization

- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

## Credits

### List credit purchases

`GET https://app.erikawork.com/api/v1/credit-purchases`

Chronological history (newest first) of credit top-ups, auto-recharges and subscription plan grants with the amount + currency charged — for billing transparency. Readable with either the VIEW_CAMPAIGN_API or MANAGE_CAMPAIGN_API scope.

Parameters:

| Parameter | Type | Description |
| --- | --- | --- |
| `limit` | string | Page size, 1–100 (default 20). · query |
| `before` | string | ISO timestamp cursor from a previous page's nextCursor — returns purchases strictly older than this. · query |

Response: **200** Credit purchase history. `data`: CreditPurchaseHistory

- **400** Invalid 'before' cursor
- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

## Billing

### Get plan and credit balance

`GET https://app.erikawork.com/api/billing/balance`

Returns the org's active plan and remaining AI credits. Useful for external monitoring or budget alerts.

Response: **200** Current balance. `data`: BillingBalance

- **401** Missing or invalid API key
- **403** API key lacks the permission for this endpoint, or endpoint is web-only
- **500** Internal server error

## Schemas

### Organization

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | required |
| `pid` | integer | required |
| `name` | string | required |
| `slug` | string | required |
| `timezone` | string | required |
| `language` | string | required |
| `dateCreated` | string (date-time) | required |

### ErrorResponse

Error response. Quote the requestId when contacting support.

| Field | Type | Description |
| --- | --- | --- |
| `success` | false | required |
| `error` | string | required |
| `code` | string |  |
| `requestId` | string |  |

### Job

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | required |
| `pid` | integer | required |
| `title` | string | required |
| `externalRef` | string \| null | Your own reference for this position, typically the job ID from your ATS. Free text, up to 100 characters. Every application on this job returns it as `jobExternalRef`, so you can match applications without a lookup. · required, max 100 chars |
| `description` | string \| null | required |
| `status` | "draft" \| "open" \| "paused" \| "closed" \| "archived" | required |
| `hiringManagerId` | string \| null | required |
| `jobDetails` | JobDetails | required |
| `dateCreated` | string (date-time) | required |
| `dateUpdated` | string (date-time) | required |

### JobDetails

| Field | Type | Description |
| --- | --- | --- |
| `location` | string \| null |  |
| `department` | string \| null |  |
| `employmentType` | string \| null |  |
| `remote` | boolean \| null |  |
| `salaryMin` | number \| null |  |
| `salaryMax` | number \| null |  |
| `salaryCurrency` | string \| null |  |

### Candidate

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | required |
| `pid` | integer | required |
| `firstName` | string | required |
| `lastName` | string | required |
| `email` | string (email) \| null | required |
| `phonePrefix` | string \| null | required |
| `phoneNumber` | string \| null | required |
| `linkedinUrl` | string \| null | required |
| `source` | string \| null | required |
| `tags` | string[] | required |
| `dateCreated` | string (date-time) | required |
| `dateUpdated` | string (date-time) | required |

### CandidateDetail

Type: any

### CandidateApplication

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | required |
| `jobId` | string | required |
| `jobTitle` | string | required |
| `jobStatus` | string | required |
| `currentStage` | object | required |
| `status` | string | required |
| `averageRating` | number \| null | required |
| `ratingCount` | integer | required |
| `applicationAnswers` | object[] | Answers the candidate gave on the application form · required |
| `candidateProfile` | CandidateProfile | required |
| `dateCreated` | string (date-time) | required |

### CandidateProfile

Candidate-stated situation facts, collected during screening. Null when the API key lacks VIEW_SCREENING or the candidate stated none of them.

| Field | Type | Description |
| --- | --- | --- |
| `startAvailability` | object \| null | How soon they can start / notice period · required |
| `salaryExpectation` | object \| null | Verbatim, with currency and period as they said it · required |
| `jobSearchStatus` | object \| null | required |
| `bestTimeToCall` | object \| null | required |
| `capturedAt` | string (date-time) \| null | When the facts were captured · required |
| `isStale` | boolean | Captured over 30 days ago — show the date, do not present as current · required |

### CallWindow

| Field | Type | Description |
| --- | --- | --- |
| `days` | integer[] | ISO weekdays, 1 = Monday … 7 = Sunday · required |
| `from` | string | "HH:mm", candidate-local · required |
| `to` | string | "HH:mm", exclusive end · required |

### CallException

| Field | Type | Description |
| --- | --- | --- |
| `date` | string | "YYYY-MM-DD", candidate-local · required |
| `from` | string | required |
| `to` | string | required |

### ApplicationSummary

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | required |
| `candidateId` | string | required |
| `candidateName` | string | required |
| `candidateEmail` | string \| null | required |
| `candidateTags` | string[] | required |
| `jobId` | string | required |
| `jobTitle` | string | required |
| `jobExternalRef` | string \| null | The job's `externalRef`: your own ATS position ID, read live from the job (so it reflects the job's current value, also on older applications). Null when the job has none. · required |
| `stageName` | string | required |
| `stageCategory` | string | required |
| `source` | string | required |

### ApplicationListItem

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | required |
| `pid` | integer | required |
| `candidate` | object | required |
| `jobExternalRef` | string \| null | The job's `externalRef`: your own ATS position ID, read live from the job (so it reflects the job's current value, also on older applications). Null when the job has none. · required |
| `currentStage` | object | required |
| `status` | "active" \| "rejected" \| "withdrawn" \| "hired" | required |
| `source` | string | required |
| `isViewed` | boolean | required |
| `averageRating` | number \| null | required |
| `ratingCount` | integer | required |
| `recentActivities` | object[] | required |
| `candidateProfile` | CandidateProfile | required |
| `dateCreated` | string (date-time) | required |

### Application

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | required |
| `pid` | integer | required |
| `candidate` | object | required |
| `candidateProfile` | CandidateProfile | required |
| `job` | object | required |
| `jobExternalRef` | string \| null | The job's `externalRef`: your own ATS position ID, read live from the job (so it reflects the job's current value, also on older applications). Null when the job has none. · required |
| `currentStage` | object | required |
| `status` | "active" \| "rejected" \| "withdrawn" \| "hired" | required |
| `source` | string | required |
| `firstViewedAt` | string (date-time) \| null | required |
| `firstViewedBy` | string \| null | required |
| `isViewed` | boolean | required |
| `rejectedAt` | string (date-time) \| null | required |
| `rejectionReason` | string \| null | required |
| `activities` | object[] | required |
| `attachments` | object[] | required |
| `ratings` | object[] | required |
| `averageRating` | number \| null | required |
| `ratingCount` | integer | required |
| `applicationAnswers` | object[] | required |
| `answerFit` | map of any \| null \| null | Advisory per-answer requirement labels. Null when not evaluated. · required |
| `qualifier` | object \| null | AI qualifier result for sourced candidates: the position's must-haves checked one by one. It never rejects. Null when not judged (qualifier off, not a sourced candidate, or not run yet). · required |
| `applicationConsents` | object[] | required |
| `dateCreated` | string (date-time) | required |
| `dateUpdated` | string (date-time) | required |

### ApplicationBulkDeleteResult

| Field | Type | Description |
| --- | --- | --- |
| `total` | integer | required |
| `deletedCount` | integer | required |
| `skippedCount` | integer | required |
| `deletedIds` | string[] | required |
| `jobTitle` | string \| null | required |

### Rating

| Field | Type | Description |
| --- | --- | --- |
| `workerId` | string | required |
| `workerName` | string \| null | required |
| `rating` | integer | required, ≥ 1, ≤ 5 |
| `commentKeys` | string[] | required |
| `comment` | string \| null | required |
| `dateCreated` | string (date-time) | required |
| `dateUpdated` | string (date-time) | required |

### Attachment

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | required |
| `fileName` | string | required |
| `fileType` | string \| null | required |
| `contentType` | string | required |
| `size` | integer | required |
| `dateCreated` | string (date-time) | required |

### PipelineTemplate

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | required |
| `name` | string | required |
| `isDefault` | boolean | required |
| `stages` | object[] | required |
| `dateCreated` | string (date-time) | required |

### RejectionReason

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | required |
| `label` | string | required |
| `description` | string \| null | required |
| `isArchived` | boolean | required |
| `orderIndex` | integer | required |

### WebhookRegistration

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | required |
| `name` | string | required |
| `endpointUrl` | string (uri) | required |
| `description` | string \| null | required |
| `events` | "candidate.created" \| "candidate.updated" \| "application.created" \| "application.stage_changed" \| "application.qualified" \| "application.unqualified" \| "application.status_changed" \| "screening.completed" \| "screening.failed"[] | required |
| `isActive` | boolean | required |
| `dateCreated` | string (date-time) | required |

### WebhookEvent

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | required |
| `eventType` | "candidate.created" \| "candidate.updated" \| "application.created" \| "application.stage_changed" \| "application.qualified" \| "application.unqualified" \| "application.status_changed" \| "screening.completed" \| "screening.failed" | Subscribe to one of the documented event names. See the event catalog in the Developers page. · required |
| `status` | "pending" \| "processing" \| "delivered" \| "failed" | required |
| `attempts` | integer | required |
| `statusCode` | integer \| null | required |
| `error` | string \| null | required |
| `deliveredAt` | string (date-time) \| null | required |
| `dateCreated` | string (date-time) | required |

### SourcingCampaign

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | required |
| `title` | string | required |
| `jobId` | string \| null | required |
| `status` | "draft" \| "ready" \| "launching" \| "active" \| "paused" \| "completed" \| "cancelled" | required |
| `finishedOn` | string \| null | Day the campaign ran to its end date (YYYY-MM-DD); null while it can still deliver |
| `archivedAt` | string (date-time) \| null | Set when a user archived (hid) the campaign in the app. Nothing is deleted. |
| `creatives` | object[] |  |
| `dateCreated` | string (date-time) | required |

### SourcingCreative

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | required |
| `semanticName` | string \| null | required |
| `creativeType` | "ugc" \| "street_interview" \| "radio_interview" \| "pov_walkup" \| "studio_backstage" | required |
| `languageCode` | string \| null | required |
| `sourcingCampaignId` | string | required |
| `script` | string \| null | required |
| `prompt` | string \| null | required |
| `files` | map of any \| null[] |  |
| `dateCreated` | string (date-time) | required |

### SourcingAdvert

| Field | Type | Description |
| --- | --- | --- |
| `id` | string | required |
| `creativeId` | string | required |
| `metaAdId` | string \| null | required |
| `status` | "active" \| "paused" \| "archived" | required |
| `dateCreated` | string (date-time) | required |

### CampaignBuildAccepted

| Field | Type | Description |
| --- | --- | --- |
| `buildId` | string | required |
| `status` | string | required |
| `statusUrl` | string | required |
| `webhookSecret` | string | Shown ONCE. Verify the x-webhook-signature (base64 HMAC-SHA256) on the callback with it. |

### CreateCampaignBuild

| Field | Type | Description |
| --- | --- | --- |
| `jobDescription` | string | Description Erika builds a NEW job from. Provide exactly one of jobDescription or jobId. · 50–20000 chars |
| `jobId` | string (uuid) | Reuse an EXISTING Erika job (no new job is created); targeting is seeded from its details. Provide exactly one of jobDescription or jobId. |
| `budget` | number | Total credit envelope. Production (videos + AI) is spent from it; the remainder funds Meta. · required, > 0 |
| `budgetCurrency` | "CREDITS" \| "USD" \| "EUR" | default "CREDITS" |
| `duration` | integer | Campaign run length in days · required, > 0 |
| `city` | string | Work-location city. Inferred from the job description when omitted. · 1–120 chars |
| `countryCode` | string | ISO-3166 alpha-2 (e.g. CZ). Inferred from the job description when omitted. · 2–2 chars |
| `language` | string | Ad + screening language (e.g. cs). Inferred from the job description when omitted. · 2–5 chars |
| `startDate` | string | Scheduled launch day, ISO YYYY-MM-DD. Omitted launches today. A past date is clamped to now. · pattern ^\d{4}-\d{2}-\d{2}$ |
| `callbackUrl` | string (uri) | HTTPS endpoint POSTed a signed result on completion/failure |
| `metadata` | map of string | Caller-defined tracking, echoed back verbatim on the status endpoint and the completion webhook (your job id, client, brand, campaign ref). Opaque to Erika. Up to 20 keys; key <= 40 chars, value <= 500 chars. |
| `idempotencyKey` | string | Required. A retry with the same key returns the same build (no double spend). · required, 8–255 chars |

### CampaignBuildStatus

| Field | Type | Description |
| --- | --- | --- |
| `buildId` | string | required |
| `status` | "queued" \| "running" \| "completed" \| "failed" | required |
| `step` | string | required |
| `campaignId` | string \| null | required |
| `jobId` | string \| null | required |
| `metadata` | map of string | The tracking data you submitted on create, echoed back. Absent if none was sent. |
| `links` | object | required |
| `error` | object |  |
| `dateCreated` | string (date-time) | required |
| `dateUpdated` | string (date-time) | required |

### CreditPurchaseHistory

| Field | Type | Description |
| --- | --- | --- |
| `purchases` | CreditPurchase[] | required |
| `hasMore` | boolean | True when more purchases exist past this page. · required |
| `nextCursor` | string \| null | Pass as `before` to fetch the next page. Null on the last page. · required |

### CreditPurchase

| Field | Type | Description |
| --- | --- | --- |
| `date` | string (date-time) | When the purchase was made. · required |
| `kind` | "manual" \| "auto_recharge" \| "subscription" \| "granted" | manual = one-off top-up you bought; auto_recharge = charged automatically when the balance ran low; subscription = monthly plan credit grant (renewal, initial subscribe, or plan change); granted = credits Erika added to your account at no charge (no amount, no invoice). · required |
| `credits` | integer | Credits granted by this purchase. · required |
| `amount` | integer \| null | Money charged in MINOR units of `currency` (e.g. 425000 = 4,250.00 CZK). Null only for legacy top-ups recorded before the amount was stored. · required |
| `currency` | string \| null | Lowercase ISO currency code (e.g. "czk"), or null if unknown. · required |
| `invoiceId` | string \| null | Stripe invoice id, when one exists. · required |
| `reference` | string \| null | Stripe checkout-session/invoice id or auto-recharge attempt id. · required |

### BillingBalance

| Field | Type | Description |
| --- | --- | --- |
| `plan` | "free" \| "starter" \| "growth" \| "scale" \| "pro" | Active subscription plan · required |
| `creditsRemaining` | integer | required, ≥ 0 |
| `creditsMonthly` | integer \| null | required, ≥ 0 |
| `trialEndsAt` | string (date-time) \| null | required |
