# Business Made · Schedules

Part of AppEngine API Documentation. Full index: https://appengine.appmint.io/documentation.md
## GET /business-made/schedules

**List schedules**

`operationId: ScheduleController_getSchedules`

Work schedules across the org.

#### Signature

```http
GET /business-made/schedules () -> Schedules
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /business-made/schedules/shifts/today`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Schedules |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /business-made/schedules

**Create a schedule**

`operationId: ScheduleController_createSchedule`

Creates a schedule as a draft. Staff do not see it until it is published, so a rota can be built and reworked freely.

#### Signature

```http
POST /business-made/schedules (body) -> The created schedule
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /business-made/schedules/{id}/shifts`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

The schedule to create.

```json
{
  "name": "Week 40",
  "startDate": "2026-09-28",
  "endDate": "2026-10-04",
  "location": "london"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The created schedule |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /business-made/schedules/settings

**Get scheduling settings**

`operationId: ScheduleController_getSchedulingSettings`

The shift definitions (`shiftTemplates`: times, headcount, roles, break, days, computed `paidHours`) and planning rules (`patternType` weekly or cycle, `cycleDays`, `cycleStart`, `planningHorizonDays`). **Scheduling is per location**: with a location its own settings are laid over the org's and `scope` says which applied; `all`/nothing is the rollup.

#### Signature

```http
GET /business-made/schedules/settings (businessLocationId?: string) -> `{ shiftTemplates, patternType, cycleDays, cycleStart, planningHorizonDays, locationId, scope: "org" \| "location", rollup }`
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `businessLocationId` | query | string | — | Location sk or slug; `all` = rollup. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | `{ shiftTemplates, patternType, cycleDays, cycleStart, planningHorizonDays, locationId, scope: "org" \| "location", rollup }` |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /business-made/schedules/settings

**Save scheduling settings**

`operationId: ScheduleController_saveSchedulingSettings`

Saves shift definitions and planning rules — for a location when one is given (query or body `businessLocationId`), otherwise org-wide. Returns the settings as GET shows them.

#### Signature

```http
POST /business-made/schedules/settings (businessLocationId?: string, body) -> The saved settings
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `businessLocationId` | query | string | — |  |

### Request body

```json
{
  "patternType": "weekly",
  "planningHorizonDays": 14,
  "shiftTemplates": [
    {
      "name": "Lunch",
      "startTime": "11:00",
      "endTime": "15:00",
      "headcount": 3,
      "breakMinutes": 15,
      "days": [
        "mon",
        "tue",
        "wed",
        "thu",
        "fri"
      ]
    }
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The saved settings |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /business-made/schedules/month

**A month of coverage**

`operationId: ScheduleController_getMonth`

For every day on the calendar grid (whole weeks around the month): shifts, people, open shifts, how many are needed and how many short, hours, cost, whether it is published, and one `state` (`empty`, `short`, `open`, `covered`). Totals cover the month's own days; only days with a rota count as short.

#### Signature

```http
GET /business-made/schedules/month (month?: string, businessLocationId?: string, q?: string) -> `{ month, gridStart, days:[{date, inMonth, shifts, people, open, needed, short, hours, cost, published, state}], totals:{shifts, open, short, hours, cost, daysScheduled, daysInMonth} }`
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | BAD_MONTH | month must be YYYY-MM | `month` is missing or malformed. | Send YYYY-MM. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `month` | query | string | yes | YYYY-MM. |
| `businessLocationId` | query | string | — |  |
| `q` | query | string | — | Filter by person or shift text. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | `{ month, gridStart, days:[{date, inMonth, shifts, people, open, needed, short, hours, cost, published, state}], totals:{shifts, open, short, hours, cost, daysScheduled, daysInMonth} }` |
| `400` | month must be YYYY-MM — `month` is missing or malformed. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /business-made/schedules/running

**The day as it is going**

`operationId: ScheduleController_getRunning`

Every shift on the date (default today) joined to its clock-ins, with a status decided on the server — upcoming, on, late (with `minutesLate`), no-show, done or open.

#### Signature

```http
GET /business-made/schedules/running (date?: string, businessLocationId?: string) -> `{ date, isToday, asOf, rows:[shift + {status, minutesLate, clockInTime, clockOutTime}], totals:{shifts, on, late, noShow, done, upcoming, open} }`
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `date` | query | string | — |  |
| `businessLocationId` | query | string | — |  |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | `{ date, isToday, asOf, rows:[shift + {status, minutesLate, clockInTime, clockOutTime}], totals:{shifts, on, late, noShow, done, upcoming, open} }` |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /business-made/schedules/availability/{employeeId}

**Get a person's availability**

`operationId: ScheduleController_getAvailability`

What the person said they can work (bm_availability): weekly pattern, exceptions, maximum hours a week, notes. Returns null when nothing is recorded.

#### Signature

```http
GET /business-made/schedules/availability/{employeeId} (employeeId: string) -> The availability record, or null
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `employeeId` | path | string | yes | Employee id. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The availability record, or null |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## PUT /business-made/schedules/availability/{employeeId}

**Set a person's availability**

`operationId: ScheduleController_setAvailability`

Creates or replaces the person's availability as their manager. Availability is a preference: someone outside it can still be scheduled, flagged.

#### Signature

```http
PUT /business-made/schedules/availability/{employeeId} (employeeId: string, body) -> The saved availability record
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `employeeId` | path | string | yes | Employee id. |

### Request body

```json
{
  "weekly": [
    {
      "day": "mon",
      "from": "09:00",
      "to": "17:00"
    }
  ],
  "exceptions": [
    {
      "date": "2026-10-12",
      "available": false
    }
  ],
  "maxHoursPerWeek": 30
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The saved availability record |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /business-made/schedules/{id}

**Get a schedule**

`operationId: ScheduleController_getSchedule`

Fetches one schedule with its shifts.

#### Signature

```http
GET /business-made/schedules/{id} (id: string) -> The schedule
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVALID_ID | invalid id <id> | The id is not a valid record id. A well-formed id that matches nothing returns an empty 200, not a 404. | — |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /business-made/schedules/{id}/publish`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Record id. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The schedule |
| `400` | invalid id <id> — The id is not a valid record id. A well-formed id that matches nothing returns an empty 200, not a 404. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## DELETE /business-made/schedules/{id}

**Delete a schedule**

`operationId: ScheduleController_deleteSchedule`

Deletes a draft schedule. **A published schedule cannot be deleted** — staff have already planned around it. Archive it instead.

#### Signature

```http
DELETE /business-made/schedules/{id} (id: string) -> Deletion result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | SCHEDULE_NOT_FOUND | Schedule not found | No schedule has that id. | The body carries `code` and the `scheduleId`. |
| `400` | CANNOT_DELETE_PUBLISHED | Cannot delete a published schedule | The schedule has been published. | Archive it with `POST /business-made/schedules/{id}/archive`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /business-made/schedules/{id}/archive`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Record id. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Deletion result |
| `400` | Cannot delete a published schedule — The schedule has been published. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Schedule not found — No schedule has that id. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /business-made/schedules/generate

**Build a period from the shift definitions**

`operationId: ScheduleController_generatePeriod`

For every shift definition, on every day it runs, creates the shifts it asks for. Existing shifts are counted first, so running it twice tops a period up rather than duplicating it. With `assign: true` people are placed too — ready, available, not on leave, the least-loaded first. Needs one location: each location has its own rota.

#### Signature

```http
POST /business-made/schedules/generate (body) -> `{ scheduleId, from, lengthDays, created, filled, leftOpen }`
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | LOCATION_REQUIRED | Pick a location to fill — each location has its own rota. | No location (or `all`) was given. | Send `businessLocationId`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

```json
{
  "from": "2026-10-05",
  "lengthDays": 7,
  "businessLocationId": "harbor-grill",
  "assign": true
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | `{ scheduleId, from, lengthDays, created, filled, leftOpen }` |
| `400` | Pick a location to fill — each location has its own rota. — No location (or `all`) was given. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /business-made/schedules/person/clear

**Take one person off a period**

`operationId: ScheduleController_clearPersonShifts`

Removes the person from every shift in the period. The shifts are **not deleted** — the work still needs doing, so each goes back to needing cover. Nobody is emailed; ask for cover explicitly.

#### Signature

```http
POST /business-made/schedules/person/clear (body) -> `{ employeeId, from, cleared, stillNeedCover }`
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /business-made/schedules/shift/cover`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

```json
{
  "employeeId": "EMP-4821",
  "from": "2026-10-05",
  "lengthDays": 7
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | `{ employeeId, from, cleared, stillNeedCover }` |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /business-made/schedules/shift/clear

**Clear everyone off a shift**

`operationId: ScheduleController_clearShiftAssignments`

Unassigns everyone from one shift definition — on a single `date`, or across the whole period. The shifts remain, open.

#### Signature

```http
POST /business-made/schedules/shift/clear (body) -> `{ definitionId, date, cleared }`
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

```json
{
  "definitionId": "st-0",
  "from": "2026-10-05",
  "date": "2026-10-07"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | `{ definitionId, date, cleared }` |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /business-made/schedules/shift/cover

**Ask for cover on open shifts**

`operationId: ScheduleController_requestCover`

Emails the people free to take the open shifts in the period (narrowed by `definitionId` and/or `date`). Batched: each person gets **at most one** email listing every shift they could pick up.

#### Signature

```http
POST /business-made/schedules/shift/cover (body) -> `{ notified, shifts }`
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

```json
{
  "from": "2026-10-05",
  "lengthDays": 7,
  "businessLocationId": "harbor-grill"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | `{ notified, shifts }` |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /business-made/schedules/copy

**Copy one period onto another**

`operationId: ScheduleController_copyPeriod`

Copies every shift in the period starting `from` onto the period starting `to`, in one call.

#### Signature

```http
POST /business-made/schedules/copy (body) -> `{ copied, from, to, lengthDays }` — or `{ copied: 0, reason: "nothing to copy" }`
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

```json
{
  "from": "2026-10-05",
  "to": "2026-10-12",
  "lengthDays": 7,
  "businessLocationId": "harbor-grill"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | `{ copied, from, to, lengthDays }` — or `{ copied: 0, reason: "nothing to copy" }` |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /business-made/schedules/update

**Update a schedule**

`operationId: ScheduleController_updateSchedule`

Updates a schedule. Changing a published one changes what staff already saw — announce it rather than relying on them noticing. **Replaces the stored record** — send the whole record as read (`sk` plus `data`), not just the changed fields.

#### Signature

```http
POST /business-made/schedules/update (body) -> The updated schedule
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | SCHEDULE_NOT_FOUND | Schedule not found | No schedule has that id. | The body carries `code` and the `scheduleId`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /business-made/schedules/{id}/publish`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

The schedule to update.

```json
{
  "sk": "SCH-4821",
  "data": {
    "name": "Week 40 (revised)"
  }
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated schedule |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Schedule not found — No schedule has that id. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /business-made/schedules/{id}/publish

**Publish a schedule**

`operationId: ScheduleController_publishSchedule`

Makes a schedule visible to staff — the point the rota becomes real and people plan around it. Publishing twice is refused rather than silently re-notifying everyone.

#### Signature

```http
POST /business-made/schedules/{id}/publish (id: string, body) -> The published schedule
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Scheduler gate over the record’s assigned shifts — same 409 shape and `override` as `POST /business-made/schedules/week/publish`.
- Workforce Readiness: with no requirement rules this endpoint behaves exactly as before. `warn` never stops anything — it comes back as `readiness.warnings` in the response.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | SCHEDULE_NOT_FOUND | Schedule not found | No schedule has that id. | The body carries `code` and the `scheduleId`. |
| `400` | ALREADY_PUBLISHED | Schedule is already published | The schedule has already been published. | Not idempotent — read the status before publishing. |
| `409` | READINESS_BLOCK | <name> can't be scheduled: <requirement titles>. | A requirement rule whose `enforcement.scheduler` effect is block (or override-with-reason) is unmet for this person, and no override is active. | Show `message` and `reasons`. A location manager or HR can resend the same request with `override: { reasonCode, reason, expiresAt }` — it is written to the readiness ledger with the approver and expiry. The person’s own override is refused. |
| `403` | OVERRIDE_FORBIDDEN | Sign in as a location manager or HR to override. | The caller may not approve an override (no caller, the person themselves, or only their own supervisor). | — |

Plus the standard platform errors: `401`, `429`, `500`.

#### See also

- `POST /business-made/schedules/{id}/archive`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Record id. |

### Request body

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The published schedule |
| `400` | Schedule is already published — The schedule has already been published. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | Sign in as a location manager or HR to override. — The caller may not approve an override (no caller, the person themselves, or only their own supervisor). |
| `404` | Schedule not found — No schedule has that id. |
| `409` | <name> can't be scheduled: <requirement titles>. — A requirement rule whose `enforcement.scheduler` effect is block (or override-with-reason) is unmet for this person, and no override is active. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /business-made/schedules/{id}/archive

**Archive a schedule**

`operationId: ScheduleController_archiveSchedule`

Retires a schedule while keeping it — how past rotas are preserved, and the way to retire a published one.

#### Signature

```http
POST /business-made/schedules/{id}/archive (id: string) -> The archived schedule
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | SCHEDULE_NOT_FOUND | Schedule not found | No schedule has that id. | The body carries `code` and the `scheduleId`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `DELETE /business-made/schedules/{id}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Record id. |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The archived schedule |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Schedule not found — No schedule has that id. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /business-made/schedules/{id}/duplicate

**Duplicate a schedule**

`operationId: ScheduleController_duplicateSchedule`

Copies a schedule into a new draft — the fast path for a recurring rota. The copy is unpublished.

#### Signature

```http
POST /business-made/schedules/{id}/duplicate (id: string, body) -> The new draft schedule
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | SCHEDULE_NOT_FOUND | Schedule not found | No schedule has that id. | The body carries `code` and the `scheduleId`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /business-made/schedules/{id}/publish`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Record id. |

### Request body

Optional overrides for the copy.

```json
{
  "name": "Week 41",
  "startDate": "2026-10-05"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The new draft schedule |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Schedule not found — No schedule has that id. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /business-made/schedules/{id}/shifts

**Add a shift**

`operationId: ScheduleController_addShift`

Adds a shift to a schedule. **Overlapping shifts for the same employee are refused** with a `409` naming the conflicting shift, so nobody is rostered in two places at once.

#### Signature

```http
POST /business-made/schedules/{id}/shifts (id: string, body) -> The updated schedule
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Scheduler gate: placing someone a rule blocks from this shift’s station/time is refused with 409 `readiness_block` unless an override is active or the body carries `override`. An override is stamped on the shift as `readinessOverride`. A warn returns the shift with `readiness: { effect, warnings, reasons }`.
- Workforce Readiness: with no requirement rules this endpoint behaves exactly as before. `warn` never stops anything — it comes back as `readiness.warnings` in the response.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | SCHEDULE_NOT_FOUND | Schedule not found | No schedule has that id. | The body carries `code` and the `scheduleId`. |
| `409` | SHIFT_CONFLICT | Shift conflicts with existing shift | The employee already has a shift overlapping that time. | The body identifies the conflicting shift. Move one of them. |
| `400` | OVERRIDE_INVALID | An override needs reasonCode, reason, expiresAt. | `override` is missing a field, has an unknown reason code, or an expiry in the past. | — |
| `403` | OVERRIDE_FORBIDDEN | Sign in as a location manager or HR to override. | The caller may not approve an override (no caller, the person themselves, or only their own supervisor). | — |

Plus the standard platform errors: `401`, `429`, `500`.

#### See also

- `POST /business-made/schedules/{id}/shifts/{shiftId}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Record id. |

### Request body

The shift to add.

```json
{
  "employeeId": "EMP-4821",
  "startTime": "2026-09-28T09:00:00.000Z",
  "endTime": "2026-09-28T17:00:00.000Z",
  "role": "barista"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated schedule |
| `400` | An override needs reasonCode, reason, expiresAt. — `override` is missing a field, has an unknown reason code, or an expiry in the past. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | Sign in as a location manager or HR to override. — The caller may not approve an override (no caller, the person themselves, or only their own supervisor). |
| `404` | Schedule not found — No schedule has that id. |
| `409` | Shift conflicts with existing shift — The employee already has a shift overlapping that time. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /business-made/schedules/{id}/shifts/{shiftId}

**Update a shift**

`operationId: ScheduleController_updateShift`

Updates a shift. Overlap checking applies here too — a change that would double-book someone is refused.

#### Signature

```http
POST /business-made/schedules/{id}/shifts/{shiftId} (id: string, shiftId: string, body) -> The updated schedule
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Scheduler gate: checked only when the change moves who works, the date/time, or the station (a note edit never trips it). Same 409 / `override` / `readiness` shape as adding a shift.
- Workforce Readiness: with no requirement rules this endpoint behaves exactly as before. `warn` never stops anything — it comes back as `readiness.warnings` in the response.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | SCHEDULE_NOT_FOUND | Schedule not found | No schedule has that id. | The body carries `code` and the `scheduleId`. |
| `409` | SHIFT_CONFLICT | Shift conflicts with existing shift | The change would overlap another shift for that employee. | The body identifies the conflicting shift. |
| `400` | OVERRIDE_INVALID | An override needs reasonCode, reason, expiresAt. | `override` is missing a field, has an unknown reason code, or an expiry in the past. | — |
| `403` | OVERRIDE_FORBIDDEN | Sign in as a location manager or HR to override. | The caller may not approve an override (no caller, the person themselves, or only their own supervisor). | — |

Plus the standard platform errors: `401`, `429`, `500`.

#### See also

- `DELETE /business-made/schedules/{id}/shifts/{shiftId}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Record id. |
| `shiftId` | path | string | yes | Shift id. |

### Request body

Fields to change.

```json
{
  "endTime": "2026-09-28T18:00:00.000Z"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated schedule |
| `400` | An override needs reasonCode, reason, expiresAt. — `override` is missing a field, has an unknown reason code, or an expiry in the past. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | Sign in as a location manager or HR to override. — The caller may not approve an override (no caller, the person themselves, or only their own supervisor). |
| `404` | Schedule not found — No schedule has that id. |
| `409` | Shift conflicts with existing shift — The change would overlap another shift for that employee. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## DELETE /business-made/schedules/{id}/shifts/{shiftId}

**Remove a shift**

`operationId: ScheduleController_removeShift`

Removes a shift from a schedule. **A shift that has started cannot be removed** — someone is working it, and deleting it would erase hours that are owed.

#### Signature

```http
DELETE /business-made/schedules/{id}/shifts/{shiftId} (id: string, shiftId: string) -> The updated schedule
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | SCHEDULE_NOT_FOUND | Schedule not found | No schedule has that id. | The body carries `code` and the `scheduleId`. |
| `400` | SHIFT_IN_PROGRESS | Cannot remove a shift that has started | The shift has already begun. | Let it finish and correct the timesheet instead. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /business-made/schedules/{id}/shifts`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Record id. |
| `shiftId` | path | string | yes | Shift id. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The updated schedule |
| `400` | Cannot remove a shift that has started — The shift has already begun. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Schedule not found — No schedule has that id. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /business-made/schedules/{id}/shifts/{shiftId}/swap/request

**Request a shift swap**

`operationId: ScheduleController_requestShiftSwap`

Asks to hand a shift to a colleague. Nothing changes until a manager approves — the rota stays authoritative in the meantime.

#### Signature

```http
POST /business-made/schedules/{id}/shifts/{shiftId}/swap/request (id: string, shiftId: string, body) -> The swap request
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | SCHEDULE_NOT_FOUND | Schedule not found | No schedule has that id. | The body carries `code` and the `scheduleId`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /business-made/schedules/{id}/shifts/{shiftId}/swap/approve`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Record id. |
| `shiftId` | path | string | yes | Shift id. |

### Request body

The swap request.

```json
{
  "toEmployeeId": "EMP-4822",
  "reason": "Medical appointment"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The swap request |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Schedule not found — No schedule has that id. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /business-made/schedules/{id}/shifts/{shiftId}/swap/approve

**Approve a shift swap**

`operationId: ScheduleController_approveShiftSwap`

Approves a swap and reassigns the shift. Overlap rules still apply to the receiving employee.

#### Signature

```http
POST /business-made/schedules/{id}/shifts/{shiftId}/swap/approve (id: string, shiftId: string, body) -> The updated schedule
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Scheduler gate for the person taking the shift over; same 409 / `override` shape.
- Workforce Readiness: with no requirement rules this endpoint behaves exactly as before. `warn` never stops anything — it comes back as `readiness.warnings` in the response.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | SCHEDULE_NOT_FOUND | Schedule not found | No schedule has that id. | The body carries `code` and the `scheduleId`. |
| `409` | READINESS_BLOCK | <name> can't take over this shift: <requirement titles>. | A requirement rule whose `enforcement.scheduler` effect is block (or override-with-reason) is unmet for this person, and no override is active. | Show `message` and `reasons`. A location manager or HR can resend the same request with `override: { reasonCode, reason, expiresAt }` — it is written to the readiness ledger with the approver and expiry. The person’s own override is refused. |
| `400` | OVERRIDE_INVALID | An override needs reasonCode, reason, expiresAt. | `override` is missing a field, has an unknown reason code, or an expiry in the past. | — |
| `403` | OVERRIDE_FORBIDDEN | Sign in as a location manager or HR to override. | The caller may not approve an override (no caller, the person themselves, or only their own supervisor). | — |

Plus the standard platform errors: `401`, `429`, `500`.

#### See also

- `POST /business-made/schedules/{id}/shifts/{shiftId}/swap/reject`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Record id. |
| `shiftId` | path | string | yes | Shift id. |

### Request body

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated schedule |
| `400` | An override needs reasonCode, reason, expiresAt. — `override` is missing a field, has an unknown reason code, or an expiry in the past. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | Sign in as a location manager or HR to override. — The caller may not approve an override (no caller, the person themselves, or only their own supervisor). |
| `404` | Schedule not found — No schedule has that id. |
| `409` | <name> can't take over this shift: <requirement titles>. — A requirement rule whose `enforcement.scheduler` effect is block (or override-with-reason) is unmet for this person, and no override is active. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /business-made/schedules/{id}/shifts/{shiftId}/swap/reject

**Reject a shift swap**

`operationId: ScheduleController_rejectShiftSwap`

Declines a swap request. The shift stays with whoever was originally rostered.

#### Signature

```http
POST /business-made/schedules/{id}/shifts/{shiftId}/swap/reject (id: string, shiftId: string, body) -> The updated schedule
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | SCHEDULE_NOT_FOUND | Schedule not found | No schedule has that id. | The body carries `code` and the `scheduleId`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /business-made/schedules/{id}/shifts/{shiftId}/swap/approve`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Record id. |
| `shiftId` | path | string | yes | Shift id. |

### Request body

Why it was rejected.

```json
{
  "reason": "Receiving employee would exceed weekly hours"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated schedule |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Schedule not found — No schedule has that id. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /business-made/schedules/{id}/shifts/{shiftId}/clock-in

**Clock in to a shift**

`operationId: ScheduleController_clockIn`

Records an employee starting their rostered shift, tying actual hours to the planned ones so variance is visible.

#### Signature

```http
POST /business-made/schedules/{id}/shifts/{shiftId}/clock-in (id: string, shiftId: string, body) -> The updated shift
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- ClockIn gate for the shift’s station: 423 `readiness_block` on a block; a manager resends with `override`. A warn clocks in and returns `readiness`.
- Workforce Readiness: with no requirement rules this endpoint behaves exactly as before. `warn` never stops anything — it comes back as `readiness.warnings` in the response.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | SCHEDULE_NOT_FOUND | Schedule not found | No schedule has that id. | The body carries `code` and the `scheduleId`. |
| `423` | READINESS_BLOCK | <name> can't clock in: <requirement titles>. | A requirement rule whose `enforcement.clockIn` effect is block (or override-with-reason) is unmet for this person, and no override is active. | Show `message` and `reasons`. A location manager or HR can resend the same request with `override: { reasonCode, reason, expiresAt }` — it is written to the readiness ledger with the approver and expiry. The person’s own override is refused. |
| `400` | OVERRIDE_INVALID | An override needs reasonCode, reason, expiresAt. | `override` is missing a field, has an unknown reason code, or an expiry in the past. | — |
| `403` | OVERRIDE_FORBIDDEN | Sign in as a location manager or HR to override. | The caller may not approve an override (no caller, the person themselves, or only their own supervisor). | — |

Plus the standard platform errors: `401`, `429`, `500`.

#### See also

- `POST /business-made/schedules/{id}/shifts/{shiftId}/clock-out`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Record id. |
| `shiftId` | path | string | yes | Shift id. |

### Request body

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated shift |
| `400` | An override needs reasonCode, reason, expiresAt. — `override` is missing a field, has an unknown reason code, or an expiry in the past. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | Sign in as a location manager or HR to override. — The caller may not approve an override (no caller, the person themselves, or only their own supervisor). |
| `404` | Schedule not found — No schedule has that id. |
| `423` | <name> can't clock in: <requirement titles>. — A requirement rule whose `enforcement.clockIn` effect is block (or override-with-reason) is unmet for this person, and no override is active. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /business-made/schedules/{id}/shifts/{shiftId}/clock-out

**Clock out of a shift**

`operationId: ScheduleController_clockOut`

Records an employee finishing their shift. The difference between rostered and actual hours is what the labour report measures.

#### Signature

```http
POST /business-made/schedules/{id}/shifts/{shiftId}/clock-out (id: string, shiftId: string) -> The updated shift
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | SCHEDULE_NOT_FOUND | Schedule not found | No schedule has that id. | The body carries `code` and the `scheduleId`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /business-made/schedules/reports/labor-summary`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Record id. |
| `shiftId` | path | string | yes | Shift id. |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated shift |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Schedule not found — No schedule has that id. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /business-made/schedules/{id}/shifts/{shiftId}/break/start

**Start a break**

`operationId: ScheduleController_startBreak`

Records the start of a break within a shift. Where breaks are unpaid, this is what keeps paid hours correct.

#### Signature

```http
POST /business-made/schedules/{id}/shifts/{shiftId}/break/start (id: string, shiftId: string) -> The updated shift
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | SCHEDULE_NOT_FOUND | Schedule not found | No schedule has that id. | The body carries `code` and the `scheduleId`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /business-made/schedules/{id}/shifts/{shiftId}/break/{breakId}/end`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Record id. |
| `shiftId` | path | string | yes | Shift id. |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated shift |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Schedule not found — No schedule has that id. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /business-made/schedules/{id}/shifts/{shiftId}/break/{breakId}/end

**End a break**

`operationId: ScheduleController_endBreak`

Records the end of a break. An unended break can leave the shift under-counting paid hours.

#### Signature

```http
POST /business-made/schedules/{id}/shifts/{shiftId}/break/{breakId}/end (id: string, shiftId: string, breakId: string) -> The updated shift
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | SCHEDULE_NOT_FOUND | Schedule not found | No schedule has that id. | The body carries `code` and the `scheduleId`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /business-made/schedules/{id}/shifts/{shiftId}/break/start`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `id` | path | string | yes | Record id. |
| `shiftId` | path | string | yes | Shift id. |
| `breakId` | path | string | yes | Break id. |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated shift |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Schedule not found — No schedule has that id. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /business-made/schedules/shifts/employee/{employeeId}

**Get an employee's shifts**

`operationId: ScheduleController_getShiftsByEmployee`

Every shift rostered to one employee.

#### Signature

```http
GET /business-made/schedules/shifts/employee/{employeeId} (employeeId: string) -> Shifts
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /business-made/schedules/shifts/employee/{employeeId}/upcoming`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `employeeId` | path | string | yes | Employee id. |
| `startDate` | query | string | yes |  |
| `endDate` | query | string | yes |  |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Shifts |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /business-made/schedules/shifts/employee/{employeeId}/upcoming

**Get an employee's upcoming shifts**

`operationId: ScheduleController_getUpcomingShifts`

The shifts an employee has coming up — their "when am I next in" view.

#### Signature

```http
GET /business-made/schedules/shifts/employee/{employeeId}/upcoming (employeeId: string) -> Upcoming shifts
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /business-made/schedules/shifts/today`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `employeeId` | path | string | yes | Employee id. |
| `days` | query | number | yes |  |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Upcoming shifts |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /business-made/schedules/shifts/today

**Get today's shifts**

`operationId: ScheduleController_getTodaysShifts`

Everyone rostered today — the shift-supervisor view of who should be in.

#### Signature

```http
GET /business-made/schedules/shifts/today () -> Today's shifts
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /business-made/timesheets/reports/on-the-clock`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `locationId` | query | string | yes |  |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Today's shifts |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /business-made/schedules/reports/labor-summary

**Get the labour summary**

`operationId: ScheduleController_getLaborSummary`

Rostered against actual hours and their cost — where overtime and understaffing show up before they reach payroll.

#### Signature

```http
GET /business-made/schedules/reports/labor-summary () -> Labour summary
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /business-made/timesheets/reports/summary/{employeeId}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `periodStart` | query | string | yes |  |
| `periodEnd` | query | string | yes |  |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Labour summary |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /business-made/schedules/{scheduleId}/shifts/{shiftId}/offer

**Offer a shift to its person**

`operationId: ScheduleController_offerShift`

Puts the shift to the person on it (`status: offered`) and emails them. They accept or decline; nothing is imposed.

#### Signature

```http
POST /business-made/schedules/{scheduleId}/shifts/{shiftId}/offer (scheduleId: string, shiftId: string) -> The updated schedule
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | SHIFT_NOT_FOUND | Shift not found | No shift on that schedule has that id. | The body carries `code` and the `shiftId`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /business-made/schedules/{scheduleId}/shifts/{shiftId}/respond`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `scheduleId` | path | string | yes |  |
| `shiftId` | path | string | yes | Shift id. |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated schedule |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Shift not found — No shift on that schedule has that id. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /business-made/schedules/{scheduleId}/shifts/{shiftId}/respond

**Answer a shift offer**

`operationId: ScheduleController_respondToShift`

`accept: true` marks it accepted. A decline hands the shift back (`status: open`, person removed, `declineReason` kept), tells the managers and asks the free pool for cover.

#### Signature

```http
POST /business-made/schedules/{scheduleId}/shifts/{shiftId}/respond (scheduleId: string, shiftId: string, body) -> The updated schedule
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | SHIFT_NOT_FOUND | Shift not found | No shift on that schedule has that id. | The body carries `code` and the `shiftId`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `scheduleId` | path | string | yes |  |
| `shiftId` | path | string | yes | Shift id. |

### Request body

```json
{
  "accept": false,
  "reason": "Exam that morning"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated schedule |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Shift not found — No shift on that schedule has that id. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /business-made/schedules/{scheduleId}/shifts/{shiftId}/no-show

**Mark a no-show**

`operationId: ScheduleController_markNoShow`

The person did not turn up: the shift goes back out (`status: open`, `noShowOf` records who), the person and the managers are told, and the free pool is asked for cover.

#### Signature

```http
POST /business-made/schedules/{scheduleId}/shifts/{shiftId}/no-show (scheduleId: string, shiftId: string, body) -> The updated schedule
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | SHIFT_NOT_FOUND | Shift not found | No shift on that schedule has that id. | The body carries `code` and the `shiftId`. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `scheduleId` | path | string | yes |  |
| `shiftId` | path | string | yes | Shift id. |

### Request body

```json
{
  "reason": "No call, no show"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated schedule |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Shift not found — No shift on that schedule has that id. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

