# Account and settings

> Read the key's user, change their settings, check the AI allowance and export their data.

Source: https://workroad.app/docs/api/account

## Settings

`/settings/profile`, `/settings/preferences`, `/settings/notifications`, `/settings/review`, `/settings/tasks` and `/settings/dashboard` read with `GET` and change with `PATCH` (`PUT` for the dashboard), the same as the Settings pages. A `PATCH` changes only the fields you send. Each writes through the code the page saves with, so the limits and errors match the app's.

- Changing `timeZone` moves every task's due date so it keeps its date and time on the clock. A zone the server doesn't know returns 400, and a change made elsewhere during the request returns 409.
- `PATCH /settings/notifications/{kind}` changes one kind's channels and timing. A timing field the kind doesn't use, or a value off its list, returns 400 with `details.field` and the allowed values in `details.reason`.
- `subjectOverrides` on `PATCH /settings/review` replaces the whole list, and every `subjectId` must be the user's.
- `PUT /settings/dashboard` takes the cards to show, in order. Send the `updatedAt` you read as `baseUpdatedAt` to get 409 instead of overwriting a layout saved elsewhere.

## What the API can't change

The API can't change the password, the email address, two-factor, passkeys, sign-in methods, sessions, API keys, connected apps, the plan or payment, and can't delete the account. The theme and the AI model choice are kept in the browser.

## Get the key's user

`GET https://workroad.app/api/v1/me`

Returns the user who owns the API key. Any valid key may call it.

- MCP tool: `getMe` on `https://workroad.app/api/mcp`
- Required scopes: None beyond a valid key.
- Test mode: `GET https://workroad.app/api/test/v1/me` checks the request and returns the example response without changing anything.

### Response

`200` OK

- `data`: object, required
  - `id`: string (uuid), required
  - `name`: string, required
  - `username`: string, required
  - `subscription`: one of `"FREE"`, `"PAID"`, required
  - `premium`: boolean, required
  - `admin`: boolean, required
  - `timeZone`: string or null, required. The user's IANA time zone, such as `Europe/London`. Task due days are read in it; `null` means UTC.
  - `createdAt`: string (date-time), required. Account creation timestamp.

#### Example

```json
{
  "data": {
    "id": "0b7d4c3e-1f2a-4b5c-9d6e-7f8a9b0c1d2e",
    "name": "Ada Lovelace",
    "username": "ada",
    "subscription": "PAID",
    "premium": true,
    "admin": false,
    "timeZone": "Europe/London",
    "createdAt": "2026-01-15T09:30:00.000Z"
  }
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: Invalid request body or query
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No endpoint exists at this path.
- `405`: Method not allowed. Use a method from the Allow header.
- `429`: Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error

## Get the profile

`GET https://workroad.app/api/v1/settings/profile`

The user's name, username, bio and what their public profile shows.

- MCP tool: `getProfile` on `https://workroad.app/api/mcp`
- Required scopes: settings:view
- Test mode: `GET https://workroad.app/api/test/v1/settings/profile` checks the request and returns the example response without changing anything.

### Response

`200` OK

- `data`: object, required
  - `name`: string, required. The display name.
  - `username`: string, required. Lower case letters, digits, `_` and `.`. The public profile lives at `/{username}`.
  - `description`: string, required. The profile bio.
  - `status`: string, required. A short status line shown on the profile.
  - `location`: string or null, required. Free text, or null.
  - `showLastLogin`: boolean, required. The profile shows when they were last on.
  - `showCreated`: boolean, required. The profile shows when they joined.
  - `showTimezone`: boolean, required. The profile shows their time zone.
  - `showMembership`: boolean, required. The profile shows their plan.

#### Example

```json
{
  "data": {
    "name": "Ada Lovelace",
    "username": "ada",
    "description": "Maths and computing, mostly.",
    "status": "Revising for finals",
    "location": "London",
    "showLastLogin": true,
    "showCreated": true,
    "showTimezone": false,
    "showMembership": true
  }
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: Invalid request body or query
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No endpoint exists at this path.
- `405`: Method not allowed. Use a method from the Allow header.
- `429`: Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error

## Update the profile

`PATCH https://workroad.app/api/v1/settings/profile`

Changes the name, username, bio, status, location or what the public profile shows. The sign-in email address can't be changed here.

- MCP tool: `updateProfile` on `https://workroad.app/api/mcp`
- Required scopes: settings:update
- Test mode: `PATCH https://workroad.app/api/test/v1/settings/profile` checks the request and returns the example response without changing anything.

### Request body

JSON.

- `name`: string, 2-100 characters. 2 to 100 characters.
- `username`: string, 3-30 characters, matches `^[a-zA-Z0-9_.]+$`. 3 to 30 letters, digits, `_` or `.`, stored in lower case. Fails with 409 when another account has it.
- `description`: string, 0-512 characters. Up to 512 characters. Send an empty string to clear it.
- `status`: string, 0-256 characters. Up to 256 characters. Send an empty string to clear it.
- `location`: string, 0-256 characters. Up to 256 characters. Send an empty string to clear it.
- `showLastLogin`: boolean
- `showCreated`: boolean
- `showTimezone`: boolean
- `showMembership`: boolean

### Response

`200` OK

- `data`: object, required
  - `name`: string, required. The display name.
  - `username`: string, required. Lower case letters, digits, `_` and `.`. The public profile lives at `/{username}`.
  - `description`: string, required. The profile bio.
  - `status`: string, required. A short status line shown on the profile.
  - `location`: string or null, required. Free text, or null.
  - `showLastLogin`: boolean, required. The profile shows when they were last on.
  - `showCreated`: boolean, required. The profile shows when they joined.
  - `showTimezone`: boolean, required. The profile shows their time zone.
  - `showMembership`: boolean, required. The profile shows their plan.

#### Example

```json
{
  "data": {
    "name": "Ada Lovelace",
    "username": "ada",
    "description": "Maths and computing, mostly.",
    "status": "Revising for finals",
    "location": "London",
    "showLastLogin": true,
    "showCreated": true,
    "showTimezone": false,
    "showMembership": true
  }
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: Invalid body or no field to update.
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No endpoint exists at this path.
- `405`: Method not allowed. Use a method from the Allow header.
- `409`: Another account already has that username.
- `429`: Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error

## Get language, time zone and privacy

`GET https://workroad.app/api/v1/settings/preferences`

The app language, the account's time zone and leaderboard visibility.

- MCP tool: `getPreferences` on `https://workroad.app/api/mcp`
- Required scopes: settings:view
- Test mode: `GET https://workroad.app/api/test/v1/settings/preferences` checks the request and returns the example response without changing anything.

### Response

`200` OK

- `data`: object, required
  - `locale`: one of `"en"`, `"de"`, `"es"`, `"fr"`, `"it"` or null, required. The language of the app, emails, notifications and calendar feeds. Null until the user picks one; the app then follows the browser.
  - `timeZone`: string or null, required. The IANA time zone the account's days and due dates use, such as `Europe/London`. Null until the app has captured one; the API then uses UTC.
  - `showOnLeaderboards`: boolean, required. Whether the user appears on the global task and streak leaderboards and shows study stats on their profile.

#### Example

```json
{
  "data": {
    "locale": "en",
    "timeZone": "Europe/London",
    "showOnLeaderboards": true
  }
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: Invalid request body or query
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No endpoint exists at this path.
- `405`: Method not allowed. Use a method from the Allow header.
- `429`: Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error

## Update language, time zone or privacy

`PATCH https://workroad.app/api/v1/settings/preferences`

Changing the time zone moves every task's due date so it keeps its date and time on the clock, the same as changing it in Settings → Account.

- MCP tool: `updatePreferences` on `https://workroad.app/api/mcp`
- Required scopes: settings:update
- Test mode: `PATCH https://workroad.app/api/test/v1/settings/preferences` checks the request and returns the example response without changing anything.

### Request body

JSON.

- `locale`: one of `"en"`, `"de"`, `"es"`, `"fr"`, `"it"`. One of the five app languages.
- `timeZone`: string, 1-64 characters. An IANA zone name. Aliases such as `Asia/Calcutta` are stored by their canonical name. Every task keeps its due date and time on the clock, so a task due at 09:00 is still due at 09:00 in the new zone.
- `showOnLeaderboards`: boolean

### Response

`200` OK

- `data`: object, required
  - `locale`: one of `"en"`, `"de"`, `"es"`, `"fr"`, `"it"` or null, required. The language of the app, emails, notifications and calendar feeds. Null until the user picks one; the app then follows the browser.
  - `timeZone`: string or null, required. The IANA time zone the account's days and due dates use, such as `Europe/London`. Null until the app has captured one; the API then uses UTC.
  - `showOnLeaderboards`: boolean, required. Whether the user appears on the global task and streak leaderboards and shows study stats on their profile.

#### Example

```json
{
  "data": {
    "locale": "en",
    "timeZone": "Europe/London",
    "showOnLeaderboards": true
  }
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: Invalid body, no field to update, or an unknown zone.
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No endpoint exists at this path.
- `405`: Method not allowed. Use a method from the Allow header.
- `409`: The time zone changed elsewhere during the request. Read it again.
- `429`: Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error

## Get notification settings

`GET https://workroad.app/api/v1/settings/notifications`

Quiet hours, the email digest, push content, calendar alarms, and each notification kind's channels and timing.

- MCP tool: `getNotificationSettings` on `https://workroad.app/api/mcp`
- Required scopes: settings:view
- Test mode: `GET https://workroad.app/api/test/v1/settings/notifications` checks the request and returns the example response without changing anything.

### Response

`200` OK

- `data`: object, required
  - `quietHoursEnabled`: boolean, required. Push and email wait until quiet hours end.
  - `quietStart`: string, required. Quiet hours start, as local HH:MM in the account's zone.
  - `quietEnd`: string, required. Quiet hours end, as local HH:MM in the account's zone.
  - `pushShowContent`: boolean, required. Push messages name the task, exam or subject. Off sends a generic line instead.
  - `calendarAlarms`: boolean, required. The all-schedules calendar feed adds alarms to task and exam events.
  - `digestFrequency`: one of `"OFF"`, `"DAILY"`, `"WEEKLY"`, required
  - `digestTime`: string, required. When the digest is sent, as local HH:MM in the account's zone.
  - `digestWeekday`: integer, required. The weekly digest's day, 0 for Sunday to 6 for Saturday.
  - `emailSuppressedAt`: string (date-time) or null, required. Set after an email bounced or was reported as spam. Notification email stays off until `resumeNotificationEmail`.
  - `emailSuppressedReason`: one of `"BOUNCE"`, `"COMPLAINT"` or null, required. Why email was suppressed, or null.
  - `emailVerified`: boolean, required. No notification email is sent until the account's address is verified.
  - `kinds`: array of objects, required. Every notification kind's channels and timing, defaults included.
    - `kind`: one of `"TASK_DUE"`, `"EXAM_SOON"`, `"STREAK_AT_RISK"`, `"LESSON_STARTING"`, `"SCHEDULE_SHARE_CHANGED"`, `"GENERATION_FINISHED"`, `"RECORDING_READY"`, `"DIGEST"`, required. `TASK_DUE` before a task is due, `EXAM_SOON` before an exam, `STREAK_AT_RISK` when today's goal isn't met, `LESSON_STARTING` before a lesson, `SCHEDULE_SHARE_CHANGED` when someone shares a timetable or stops, `GENERATION_FINISHED` and `RECORDING_READY` when AI work finishes, `DIGEST` for the email digest.
    - `inApp`: boolean, required. Shown in the bell and the notifications page.
    - `email`: boolean, required. Sent by email once the address is verified.
    - `push`: boolean, required. Sent as a push to every browser the user enabled.
    - `leadMinutes`: integer or null, required. Minutes before a timed task is due (`TASK_DUE`) or a lesson starts (`LESSON_STARTING`). Null for a kind without it.
    - `leadDays`: array of integer or null, required. Days before an all-day task is due (`TASK_DUE`) or an exam (`EXAM_SOON`). Null for a kind without it.
    - `localTime`: string or null, required. When all-day task, exam and streak reminders go out, as local HH:MM. Null for a kind without it.

#### Example

```json
{
  "data": {
    "quietHoursEnabled": true,
    "quietStart": "22:00",
    "quietEnd": "07:00",
    "pushShowContent": false,
    "calendarAlarms": false,
    "digestFrequency": "WEEKLY",
    "digestTime": "07:30",
    "digestWeekday": 1,
    "emailSuppressedAt": null,
    "emailSuppressedReason": null,
    "emailVerified": true,
    "kinds": [
      {
        "kind": "TASK_DUE",
        "inApp": true,
        "email": false,
        "push": true,
        "leadMinutes": 60,
        "leadDays": [
          1
        ],
        "localTime": "18:00"
      },
      {
        "kind": "EXAM_SOON",
        "inApp": true,
        "email": true,
        "push": true,
        "leadMinutes": null,
        "leadDays": [
          3,
          1
        ],
        "localTime": "18:00"
      }
    ]
  }
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: Invalid request body or query
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No endpoint exists at this path.
- `405`: Method not allowed. Use a method from the Allow header.
- `429`: Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error

## Update notification settings

`PATCH https://workroad.app/api/v1/settings/notifications`

Changes quiet hours, the digest, push content or calendar alarms. Change one notification kind with `updateNotificationKind`.

- MCP tool: `updateNotificationSettings` on `https://workroad.app/api/mcp`
- Required scopes: settings:update
- Test mode: `PATCH https://workroad.app/api/test/v1/settings/notifications` checks the request and returns the example response without changing anything.

### Request body

JSON.

- `quietHoursEnabled`: boolean
- `quietStart`: string. HH:MM, such as 22:00.
- `quietEnd`: string. HH:MM, such as 07:00.
- `pushShowContent`: boolean
- `calendarAlarms`: boolean
- `digestFrequency`: one of `"OFF"`, `"DAILY"`, `"WEEKLY"`
- `digestTime`: string. HH:MM, such as 07:30.
- `digestWeekday`: integer, 0 to 6. 0 for Sunday to 6 for Saturday.

### Response

`200` OK

- `data`: object, required
  - `quietHoursEnabled`: boolean, required. Push and email wait until quiet hours end.
  - `quietStart`: string, required. Quiet hours start, as local HH:MM in the account's zone.
  - `quietEnd`: string, required. Quiet hours end, as local HH:MM in the account's zone.
  - `pushShowContent`: boolean, required. Push messages name the task, exam or subject. Off sends a generic line instead.
  - `calendarAlarms`: boolean, required. The all-schedules calendar feed adds alarms to task and exam events.
  - `digestFrequency`: one of `"OFF"`, `"DAILY"`, `"WEEKLY"`, required
  - `digestTime`: string, required. When the digest is sent, as local HH:MM in the account's zone.
  - `digestWeekday`: integer, required. The weekly digest's day, 0 for Sunday to 6 for Saturday.
  - `emailSuppressedAt`: string (date-time) or null, required. Set after an email bounced or was reported as spam. Notification email stays off until `resumeNotificationEmail`.
  - `emailSuppressedReason`: one of `"BOUNCE"`, `"COMPLAINT"` or null, required. Why email was suppressed, or null.
  - `emailVerified`: boolean, required. No notification email is sent until the account's address is verified.
  - `kinds`: array of objects, required. Every notification kind's channels and timing, defaults included.
    - `kind`: one of `"TASK_DUE"`, `"EXAM_SOON"`, `"STREAK_AT_RISK"`, `"LESSON_STARTING"`, `"SCHEDULE_SHARE_CHANGED"`, `"GENERATION_FINISHED"`, `"RECORDING_READY"`, `"DIGEST"`, required. `TASK_DUE` before a task is due, `EXAM_SOON` before an exam, `STREAK_AT_RISK` when today's goal isn't met, `LESSON_STARTING` before a lesson, `SCHEDULE_SHARE_CHANGED` when someone shares a timetable or stops, `GENERATION_FINISHED` and `RECORDING_READY` when AI work finishes, `DIGEST` for the email digest.
    - `inApp`: boolean, required. Shown in the bell and the notifications page.
    - `email`: boolean, required. Sent by email once the address is verified.
    - `push`: boolean, required. Sent as a push to every browser the user enabled.
    - `leadMinutes`: integer or null, required. Minutes before a timed task is due (`TASK_DUE`) or a lesson starts (`LESSON_STARTING`). Null for a kind without it.
    - `leadDays`: array of integer or null, required. Days before an all-day task is due (`TASK_DUE`) or an exam (`EXAM_SOON`). Null for a kind without it.
    - `localTime`: string or null, required. When all-day task, exam and streak reminders go out, as local HH:MM. Null for a kind without it.

#### Example

```json
{
  "data": {
    "quietHoursEnabled": true,
    "quietStart": "22:00",
    "quietEnd": "07:00",
    "pushShowContent": false,
    "calendarAlarms": false,
    "digestFrequency": "WEEKLY",
    "digestTime": "07:30",
    "digestWeekday": 1,
    "emailSuppressedAt": null,
    "emailSuppressedReason": null,
    "emailVerified": true,
    "kinds": [
      {
        "kind": "TASK_DUE",
        "inApp": true,
        "email": false,
        "push": true,
        "leadMinutes": 60,
        "leadDays": [
          1
        ],
        "localTime": "18:00"
      },
      {
        "kind": "EXAM_SOON",
        "inApp": true,
        "email": true,
        "push": true,
        "leadMinutes": null,
        "leadDays": [
          3,
          1
        ],
        "localTime": "18:00"
      }
    ]
  }
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: Invalid body or no field to update.
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No endpoint exists at this path.
- `405`: Method not allowed. Use a method from the Allow header.
- `429`: Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error

## Update one notification kind

`PATCH https://workroad.app/api/v1/settings/notifications/{kind}`

Turns a kind's bell, email or push on or off, or changes when it is sent. Returns the kind as it now resolves.

- MCP tool: `updateNotificationKind` on `https://workroad.app/api/mcp`
- Required scopes: settings:update
- Test mode: `PATCH https://workroad.app/api/test/v1/settings/notifications/{kind}` checks the request and returns the example response without changing anything.

### Path parameters

- `kind`: one of `"TASK_DUE"`, `"EXAM_SOON"`, `"STREAK_AT_RISK"`, `"LESSON_STARTING"`, `"SCHEDULE_SHARE_CHANGED"`, `"GENERATION_FINISHED"`, `"RECORDING_READY"`, `"DIGEST"`, required. `TASK_DUE` before a task is due, `EXAM_SOON` before an exam, `STREAK_AT_RISK` when today's goal isn't met, `LESSON_STARTING` before a lesson, `SCHEDULE_SHARE_CHANGED` when someone shares a timetable or stops, `GENERATION_FINISHED` and `RECORDING_READY` when AI work finishes, `DIGEST` for the email digest.

### Request body

JSON.

- `inApp`: boolean
- `email`: boolean
- `push`: boolean
- `leadMinutes`: integer or null. `TASK_DUE`: 15, 60, 180, 1440. `LESSON_STARTING`: 5, 10, 15, 30. Null restores the default.
- `leadDays`: integer or null. `TASK_DUE` (all-day tasks): 0, 1, 2. `EXAM_SOON`: 1, 2, 3, 5, 7, 14; one value replaces the default of 3 days and 1 day before. Null restores the default.
- `localTime`: string or null. HH:MM for `TASK_DUE` (all-day tasks), `EXAM_SOON` and `STREAK_AT_RISK`. Null restores the default.

### Response

`200` OK

- `data`: object, required
  - `kind`: one of `"TASK_DUE"`, `"EXAM_SOON"`, `"STREAK_AT_RISK"`, `"LESSON_STARTING"`, `"SCHEDULE_SHARE_CHANGED"`, `"GENERATION_FINISHED"`, `"RECORDING_READY"`, `"DIGEST"`, required. `TASK_DUE` before a task is due, `EXAM_SOON` before an exam, `STREAK_AT_RISK` when today's goal isn't met, `LESSON_STARTING` before a lesson, `SCHEDULE_SHARE_CHANGED` when someone shares a timetable or stops, `GENERATION_FINISHED` and `RECORDING_READY` when AI work finishes, `DIGEST` for the email digest.
  - `inApp`: boolean, required. Shown in the bell and the notifications page.
  - `email`: boolean, required. Sent by email once the address is verified.
  - `push`: boolean, required. Sent as a push to every browser the user enabled.
  - `leadMinutes`: integer or null, required. Minutes before a timed task is due (`TASK_DUE`) or a lesson starts (`LESSON_STARTING`). Null for a kind without it.
  - `leadDays`: array of integer or null, required. Days before an all-day task is due (`TASK_DUE`) or an exam (`EXAM_SOON`). Null for a kind without it.
  - `localTime`: string or null, required. When all-day task, exam and streak reminders go out, as local HH:MM. Null for a kind without it.

#### Example

```json
{
  "data": {
    "kind": "TASK_DUE",
    "inApp": true,
    "email": false,
    "push": true,
    "leadMinutes": 60,
    "leadDays": [
      1
    ],
    "localTime": "18:00"
  }
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: Invalid body, no field to update, or a timing the kind doesn't use or allow. `details.field` names it and `details.reason` lists the allowed values.
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No resource with that id exists for this key's user
- `405`: Method not allowed. Use a method from the Allow header.
- `429`: Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error

## Resume notification email

`POST https://workroad.app/api/v1/settings/notifications/resume-email`

Turns notification email back on after it was suppressed because a message bounced or was reported as spam. Fix the address first, or the next email suppresses it again.

- MCP tool: `resumeNotificationEmail` on `https://workroad.app/api/mcp`
- Required scopes: settings:update
- Test mode: `POST https://workroad.app/api/test/v1/settings/notifications/resume-email` checks the request and returns the example response without changing anything.

### Headers

- `Idempotency-Key`: string, optional, 1-255 visible ASCII characters. Makes the POST safe to retry: a retry with the same key and body replays the first response, and the same key with a different body returns 409 `idempotency_conflict`. Kept for 24 hours. Over MCP it is the `idempotencyKey` argument.

### Response

`200` OK

- `data`: object, required
  - `quietHoursEnabled`: boolean, required. Push and email wait until quiet hours end.
  - `quietStart`: string, required. Quiet hours start, as local HH:MM in the account's zone.
  - `quietEnd`: string, required. Quiet hours end, as local HH:MM in the account's zone.
  - `pushShowContent`: boolean, required. Push messages name the task, exam or subject. Off sends a generic line instead.
  - `calendarAlarms`: boolean, required. The all-schedules calendar feed adds alarms to task and exam events.
  - `digestFrequency`: one of `"OFF"`, `"DAILY"`, `"WEEKLY"`, required
  - `digestTime`: string, required. When the digest is sent, as local HH:MM in the account's zone.
  - `digestWeekday`: integer, required. The weekly digest's day, 0 for Sunday to 6 for Saturday.
  - `emailSuppressedAt`: string (date-time) or null, required. Set after an email bounced or was reported as spam. Notification email stays off until `resumeNotificationEmail`.
  - `emailSuppressedReason`: one of `"BOUNCE"`, `"COMPLAINT"` or null, required. Why email was suppressed, or null.
  - `emailVerified`: boolean, required. No notification email is sent until the account's address is verified.
  - `kinds`: array of objects, required. Every notification kind's channels and timing, defaults included.
    - `kind`: one of `"TASK_DUE"`, `"EXAM_SOON"`, `"STREAK_AT_RISK"`, `"LESSON_STARTING"`, `"SCHEDULE_SHARE_CHANGED"`, `"GENERATION_FINISHED"`, `"RECORDING_READY"`, `"DIGEST"`, required. `TASK_DUE` before a task is due, `EXAM_SOON` before an exam, `STREAK_AT_RISK` when today's goal isn't met, `LESSON_STARTING` before a lesson, `SCHEDULE_SHARE_CHANGED` when someone shares a timetable or stops, `GENERATION_FINISHED` and `RECORDING_READY` when AI work finishes, `DIGEST` for the email digest.
    - `inApp`: boolean, required. Shown in the bell and the notifications page.
    - `email`: boolean, required. Sent by email once the address is verified.
    - `push`: boolean, required. Sent as a push to every browser the user enabled.
    - `leadMinutes`: integer or null, required. Minutes before a timed task is due (`TASK_DUE`) or a lesson starts (`LESSON_STARTING`). Null for a kind without it.
    - `leadDays`: array of integer or null, required. Days before an all-day task is due (`TASK_DUE`) or an exam (`EXAM_SOON`). Null for a kind without it.
    - `localTime`: string or null, required. When all-day task, exam and streak reminders go out, as local HH:MM. Null for a kind without it.

#### Example

```json
{
  "data": {
    "quietHoursEnabled": true,
    "quietStart": "22:00",
    "quietEnd": "07:00",
    "pushShowContent": false,
    "calendarAlarms": false,
    "digestFrequency": "WEEKLY",
    "digestTime": "07:30",
    "digestWeekday": 1,
    "emailSuppressedAt": null,
    "emailSuppressedReason": null,
    "emailVerified": true,
    "kinds": [
      {
        "kind": "TASK_DUE",
        "inApp": true,
        "email": false,
        "push": true,
        "leadMinutes": 60,
        "leadDays": [
          1
        ],
        "localTime": "18:00"
      },
      {
        "kind": "EXAM_SOON",
        "inApp": true,
        "email": true,
        "push": true,
        "leadMinutes": null,
        "leadDays": [
          3,
          1
        ],
        "localTime": "18:00"
      }
    ]
  }
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: Invalid request body or query
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No endpoint exists at this path.
- `405`: Method not allowed. Use a method from the Allow header.
- `409`: The Idempotency-Key was reused with a different request body, or a request with the same key is still in progress.
- `429`: Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error

## Get review settings

`GET https://workroad.app/api/v1/settings/review`

Daily new-card and review limits, FSRS desired retention, the daily goal, leech handling and per-subject overrides.

- MCP tool: `getReviewSettings` on `https://workroad.app/api/mcp`
- Required scopes: settings:view
- Test mode: `GET https://workroad.app/api/test/v1/settings/review` checks the request and returns the example response without changing anything.

### Response

`200` OK

- `data`: object, required
  - `newPerDay`: integer, required. New cards introduced a day.
  - `reviewsPerDay`: integer or null, required. Reviews a day. Null: no limit.
  - `retention`: number, required. FSRS desired retention, the chance of recalling a card when it comes due, from 0.7 to 0.99.
  - `goalKind`: one of `"cards"`, `"minutes"`, required. Whether the daily goal counts cards reviewed or minutes studied.
  - `goalTarget`: integer, required. The daily goal, in `goalKind` units.
  - `leechThreshold`: integer, required. Lapses after which a card is marked a leech.
  - `suspendLeeches`: boolean, required. Leeches are suspended automatically.
  - `subjectOverrides`: array of objects, required. Limits and retention that differ for one subject.
    - `subjectId`: string (uuid), required
    - `newPerDay`: integer or null, required. New cards a day for this subject, within the global limit. Null: no limit.
    - `reviewsPerDay`: integer or null, required. Reviews a day for this subject. Null: no limit.
    - `retention`: number or null, required. Desired retention for this subject. Null: the global value.
  - `optimizedAt`: string (date-time) or null, required. When the FSRS optimizer last fitted this user's weights, or null.

#### Example

```json
{
  "data": {
    "newPerDay": 20,
    "reviewsPerDay": null,
    "retention": 0.9,
    "goalKind": "cards",
    "goalTarget": 30,
    "leechThreshold": 8,
    "suspendLeeches": true,
    "subjectOverrides": [
      {
        "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f",
        "newPerDay": 10,
        "reviewsPerDay": null,
        "retention": 0.95
      }
    ],
    "optimizedAt": null
  }
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: Invalid request body or query
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No endpoint exists at this path.
- `405`: Method not allowed. Use a method from the Allow header.
- `429`: Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error

## Update review settings

`PATCH https://workroad.app/api/v1/settings/review`

Changes the limits, retention, daily goal or leech handling the review queue and practice sessions use.

- MCP tool: `updateReviewSettings` on `https://workroad.app/api/mcp`
- Required scopes: settings:update
- Test mode: `PATCH https://workroad.app/api/test/v1/settings/review` checks the request and returns the example response without changing anything.

### Request body

JSON.

- `newPerDay`: integer, 0 to 2000. 0 to 2000.
- `reviewsPerDay`: integer or null, 0 to 10000. 0 to 10000, or null for no limit.
- `retention`: number, 0.7 to 0.99. 0.7 to 0.99. Higher means more reviews and fewer forgotten cards.
- `goalKind`: one of `"cards"`, `"minutes"`
- `goalTarget`: integer, 1 to 2000. 1 to 2000.
- `leechThreshold`: integer, 2 to 100. 2 to 100.
- `suspendLeeches`: boolean
- `subjectOverrides`: array of objects, up to 200 items. Replaces every override when sent. Send an empty list to remove them all.
  - `subjectId`: string (uuid), required. One of the user's subjects.
  - `newPerDay`: integer, 0 to 2000
  - `reviewsPerDay`: integer or null, 0 to 10000
  - `retention`: number, 0.7 to 0.99

### Response

`200` OK

- `data`: object, required
  - `newPerDay`: integer, required. New cards introduced a day.
  - `reviewsPerDay`: integer or null, required. Reviews a day. Null: no limit.
  - `retention`: number, required. FSRS desired retention, the chance of recalling a card when it comes due, from 0.7 to 0.99.
  - `goalKind`: one of `"cards"`, `"minutes"`, required. Whether the daily goal counts cards reviewed or minutes studied.
  - `goalTarget`: integer, required. The daily goal, in `goalKind` units.
  - `leechThreshold`: integer, required. Lapses after which a card is marked a leech.
  - `suspendLeeches`: boolean, required. Leeches are suspended automatically.
  - `subjectOverrides`: array of objects, required. Limits and retention that differ for one subject.
    - `subjectId`: string (uuid), required
    - `newPerDay`: integer or null, required. New cards a day for this subject, within the global limit. Null: no limit.
    - `reviewsPerDay`: integer or null, required. Reviews a day for this subject. Null: no limit.
    - `retention`: number or null, required. Desired retention for this subject. Null: the global value.
  - `optimizedAt`: string (date-time) or null, required. When the FSRS optimizer last fitted this user's weights, or null.

#### Example

```json
{
  "data": {
    "newPerDay": 20,
    "reviewsPerDay": null,
    "retention": 0.9,
    "goalKind": "cards",
    "goalTarget": 30,
    "leechThreshold": 8,
    "suspendLeeches": true,
    "subjectOverrides": [
      {
        "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f",
        "newPerDay": 10,
        "reviewsPerDay": null,
        "retention": 0.95
      }
    ],
    "optimizedAt": null
  }
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: Invalid body, no field to update, or an override names a subject the user doesn't own.
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No endpoint exists at this path.
- `405`: Method not allowed. Use a method from the Allow header.
- `429`: Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error

## Get task preferences

`GET https://workroad.app/api/v1/settings/tasks`

The task types the app offers and the colours of overdue and due tasks.

- MCP tool: `getTaskSettings` on `https://workroad.app/api/mcp`
- Required scopes: settings:view
- Test mode: `GET https://workroad.app/api/test/v1/settings/tasks` checks the request and returns the example response without changing anything.

### Response

`200` OK

- `data`: object, required
  - `types`: array of string, required. The task types, in the order the app offers them.
  - `overdueColor`: string, required. The colour of an overdue task.
  - `dueTodayColor`: string, required. The colour of a task due today.
  - `nearlyDueColor`: string, required. The colour of a task due soon.

#### Example

```json
{
  "data": {
    "types": [
      "Homework",
      "Test",
      "Study",
      "Project"
    ],
    "overdueColor": "#ef4444",
    "dueTodayColor": "#f97316",
    "nearlyDueColor": "#eab308"
  }
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: Invalid request body or query
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No endpoint exists at this path.
- `405`: Method not allowed. Use a method from the Allow header.
- `429`: Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error

## Update task preferences

`PATCH https://workroad.app/api/v1/settings/tasks`

Replaces the task types or changes the due-date colours.

- MCP tool: `updateTaskSettings` on `https://workroad.app/api/mcp`
- Required scopes: settings:update
- Test mode: `PATCH https://workroad.app/api/test/v1/settings/tasks` checks the request and returns the example response without changing anything.

### Request body

JSON.

- `types`: array of string, 1-50 items. Replaces the whole list, in order. Tasks keep their type when it is removed here.
- `overdueColor`: string, matches `^#[0-9A-F]{6}$`. Overdue tasks, as #RRGGBB.
- `dueTodayColor`: string, matches `^#[0-9A-F]{6}$`. Tasks due today, as #RRGGBB.
- `nearlyDueColor`: string, matches `^#[0-9A-F]{6}$`. Tasks due soon, as #RRGGBB.

### Response

`200` OK

- `data`: object, required
  - `types`: array of string, required. The task types, in the order the app offers them.
  - `overdueColor`: string, required. The colour of an overdue task.
  - `dueTodayColor`: string, required. The colour of a task due today.
  - `nearlyDueColor`: string, required. The colour of a task due soon.

#### Example

```json
{
  "data": {
    "types": [
      "Homework",
      "Test",
      "Study",
      "Project"
    ],
    "overdueColor": "#ef4444",
    "dueTodayColor": "#f97316",
    "nearlyDueColor": "#eab308"
  }
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: Invalid body or no field to update.
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No endpoint exists at this path.
- `405`: Method not allowed. Use a method from the Allow header.
- `429`: Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error

## Get the dashboard layout

`GET https://workroad.app/api/v1/settings/dashboard`

The dashboard cards shown, in order, and their sizes.

- MCP tool: `getDashboardLayout` on `https://workroad.app/api/mcp`
- Required scopes: settings:view
- Test mode: `GET https://workroad.app/api/test/v1/settings/dashboard` checks the request and returns the example response without changing anything.

### Response

`200` OK

- `data`: object, required
  - `widgets`: array of objects, required. The cards shown, in order. A card left out is hidden.
    - `id`: one of `"reviews"`, `"schedule"`, `"tasks"`, `"exams"`, `"copilot"`, `"continue-studying"`, `"streaks"`, `"contribution"`, `"generation-activity"`, required
    - `size`: one of `"single"`, `"wide"`, required
  - `updatedAt`: string (date-time) or null, required. When the layout was last saved, or null while the default is shown.

#### Example

```json
{
  "data": {
    "widgets": [
      {
        "id": "reviews",
        "size": "single"
      },
      {
        "id": "schedule",
        "size": "wide"
      },
      {
        "id": "tasks",
        "size": "single"
      }
    ],
    "updatedAt": "2026-02-01T12:00:00.000Z"
  }
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: Invalid request body or query
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No endpoint exists at this path.
- `405`: Method not allowed. Use a method from the Allow header.
- `429`: Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error

## Replace the dashboard layout

`PUT https://workroad.app/api/v1/settings/dashboard`

Sets which cards the dashboard shows, in order, and their sizes. A card listed twice is shown once.

- MCP tool: `replaceDashboardLayout` on `https://workroad.app/api/mcp`
- Required scopes: settings:update
- Test mode: `PUT https://workroad.app/api/test/v1/settings/dashboard` checks the request and returns the example response without changing anything.

### Request body

JSON.

- `widgets`: array of objects, required, up to 9 items. Every card to show, in order. Leave a card out to hide it.
  - `id`: one of `"reviews"`, `"schedule"`, `"tasks"`, `"exams"`, `"copilot"`, `"continue-studying"`, `"streaks"`, `"contribution"`, `"generation-activity"`, required
  - `size`: one of `"single"`, `"wide"`, required. `wide` spans two columns.
- `baseUpdatedAt`: string (date-time) or null. The `updatedAt` you read. When sent, a layout saved elsewhere since then fails with 409; null means no layout was saved yet. Leave it out to replace whatever is saved.

### Response

`200` OK

- `data`: object, required
  - `widgets`: array of objects, required. The cards shown, in order. A card left out is hidden.
    - `id`: one of `"reviews"`, `"schedule"`, `"tasks"`, `"exams"`, `"copilot"`, `"continue-studying"`, `"streaks"`, `"contribution"`, `"generation-activity"`, required
    - `size`: one of `"single"`, `"wide"`, required
  - `updatedAt`: string (date-time) or null, required. When the layout was last saved, or null while the default is shown.

#### Example

```json
{
  "data": {
    "widgets": [
      {
        "id": "reviews",
        "size": "single"
      },
      {
        "id": "schedule",
        "size": "wide"
      },
      {
        "id": "tasks",
        "size": "single"
      }
    ],
    "updatedAt": "2026-02-01T12:00:00.000Z"
  }
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: Invalid body.
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No endpoint exists at this path.
- `405`: Method not allowed. Use a method from the Allow header.
- `409`: The layout was saved elsewhere after `baseUpdatedAt`. Read it again and resend.
- `429`: Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error

## Get the AI allowance

`GET https://workroad.app/api/v1/ai-allowance`

This month's AI use and the credit balance for the key's user, the same numbers the app shows. The free plan allows 3 AI uses per UTC calendar month. The paid plan has no count limit but allows 100 recordings a month under fair use. Once those are spent, each AI use spends one credit while any are left. Any valid key may call it.

- MCP tool: `getAiAllowance` on `https://workroad.app/api/mcp`
- Required scopes: None beyond a valid key.
- Test mode: `GET https://workroad.app/api/test/v1/ai-allowance` checks the request and returns the example response without changing anything.

### Response

`200` OK

- `data`: object, required
  - `plan`: one of `"free"`, `"paid"`, required. `paid` for a subscription or an admin account.
  - `used`: integer, required. AI uses this month that the monthly allowance paid for, including ones still running. A use that produced nothing is not counted. Uses paid with credits are in `creditUses`.
  - `creditUses`: integer, required. AI uses this month that credits paid for, because the allowance (or, on the paid plan, the fair-use recordings) was spent. Counted like `used`. `used` plus `creditUses` is every AI use this month.
  - `limit`: integer or null, required. AI uses allowed per month. Null on the paid plan, which has no count limit.
  - `recordings`: object, required. This month's recordings.
    - `used`: integer, required. Recordings this month that the allowance paid for, including ones still running. Recordings that credits paid for count in `creditUses` instead.
    - `limit`: integer or null, required. Recordings allowed per month: 100 on the paid plan, under fair use. Null for an admin account, which has no cap, and on the free plan, where each recording counts toward `limit` instead.
  - `resetsAt`: string (date-time), required. When the count starts again: the first of next month, UTC.
  - `credits`: object, required. AI credits, bought in packs or granted. Buying them is only possible in the app.
    - `balance`: integer, required. Credits left to spend. Each pays for one AI use once this month's uses (or the paid plan's fair-use recordings) are spent. They never expire.

#### Example

```json
{
  "data": {
    "plan": "free",
    "used": 1,
    "creditUses": 0,
    "limit": 3,
    "recordings": {
      "used": 1,
      "limit": null
    },
    "resetsAt": "2026-02-01T00:00:00.000Z",
    "credits": {
      "balance": 7
    }
  }
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: Invalid request body or query
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No endpoint exists at this path.
- `405`: Method not allowed. Use a method from the Allow header.
- `429`: Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error

## Get the account export

`GET https://workroad.app/api/v1/export`

The latest account export: everything the user's account holds in one ZIP (notes as Markdown with their images and files, recordings, study sets with their review history, tasks, schedules, tests, Copilot chats and settings as JSON). Start one with `POST /export`. `data` is null when none was started in the last 7 days.

- MCP tool: `getExport` on `https://workroad.app/api/mcp`
- Required scopes: account:export
- Test mode: `GET https://workroad.app/api/test/v1/export` checks the request and returns the example response without changing anything.
- Next: when `status` is `pending` or `running`, call it again in a minute.

### Response

`200` OK

- `data`: object or null, required
  - `id`: string (uuid), required. The export's id.
  - `status`: one of `"pending"`, `"running"`, `"ready"`, `"failed"`, `"expired"`, required. `pending` and `running` while it is built, `ready` while it can be downloaded, `expired` once its download window has passed, `failed` if it could not be built.
  - `createdAt`: string (date-time), required. When the export was requested.
  - `downloadUrl`: string or null, required. When `status` is `ready`: a URL that downloads the ZIP for the next 5 minutes, without credentials. Ask again for a fresh one. Null otherwise.
  - `expiresAt`: string (date-time) or null, required. When `status` is `ready`: when the export stops being available. Null otherwise.
  - `sizeBytes`: integer or null, required. The ZIP's size when `status` is `ready`, else null.

#### Example

```json
{
  "data": {
    "id": "4f1c2b3a-5d6e-4f70-8a9b-0c1d2e3f4a5b",
    "status": "ready",
    "createdAt": "2026-10-28T09:15:00.000Z",
    "downloadUrl": "https://storage.example.com/export/…/4f1c2b3a.zip?X-Amz-Signature=…",
    "expiresAt": "2026-11-04T09:21:42.000Z",
    "sizeBytes": 48213504
  }
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: Invalid request body or query
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No endpoint exists at this path.
- `405`: Method not allowed. Use a method from the Allow header.
- `429`: Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error

## Start an account export

`POST https://workroad.app/api/v1/export`

Starts building the account export, or returns the one already being built. It takes a few minutes for a large account; poll `GET /export` until `status` is `ready`. The user also gets an email with a link when it is ready.

- MCP tool: `startExport` on `https://workroad.app/api/mcp`
- Required scopes: account:export
- Test mode: `POST https://workroad.app/api/test/v1/export` checks the request and returns the example response without changing anything.
- Next: poll `getExport` until `status` is `ready`, then download `downloadUrl`.

### Headers

- `Idempotency-Key`: string, optional, 1-255 visible ASCII characters. Makes the POST safe to retry: a retry with the same key and body replays the first response, and the same key with a different body returns 409 `idempotency_conflict`. Kept for 24 hours. Over MCP it is the `idempotencyKey` argument.

### Response

`202` Accepted

- `data`: object, required
  - `id`: string (uuid), required. The export's id.
  - `status`: one of `"pending"`, `"running"`, `"ready"`, `"failed"`, `"expired"`, required. `pending` and `running` while it is built, `ready` while it can be downloaded, `expired` once its download window has passed, `failed` if it could not be built.
  - `createdAt`: string (date-time), required. When the export was requested.
  - `downloadUrl`: string or null, required. When `status` is `ready`: a URL that downloads the ZIP for the next 5 minutes, without credentials. Ask again for a fresh one. Null otherwise.
  - `expiresAt`: string (date-time) or null, required. When `status` is `ready`: when the export stops being available. Null otherwise.
  - `sizeBytes`: integer or null, required. The ZIP's size when `status` is `ready`, else null.

#### Example

```json
{
  "data": {
    "id": "4f1c2b3a-5d6e-4f70-8a9b-0c1d2e3f4a5b",
    "status": "pending",
    "createdAt": "2026-10-28T09:15:00.000Z",
    "downloadUrl": null,
    "expiresAt": null,
    "sizeBytes": null
  }
}
```

### Errors

Every error has the body `{ "error": { "code", "message", "details" } }`.

- `400`: Invalid request body or query
- `401`: Missing, invalid, disabled or expired API key
- `402`: `api_allowance_reached`: this month's API and AI app requests on the free plan are used up. `details` has `limit`, `used`, `resetsAt` and `upgradeUrl`. Nothing ran; WorkRoad Plus has no monthly limit.
- `403`: The API key lacks the required scope
- `404`: No endpoint exists at this path.
- `405`: Method not allowed. Use a method from the Allow header.
- `409`: The Idempotency-Key was reused with a different request body, or a request with the same key is still in progress.
- `429`: Too many exports were started in the last hour, or WorkRoad is busy. Wait for the seconds in `Retry-After`. Rate limit exceeded. Wait for Retry-After before retrying. The rate-limit headers are absent when the key's own limiter rejected the request before the per-endpoint bucket check ran.
- `500`: Internal server error
- `503`: WorkRoad can't start background work right now. Send the same request again after `Retry-After` seconds.
