REST API
Create an API key, make your first request, and learn the rules every request follows.
Use the REST API to work with your WorkRoad notes, study sets, tasks, subjects and schedules, run practice sessions, and use the copilot and generation features. Every request runs as the user who owns the key and returns only that user's data.
The live base URL is https://workroad.app/api/v1. For an agent connection through ChatGPT, Claude or another client, start with the MCP server guide instead.
Create a key and choose its scopes
Open Settings → AI apps & API → API keys at /user/developers/keys. Give the key a name and tick the scopes your integration needs. WorkRoad shows the secret once, right after you create the key, so copy it then. If you lose it, revoke the key and create a new one.
Each operation in the reference lists the scopes it needs. Reading notes needs notes:view, for example, and creating a note needs notes:create. The Full access template ticks every scope. A request outside the key's scopes returns 403 forbidden.
Every account can use the API and the MCP server. The free plan allows 100 requests a month and one API key (see Monthly requests on the free plan), and WorkRoad Plus has no monthly count. The AI features keep the same plan limits as in the app (see AI allowance).
Make your first request
Set your key in the environment and ask for the user who owns it. This needs no scopes:
export WORKROAD_API_KEY=wr_live_...
curl https://workroad.app/api/v1/me \
-H "Authorization: Bearer $WORKROAD_API_KEY"The reference's cURL examples read the same environment variable, so you can run them as they are. The other language tabs contain $WORKROAD_API_KEY as literal text; replace it or read the variable in your code.
To try an integration without changing your data, send the same requests to https://workroad.app/api/test/v1. See Test mode.
Find what you're building
The reference has one page per part of WorkRoad: how that part works, then every operation with its inputs, response, scopes and errors.
- Notes and Files and recordings: writing Markdown, folders, uploads and transcripts.
- Tasks, Subjects and Schedules and planning: due dates, recurrence, timetables, study blocks and today's plan.
- Study sets, Reviews and practice and Practice tests: cards, spaced-repetition reviews and tests.
- Copilot and Generation and jobs: copilot messages and actions, AI generation and background jobs.
- Search and docs, Account and settings and Webhooks.
An agent that writes its own notes, cards or tests can save them through ordinary write operations without spending WorkRoad AI uses. See Let your AI do the writing.
The rest of this page covers what works the same on every operation.
Authentication
Send the key in the Authorization header:
Authorization: Bearer <key>A missing, unknown or revoked key returns 401 unauthorized with WWW-Authenticate: Bearer realm="workroad-api". So does a key whose user is banned, and a key made for the calendar feed.
Pagination
A list that can grow long returns one page at a time. Its reference entry takes limit and cursor query parameters, and its response looks like this:
{
"data": [{ "id": "2b0c6c1e-8f0e-4c55-9a53-0d7f5b1f6a10" }],
"nextCursor": "eyJrIjoidGFza3MiLCJ2Ijp7ImNyZWF0ZWQiOiIyMDMwLTAxLTAxVDAwOjAwOjAwLjAwMFoiLCJpZCI6IjJiMGM2YzFlLThmMGUtNGM1NS05YTUzLTBkN2Y1YjFmNmExMCJ9fQ"
}limitsets the page size. It defaults to 25 and goes up to 100, unless the list's reference entry gives other bounds.cursortakes thenextCursorvalue from the previous page. Leave it out for the first page.
nextCursor is null on the last page. It is opaque. Pass it back unchanged as cursor, with the same filters. It stays valid when rows on earlier pages are deleted. A limit outside the list's bounds, or a cursor that did not come from the same list, returns 400 invalid_query. Never build a cursor yourself, and never use a row id as one.
A short list, such as the user's subjects or folders, returns all its rows in one response and takes no limit or cursor. Sending either returns 400 invalid_query. A list's reference entry shows which kind it is.
Errors
Every error has the same JSON body, with content-type: application/json; charset=utf-8:
{
"error": {
"code": "invalid_query",
"message": "Invalid query parameters.",
"details": {
"formErrors": [],
"fieldErrors": { "limit": ["Too big: expected number to be <=100"] }
}
}
}Branch on code, not on message, because messages can change. details is optional:
- validation errors list the failing fields
- 409
conflictexplains the clash, usually with areason - 402
ai_limit_reachedgives thecode,limitandresetsAtof the limit that was reached - 402
api_allowance_reachedgives the free plan's monthlylimit, the requestsused,resetsAtandupgradeUrl
| Status | Code | When |
|---|---|---|
| 400 | invalid_request | The body is missing, is not JSON or fails validation, or it names a subjectId or folderId the key's user does not own. |
| 400 | invalid_query | A query parameter fails validation or appears twice, or a name the operation does not take is present. |
| 401 | unauthorized | The key is missing or not valid. |
| 402 | ai_limit_reached | The user has used this month's AI allowance. See AI allowance. |
| 402 | api_allowance_reached | The user is on the free plan and has used this month's API and MCP requests. Nothing ran. See Monthly requests on the free plan. |
| 403 | forbidden | The key lacks a scope the operation needs. |
| 404 | not_found | No endpoint exists at the path, or no resource with that id in the path belongs to the key's user. A malformed id also returns 404. |
| 405 | method_not_allowed | The path exists but not for this method. The Allow header lists the methods it takes. |
| 409 | conflict | The write clashes with existing data. |
| 409 | idempotency_conflict | The Idempotency-Key was reused with a different body or while its first request is still running, a reviewId was reused with a different card, rating or duration, a copilot requestId was reused with a different message, or a generation requestId was reused with other inputs. |
| 409 | idempotency_replay_unavailable | The first request with this Idempotency-Key succeeded, but its response was over 256 KB and is not stored. details has the status and the created resource's id, or null when the response had none. |
| 429 | rate_limited | The key is over a rate limit, or an operation's own limit was reached, such as the user's AI rate limit or the free plan's daily copilot turns or actions. Wait for the seconds in Retry-After. |
| 500 | internal_error | WorkRoad failed. Retry later. |
| 503 | service_unavailable | A dependency WorkRoad relies on, such as the job queue or a timetable integration, is unavailable. Wait for the seconds in Retry-After. |
Retry safely with an idempotency key
Send an Idempotency-Key header on a POST to make it safe to retry. Use a new key per logical operation, for example a UUID generated once before the first attempt.
POST /api/v1/tasks
Idempotency-Key: 3f29c1de-9f0a-4b7e-9c0d-2a6e6f1b7a44A retry with the same key and the same body returns the original response again, unchanged, with Idempotent-Replayed: true added. It creates nothing new, and it doesn't count toward the free plan's monthly requests.
Input validation
Resource names are trimmed before their length checks; whitespace-only names are rejected. Strings and property names must not contain NUL bytes or unpaired Unicode surrogates. An own __proto__ property is rejected at any depth. These failures return 400, including in query filters and MCP arguments.
Task dates accept a valid YYYY-MM-DD day, read in the user's time zone as Tasks describes, or an ISO timestamp with Z or a numeric offset. Years must be 0001 through 9999, including after offset conversion. Impossible dates, such as 2026-02-30, and timestamps without an offset are rejected rather than normalized.
Validation errors use prose in error.message and stable machine codes in error.details.reason when the failure comes from a domain rule.
Test mode
Send the same request to /api/test/v1 instead of /api/v1 to try an integration without touching your data. Test mode checks the key, the scopes, the rate limits and the input exactly as live mode does. Instead of running the operation, it returns the fixed example response shown in each operation's reference. It never changes your data.
Test requests count against the same per-key rate limits as live requests. They don't count toward the free plan's monthly requests.
The playground on each operation saves a key you paste into it in your browser's local storage. Use a test-mode-only key with the playground, or clear the saved key from local storage after you finish.
Limits and the OpenAPI document
The free plan includes 100 API and MCP requests per UTC calendar month, shared across the user's keys and connected apps. WorkRoad Plus has no monthly request count. AI operations have a separate allowance, and per-key rate limits apply on both plans. Limits and allowances explains what counts and how to handle 402 and 429 responses.
Download the OpenAPI 3.1 document to generate a client. Write clients that ignore unknown response fields.