REST API
Authenticate with an API key, page through lists, handle errors and stay under the rate limits.
The REST API reads and writes your notes, study sets, tasks and subjects. Every request runs as the user who owns the key, and every response contains only that user's data.
Live requests go to https://workroad.app/api/v1. The pages in the sidebar describe each operation, and /api/v1/openapi.json has the same information as an OpenAPI 3.1 document you can feed to a client generator.
curl https://workroad.app/api/v1/me \
-H "Authorization: Bearer $WORKROAD_KEY"Keys and scopes
Create a key in Settings → API keys at /user/keys. Give it a name and tick the scopes it 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.
A key can only call operations its scopes allow:
| Operation | Scope |
|---|---|
| Get the key's user | none |
| List and get notes | notes:view |
| Create a note | notes:create |
| List and get study sets | sets:view |
| List tasks | tasks:view |
| Create a task | tasks:create |
| Update a task | tasks:update |
| List subjects | any one of notes:view, sets:view, tasks:view or schedule:view |
Each operation page lists its required scopes under the description. A call outside the key's scopes returns 403 forbidden.
Authentication
Send the key in the Authorization header:
Authorization: Bearer <key>The API also accepts the key in an x-key header, which older integrations use. New code should use Authorization.
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
List operations return one page at a time:
{
"data": [{ "id": "2b0c6c1e-8f0e-4c55-9a53-0d7f5b1f6a10" }],
"nextCursor": "2b0c6c1e-8f0e-4c55-9a53-0d7f5b1f6a10"
}Two query parameters control the page:
limitsets the page size, from 1 to 100. It defaults to 25.cursortakes thenextCursorvalue from the previous page. Leave it out for the first page.
nextCursor is null on the last page. A limit outside 1 to 100, or a cursor that is not an id from the same list, returns 400 invalid_query.
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"] }
}
}
}details appears only on validation errors, where it lists the failing fields. Branch on code, not on message, because messages can change.
| Status | Code | When |
|---|---|---|
| 400 | invalid_request | The body is missing, is not JSON or fails validation. |
| 400 | invalid_query | A query parameter fails validation or appears twice. Unknown parameters are ignored. |
| 401 | unauthorized | The key is missing or not valid. |
| 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 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. |
| 429 | rate_limited | The key is over a rate limit. Wait for the seconds in Retry-After. |
| 500 | internal_error | WorkRoad failed. Retry later. |
Rate limits
Each key gets 120 reads (GET and HEAD) and 30 writes (POST, PATCH and DELETE) per minute. Separately, key verification allows 120 requests per minute per key, counting reads and writes together. Going over either limit returns 429 rate_limited with a Retry-After header in seconds.
Responses that get past the scope check carry the state of the bucket they used:
RateLimit-Policy: "rest:v1.read";q=120;w=60
RateLimit: "rest:v1.read";r=117;t=42
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 1790000000r and X-RateLimit-Remaining count the requests left in the window. t is the number of seconds until the window resets, and X-RateLimit-Reset is the same moment as a Unix timestamp. The bucket is rest:v1.read for reads and rest:v1.write for writes.
401, 403 and 405 responses have no rate-limit headers, and neither does a 404 for a path with no endpoint, because the API rejects those requests before it counts them. A 429 from key verification has only Retry-After.
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 on each operation page. It never changes your data.
Test requests count against the same per-key rate limits as live requests.
The playground on each operation page 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.
Versioning
Changes within /api/v1 are additive: new operations, new optional parameters and new response fields. Write clients that ignore fields they do not know. A change that removes or renames a field, changes a type or tightens validation ships as /api/v2, and /api/v1 keeps working alongside it.