Skip to content
REST API

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:

OperationScope
Get the key's usernone
List and get notesnotes:view
Create a notenotes:create
List and get study setssets:view
List taskstasks:view
Create a tasktasks:create
Update a tasktasks:update
List subjectsany 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:

  • limit sets the page size, from 1 to 100. It defaults to 25.
  • cursor takes the nextCursor value 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.

StatusCodeWhen
400invalid_requestThe body is missing, is not JSON or fails validation.
400invalid_queryA query parameter fails validation or appears twice. Unknown parameters are ignored.
401unauthorizedThe key is missing or not valid.
403forbiddenThe key lacks a scope the operation needs.
404not_foundNo endpoint exists at the path, or no resource with that id belongs to the key's user. A malformed id also returns 404.
405method_not_allowedThe path exists but not for this method. The Allow header lists the methods it takes.
409conflictThe write clashes with existing data.
429rate_limitedThe key is over a rate limit. Wait for the seconds in Retry-After.
500internal_errorWorkRoad 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: 1790000000

r 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.