Schedules and planning
Build timetables, plan study blocks, read what is on today, and manage calendar feeds.
Timetables
POST /schedules/timetable creates a timetable with its periods and every day's lessons in one call, all or nothing, as the app's setup does. A defined timetable has periods and one entry per period on each day; a dynamic one has none, and each lesson carries timeStart and timeEnd. A lesson's subjectName links it to the user's subject with that name; one that matches none stays as written on the timetable, or becomes a new subject with createSubjectsIfMissing: true.
GET /schedules/{scheduleId} says what a key can change. owned is true for the user's own schedules, and editable is true when the lesson grid can be written. Only the owner writes. A schedule shared with the user, or anyone else's, returns 404 on every write.
PATCH /schedules/{scheduleId} renames, archives, restores or sets the default, and changes the days the schedule shows. It works on any schedule the user owns, including one read from an integration. Every field in a call applies, or none does. Archiving the default clears it. To make an archived schedule the default, restore it in the same call with archived: false.
The grid, meaning the periods and the lessons in them, can only be written on the owner's DEFINED schedules kept in WorkRoad. On an external schedule a grid write returns 409 conflict with details.reason external-schedule, and on a DYNAMIC one dynamic-schedule. Their lessons change in the app or at the provider.
To build a timetable:
POST /scheduleswith anameand thestructureof periods. Each period has anameand either bothstartandendor neither.POST /schedules/{scheduleId}/elementswith aday(0 is Sunday), aperiodindex and alessonName, once per lesson. The first lesson lays out an empty period on every weekday. If the user has no default schedule, this one becomes it.- Change a lesson with
PATCH /schedules/{scheduleId}/elements/{elementId}and empty its period withDELETE. The element keeps its id withlessonName: null, the way the app stores an empty period, and a laterPOSTfor that period fills the same element. Removing a lesson needsschedule:delete, the same way deleting a study set term needssets:delete.
Don't store element ids. Saving the timetable in the app deletes and recreates every element, so an id from an earlier read can return 404. Read GET /schedules/{scheduleId} before each lesson write and take the id from days[].elements[].
PUT /schedules/{scheduleId}/structure renames and retimes periods at any point. It can change how many periods there are only until the first lesson is added, up to 30; after that it returns 409 with details.reason period-count. A period's start and end may be empty strings, which is how the app stores a time still being set up, so a structure you read can be sent back unchanged. Adding to a period that doesn't exist returns 409 no-such-period, adding to one that has a lesson returns 409 slot-taken, and updating an empty one returns 409 slot-empty.
Every write returns the schedule as GET /schedules/{scheduleId} shows it. A subjectId must be one of the user's subject ids; any other returns 400 with details.field subjectId and details.reason unknown-subject. PATCH /schedules/{scheduleId} with isDefault: true on an external schedule first asks the provider whether setup finished. If that check fails it returns 503 before saving anything, and retrying the same body is safe. A lesson reads back with subjectId when it points at a subject, or with subjectLabel when an older timetable stored free text there; at most one is set. Writing subjectId replaces the label. When the provider can't be read for the response after a save, the schedule comes back with issue: "failed".
Study blocks
A study block is study time planned on the user's calendar, the way the planning copilot plans it. POST /study-blocks takes a title, a future startsAt and an endsAt 5 minutes to 8 hours later on the same day in the user's time zone, and optionally the subjectId, taskId or setId it is for. A block can't overlap a lesson, including lessons on connected external calendars, or another block, and can't end after a linked task's due day; those return 409 conflict with details.reason planning-conflict or planning-after-deadline. With an external calendar connected, a block can only be planned this week or next, so its lessons can be checked (planning-calendar-incomplete).
GET /study-blocks?from=2026-10-20&to=2026-10-26 lists blocks by day in the user's time zone, up to 62 days at a time. PATCH moves or relinks one and checks it again as a whole; DELETE removes it.
Today
GET /today answers "what should I study now?" in one call, from the same data the app's dashboard and planning copilot read. study has the cards due now by subject and the sets with the most due, today's goal, streak and remaining new cards. tasks has overdue tasks and tasks due in the next 7 days, with exams flagged. lessons and studyBlocks hold today's timetable and planned study time.
Each section needs its own scope and is null without it: study needs activity:view (its sets list also needs sets:view), tasks needs tasks:view, and lessons and studyBlocks need schedule:view. suggestions lists what to do next, most useful first. Each one has a sentence for the user, the operation that acts on it as tool, and its arguments, such as startPracticeSession with { "subjectId": "..." }. A suggestion only names an operation the key may call.
Calendar feeds
GET /calendar-feeds lists feed metadata without returning secret subscription URLs. DELETE /calendar-feeds/{feedId} revokes a feed. Create feeds or rotate their URLs in Settings → Calendar. Anyone holding a subscription URL can read its calendar data, even after the API key used to list the feed is revoked.
List schedules
Every active schedule the user owns, default first, then the ones other users shared with them. Archived schedules are left out.
Required scopes: schedule:view
AuthorizationBearer <token>OK
application/json- response
data*array<>curl -X GET 'https://workroad.app/api/v1/schedules' \ -H 'Authorization: Bearer $WORKROAD_API_KEY'{ "data": [ { "id": "4d3c2b1a-0f9e-4d8c-8b7a-6f5e4d3c2b1a", "name": "Year 12", "type": "DEFINED", "external": null, "owned": true, "isDefault": true, "selectedDays": [ 1, 2, 3, 4, 5 ], "archived": false } ]}Get a week of lessons
Every lesson in one week across the user's active schedules, or one schedule, as dated occurrences in the user's time zone. External schedules are read from their provider, up to 7 seconds each; one that fails or times out is listed in sources with an issue and adds no lessons. Archived schedules are not part of the week, so a scheduleId naming one returns 404.
Required scopes: schedule:view
AuthorizationBearer <token>date?stringAny day (YYYY-MM-DD) in the week to read. Defaults to today in the user's time zone. Only the previous, current and next week can be read.
^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))$datescheduleId?stringOnly this schedule. Omit for every active schedule the user can read.
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$uuidOK
application/json- response
data*curl -X GET 'https://workroad.app/api/v1/schedules/week' \ -H 'Authorization: Bearer $WORKROAD_API_KEY'{ "data": { "weekStart": "2026-01-12", "timeZone": "Europe/London", "sources": [ { "scheduleId": "4d3c2b1a-0f9e-4d8c-8b7a-6f5e4d3c2b1a", "name": "Year 12", "external": null, "owned": true, "issue": null } ], "occurrences": [ { "scheduleId": "4d3c2b1a-0f9e-4d8c-8b7a-6f5e4d3c2b1a", "date": "2026-01-12", "endDate": null, "time": { "kind": "timed", "startMinute": 540, "endMinute": 600 }, "timeZone": null, "title": "Biology", "subtitle": "S2 · Dr Patel" } ] }}Get a schedule
The schedule's periods and its days with their lessons. The owner can also open an archived schedule. An external schedule shows the current week, read from its provider. editable covers the lesson grid only. Metadata changes (name, days, archived, default) apply to any schedule the user owns, external ones included.
Required scopes: schedule:view
AuthorizationBearer <token>scheduleId*stringSchedule id, scoped to the key's user.
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$uuidOK
application/json- response
data*curl -X GET 'https://workroad.app/api/v1/schedules/497f6eca-6276-4993-bfeb-53cbbbba6f08' \ -H 'Authorization: Bearer $WORKROAD_API_KEY'{ "data": { "id": "4d3c2b1a-0f9e-4d8c-8b7a-6f5e4d3c2b1a", "name": "Year 12", "type": "DEFINED", "external": null, "owned": true, "editable": true, "firstDay": 1, "selectedDays": [ 1, 2, 3, 4, 5 ], "issue": null, "structure": { "periods": [ { "index": 0, "name": "Period 1", "start": "09:00", "end": "10:00" }, { "index": 1, "name": "Period 2", "start": "10:00", "end": "11:00" } ] }, "days": [ { "day": 1, "elements": [ { "id": "6e5d4c3b-2a19-4f08-9e7d-6c5b4a392817", "period": 0, "lessonName": "Biology", "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f", "subjectLabel": null, "room": "S2", "teacher": "Dr Patel", "timeStart": null, "timeEnd": null }, { "id": "7f6e5d4c-3b2a-4190-8f8e-7d6c5b4a3928", "period": 1, "lessonName": null, "subjectId": null, "subjectLabel": null, "room": null, "teacher": null, "timeStart": null, "timeEnd": null } ] } ] }}Create a schedule
Creates a DEFINED timetable kept in WorkRoad, optionally with its periods. It has no lessons yet: add them with POST /schedules/{scheduleId}/elements. The first lesson lays out every weekday and, if the user has no default schedule, makes this one the default.
Required scopes: schedule:create
AuthorizationBearer <token>Idempotency-Key?stringA client-generated key (1 to 255 visible ASCII characters) that makes this POST safe to retry. A retry with the same key and body replays the original response; the same key with a different body returns 409 idempotency_conflict. Stored for 24 hours.
^[\x21-\x7E]{1,255}$1 <= length <= 255application/json- body
name*string2-64 characters. Unique among the user's active schedules.
2 <= length <= 64firstDay?integerThe weekday the timetable starts on. Defaults to 1 (Monday).
0 <= value <= 6selectedDays?array<>Weekdays the schedule shows. Defaults to all seven.
items <= 7structure?The periods. Omit to send them later with PUT /schedules/{scheduleId}/structure.
Created
application/json- response
data*curl -X POST 'https://workroad.app/api/v1/schedules' \ -H 'Authorization: Bearer $WORKROAD_API_KEY' \ -H 'Content-Type: application/json' \ -d '{ "name": "string"}'{ "data": { "id": "4d3c2b1a-0f9e-4d8c-8b7a-6f5e4d3c2b1a", "name": "Year 12", "type": "DEFINED", "external": null, "owned": true, "editable": true, "firstDay": 1, "selectedDays": [ 1, 2, 3, 4, 5 ], "issue": null, "structure": { "periods": [ { "index": 0, "name": "Period 1", "start": "09:00", "end": "10:00" }, { "index": 1, "name": "Period 2", "start": "10:00", "end": "11:00" } ] }, "days": [ { "day": 1, "elements": [ { "id": "6e5d4c3b-2a19-4f08-9e7d-6c5b4a392817", "period": 0, "lessonName": "Biology", "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f", "subjectLabel": null, "room": "S2", "teacher": "Dr Patel", "timeStart": null, "timeEnd": null }, { "id": "7f6e5d4c-3b2a-4190-8f8e-7d6c5b4a3928", "period": 1, "lessonName": null, "subjectId": null, "subjectLabel": null, "room": null, "teacher": null, "timeStart": null, "timeEnd": null } ] } ] }}Create a timetable with its lessons
Creates a timetable kept in WorkRoad with its periods and every day's lessons in one call, as the app's setup does: the first timetable becomes the default, and the days with lessons are the days it shows. Use it to save a timetable you read from a photo, a PDF or what the user typed; it uses no AI. Link lessons to the user's subjects with subjectId or subjectName, and list the subjects first (GET /subjects) so the names match. The timetable is saved whole or not at all.
Use this to save content you wrote yourself. It uses none of the user's WorkRoad AI uses.
Required scopes: schedule:create
AuthorizationBearer <token>Idempotency-Key?stringA client-generated key (1 to 255 visible ASCII characters) that makes this POST safe to retry. A retry with the same key and body replays the original response; the same key with a different body returns 409 idempotency_conflict. Stored for 24 hours.
^[\x21-\x7E]{1,255}$1 <= length <= 255application/json- body
name*string2-64 characters. Unique among the user's active schedules.
2 <= length <= 64scheduleType*stringdefined: every day shares one grid of periods, and each day lists one entry per period (null for a free period). dynamic: no periods; each day lists its lessons, each with timeStart and timeEnd.
"defined""dynamic"firstDay?integerThe weekday the timetable starts on, which is the first entry of days. Defaults to 1 (Monday).
0 <= value <= 6periods?array<>defined only: 1-30 periods, in order, not overlapping.
1 <= items <= 30days*array<array<|null>>One entry per weekday from firstDay, up to 7: the day's lessons in order. For a defined timetable every day has exactly one entry per period. A day with no lessons is an empty list (dynamic) or all null (defined), and days left off the end are free.
1 <= items <= 7createSubjectsIfMissing?booleanCreates a subject for each subjectName the user doesn't have yet, so the lessons link to it. Needs the subjects:create scope.
Created
application/json- response
data*curl -X POST 'https://workroad.app/api/v1/schedules/timetable' \ -H 'Authorization: Bearer $WORKROAD_API_KEY' \ -H 'Content-Type: application/json' \ -d '{ "name": "string", "scheduleType": "defined", "days": [ [ {} ] ]}'{ "data": { "id": "4d3c2b1a-0f9e-4d8c-8b7a-6f5e4d3c2b1a", "name": "Year 12", "type": "DEFINED", "external": null, "owned": true, "editable": true, "firstDay": 1, "selectedDays": [ 1, 2, 3, 4, 5 ], "issue": null, "structure": { "periods": [ { "index": 0, "name": "Period 1", "start": "09:00", "end": "10:00" }, { "index": 1, "name": "Period 2", "start": "10:00", "end": "11:00" } ] }, "days": [ { "day": 1, "elements": [ { "id": "6e5d4c3b-2a19-4f08-9e7d-6c5b4a392817", "period": 0, "lessonName": "Biology", "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f", "subjectLabel": null, "room": "S2", "teacher": "Dr Patel", "timeStart": null, "timeEnd": null }, { "id": "7f6e5d4c-3b2a-4190-8f8e-7d6c5b4a3928", "period": 1, "lessonName": null, "subjectId": null, "subjectLabel": null, "room": null, "teacher": null, "timeStart": null, "timeEnd": null } ] } ] }}Update a schedule
Renames, archives or restores a schedule the user owns, changes the days it shows, or makes it the default. Works on external schedules too, and the response then reads the current week from the provider. Everything in one call succeeds or fails together. Retrying with the same body is safe: every field sets a value, so a second call changes nothing more.
Required scopes: schedule:update
AuthorizationBearer <token>scheduleId*stringSchedule id, scoped to the key's user.
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$uuidapplication/json- body
Send only the fields to change. An omitted field stays as it is.
name?string2-64 characters. Unique among the user's active schedules.
2 <= length <= 64selectedDays?array<>Weekdays the schedule shows, each once.
items <= 7archived?booleantrue archives the schedule and, if it was the default, clears the default. false restores it.
isDefault?booleantrue makes it the user's default schedule. false clears the default if this schedule is it.
OK
application/json- response
data*curl -X PATCH 'https://workroad.app/api/v1/schedules/497f6eca-6276-4993-bfeb-53cbbbba6f08' \ -H 'Authorization: Bearer $WORKROAD_API_KEY' \ -H 'Content-Type: application/json' \ -d '{}'{ "data": { "id": "4d3c2b1a-0f9e-4d8c-8b7a-6f5e4d3c2b1a", "name": "Year 12", "type": "DEFINED", "external": null, "owned": true, "editable": true, "firstDay": 1, "selectedDays": [ 1, 2, 3, 4, 5 ], "issue": null, "structure": { "periods": [ { "index": 0, "name": "Period 1", "start": "09:00", "end": "10:00" }, { "index": 1, "name": "Period 2", "start": "10:00", "end": "11:00" } ] }, "days": [ { "day": 1, "elements": [ { "id": "6e5d4c3b-2a19-4f08-9e7d-6c5b4a392817", "period": 0, "lessonName": "Biology", "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f", "subjectLabel": null, "room": "S2", "teacher": "Dr Patel", "timeStart": null, "timeEnd": null }, { "id": "7f6e5d4c-3b2a-4190-8f8e-7d6c5b4a3928", "period": 1, "lessonName": null, "subjectId": null, "subjectLabel": null, "room": null, "teacher": null, "timeStart": null, "timeEnd": null } ] } ] }}Set a schedule's periods
Replaces the periods of a DEFINED schedule kept in WorkRoad. Once the first lesson is added, the number of periods is fixed; names and times can still change. The app stores a time that is still being set up as an empty string, so the structure from GET /schedules/{scheduleId} can be sent back unchanged.
Required scopes: schedule:update
AuthorizationBearer <token>scheduleId*stringSchedule id, scoped to the key's user.
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$uuidapplication/json- body
periods*array<>The periods, in the order of the day. Elements refer to a period by its position. At most 30 while the number of periods can still change.
1 <= itemsOK
application/json- response
data*curl -X PUT 'https://workroad.app/api/v1/schedules/497f6eca-6276-4993-bfeb-53cbbbba6f08/structure' \ -H 'Authorization: Bearer $WORKROAD_API_KEY' \ -H 'Content-Type: application/json' \ -d '{ "periods": [ { "name": "string", "start": "string", "end": "string" } ]}'{ "data": { "id": "4d3c2b1a-0f9e-4d8c-8b7a-6f5e4d3c2b1a", "name": "Year 12", "type": "DEFINED", "external": null, "owned": true, "editable": true, "firstDay": 1, "selectedDays": [ 1, 2, 3, 4, 5 ], "issue": null, "structure": { "periods": [ { "index": 0, "name": "Period 1", "start": "09:00", "end": "10:00" }, { "index": 1, "name": "Period 2", "start": "10:00", "end": "11:00" } ] }, "days": [ { "day": 1, "elements": [ { "id": "6e5d4c3b-2a19-4f08-9e7d-6c5b4a392817", "period": 0, "lessonName": "Biology", "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f", "subjectLabel": null, "room": "S2", "teacher": "Dr Patel", "timeStart": null, "timeEnd": null }, { "id": "7f6e5d4c-3b2a-4190-8f8e-7d6c5b4a3928", "period": 1, "lessonName": null, "subjectId": null, "subjectLabel": null, "room": null, "teacher": null, "timeStart": null, "timeEnd": null } ] } ] }}Add a lesson
Puts a lesson into an empty period of a DEFINED schedule kept in WorkRoad. Set the periods first with PUT /schedules/{scheduleId}/structure. Returns the schedule; the lesson is the entry in days[] for day, at period in its elements. Element ids change whenever the timetable is saved in the app, so read GET /schedules/{scheduleId} for the current id instead of keeping one.
Required scopes: schedule:update
AuthorizationBearer <token>scheduleId*stringSchedule id, scoped to the key's user.
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$uuidIdempotency-Key?stringA client-generated key (1 to 255 visible ASCII characters) that makes this POST safe to retry. A retry with the same key and body replays the original response; the same key with a different body returns 409 idempotency_conflict. Stored for 24 hours.
^[\x21-\x7E]{1,255}$1 <= length <= 255application/json- body
day*integerWeekday, 0 (Sunday) to 6 (Saturday).
0 <= value <= 6period*integerThe period's index in structure.
0 <= value <= 9007199254740991lessonName*string1 <= length <= 200subjectId?|Subject id owned by the user, or null.
room?|nullteacher?|nullCreated
application/json- response
data*curl -X POST 'https://workroad.app/api/v1/schedules/497f6eca-6276-4993-bfeb-53cbbbba6f08/elements' \ -H 'Authorization: Bearer $WORKROAD_API_KEY' \ -H 'Content-Type: application/json' \ -d '{ "day": 0, "period": 0, "lessonName": "string"}'{ "data": { "id": "4d3c2b1a-0f9e-4d8c-8b7a-6f5e4d3c2b1a", "name": "Year 12", "type": "DEFINED", "external": null, "owned": true, "editable": true, "firstDay": 1, "selectedDays": [ 1, 2, 3, 4, 5 ], "issue": null, "structure": { "periods": [ { "index": 0, "name": "Period 1", "start": "09:00", "end": "10:00" }, { "index": 1, "name": "Period 2", "start": "10:00", "end": "11:00" } ] }, "days": [ { "day": 1, "elements": [ { "id": "6e5d4c3b-2a19-4f08-9e7d-6c5b4a392817", "period": 0, "lessonName": "Biology", "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f", "subjectLabel": null, "room": "S2", "teacher": "Dr Patel", "timeStart": null, "timeEnd": null }, { "id": "7f6e5d4c-3b2a-4190-8f8e-7d6c5b4a3928", "period": 1, "lessonName": null, "subjectId": null, "subjectLabel": null, "room": null, "teacher": null, "timeStart": null, "timeEnd": null } ] } ] }}Update a lesson
Changes the lesson in one period of a DEFINED schedule kept in WorkRoad. Only the fields you send change. Returns the schedule. Element ids change whenever the timetable is saved in the app, so read GET /schedules/{scheduleId} for the current id instead of keeping one.
Required scopes: schedule:update
AuthorizationBearer <token>scheduleId*stringSchedule id, scoped to the key's user.
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$uuidelementId*stringElement id from the schedule's days[].elements[].
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$uuidapplication/json- body
Send only the fields to change. null clears subjectId, room or teacher. To empty the period, delete the element.
lessonName?string1 <= length <= 200subjectId?|Subject id owned by the user, or null.
room?|nullteacher?|nullOK
application/json- response
data*curl -X PATCH 'https://workroad.app/api/v1/schedules/497f6eca-6276-4993-bfeb-53cbbbba6f08/elements/497f6eca-6276-4993-bfeb-53cbbbba6f08' \ -H 'Authorization: Bearer $WORKROAD_API_KEY' \ -H 'Content-Type: application/json' \ -d '{}'{ "data": { "id": "4d3c2b1a-0f9e-4d8c-8b7a-6f5e4d3c2b1a", "name": "Year 12", "type": "DEFINED", "external": null, "owned": true, "editable": true, "firstDay": 1, "selectedDays": [ 1, 2, 3, 4, 5 ], "issue": null, "structure": { "periods": [ { "index": 0, "name": "Period 1", "start": "09:00", "end": "10:00" }, { "index": 1, "name": "Period 2", "start": "10:00", "end": "11:00" } ] }, "days": [ { "day": 1, "elements": [ { "id": "6e5d4c3b-2a19-4f08-9e7d-6c5b4a392817", "period": 0, "lessonName": "Biology", "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f", "subjectLabel": null, "room": "S2", "teacher": "Dr Patel", "timeStart": null, "timeEnd": null }, { "id": "7f6e5d4c-3b2a-4190-8f8e-7d6c5b4a3928", "period": 1, "lessonName": null, "subjectId": null, "subjectLabel": null, "room": null, "teacher": null, "timeStart": null, "timeEnd": null } ] } ] }}Remove a lesson
Empties one period of a DEFINED schedule kept in WorkRoad. The element keeps its id and stays in the grid with lessonName: null, as when a lesson is removed in the app. Emptying an empty period succeeds. Returns the schedule. Element ids change whenever the timetable is saved in the app, so read GET /schedules/{scheduleId} for the current id instead of keeping one.
Required scopes: schedule:delete
AuthorizationBearer <token>scheduleId*stringSchedule id, scoped to the key's user.
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$uuidelementId*stringElement id from the schedule's days[].elements[].
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$uuidOK
application/json- response
data*curl -X DELETE 'https://workroad.app/api/v1/schedules/497f6eca-6276-4993-bfeb-53cbbbba6f08/elements/497f6eca-6276-4993-bfeb-53cbbbba6f08' \ -H 'Authorization: Bearer $WORKROAD_API_KEY'{ "data": { "id": "4d3c2b1a-0f9e-4d8c-8b7a-6f5e4d3c2b1a", "name": "Year 12", "type": "DEFINED", "external": null, "owned": true, "editable": true, "firstDay": 1, "selectedDays": [ 1, 2, 3, 4, 5 ], "issue": null, "structure": { "periods": [ { "index": 0, "name": "Period 1", "start": "09:00", "end": "10:00" }, { "index": 1, "name": "Period 2", "start": "10:00", "end": "11:00" } ] }, "days": [ { "day": 1, "elements": [ { "id": "6e5d4c3b-2a19-4f08-9e7d-6c5b4a392817", "period": 0, "lessonName": "Biology", "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f", "subjectLabel": null, "room": "S2", "teacher": "Dr Patel", "timeStart": null, "timeEnd": null }, { "id": "7f6e5d4c-3b2a-4190-8f8e-7d6c5b4a3928", "period": 1, "lessonName": null, "subjectId": null, "subjectLabel": null, "room": null, "teacher": null, "timeStart": null, "timeEnd": null } ] } ] }}List study blocks
Study time the user planned on their calendar, by start time, for a range of days in their time zone (today and the next 6 by default, at most 62 days).
Required scopes: schedule:view
AuthorizationBearer <token>from?stringFirst day (YYYY-MM-DD) in the user's time zone. Defaults to today. At most 62 days before to.
^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))$dateto?stringLast day (YYYY-MM-DD), inclusive. Defaults to 6 days after from.
^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))$dateOK
application/json- response
data*array<>curl -X GET 'https://workroad.app/api/v1/study-blocks' \ -H 'Authorization: Bearer $WORKROAD_API_KEY'{ "data": [ { "id": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d", "title": "Spanish verbs", "planned": false, "activity": null, "assessmentId": null, "doneAt": null, "planKind": null, "planCardIds": [], "startsAt": "2026-02-02T17:00:00.000Z", "endsAt": "2026-02-02T17:45:00.000Z", "date": "2026-02-02", "timeZone": "Europe/London", "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f", "taskId": null, "setId": "7e6d5c4b-3a2f-4e1d-8c0b-9a8f7e6d5c4b", "createdAt": "2026-01-15T09:30:00.000Z", "updatedAt": "2026-02-01T12:00:00.000Z" } ]}Plan a study block
Puts study time on the user's calendar, the way the planning copilot does: a title, a future start and end in the user's time zone, and optionally the subject, task or study set it is for. The block can't overlap a lesson (including external calendars, read live) or another block, and can't end after a linked task's due day. Find free time first with getToday or getScheduleWeek.
Required scopes: schedule:create
AuthorizationBearer <token>Idempotency-Key?stringA client-generated key (1 to 255 visible ASCII characters) that makes this POST safe to retry. A retry with the same key and body replays the original response; the same key with a different body returns 409 idempotency_conflict. Stored for 24 hours.
^[\x21-\x7E]{1,255}$1 <= length <= 255application/json- body
title*stringWhat the user studies, 1-120 characters.
1 <= length <= 120startsAt*stringWhen the block starts, in the future. ISO timestamp with Z or an offset, such as 2026-10-21T16:00:00+01:00.
^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$date-timeendsAt*stringWhen it ends: 5 minutes to 8 hours after startsAt, on the same day in the user's time zone. ISO timestamp with Z or an offset, such as 2026-10-21T16:00:00+01:00.
^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$date-timesubjectId?|The subject studied. Null clears it.
taskId?|The task this block works on. The block must end by the task's due day. Null clears it.
setId?|The study set to practise in the block. Null clears it.
Created
application/json- response
data*curl -X POST 'https://workroad.app/api/v1/study-blocks' \ -H 'Authorization: Bearer $WORKROAD_API_KEY' \ -H 'Content-Type: application/json' \ -d '{ "title": "string", "startsAt": "2019-08-24T14:15:22Z", "endsAt": "2019-08-24T14:15:22Z"}'{ "data": { "id": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d", "title": "Spanish verbs", "planned": false, "activity": null, "assessmentId": null, "doneAt": null, "planKind": null, "planCardIds": [], "startsAt": "2026-02-02T17:00:00.000Z", "endsAt": "2026-02-02T17:45:00.000Z", "date": "2026-02-02", "timeZone": "Europe/London", "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f", "taskId": null, "setId": "7e6d5c4b-3a2f-4e1d-8c0b-9a8f7e6d5c4b", "createdAt": "2026-01-15T09:30:00.000Z", "updatedAt": "2026-02-01T12:00:00.000Z" }}Update a study block
Moves, renames or relinks a planned block. Fields you leave out keep their value, and the whole block is checked again as if it were new, so a block already in the past can't be edited.
Required scopes: schedule:update
AuthorizationBearer <token>studyBlockId*stringStudy block id, scoped to the key's user.
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$uuidapplication/json- body
Send only the fields to change. The block is checked again as a whole.
title?stringWhat the user studies, 1-120 characters.
1 <= length <= 120startsAt?stringNew start, in the future. ISO timestamp with Z or an offset, such as 2026-10-21T16:00:00+01:00.
^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$date-timeendsAt?stringNew end. ISO timestamp with Z or an offset, such as 2026-10-21T16:00:00+01:00.
^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$date-timesubjectId?|The subject studied. Null clears it.
taskId?|The task this block works on. The block must end by the task's due day. Null clears it.
setId?|The study set to practise in the block. Null clears it.
OK
application/json- response
data*curl -X PATCH 'https://workroad.app/api/v1/study-blocks/497f6eca-6276-4993-bfeb-53cbbbba6f08' \ -H 'Authorization: Bearer $WORKROAD_API_KEY' \ -H 'Content-Type: application/json' \ -d '{}'{ "data": { "id": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d", "title": "Spanish verbs", "planned": false, "activity": null, "assessmentId": null, "doneAt": null, "planKind": null, "planCardIds": [], "startsAt": "2026-02-02T17:00:00.000Z", "endsAt": "2026-02-02T17:45:00.000Z", "date": "2026-02-02", "timeZone": "Europe/London", "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f", "taskId": null, "setId": "7e6d5c4b-3a2f-4e1d-8c0b-9a8f7e6d5c4b", "createdAt": "2026-01-15T09:30:00.000Z", "updatedAt": "2026-02-01T12:00:00.000Z" }}Delete a study block
Removes a planned block from the user's calendar.
Required scopes: schedule:delete
AuthorizationBearer <token>studyBlockId*stringStudy block id, scoped to the key's user.
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$uuidOK
application/json- response
data*curl -X DELETE 'https://workroad.app/api/v1/study-blocks/497f6eca-6276-4993-bfeb-53cbbbba6f08' \ -H 'Authorization: Bearer $WORKROAD_API_KEY'{ "data": { "id": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d", "deleted": true }}Get today's study overview
Answers "what should I study now?" in one call. Returns the flashcards due now by subject and set, today's goal, streak and new-card limit, overdue and upcoming tasks, today's lessons and planned study blocks, and suggestions: what to do next, most useful first, each naming the tool and arguments that act on it. Each section needs its own scope (activity:view, tasks:view, schedule:view) and is null without it. Lessons from external calendars are read live and can take a few seconds.
Required scopes: activity:view or tasks:view or schedule:view
AuthorizationBearer <token>OK
application/json- response
data*curl -X GET 'https://workroad.app/api/v1/today' \ -H 'Authorization: Bearer $WORKROAD_API_KEY'{ "data": { "date": "2026-02-02", "timeZone": "Europe/London", "serverTime": "2026-02-02T08:15:00.000Z", "study": { "dueNow": 12, "dueLaterToday": 3, "newCards": 40, "goal": { "kind": "cards", "target": 20, "progress": 4, "reached": false, "streakDays": 6, "longestStreakDays": 14 }, "limits": { "newRemaining": 46, "reviewsRemaining": null }, "studyDayEndsAt": "2026-02-03T04:00:00.000Z", "subjects": [ { "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f", "name": "Chemistry", "dueNow": 12, "overdue": 2, "dueLaterToday": 3, "newCards": 40 } ], "sets": [ { "setId": "7e6d5c4b-3a2f-4e1d-8c0b-9a8f7e6d5c4b", "name": "Spanish verbs", "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f", "dueNow": 12 } ] }, "tasks": { "overdue": [], "overdueTotal": 0, "dueSoon": [ { "id": "4d3c2b1a-0f9e-4d8c-9b7a-6f5e4d3c2b1a", "name": "Finish lab report", "type": "Generic", "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f", "subjectName": "Chemistry", "dueDate": "2026-02-10T17:00:00.000Z", "dueDay": "2026-02-10", "exam": false } ], "dueSoonTotal": 1 }, "lessons": { "occurrences": [ { "scheduleId": "4d3c2b1a-0f9e-4d8c-8b7a-6f5e4d3c2b1a", "date": "2026-02-02", "endDate": null, "time": { "kind": "timed", "startMinute": 540, "endMinute": 600 }, "timeZone": null, "title": "Biology", "subtitle": "S2 · Dr Patel" } ], "issues": [] }, "studyBlocks": [], "suggestions": [ { "kind": "practice", "message": "12 flashcards are due now, most of them in Chemistry.", "tool": "startPracticeSession", "arguments": { "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f" } }, { "kind": "lesson", "message": "Next lesson: Biology at 09:00.", "tool": null, "arguments": {} } ] }}List calendar feeds
Lists the user's calendar feed metadata, oldest first. Secret subscription URLs are not returned; each URL is shown only when it is issued.
A user can have at most 10 feeds. This list is not paginated. Create feeds or rotate their URLs in the app under Settings → Calendar.
Required scopes: settings:view and schedule:view
AuthorizationBearer <token>OK
application/json- response
data*array<>curl -X GET 'https://workroad.app/api/v1/calendar-feeds' \ -H 'Authorization: Bearer $WORKROAD_API_KEY'{ "data": [ { "id": "kq0V3mJ8yQe7Lw2nR5tXcB1dF4gH6jK9", "schedules": [ { "id": "8c2b5a1e-7d4f-4e3a-9b6c-1f0e2d3c4b5a", "name": "Year 12 timetable" } ], "createdAt": "2026-01-15T09:30:00.000Z", "lastUsedAt": "2026-02-01T12:00:00.000Z" } ]}Delete a calendar feed
The address stops working at once. Calendar apps subscribed to it keep the events they already have but get no updates.
Required scopes: settings:update
AuthorizationBearer <token>feedId*stringCalendar feed id, from the list.
1 <= length <= 128OK
application/json- response
data*curl -X DELETE 'https://workroad.app/api/v1/calendar-feeds/string' \ -H 'Authorization: Bearer $WORKROAD_API_KEY'{ "data": { "id": "kq0V3mJ8yQe7Lw2nR5tXcB1dF4gH6jK9", "deleted": true }}