# Staff portal

Part of AppEngine API Documentation. Full index: https://appengine.appmint.io/documentation.md
## GET /staff-portal/dashboard

**Get my dashboard**

`operationId: StaffPortalController_dashboard`

The employee's home view — next shift, current timesheet, latest payslip and anything needing attention, in one call.

#### Signature

```http
GET /staff-portal/dashboard () -> The dashboard
```

#### Access

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

#### Errors

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

#### See also

- `GET /staff-portal/schedule/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. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The dashboard |
| `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 /staff-portal/landing

**Where do I belong**

`operationId: StaffPortalController_landing`

Decided on the server so web, mobile and IVR agree: `isAdmin` (owner / config admin roles), `isStaff` (linked to an employee record), `landing` (`/staff-portal/onboarding` for a hire in pre-boarding, `/staff-portal/dashboard` for staff only, `/dashboard` for admins), `staffOnly`, `preboarding`, `managesTeam` (has direct reports), and any back-office screens an admin gave this person's roles. Also returns the Leave module's readiness.

#### Signature

```http
GET /staff-portal/landing () -> `{ isAdmin, isStaff, landing, staffOnly, preboarding, managesTeam, … }`
```

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

### Responses

| Status | Meaning |
| --- | --- |
| `200` | `{ isAdmin, isStaff, landing, staffOnly, preboarding, managesTeam, … }` |
| `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 /staff-portal/time-off

**My time off**

`operationId: StaffPortalController_myTimeOff`

The leave types I can request, my balances this year, my requests, and who decides them. While the Leave module is not ready, `state` says so and the lists are empty.

#### Signature

```http
GET /staff-portal/time-off () -> `{ employeeId, state, types, balances, requests, approvers, year }`
```

#### Access

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

#### Errors

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

#### See also

- `POST /staff-portal/time-off`

### 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` | `{ employeeId, state, types, balances, requests, approvers, year }` |
| `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 /staff-portal/time-off

**Request time off**

`operationId: StaffPortalController_requestTimeOff`

Creates a request as me — the employee on it is always the caller — and submits it for approval, unless `draft: true`. Hours are worked out from my weekly hours. Submission follows the same rules as HR's submit (working days only, approver resolved, balance held).

#### Signature

```http
POST /staff-portal/time-off (body) -> The request (pending, or draft)
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `412` | LEAVE_NOT_READY | Time off is not switched on for this business | The Leave module is off, or still being set up ("Time off is still being set up"). | — |
| `400` | LEAVE_TYPE_REQUIRED | Pick a type of time off | `leaveTypeId` names no leave type. | — |

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

#### See also

- `GET /staff-portal/time-off/cost`

### 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
{
  "leaveTypeId": "LT-annual",
  "startDate": "2026-10-05",
  "endDate": "2026-10-09",
  "reason": "Family visit"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The request (pending, or draft) |
| `400` | Pick a type of time off — `leaveTypeId` names no leave type. |
| `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. |
| `412` | Time off is not switched on for this business — The Leave module is off, or still being set up ("Time off is still being set up"). |
| `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 /staff-portal/time-off/cost

**What a date range would cost me**

`operationId: StaffPortalController_costTimeOff`

The amount a request would take from my balance — working days only, holidays and closures skipped — before I send it.

#### Signature

```http
GET /staff-portal/time-off/cost (startDate?: string, endDate?: string, isPartialDay?: string) -> `{ unit, amount, calendarDays, workingDays, skipped, totalDays, totalHours }`
```

#### 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. |
| `startDate` | query | string | yes |  |
| `endDate` | query | string | — |  |
| `isPartialDay` | query | string | — |  |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | `{ unit, amount, calendarDays, workingDays, skipped, totalDays, totalHours }` |
| `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 /staff-portal/time-off/{id}/edit

**Edit my request**

`operationId: StaffPortalController_editTimeOff`

Changes one of my own draft, declined or cancelled requests (dates, type, reason, half day).

#### Signature

```http
POST /staff-portal/time-off/{id}/edit (id: string, body) -> The request
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `403` | NOT_YOUR_REQUEST | That is not your request | The request belongs to someone else. | — |
| `400` | BAD_STATUS | A pending request cannot be edited — cancel it first | The request is pending or approved. | — |

Plus the standard platform errors: `401`, `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. |
| `id` | path | string | yes | bm_leave_request sk. |

### Request body

```json
{
  "endDate": "2026-10-08"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The request |
| `400` | A pending request cannot be edited — cancel it first — The request is pending or approved. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | That is not your request — The request belongs to someone else. |
| `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 /staff-portal/time-off/{id}/resubmit

**Send my request again**

`operationId: StaffPortalController_resubmitTimeOff`

Sends one of my declined requests (or one withdrawn before a decision) back for approval, with optional changes. It goes back to whoever declined it.

#### Signature

```http
POST /staff-portal/time-off/{id}/resubmit (id: string, body) -> The request, pending again
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `412` | LEAVE_NOT_READY | Time off is still being set up | The Leave module is not ready. | — |
| `403` | NOT_YOUR_REQUEST | That is not your request | The request belongs to someone else. | — |
| `400` | CANNOT_RESUBMIT | Only a declined request, or one withdrawn before a decision, can be sent again — make a new request instead | The request is pending or approved. | — |

Plus the standard platform errors: `401`, `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. |
| `id` | path | string | yes | bm_leave_request sk. |

### Request body

```json
{
  "startDate": "2026-10-12",
  "endDate": "2026-10-16"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The request, pending again |
| `400` | Only a declined request, or one withdrawn before a decision, can be sent again — make a new request instead — The request is pending or approved. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | That is not your request — The request belongs to someone else. |
| `412` | Time off is still being set up — The Leave module is not ready. |
| `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 /staff-portal/schedule/off

**Days I am off**

`operationId: StaffPortalController_myOffDays`

Days in the window when I am on approved leave or the business is shut — for my schedule page.

#### Signature

```http
GET /staff-portal/schedule/off (days?: integer) -> `{ from, to, days }`
```

#### 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. |
| `days` | query | integer | — |  |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | `{ from, to, days }` |
| `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 /staff-portal/time-off/team

**Who at my site is off**

`operationId: StaffPortalController_teamOff`

Approved time off at my site, by day — names and types only. Default window: 30 days from today.

#### Signature

```http
GET /staff-portal/time-off/team (from?: string, to?: string) -> `{ from, to, days:[{date, off:[{name, type, title, half}]}], … }`
```

#### 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. |
| `from` | query | string | — |  |
| `to` | query | string | — |  |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | `{ from, to, days:[{date, off:[{name, type, title, half}]}], … }` |
| `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 /staff-portal/approvals

**Requests waiting on me**

`operationId: StaffPortalController_myApprovals`

Time-off requests waiting on me as a supervisor or site manager — or, for HR, everything pending.

#### Signature

```http
GET /staff-portal/approvals () -> `{ count, requests }`
```

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

### Responses

| Status | Meaning |
| --- | --- |
| `200` | `{ count, requests }` |
| `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 /staff-portal/approvals/{id}/{decision}

**Decide a request**

`operationId: StaffPortalController_decideApproval`

Approves or declines a request that is waiting on me (HR may decide anything pending). A decline needs a note — the person sees it.

#### Signature

```http
POST /staff-portal/approvals/{id}/{decision} (id: string, decision: string, body) -> The request after the decision
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `403` | NOT_YOUR_DECISION | This request is not waiting on you | Someone else decides it, or it is not pending. | — |
| `400` | BAD_DECISION | Decision must be approve or reject | `decision` is anything else. | — |

Plus the standard platform errors: `401`, `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. |
| `id` | path | string | yes | bm_leave_request sk. |
| `decision` | path | "approve" \| "reject" | yes |  |

### Request body

```json
{
  "note": "Enjoy the break"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The request after the decision |
| `400` | Decision must be approve or reject — `decision` is anything else. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | This request is not waiting on you — Someone else decides it, or it is not pending. |
| `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 /staff-portal/time-off/{id}/cancel

**Cancel my request**

`operationId: StaffPortalController_cancelTimeOff`

Withdraws one of my own requests. A pending one releases its held amount; an approved one gives it back.

#### Signature

```http
POST /staff-portal/time-off/{id}/cancel (id: string, body) -> The cancelled request
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `403` | NOT_YOUR_REQUEST | That is not your request | The request belongs to someone else. | — |
| `400` | BAD_STATUS | This request is already cancelled | Already cancelled or declined. | — |

Plus the standard platform errors: `401`, `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. |
| `id` | path | string | yes | bm_leave_request sk. |

### Request body

```json
{
  "reason": "Plans changed"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The cancelled request |
| `400` | This request is already cancelled — Already cancelled or declined. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | That is not your request — The request belongs to someone else. |
| `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 /staff-portal/availability

**My availability**

`operationId: StaffPortalController_myAvailability`

What I said I can work: weekly pattern, exceptions, maximum hours a week, notes. Empty lists when nothing is recorded.

#### Signature

```http
GET /staff-portal/availability () -> `{ employeeId, weekly, exceptions, maxHoursPerWeek?, notes? }`
```

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

### Responses

| Status | Meaning |
| --- | --- |
| `200` | `{ employeeId, weekly, exceptions, maxHoursPerWeek?, notes? }` |
| `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 /staff-portal/availability

**Set my availability**

`operationId: StaffPortalController_setMyAvailability`

Replaces my availability. It is a preference: a manager can still schedule me outside it, flagged.

#### Signature

```http
PUT /staff-portal/availability (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. |

### Request body

```json
{
  "weekly": [
    {
      "day": "sat",
      "from": "10:00",
      "to": "18:00"
    }
  ],
  "maxHoursPerWeek": 20
}
```

### 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 /staff-portal/profile

**Get my profile**

`operationId: StaffPortalController_getProfile`

The employee's own record — contact details and employment information.

#### Signature

```http
GET /staff-portal/profile () -> The profile
```

#### Access

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

#### Errors

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

#### See also

- `PUT /staff-portal/profile`

### 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` | The profile |
| `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 /staff-portal/profile

**Update my profile**

`operationId: StaffPortalController_updateProfile`

Updates the employee's own details. Employment terms — pay rate, role, status — are not editable here; those are HR-controlled.

#### Signature

```http
PUT /staff-portal/profile (body) -> The updated profile
```

#### Access

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

#### Notes

- Cannot change pay or role.

#### Errors

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

#### See also

- `PUT /staff-portal/tax-info`

### 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

Fields to change.

```json
{
  "phone": "+15551234567",
  "address": {
    "line1": "1 Market St",
    "city": "San Francisco",
    "state": "CA",
    "postalCode": "94105"
  }
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The updated profile |
| `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 /staff-portal/pay-method

**Get my pay method**

`operationId: StaffPortalController_getPayMethod`

How the employee is paid — ACH, check or cash — and the accounts on file.

#### Signature

```http
GET /staff-portal/pay-method () -> The pay method
```

#### Access

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

#### Errors

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

#### See also

- `PUT /staff-portal/pay-method`

### 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` | The pay method |
| `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 /staff-portal/pay-method

**Set my pay method**

`operationId: StaffPortalController_setPayMethod`

Chooses how the employee is paid. Choosing `ach` without a bank account on file leaves the next payroll run with nowhere to send the money — add the account first.

#### Signature

```http
PUT /staff-portal/pay-method (body) -> The result
```

#### Access

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

#### Notes

- Affects where real wages are sent.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | INVALID_PAY_METHOD | payMethod must be 'ach', 'check', or 'cash' | The value is missing or not one of the three. | Send `ach`, `check` or `cash`. |

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

#### See also

- `POST /staff-portal/direct-deposit`

### 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 pay method.

```json
{
  "payMethod": "ach"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The result |
| `400` | payMethod must be 'ach', 'check', or 'cash' — The value is missing or not one of the three. |
| `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 /staff-portal/direct-deposit

**Add a direct deposit account**

`operationId: StaffPortalController_addBankAccount`

Adds a bank account for wage payments. **The body carries a routing and account number** — never log it, and send it only over TLS.

A wrong account number sends wages to someone else, and recovering a misdirected ACH credit is slow and often unsuccessful. Verify the digits before submitting.

#### Signature

```http
POST /staff-portal/direct-deposit (body) -> The account
```

#### Access

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

#### Notes

- Sensitive payload; determines where wages are paid.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | ACCOUNT_FIELDS_REQUIRED | routingNumber and accountNumber required | Either field is missing. | Supply both. |

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

#### See also

- `DELETE /staff-portal/direct-deposit/{accountId}`

### 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 bank account.

```json
{
  "routingNumber": "021000021",
  "accountNumber": "000123456789",
  "accountType": "checking"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The account |
| `400` | routingNumber and accountNumber required — Either field is missing. |
| `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 /staff-portal/direct-deposit/{accountId}

**Remove a direct deposit account**

`operationId: StaffPortalController_removeBankAccount`

Removes a bank account. Removing the only account while the pay method is `ach` leaves the next payroll run with no destination — change the pay method first, or add a replacement.

#### Signature

```http
DELETE /staff-portal/direct-deposit/{accountId} (accountId: string) -> The result
```

#### Access

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

#### Notes

- Can leave payroll with nowhere to pay.

#### Errors

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

#### See also

- `PUT /staff-portal/pay-method`

### 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. |
| `accountId` | path | string | yes | Account id. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The result |
| `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 /staff-portal/tax-info

**Get my tax information**

`operationId: StaffPortalController_getTaxInfo`

The employee's W-4 withholding details.

#### Signature

```http
GET /staff-portal/tax-info () -> Tax information
```

#### Access

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

#### Errors

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

#### See also

- `PUT /staff-portal/tax-info`

### 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` | Tax information |
| `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 /staff-portal/tax-info

**Update my tax information**

`operationId: StaffPortalController_updateTaxInfo`

Updates W-4 withholding. This changes how much tax is withheld from future pay — an error here shows up as an under- or over-withheld paycheck, and cannot be corrected retroactively on payslips already issued.

#### Signature

```http
PUT /staff-portal/tax-info (body) -> The result
```

#### Access

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

#### Notes

- Changes withholding on future pay only.

#### Errors

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

#### See also

- `GET /staff-portal/payslips`

### 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

W-4 fields.

```json
{
  "filingStatus": "single",
  "dependents": 0,
  "additionalWithholding": 0
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The result |
| `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 /staff-portal/schedule/upcoming

**Get my upcoming shifts**

`operationId: StaffPortalController_upcoming`

The employee's next shifts within a window of days.

#### Signature

```http
GET /staff-portal/schedule/upcoming (days?: integer) -> Upcoming shifts
```

#### Access

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

#### Errors

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

#### See also

- `GET /staff-portal/schedule`

### 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. |
| `days` | query | integer | — | Look-ahead window in days. |

### 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 /staff-portal/schedule/takeable

**Shifts I could take**

`operationId: StaffPortalController_takeable`

Open shifts and shifts a colleague wants covered at my location, that I am free for. Default 14 days from `from` (today).

#### Signature

```http
GET /staff-portal/schedule/takeable (from?: string, days?: integer) -> Takeable shifts
```

#### Access

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

#### Errors

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

#### See also

- `POST /staff-portal/schedule/{scheduleId}/shifts/{shiftId}/{action}`

### 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. |
| `from` | query | string | — |  |
| `days` | query | integer | — |  |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Takeable 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 /staff-portal/schedule/colleagues

**Who could swap with me**

`operationId: StaffPortalController_colleagues`

Colleagues at my location who are free for one of my shifts (me excluded).

#### Signature

```http
GET /staff-portal/schedule/colleagues (date?: string, startTime?: string, endTime?: string, shiftId?: string) -> `{ available:[…], … }`
```

#### 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 | yes |  |
| `startTime` | query | string | yes |  |
| `endTime` | query | string | yes |  |
| `shiftId` | query | string | — |  |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | `{ available:[…], … }` |
| `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 /staff-portal/schedule/{scheduleId}/shifts/{shiftId}/{action}

**Act on a shift as me**

`operationId: StaffPortalController_shiftAction`

Everything I may do to a shift, checked against who I am:
- `accept` / `decline` (with `reason`) — answer an offered shift; a decline hands it back for cover.
- `cover` (with `reason`) — ask for someone to take my shift; it stays mine until someone does.
- `withdraw` — take back a cover request.
- `swap` (with `targetEmployeeId`) — ask a colleague to swap.
- `take` — claim an open or cover-requested shift; the same readiness and double-booking checks as a manager placing me apply (`override` for a manager-approved readiness exception).

#### Signature

```http
POST /staff-portal/schedule/{scheduleId}/shifts/{shiftId}/{action} (scheduleId: string, shiftId: string, action: 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 the id. | — |
| `403` | NOT_YOUR_SHIFT | That is not your shift | cover / withdraw / swap on someone else's shift. | — |
| `400` | SHIFT_TAKEN | That shift is already assigned | `take` on a shift someone holds and is not handing over. | — |
| `409` | SHIFT_CONFLICT | You are already working at that time | `take` would double-book me. | — |

Plus the standard platform errors: `401`, `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 |  |
| `action` | path | "accept" \| "decline" \| "cover" \| "withdraw" \| "swap" \| "take" | yes |  |

### Request body

```json
{
  "reason": "Doctor appointment"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated schedule |
| `400` | That shift is already assigned — `take` on a shift someone holds and is not handing over. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | That is not your shift — cover / withdraw / swap on someone else's shift. |
| `404` | Shift not found — No shift on that schedule has the id. |
| `409` | You are already working at that time — `take` would double-book me. |
| `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 /staff-portal/schedule

**Get my schedule**

`operationId: StaffPortalController_schedule`

The employee's shifts over an explicit date range.

#### Signature

```http
GET /staff-portal/schedule (from?: string, to?: string) -> Shifts
```

#### Access

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

#### Errors

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

#### See also

- `GET /staff-portal/schedule/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. |
| `from` | query | string | — | ISO date. |
| `to` | query | string | — | ISO date. |

### 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 /staff-portal/timesheets/current

**Get my current timesheet**

`operationId: StaffPortalController_currentTimesheet`

The timesheet for the pay period in progress — what a "this week" view shows.

#### Signature

```http
GET /staff-portal/timesheets/current () -> The current timesheet
```

#### Access

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

#### Errors

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

#### See also

- `POST /staff-portal/timesheets/{timesheetId}/submit`

### 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` | The current timesheet |
| `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 /staff-portal/timesheets

**List my timesheets**

`operationId: StaffPortalController_listTimesheets`

The employee's timesheets, newest first.

#### Signature

```http
GET /staff-portal/timesheets (p?: integer, ps?: integer) -> Timesheets
```

#### Access

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

#### Errors

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

#### See also

- `GET /staff-portal/timesheets/current`

### 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. |
| `p` | query | integer | — | Page number. |
| `ps` | query | integer | — | Page size. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Timesheets |
| `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 /staff-portal/timesheets/{timesheetId}/entries

**Add a time entry**

`operationId: StaffPortalController_addEntry`

Adds an entry to a timesheet manually — for time worked that was not clocked. Entries feed the hours payroll pays on, so they are checked at approval.

#### Signature

```http
POST /staff-portal/timesheets/{timesheetId}/entries (timesheetId: string, body) -> The entry
```

#### Access

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

#### Errors

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

#### See also

- `POST /staff-portal/timesheets/clock-in`

### 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. |
| `timesheetId` | path | string | yes | Timesheet id. |

### Request body

The entry.

```json
{
  "date": "2026-08-30",
  "start": "09:00",
  "end": "17:00",
  "note": "Forgot to clock in"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The entry |
| `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 /staff-portal/timesheets/clock-in

**Clock in**

`operationId: StaffPortalController_clockIn`

Starts a work session. Only one can be open at a time — check `timesheets/clock-in/active` before starting another.

#### Signature

```http
POST /staff-portal/timesheets/clock-in (body) -> The open session
```

#### Access

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

#### Notes

- Same clockIn gate as the admin clock (423 `readiness_block` with `blocking[]` and `trainingAtClockIn[]`; success may carry `readiness.warnings` and `trainingAtClockIn[]`). The person cannot override their own block — a manager clocks them in with an override from the admin clock.
- 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 |
| --- | --- | --- | --- | --- |
| `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. |

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

#### See also

- `POST /staff-portal/timesheets/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. |

### Request body

Optional clock-in detail.

```json
{
  "location": "Store 3"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The open session |
| `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. |
| `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 /staff-portal/timesheets/clock-out

**Clock out**

`operationId: StaffPortalController_clockOut`

Closes the open work session and writes the hours to the timesheet. Forgetting to clock out leaves the session running and the hours wrong — the fix is a manual entry, which needs approval.

#### Signature

```http
POST /staff-portal/timesheets/clock-out (body) -> The closed session
```

#### Access

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

#### Errors

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

#### See also

- `GET /staff-portal/timesheets/clock-in/active`

### 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

Optional clock-out detail.

```json
{}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The closed session |
| `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 /staff-portal/timesheets/clock-in/active

**Get my active clock-in**

`operationId: StaffPortalController_activeClock`

The open work session, if there is one — what a "you are still clocked in" banner reads.

#### Signature

```http
GET /staff-portal/timesheets/clock-in/active () -> The active session, or none
```

#### Access

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

#### Errors

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

#### See also

- `POST /staff-portal/timesheets/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. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The active session, or none |
| `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 /staff-portal/timesheets/{timesheetId}/submit

**Submit a timesheet**

`operationId: StaffPortalController_submit`

Submits a timesheet for approval. Once submitted it is generally locked to further edits — clock out and check the entries first, because a correction after submission goes through a manager.

#### Signature

```http
POST /staff-portal/timesheets/{timesheetId}/submit (timesheetId: string) -> The submitted timesheet
```

#### Access

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

#### Notes

- Locks the timesheet to further self-service edits.

#### Errors

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

#### See also

- `GET /staff-portal/timesheets/current`

### 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. |
| `timesheetId` | path | string | yes | Timesheet id. |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The submitted timesheet |
| `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 /staff-portal/payslips

**List my payslips**

`operationId: StaffPortalController_listPayslips`

The employee's pay stubs, newest first.

#### Signature

```http
GET /staff-portal/payslips (p?: integer, ps?: integer) -> Payslips
```

#### Access

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

#### Errors

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

#### See also

- `GET /staff-portal/payslips/{stubId}`

### 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. |
| `p` | query | integer | — | Page number. |
| `ps` | query | integer | — | Page size. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Payslips |
| `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 /staff-portal/payslips/year/{year}/summary

**Get a year-to-date summary**

`operationId: StaffPortalController_yearSummary`

Earnings, taxes and deductions totalled for a calendar year.

#### Signature

```http
GET /staff-portal/payslips/year/{year}/summary (year: string) -> The year summary
```

#### Access

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

#### Errors

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

#### See also

- `GET /staff-portal/w2/{year}`

### 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. |
| `year` | path | string | yes | Calendar year. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The year 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. |

## GET /staff-portal/payslips/{stubId}

**Get a payslip**

`operationId: StaffPortalController_getPayslip`

One pay stub with its earnings, deductions and taxes.

#### Signature

```http
GET /staff-portal/payslips/{stubId} (stubId: string) -> The payslip
```

#### Access

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

#### Errors

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

#### See also

- `GET /staff-portal/payslips/{stubId}/pdf`

### 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. |
| `stubId` | path | string | yes | Pay stub id. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The payslip |
| `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 /staff-portal/payslips/{stubId}/pdf

**Download a payslip PDF**

`operationId: StaffPortalController_payslipPdf`

The pay stub as a PDF — what an employee sends to a landlord or a lender. Binary response, not JSON.

#### Signature

```http
GET /staff-portal/payslips/{stubId}/pdf (stubId: string) -> The payslip PDF
```

#### Access

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

#### Errors

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

#### See also

- `GET /staff-portal/payslips/{stubId}`

### 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. |
| `stubId` | path | string | yes | Pay stub id. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The payslip PDF |
| `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 /staff-portal/w2/{year}

**Get my W-2**

`operationId: StaffPortalController_w2`

The employee's W-2 for a tax year — a statutory tax document, so the figures must match what was filed.

#### Signature

```http
GET /staff-portal/w2/{year} (year: string) -> The W-2
```

#### Access

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

#### Errors

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

#### See also

- `GET /staff-portal/w2/{year}/pdf`

### 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. |
| `year` | path | string | yes | Tax year. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The W-2 |
| `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 /staff-portal/w2/{year}/pdf

**Download my W-2 PDF**

`operationId: StaffPortalController_w2Pdf`

The W-2 as a PDF, in the form an employee files with. Binary response, not JSON.

#### Signature

```http
GET /staff-portal/w2/{year}/pdf (year: string) -> The W-2 PDF
```

#### Access

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

#### Errors

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

#### See also

- `GET /staff-portal/w2/{year}`

### 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. |
| `year` | path | string | yes | Tax year. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The W-2 PDF |
| `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 /staff-portal/deductions

**Get my deductions**

`operationId: StaffPortalController_deductions`

Recurring deductions from the employee's pay — benefits, garnishments, contributions.

#### Signature

```http
GET /staff-portal/deductions () -> Deductions
```

#### Access

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

#### Errors

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

#### See also

- `GET /staff-portal/payslips`

### 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` | Deductions |
| `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 /staff-portal/onboarding

**My onboarding**

`operationId: JourneysStaffController_onboarding`

The caller’s onboarding (or contractor) journey: their own tasks by stage (before day 1 only the pre-boarding ones), what others are doing for them, the next things to do. Looks at the evidence first, so what they just did shows as done.

#### Signature

```http
GET /staff-portal/onboarding () -> `{ journey\|null, counts, myCounts, stageNow, preboarding, stages:[{stage,title,dueDate,status,tasks:[TaskView]}], nextTasks:[TaskView], othersForMe:[{title, assigneeName, status, dueDate, meeting?}] }`
```

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

### Responses

| Status | Meaning |
| --- | --- |
| `200` | `{ journey\|null, counts, myCounts, stageNow, preboarding, stages:[{stage,title,dueDate,status,tasks:[TaskView]}], nextTasks:[TaskView], othersForMe:[{title, assigneeName, status, dueDate, meeting?}] }` |
| `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 /staff-portal/tasks

**My own journey tasks**

`operationId: JourneysStaffController_tasks`

Only the caller’s tasks as the person on the journey.

#### Signature

```http
GET /staff-portal/tasks (status?: string) -> `{ data:[TaskView] }`
```

#### 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. |
| `status` | query | string | — |  |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | `{ data:[TaskView] }` |
| `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. |

