Scheduling API
The Scheduling API reads the schedule calendar Runnit builds from task assignments: who is booked on what, when, and for how long. It also manages organisation closure dates (public holidays and office closures), per-user blocked-out dates (leave and appointments), both of which the scheduler skips when it books work, and recurring working patterns, which set how long each person’s working days actually are.
Unscheduled project tasks do not appear here. They remain available through project and task APIs until a planned start creates schedule entries. This keeps backlog capture separate from resource capacity.
Planning entries are included. Each schedule row carries projectStatus and
bookingState (tentative, held, or confirmed). Use all entries for capacity
planning. A personal calendar should show only rows with a projectStatus other
than draft and a bookingState of confirmed. This also protects against older
confirmed rows on a draft project. Google/Microsoft sync and calendar feeds apply
the same rule. For the user’s actionable task list, use
GET /api/v1/tasks/my.
Deleted tasks do not appear, even if legacy schedule data still refers to them.
Authentication: every endpoint requires an API key. See API Keys & Authentication.
GET /api/v1/schedule
Section titled “GET /api/v1/schedule”Get the schedule entries for one or more users. Each entry is one booked segment of a task on a user’s calendar, enriched with task, project, and assignee details. Entries for projects you cannot see are filtered out.
Any organisation member with schedule access can call this. Every requested user must share an organisation with you.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
| userIds | string | Yes | Comma-separated list of user IDs to fetch schedules for. |
The endpoint returns each user’s full schedule; there is no date-range filter. Filter client-side if you only need a window.
Segment start and end values are absolute timestamps. Convert them to the
organisation timezone when displaying a calendar day. Shared tasks have a booking
for each assigned person; use one person’s segments for a per-person daily plan.
This user-filtered endpoint does not return unassigned bookings. The same reads
are available through MCP call_public_api with the same authority.
Example request
curl 'https://api.runnit.io/api/v1/schedule?userIds=<user-uuid-1>,<user-uuid-2>' \ -H 'Authorization: Bearer rnk_your_key'Response
A JSON array of schedule entries:
[ { "id": "8fd2b3c4-...", "taskId": "4be2d871-...", "taskNumber": "TASK-014", "projectId": "1f0e9a52-...", "projectName": "Winter Launch", "projectStatus": "active", "clientName": "Northwind", "name": "Draft homepage copy", "description": "First pass for internal review.", "status": "scheduled", "priority": "high", "assignedToId": "d3a6f0c1-...", "assignedTo": { "id": "d3a6f0c1-...", "firstName": "Alex", "lastName": "Chen", "displayName": "Alex Chen", "avatarUrl": "https://...", "currentTitle": "Creative Director" }, "plannedStartDate": "2026-07-13T09:00:00.000Z", "plannedEndDate": "2026-07-13T15:30:00.000Z", "durationMinutes": 360, "allocatedHours": 6, "estimatedHours": 6, "segmentIndex": 0, "totalSegments": 1, "isOverdue": false, "daysOverdue": 0, "bookingState": "confirmed", "tags": ["copy", "homepage"], "metadata": {} }]A task that spans several days appears as several entries; segmentIndex and
totalSegments tell you which piece you are looking at. plannedStartDate
and plannedEndDate are the segment’s start and end times, not the task’s
overall dates. status is one of pending, scheduled, in_progress,
completed, cancelled; priority is one of low, medium, high,
critical. clientName, description, taskNumber, assignedTo,
allocatedHours, estimatedHours, and tags can be null or absent.
An empty array means the requested users have no schedule entries. It does not mean their accessible projects have no backlog tasks.
Errors
400ifuserIdsis missing, or you have no active organisation.403if any requested user does not share an organisation with you, or none of the entries belong to projects you can see.
GET /api/v1/schedule/milestones
Section titled “GET /api/v1/schedule/milestones”Get the milestones of every project that appears in the selected users’ schedules. Useful for drawing key dates alongside the schedule calendar.
Any organisation member with schedule access can call this. Every requested user must share an organisation with you.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
| userIds | string | Yes | Comma-separated list of user IDs. |
Example request
curl 'https://api.runnit.io/api/v1/schedule/milestones?userIds=<user-uuid-1>,<user-uuid-2>' \ -H 'Authorization: Bearer rnk_your_key'Response
A JSON array of milestones:
[ { "id": "0a4f6c33-...", "projectId": "1f0e9a52-...", "projectName": "Winter Launch", "name": "Client sign-off", "type": "custom", "targetDate": "2026-08-15", "isFirm": true, "description": "Final approval from the client team.", "sourceKey": null, "orderIndex": 0, "metadata": {} }]targetDate is a plain YYYY-MM-DD date. description and sourceKey can
be null. An empty array means the selected users have no scheduled work, or
their projects have no milestones.
Errors
400ifuserIdsis missing, or you have no active organisation.403if any requested user does not share an organisation with you.
GET /api/v1/schedule/tasks/:taskId
Section titled “GET /api/v1/schedule/tasks/:taskId”Get a task with its project, assigned user, and subtasks in one call. This is the detail view behind a schedule entry.
Any organisation member with schedule access can call this, for tasks in projects their organisation can see.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| taskId | UUID | Yes | Task ID. |
Example request
curl https://api.runnit.io/api/v1/schedule/tasks/<task-id> \ -H 'Authorization: Bearer rnk_your_key'Response
The task object with related records attached:
{ "id": "4be2d871-...", "projectId": "1f0e9a52-...", "name": "Draft homepage copy", "status": "scheduled", "priority": "high", "progress": 0, "assignedToId": "d3a6f0c1-...", "estimatedHours": 6, "plannedStartDate": "2026-07-13T00:00:00.000Z", "plannedEndDate": "2026-07-14T00:00:00.000Z", "project": { "id": "1f0e9a52-...", "name": "Winter Launch", "status": "active" }, "assignedUser": { "id": "d3a6f0c1-...", "displayName": "Alex Chen", "email": "alex@example.com" }, "subtasks": []}assignedUser is null for unassigned tasks. The task and project include
their other standard fields.
Errors
404if the task or its project does not exist.403if the project belongs to an organisation you cannot access.
Closure dates
Section titled “Closure dates”Closure dates are days the organisation does not work: public holidays and office closures. The scheduler skips them when booking task time. All closure-date endpoints operate on your own organisation.
A closure date looks like this:
{ "id": "e19c50f7-...", "organizationId": "8c2d41b7-...", "date": "2026-12-25", "name": "Christmas Day", "description": "Public holiday", "type": "holiday", "isRecurring": true, "createdAt": "2026-01-05T00:00:00.000Z", "updatedAt": "2026-01-05T00:00:00.000Z", "createdById": "d3a6f0c1-..."}type is one of holiday, closure, other. description and
createdById can be null.
Reading closure dates is open to any organisation member. Creating, updating, and deleting them requires permission to manage organisation settings, which organisation admins and owners have.
GET /api/v1/closure-dates
Section titled “GET /api/v1/closure-dates”List your organisation’s closure dates, optionally filtered to a date range.
Any organisation member can call this.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
| startDate | ISO 8601 date | No | Start of the range. Only applied when endDate is also supplied. |
| endDate | ISO 8601 date | No | End of the range. Only applied when startDate is also supplied. |
Example request
curl 'https://api.runnit.io/api/v1/closure-dates?startDate=2026-12-01&endDate=2027-01-31' \ -H 'Authorization: Bearer rnk_your_key'Response
A JSON array of closure dates in the shape shown above.
Errors
404if your account is not part of any organisation.
GET /api/v1/closure-dates/check
Section titled “GET /api/v1/closure-dates/check”Check whether a specific date is a closure date for your organisation.
Any organisation member can call this.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
| date | ISO 8601 date | Yes | The date to check. |
Example request
curl 'https://api.runnit.io/api/v1/closure-dates/check?date=2026-12-25' \ -H 'Authorization: Bearer rnk_your_key'Response
{ "isClosureDate": true }Errors
400ifdateis missing or not a valid date.
GET /api/v1/closure-dates/:id
Section titled “GET /api/v1/closure-dates/:id”Get a single closure date by ID.
Any organisation member can call this, for closure dates in their own organisation.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | UUID | Yes | Closure date ID. |
Example request
curl https://api.runnit.io/api/v1/closure-dates/<closure-date-id> \ -H 'Authorization: Bearer rnk_your_key'Response
A single closure date object in the shape shown above.
Errors
404if the closure date does not exist or belongs to a different organisation.
POST /api/v1/closure-dates
Section titled “POST /api/v1/closure-dates”Create a closure date.
Requires permission to manage organisation settings (organisation admins and owners).
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| date | ISO 8601 date | Yes | The closure date. |
| name | string | Yes | Label, for example “Christmas Day”. |
| description | string | No | Extra detail. |
| type | string | No | holiday, closure, or other. |
| isRecurring | boolean | No | Whether the closure repeats every year. |
Example request
curl -X POST https://api.runnit.io/api/v1/closure-dates \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "date": "2026-12-25", "name": "Christmas Day", "type": "holiday", "isRecurring": true }'Response
Returns 201 with the created closure date object.
Errors
400ifdateornameis missing, ortypeis invalid.403if you cannot manage organisation settings.404if your account is not part of any organisation.
POST /api/v1/closure-dates/bulk
Section titled “POST /api/v1/closure-dates/bulk”Create several closure dates in one request, for example a full public holiday calendar.
Requires permission to manage organisation settings (organisation admins and owners).
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| dates | array | Yes | At least one entry. |
| dates[].date | ISO 8601 date | Yes | The closure date. |
| dates[].name | string | Yes | Label. |
| dates[].description | string | No | Extra detail. |
| dates[].type | string | No | holiday, closure, or other. |
| dates[].isRecurring | boolean | No | Whether the closure repeats every year. |
Example request
curl -X POST https://api.runnit.io/api/v1/closure-dates/bulk \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "dates": [ { "date": "2026-12-25", "name": "Christmas Day", "type": "holiday", "isRecurring": true }, { "date": "2026-12-28", "name": "Boxing Day (observed)", "type": "holiday", "isRecurring": false } ] }'Response
Returns 201 with a JSON array of the created closure date objects.
Errors
400ifdatesis empty, or any entry is missingdateorname.403if you cannot manage organisation settings.
PATCH /api/v1/closure-dates/:id
Section titled “PATCH /api/v1/closure-dates/:id”Update a closure date. Send only the fields you want to change.
Requires permission to manage organisation settings (organisation admins and owners).
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | UUID | Yes | Closure date ID. |
Request body (all optional)
| Name | Type | Required | Description |
|---|---|---|---|
| date | ISO 8601 date | No | New date. |
| name | string | No | New label. |
| description | string | No | Extra detail. |
| type | string | No | holiday, closure, or other. |
| isRecurring | boolean | No | Whether the closure repeats every year. |
Example request
curl -X PATCH https://api.runnit.io/api/v1/closure-dates/<closure-date-id> \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "isRecurring": false }'Response
The updated closure date object.
Errors
400if a field fails validation.403if you cannot manage organisation settings.404if the closure date does not exist or belongs to a different organisation.
DELETE /api/v1/closure-dates/:id
Section titled “DELETE /api/v1/closure-dates/:id”Delete a closure date.
Requires permission to manage organisation settings (organisation admins and owners).
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | UUID | Yes | Closure date ID. |
Example request
curl -X DELETE https://api.runnit.io/api/v1/closure-dates/<closure-date-id> \ -H 'Authorization: Bearer rnk_your_key'Response
Returns 204 with no body.
Errors
403if you cannot manage organisation settings.404if the closure date does not exist or belongs to a different organisation.
Blocked dates
Section titled “Blocked dates”Blocked-out dates are days one user is unavailable: leave, appointments, or anything else. A block can cover the whole of each day or only part of it. The scheduler treats whole-day blocks like closure dates scoped to that person and never books work on them; a partially blocked day keeps working capacity equal to that day’s capacity minus the blocked hours. That day’s capacity comes from the person’s working pattern when they have one, and from the organisation’s daily capacity otherwise. Four hours of leave on a four-hour day therefore removes the whole day. These endpoints let an external system, such as an HR platform, sync approved leave into Runnit, including half-day leave sent directly as hours, so capacity planning stays accurate. All blocked-date endpoints operate on your own organisation.
A blocked-date range looks like this:
{ "id": "5b7d20aa-...", "organizationId": "8c2d41b7-...", "userId": "d3a6f0c1-...", "startDate": "2027-09-06", "endDate": "2027-09-10", "reason": "Synced from HR", "type": "leave", "hoursUnavailable": null, "createdAt": "2027-08-30T00:00:00.000Z", "updatedAt": "2027-08-30T00:00:00.000Z", "createdById": "d3a6f0c1-..."}type is one of leave, appointment, other (the default). reason and
createdById can be null.
hoursUnavailable is null for a whole-day block. A number makes the block
partial: that many hours are unavailable on each day of the range. Multiple
partial blocks covering the same day add together, and when the remaining
capacity reaches zero the day behaves exactly like a whole-day block. Values
at or above that day’s capacity also behave as whole-day blocks. The stored
hours are never rewritten, so a later change to the organisation’s daily
capacity, or to the person’s working pattern, re-evaluates every partial day
automatically.
Permissions. An API key acts as its owning user. Everyone can manage their own blocked dates. Reading another user’s blocked dates requires schedule view access; creating, changing, or removing another user’s requires schedule management access (managers and above by default). When someone else changes a user’s blocked dates, the affected user is notified and the change is recorded in the permission audit log.
Reason privacy. The reason field is private. It is returned only for
your own blocks, or when your key’s user has schedule management access;
otherwise it is null in responses.
Validation. startDate and endDate must be plain YYYY-MM-DD calendar
dates (timestamps are rejected), endDate must be on or after startDate,
the inclusive range can span at most 366 days, and the target user must be an
active member of your organisation. hoursUnavailable, when sent, must be
greater than 0 and less than 24.
Conflict resolution. Creating or extending a block over days that already
carry scheduled work does not fail. A whole-day block conflicts with any work
on its days; a partial block conflicts only on days where the user’s total
booked hours exceed the remaining capacity. The resolution field decides
what happens to conflicting work: flag (the default) leaves the bookings in
place and raises conflict warnings; replan moves the affected tasks so each
partially blocked day holds at most the remaining hours and the excess moves
to the next available working days, shifting dependent tasks with them and
notifying the assignees. Use the preview endpoint first when you want to
inspect the impact before writing.
GET /api/v1/blocked-dates
Section titled “GET /api/v1/blocked-dates”List blocked-date ranges, defaulting to the key’s own user.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
| userIds | string | No | Comma-separated user IDs. Omit for your own blocks. Other users require schedule view access. |
| startDate | ISO 8601 date | No | Only ranges that end on or after this date. |
| endDate | ISO 8601 date | No | Only ranges that start on or before this date. |
Example request
curl 'https://api.runnit.io/api/v1/blocked-dates?userIds=<user-uuid>&startDate=2027-09-01&endDate=2027-09-30' \ -H 'Authorization: Bearer rnk_your_key'Response
A JSON array of blocked-date ranges in the shape shown above. reason is null
unless the block is your own or your user has schedule management access.
Errors
403if you request another user’s blocks without schedule view access.
POST /api/v1/blocked-dates/preview
Section titled “POST /api/v1/blocked-dates/preview”Get a read-only conflict report for a prospective block without creating or
changing anything. Useful before a bulk leave sync, or before editing an
existing block (send excludeBlockId).
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| userId | UUID | Yes | The user the block is for. |
| startDate | ISO 8601 date | Yes | First blocked day. |
| endDate | ISO 8601 date | Yes | Last blocked day, inclusive. |
| hoursUnavailable | number | No | Hours per day for a partial block. Omit for whole days. |
| excludeBlockId | UUID | No | When previewing an edit to an existing block, its ID. The preview then evaluates the values in this request as that block’s replacement instead of stacking them on top of the existing entry. |
Example request
curl -X POST https://api.runnit.io/api/v1/blocked-dates/preview \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{"userId": "<user-uuid>", "startDate": "2027-09-06", "endDate": "2027-09-10", "hoursUnavailable": 4}'Response
{ "conflicts": [ { "taskId": "4be2d871-...", "taskName": "Homepage concepts", "projectId": "91c52864-...", "projectName": "Solara website refresh", "dates": ["2027-09-06", "2027-09-07"], "hours": 9, "bookingState": "confirmed", "scheduleMode": "auto", "replannable": true, "overflowByDate": { "2027-09-06": 1, "2027-09-07": 0.5 } } ], "totalHours": 9, "affectedTaskCount": 1, "partialDayDetails": [ { "date": "2027-09-06", "blockedHours": 4, "remainingCapacity": 4, "bookedHours": 5, "overflowHours": 1 } ]}bookingState is tentative, held, or confirmed. replannable is false
when the task can’t be moved automatically.
For partial blocks, partialDayDetails breaks down each affected day: the
blocked hours, the remaining capacity, the user’s booked hours, and the hours
over the remaining capacity. Each conflict’s overflowByDate maps the dates
where that task contributes to the overflow to the hours over. Both fields are
absent for whole-day evaluations with no partial overflow.
Errors
400for invalid dates or a target who is not an active member.403if the target is another user and you lack schedule management access.
POST /api/v1/blocked-dates
Section titled “POST /api/v1/blocked-dates”Create a blocked-date range, then resolve any conflicts with scheduled work
according to resolution.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| userId | UUID | Yes | The user the block is for. Your own user unless you have schedule management access. |
| startDate | ISO 8601 date | Yes | First blocked day. |
| endDate | ISO 8601 date | Yes | Last blocked day, inclusive. Range spans at most 366 days. |
| reason | string | No | Free text, up to 500 characters. Private to the user and schedule managers. |
| type | string | No | leave, appointment, or other (default). |
| hoursUnavailable | number | No | Hours per day for a partial block, greater than 0 and less than 24. Omit or send null for whole days. For half-day leave, send the hours directly. |
| resolution | string | No | flag (default) or replan. |
Example request
curl -X POST https://api.runnit.io/api/v1/blocked-dates \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{"userId": "<user-uuid>", "startDate": "2027-09-06", "endDate": "2027-09-10", "type": "leave", "reason": "Synced from HR", "resolution": "replan"}'Response
Returns 201 with the created block and the outcome of conflict resolution:
{ "block": { "id": "5b7d20aa-...", "startDate": "2027-09-06", "...": "..." }, "replan": { "replanned": [ { "taskId": "4be2d871-...", "taskName": "Homepage concepts", "fromStart": "2027-09-06", "toStart": "2027-09-13", "scheduledPastDueDate": false } ], "cascaded": { "shifted": [], "skipped": [], "evaluated": 1 }, "flagged": [], "errors": [], "residualConflicts": [] }}With resolution: "flag" (or when it is omitted), the response carries a
conflicts report in the preview shape instead of replan. When there is no
scheduled work on the dates, both are empty reports.
If the block is created but conflict resolution fails, the response includes a
resolutionError message instead. The block exists at that point: retry with
POST /api/v1/blocked-dates/:id/replan rather than re-creating it, which
would duplicate the block.
Errors
400for invalid dates, a range over 366 days, or a target who is not an active organisation member.403if the target is another user and you lack schedule management access.
POST /api/v1/blocked-dates/:id/replan
Section titled “POST /api/v1/blocked-dates/:id/replan”Retry replanning whatever still sits on this block’s dates. Idempotent: safe
to call again after a partial replan failure or a resolutionError.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | UUID | Yes | Blocked-date range ID. |
Example request
curl -X POST https://api.runnit.io/api/v1/blocked-dates/<block-id>/replan \ -H 'Authorization: Bearer rnk_your_key'Response
{ "block": ..., "replan": ... } with the replan result in the shape shown
above.
Errors
403if the block belongs to another user and you lack schedule management access.404if the block does not exist or belongs to a different organisation.
PATCH /api/v1/blocked-dates/:id
Section titled “PATCH /api/v1/blocked-dates/:id”Update a blocked-date range. Send only the fields you want to change. Days
where the block became stricter (newly covered days, days that changed from
partial to whole, or partial hours that increased) are put through conflict
resolution using resolution (default flag); loosening a block frees
capacity without re-resolving anything. A resolution failure is reported as
resolutionError on the response, not as a failed update, and can be retried
with the replan endpoint.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| startDate | ISO 8601 date | No | New first day. |
| endDate | ISO 8601 date | No | New last day, inclusive. |
| reason | string | No | Replacement reason, up to 500 characters. |
| type | string | No | leave, appointment, or other. |
| hoursUnavailable | number | No | Omit to leave the block’s hours unchanged. Send a number (greater than 0, less than 24) to set a partial block, or an explicit null to convert the block to whole days. |
| resolution | string | No | flag (default) or replan for newly covered dates. |
Example request
curl -X PATCH https://api.runnit.io/api/v1/blocked-dates/<block-id> \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{"endDate": "2027-09-14", "resolution": "replan"}'Response
{ "block": ... } with the updated range, plus conflicts, replan, or
resolutionError when the change newly covered scheduled days.
Errors
400for invalid dates or a range over 366 days.403if the block belongs to another user and you lack schedule management access.404if the block does not exist or belongs to a different organisation.
DELETE /api/v1/blocked-dates/:id
Section titled “DELETE /api/v1/blocked-dates/:id”Delete a blocked-date range. The days become available for scheduling again, but work that was replanned away does not move earlier automatically; replan the affected tasks if the freed time should be used.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | UUID | Yes | Blocked-date range ID. |
Example request
curl -X DELETE https://api.runnit.io/api/v1/blocked-dates/<block-id> \ -H 'Authorization: Bearer rnk_your_key'Response
Returns 204 with no body.
Errors
403if the block belongs to another user and you lack schedule management access.404if the block does not exist or belongs to a different organisation.
Working patterns
Section titled “Working patterns”A working pattern is how long each of a person’s weekdays is. Somebody who works Mondays and half of Friday can say so, and the scheduler books work accordingly.
A pattern is the baseline capacity that leave is subtracted from, not another subtraction. Someone with no pattern is assumed to work the organisation’s daily capacity every weekday, which is how Runnit behaved before patterns existed.
A pattern looks like this:
{ "id": "f5d59857-...", "organizationId": "8c2d41b7-...", "userId": "d3a6f0c1-...", "effectiveFrom": "2027-09-06", "effectiveTo": null, "hoursByWeekday": [8, 0, 0, 0, 4, 0, 0], "note": "Synced from HR", "weeklyHours": 12, "createdAt": "2027-09-01T00:00:00.000Z", "updatedAt": "2027-09-01T00:00:00.000Z", "createdById": "d3a6f0c1-..."}hoursByWeekday is seven numbers starting on Monday. A zero means the person
does not work that day. The example above is eight hours on Monday and four on
Friday.
Effective dating. Setting a pattern does not edit the one before it. The
previous pattern is closed on the day before the new effectiveFrom, so last
month’s capacity does not change when someone goes part time. effectiveTo is
null on the pattern currently in force.
Permissions. An API key acts as its owning user. Everyone can manage their
own pattern. Reading another user’s requires schedule view access; setting or
removing another user’s requires schedule management access. The note is
private in the same way a blocked date’s reason is: it is returned for your own
pattern, or to a caller with schedule management access, and is null otherwise.
Validation. hoursByWeekday must be exactly seven numbers, each from 0 to
24, and at least one must be greater than zero. A week of all zeroes is rejected,
because that person could never be scheduled and nothing would say why.
effectiveFrom must be a plain YYYY-MM-DD calendar date.
GET /api/v1/work-patterns
Section titled “GET /api/v1/work-patterns”Read a person’s current pattern and their earlier ones. Defaults to the key’s own user.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
| userId | UUID | No | Whose pattern to read. Defaults to the key’s own user. |
Example request
curl 'https://api.runnit.io/api/v1/work-patterns?userId=<user-uuid>' \ -H 'Authorization: Bearer rnk_your_key'Response
{ "success": true, "userId": "d3a6f0c1-...", "organizationDefaultHours": 8, "current": { "...": "the pattern in force, or null" }, "patterns": [{ "...": "every pattern, newest first" }]}current is null when the person has no pattern, which means every weekday uses
organizationDefaultHours.
Errors
403if you ask for another user without schedule view access.
GET /api/v1/work-patterns/capacity
Section titled “GET /api/v1/work-patterns/capacity”The hours each date actually holds, for one or more people. This is the endpoint a planning integration usually wants: it answers “how much can this person do on this day” without you having to apply the pattern yourself.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
| from | ISO 8601 date | Yes | First date in the window. |
| to | ISO 8601 date | Yes | Last date in the window, inclusive. At most 400 days after from. |
| userIds | Comma-separated UUIDs | No | Whose capacity to return. Defaults to the key’s own user. Up to 200 users. |
Example request
curl 'https://api.runnit.io/api/v1/work-patterns/capacity?userIds=<user-uuid>&from=2027-09-06&to=2027-09-12' \ -H 'Authorization: Bearer rnk_your_key'Response
{ "success": true, "organizationDefaultHours": 8, "overrides": { "d3a6f0c1-...": { "2027-09-07": 0, "2027-09-08": 0, "2027-09-09": 0, "2027-09-10": 4, "2027-09-11": 0, "2027-09-12": 0 } }}overrides holds only the dates that differ from organizationDefaultHours.
Any date not listed uses the default, which is why Monday is absent from the
example above. A person with no pattern has no entry at all.
Errors
400iftois beforefrom, or the range covers more than 400 days. The message says how many days you asked for.403if you ask for another user without schedule view access.
POST /api/v1/work-patterns
Section titled “POST /api/v1/work-patterns”Set a pattern from a date onwards.
Body parameters
| Name | Type | Required | Description |
|---|---|---|---|
| userId | UUID | No | Whose pattern to set. Defaults to the key’s own user. |
| effectiveFrom | ISO 8601 date | Yes | First day the new pattern applies. |
| hoursByWeekday | Array of 7 numbers | Yes | Hours per weekday, starting Monday. 0 means the person does not work that day. |
| note | String | No | A private note, such as where the change came from. |
Example request
curl -X POST https://api.runnit.io/api/v1/work-patterns \ -H 'Authorization: Bearer rnk_your_key' \ -H 'Content-Type: application/json' \ -d '{ "userId": "<user-uuid>", "effectiveFrom": "2027-09-06", "hoursByWeekday": [8, 0, 0, 0, 4, 0, 0], "note": "Synced from HR" }'Response
201 with the created pattern under pattern.
Errors
400ifhoursByWeekdayis not seven valid numbers, or is all zeroes.403if you set another user’s pattern without schedule management access.
DELETE /api/v1/work-patterns/:id
Section titled “DELETE /api/v1/work-patterns/:id”Remove a pattern. Removing the one currently in force reopens the pattern before it, so nobody silently drops back to the organisation default when an earlier pattern exists.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
| id | UUID | Yes | Pattern ID. |
Example request
curl -X DELETE https://api.runnit.io/api/v1/work-patterns/<pattern-id> \ -H 'Authorization: Bearer rnk_your_key'Response
{ "success": true }Errors
403if the pattern belongs to another user and you lack schedule management access.404if the pattern does not exist or belongs to a different organisation.
Calendar connections, read only
Section titled “Calendar connections, read only”Runnit can read a person’s Google or Outlook calendar and can put their scheduled work into it. The endpoints below let you see the state of that. See Calendar Sync for what the feature does.
Microsoft connections use the main Outlook calendar. The same read routes and
permissions apply to Google and Microsoft through REST and MCP
call_public_api. Calendar consent and configuration still require the browser.
busyOnlyIsTransportLevel is false for both Microsoft account types.
Microsoft may transmit meeting details; Runnit discards them for availability-only
sync. This field and privacy behaviour are the same through REST and MCP.
For Microsoft, live notifications require an approved calendar read permission;
an availability-only connection can remain healthy through regular polling.
notificationsLive: false alone does not mean the connection has failed. Inspect
the last read time and errors as well. Work written to Outlook shares the main
calendar, but only Runnit’s own entries can change its scheduled work.
GET /api/v1/calendar/accounts
Section titled “GET /api/v1/calendar/accounts”The calendars the key’s own user has connected, and what this workspace does with each. Scoped to that user: nobody reads anybody else’s connections through this, including an administrator.
GET /api/v1/calendar/events
Section titled “GET /api/v1/calendar/events”The meetings behind the schedule, already filtered by what this workspace is allowed to see.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
| from | ISO 8601 date | Yes | First date. |
| to | ISO 8601 date | Yes | Last date, inclusive. |
| userIds | Comma-separated UUIDs | No | Defaults to the key’s own user. Up to 100. Reading another person needs schedule view access. |
Each event carries startsAt, endsAt, isAllDay and showAs. title is
null when the workspace reads availability only, and titleWithheld is true so
you can tell that apart from a meeting with no name.
The response also carries affectsCapacity, which says whether these meetings
currently reduce the hours Runnit will book. Read it rather than assuming.
GET /api/v1/calendar/health
Section titled “GET /api/v1/calendar/health”Per connection: when it was last read and last written, the last error, how many calendars are in error, how stale it is, and whether live change notifications are running.
That last field is the one worth alerting on. A connection whose notifications have lapsed still syncs on a fifteen-minute sweep, so the only symptom is latency and nothing reports it.
Pass scope=organization to see everyone, which needs schedule view or calendar
policy access.
GET /api/v1/calendar/drift
Section titled “GET /api/v1/calendar/drift”Counts of what has fallen out of step across the workspace: entries Runnit believes it created that are no longer there, notification-capable calendars with no live notifications, accounts needing reconnection, mirrored meetings past the retention window, and stale capacity rollups. Requires calendar policy access.
GET /api/v1/calendar/policy
Section titled “GET /api/v1/calendar/policy”The workspace ceiling: whether sync is available at all, whether meeting names may be read, and whether meetings may reduce capacity. Changing it is an in-app operation.
Next steps
Section titled “Next steps”Schedules are built from task assignments. Create and reschedule tasks through the Projects API, and update task status through the Tasks API.