# Business Made · Readiness

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

**Background jobs: settings and status**

`operationId: WorkforceJobsController_getSettings`

Every readiness background job is a per-org job on the platform schedule queue (a `schedule` record named `workforce-job:<job>`, queued as `<org>-<scheduleId>-cron`), never a server-wide loop. Jobs: `readiness-daily` (sync, renewals, expiries, reminder ladder, shift flags — daily at `time`), `journeys-sweep` (every `intervalMinutes`), `journey-exact` (a one-off job per offboarding task at its exact end time), `i9-alerts` (daily HR digest of I-9 deadlines, off by default), `course-sources` (daily re-draft of AI courses whose SOP changed, off by default); and for orgs with leave turned on, `leave-nightly` (accrual, January rollover, mark taken — daily 03:00) and `leave-digest` (approver digest — daily 08:00). Only the groups the org uses are listed (`groups`). Daily jobs run in `timezone.effective`: the settings zone, else the business time zone. `escalation` holds the org defaults a rule's own escalation overrides field by field; `journeyEscalation` sets when overdue journey tasks go to the manager and to HR. Jobs are set up when the org gets its first requirement rule or journey, on every server start (re-queued if Redis lost them), and on save.

#### Signature

```http
GET /business-made/readiness/settings () -> { timezone, escalation, journeyEscalation, jobs:[JobRow], groups:[{group,label}], options }
```

#### 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` | { timezone, escalation, journeyEscalation, jobs:[JobRow], groups:[{group,label}], options } |
| `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. |

Example response:

```json
{
  "timezone": {
    "value": "",
    "effective": "America/Los_Angeles",
    "business": "America/Los_Angeles"
  },
  "escalation": {
    "reminderDays": [
      60,
      30,
      14,
      7
    ],
    "notifyManagerAtDays": 14,
    "notifyHrAtDays": 7,
    "flagShiftsAtDays": 7
  },
  "journeyEscalation": {
    "managerAfterDays": 2,
    "hrAfterDays": 4
  },
  "jobs": [
    {
      "job": "readiness-daily",
      "label": "Readiness daily run",
      "kind": "daily",
      "enabled": true,
      "available": true,
      "time": "04:00",
      "cron": "0 4 * * *",
      "timezone": "America/Los_Angeles",
      "schedule": "Daily at 04:00 (America/Los_Angeles)",
      "scheduleId": "66f5…",
      "queued": true,
      "nextRun": "2026-09-27T11:00:00.000Z",
      "lastRun": {
        "at": "2026-09-26T11:00:00.212Z",
        "trigger": "schedule",
        "ok": true,
        "ms": 8123,
        "summary": {
          "assigned": 2,
          "withdrawn": 0,
          "renewed": 1,
          "expired": 0,
          "notices": 4,
          "flagged": 1,
          "reoffered": 0
        }
      }
    },
    {
      "job": "journeys-sweep",
      "kind": "interval",
      "enabled": true,
      "intervalMinutes": 10,
      "cron": "*/10 * * * *",
      "schedule": "Every 10 minutes",
      "queued": true,
      "nextRun": "2026-09-26T16:40:00.000Z",
      "lastRun": {
        "at": "2026-09-26T16:30:00.051Z",
        "trigger": "schedule",
        "ok": true,
        "ms": 911,
        "summary": {
          "journeys": 3,
          "escalated": 0
        }
      }
    },
    {
      "job": "journey-exact",
      "kind": "exact",
      "enabled": true,
      "schedule": "At each task’s own time",
      "pending": 1,
      "nextRun": "2026-09-30T00:59:59.999Z",
      "nextTask": "Revoke access"
    }
  ],
  "options": {
    "intervals": [
      {
        "value": 5,
        "label": "Every 5 minutes"
      },
      {
        "value": 10,
        "label": "Every 10 minutes"
      }
    ]
  }
}
```

## PUT /business-made/readiness/settings

**Save background job settings**

`operationId: WorkforceJobsController_saveSettings`

Partial body, merged over what is stored (bucket `readiness` of the org base-setting; the leave jobs' switch and time are written to `leave.jobs`, leaving every other leave setting as it was). Saving re-schedules the org's jobs at once: a changed time, zone or interval replaces the queued repeat, a job turned off is stopped (its record keeps the last result), and turning `journey-exact` off or on removes or books the one-off jobs of every open exact-time task. Returns the same shape as GET.

#### Signature

```http
PUT /business-made/readiness/settings (body) -> Same as GET /business-made/readiness/settings
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | SETTINGS_INVALID | Use HH:mm, 24-hour | An unknown job, a bad time, zone or interval, or HR set to be told before the manager. | The body carries `problems: [{ field, message }]`. |

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
{
  "timezone": "America/New_York",
  "jobs": {
    "readiness-daily": {
      "time": "05:30"
    },
    "journeys-sweep": {
      "intervalMinutes": 15
    },
    "i9-alerts": {
      "enabled": true,
      "withinDays": 60
    }
  },
  "journeyEscalation": {
    "managerAfterDays": 1,
    "hrAfterDays": 3
  }
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Same as GET /business-made/readiness/settings |
| `400` | Use HH:mm, 24-hour — An unknown job, a bad time, zone or interval, or HR set to be told before the manager. |
| `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/readiness/jobs

**Background jobs**

`operationId: WorkforceJobsController_list`

The `jobs` rows of GET settings.

#### Signature

```http
GET /business-made/readiness/jobs () -> JobRow[]
```

#### 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` | JobRow[] |
| `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/readiness/jobs/{job}/run

**Run a job now**

`operationId: WorkforceJobsController_runNow`

Runs the job for this org immediately (even when it is turned off) and waits for it. `journey-exact` run by hand runs every exact-time task already due. The result is stored as the job's last run.

#### Signature

```http
POST /business-made/readiness/jobs/{job}/run (job: string) -> { result:{ at, trigger:"manual", ok, ms, summary?, error?, by }, job: JobRow }
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `409` | JOB_RUNNING | Readiness daily run is already running | A scheduled or manual run of the same job for this org has not finished. | Wait and look at the last run. |
| `404` | JOB_NOT_FOUND | Job not found | No job has that id. | — |

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. |
| `job` | path | "readiness-daily" \| "journeys-sweep" \| "journey-exact" \| "i9-alerts" \| "course-sources" \| "leave-nightly" \| "leave-digest" | yes |  |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | { result:{ at, trigger:"manual", ok, ms, summary?, error?, by }, job: JobRow } |
| `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` | Job not found — No job has that id. |
| `409` | Readiness daily run is already running — A scheduled or manual run of the same job for this org has not finished. |
| `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/readiness/overview

**Readiness overview**

`operationId: ReadinessController_overview`

Totals, per location and per requirement. Rates are 0–100. `readyRate` = share of people with requirements who have nothing overdue or expired. `dueSoon` = open items due in 30 days; `expiring30` = completions expiring in 30 days; `blocked` = people blocked at one or more gates.

#### Signature

```http
GET /business-made/readiness/overview (location: string) -> Overview
```

#### 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. |
| `location` | query | string | — | Location name or id |

### Responses

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

Example response:

```json
{
  "totals": {
    "employees": 212,
    "requirements": 9,
    "readyRate": 87,
    "overdue": 14,
    "dueSoon": 31,
    "expiring30": 6,
    "blocked": 3
  },
  "byLocation": [
    {
      "location": "Downtown",
      "readyRate": 81,
      "overdue": 9,
      "blocked": 2,
      "employees": 64
    }
  ],
  "byRequirement": [
    {
      "id": "RULE-1",
      "title": "Food handler card (California)",
      "kind": "certification",
      "rate": 78,
      "overdue": 7,
      "dueSoon": 4
    }
  ]
}
```

## GET /business-made/readiness/matrix

**People × requirements matrix**

`operationId: ReadinessController_matrix`

Server-side filtered and paged. Every row carries all of its cells (only requirements that apply to that person). `status` filters on a cell status, or `ready`, `not_ready`, `blocked`. Page is 0-based.

#### Signature

```http
GET /business-made/readiness/matrix (location: string, department: string, status: string, requirement: string, employee: string, search: string, page: string, pageSize: string) -> Matrix page
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | BAD_FILTER | status must be one of …, ready, not_ready, blocked | `status` is not a requirement status or ready / not_ready / blocked. | — |

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. |
| `location` | query | string | — |  |
| `department` | query | string | — |  |
| `status` | query | string | — |  |
| `requirement` | query | string | — |  |
| `employee` | query | string | — |  |
| `search` | query | string | — |  |
| `page` | query | string | — |  |
| `pageSize` | query | string | — |  |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Matrix page |
| `400` | status must be one of …, ready, not_ready, blocked — `status` is not a requirement status or ready / not_ready / blocked. |
| `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. |

Example response:

```json
{
  "requirements": [
    {
      "id": "RULE-1",
      "title": "Food handler card",
      "kind": "certification"
    }
  ],
  "rows": [
    {
      "employeeId": "EMP-1043",
      "name": "Luis Ramirez",
      "location": "Downtown",
      "department": "Kitchen",
      "position": "Line cook",
      "overall": 50,
      "cells": {
        "RULE-1": {
          "status": "expired",
          "dueDate": "2026-03-02",
          "completedAt": "2023-03-02T00:00:00.000Z",
          "expiresAt": "2026-03-02T23:59:59.999Z",
          "evidenceRef": {
            "datatype": "bm_employee_document",
            "id": "DOC-9"
          }
        }
      }
    }
  ],
  "total": 1,
  "page": 0,
  "pageSize": 50
}
```

## GET /business-made/readiness/employee/{employeeId}

**One person: requirements, gates, overrides and history**

`operationId: ReadinessController_employee`

Score, effect per gate (ok/warn/block), every applicable requirement with evidence and overrides, and the full ledger history (newest first; each entry has its implied `to`).

#### Signature

```http
GET /business-made/readiness/employee/{employeeId} (employeeId: string) -> { employee, overall, byGate, items, history }
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | EMPLOYEE_NOT_FOUND | Employee not found | No employee has that id. | — |

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 | Badge number (employeeId) or record id |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | { employee, overall, byGate, items, history } |
| `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` | Employee not found — No employee 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/readiness/check/{employeeId}

**Gate check**

`operationId: ReadinessController_check`

The same answer a gate gets from `ReadinessService.check`. `effect`: none; warn (let through, show warnings: inside grace, a warn rule, or a block covered by an active override, then `overrideActive` is set); override (stop unless an override is recorded); block (stop). `at` evaluates at another instant (a future shift start). A POS rule with a permissionScope applies only when the same `permissionScope` is passed. A station-scoped rule with no station known only warns.

#### Signature

```http
GET /business-made/readiness/check/{employeeId} (employeeId: string, gate: string, at: string, station: string, locationId: string, permissionScope: string, shiftId: string) -> { effect, blocking, warnings, overrideActive? }
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | BAD_GATE | gate is required | `gate` is missing or unknown ("Unknown gate "x""; the body lists the `gates`). | — |
| `404` | EMPLOYEE_NOT_FOUND | Employee not found | No employee has that id. | — |

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 |  |
| `gate` | query | "scheduler" \| "clockIn" \| "pos" \| "kitchenStation" \| "payroll" \| "access" | yes |  |
| `at` | query | string | — |  |
| `station` | query | string | — |  |
| `locationId` | query | string | — |  |
| `permissionScope` | query | string | — |  |
| `shiftId` | query | string | — |  |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | { effect, blocking, warnings, overrideActive? } |
| `400` | gate is required — `gate` is missing or unknown ("Unknown gate "x""; the body lists the `gates`). |
| `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` | Employee not found — No employee 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. |

Example response:

```json
{
  "effect": "block",
  "blocking": [
    {
      "requirementId": "RULE-1",
      "title": "Food handler card",
      "kind": "certification",
      "status": "expired",
      "expiresAt": "2026-03-02T23:59:59.999Z",
      "graceUntil": "2026-03-09T23:59:59.999Z",
      "effect": "block"
    }
  ],
  "warnings": []
}
```

## GET /business-made/readiness/rules

**List requirement rules**

`operationId: ReadinessController_listRules`

All rules, sorted by title, filtered and paged on the server (page is 0-based).

#### Signature

```http
GET /business-made/readiness/rules (kind: string, status: string, packId: string, search: string, page: string, pageSize: string) -> { data, total, page, pageSize }
```

#### 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. |
| `kind` | query | "course" \| "certification" \| "policy" \| "document" \| "form" \| "signoff" \| "task" | — |  |
| `status` | query | "draft" \| "active" \| "retired" | — |  |
| `packId` | query | string | — |  |
| `search` | query | string | — |  |
| `page` | query | string | — |  |
| `pageSize` | query | string | — |  |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | { data, total, page, pageSize } |
| `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/readiness/rules

**Create a requirement rule**

`operationId: ReadinessController_createRule`

Body is the bm_requirement_rule data (or `{ data }`). New rules are drafts, and every gate not configured defaults to `warn` (access stays off until roles are named). Also enforced on the generic repository path.

#### Signature

```http
POST /business-made/readiness/rules (body) -> The created rule
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | RULE_INVALID | The requirement rule is not valid | A field is missing or has a bad value (title and kind are required; an active rule must name what proves it). | The body carries `problems: [{ field, message }]`. Fix each field and save again. |

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
{
  "title": "Food handler card",
  "kind": "certification",
  "target": {
    "certificationId": "CERT-1"
  },
  "appliesTo": {
    "departments": [
      "Kitchen"
    ]
  },
  "due": {
    "dueFrom": "start_date",
    "dueWithinDays": 30,
    "renewalMonths": 36
  },
  "enforcement": {
    "kitchenStation": {
      "effect": "block",
      "graceDays": 7
    }
  }
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The created rule |
| `400` | The requirement rule is not valid — A field is missing or has a bad value (title and kind are required; an active rule must name what proves it). |
| `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/readiness/rules/{id}

**Get a requirement rule**

`operationId: ReadinessController_getRule`

#### Signature

```http
GET /business-made/readiness/rules/{id} (id: string) -> The rule
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | REQUIREMENT_NOT_FOUND | Requirement not found | No requirement has that id. | — |

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. |
| `id` | path | string | yes | sk or code |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The rule |
| `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` | Requirement not found — No requirement 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. |

## PUT /business-made/readiness/rules/{id}

**Update a requirement rule**

`operationId: ReadinessController_updateRule`

Merges the body over the stored data and validates the result. The rule is re-synced for everyone a few seconds later.

#### Signature

```http
PUT /business-made/readiness/rules/{id} (id: string, body) -> The updated rule
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | RULE_INVALID | The requirement rule is not valid | A field is missing or has a bad value (title and kind are required; an active rule must name what proves it). | The body carries `problems: [{ field, message }]`. Fix each field and save again. |
| `404` | REQUIREMENT_NOT_FOUND | Requirement not found | No requirement has that id. | — |

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

### Request body

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The updated rule |
| `400` | The requirement rule is not valid — A field is missing or has a bad value (title and kind are required; an active rule must name what proves it). |
| `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` | Requirement not found — No requirement 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. |

## DELETE /business-made/readiness/rules/{id}

**Delete (or retire) a requirement rule**

`operationId: ReadinessController_deleteRule`

A rule with ledger history is retired instead of deleted, and its open items are withdrawn.

#### Signature

```http
DELETE /business-made/readiness/rules/{id} (id: string) -> { id, deleted, retired, message? }
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | REQUIREMENT_NOT_FOUND | Requirement not found | No requirement has that id. | — |

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

### Responses

| Status | Meaning |
| --- | --- |
| `200` | { id, deleted, retired, message? } |
| `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` | Requirement not found — No requirement 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/readiness/rules/{id}/preview

**Who a rule applies to**

`operationId: ReadinessController_preview`

Scope only, ignoring the rule status. Unsaved edits in the body are applied first. `id` may be `new`.

#### Signature

```http
POST /business-made/readiness/rules/{id}/preview (id: string, body) -> { matches:[{employeeId,name,location,position}], count }
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | REQUIREMENT_NOT_FOUND | Requirement not found | No requirement has that id. | — |

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

### Request body

### Responses

| Status | Meaning |
| --- | --- |
| `201` | { matches:[{employeeId,name,location,position}], count } |
| `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` | Requirement not found — No requirement 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/readiness/sync

**Re-evaluate and write the ledger**

`operationId: ReadinessController_sync`

Assigns and withdraws, opens renewals and marks expiries now (the `readiness-daily` job does the same at the time in the readiness settings, 04:00 business time by default). `{ requirementId }` limits to one rule; `{ employeeId }` to one person; `{ escalate: true }` also runs the reminder ladder.

#### Signature

```http
POST /business-made/readiness/sync (body) -> { assigned, withdrawn, renewed, expired }
```

#### 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
{
  "requirementId": "RULE-1"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | { assigned, withdrawn, renewed, expired } |
| `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. |

Example response:

```json
{
  "assigned": 12,
  "withdrawn": 1,
  "renewed": 3,
  "expired": 2
}
```

## GET /business-made/readiness/job

**Daily run status**

`operationId: ReadinessController_jobStatus`

The `readiness-daily` row of GET /business-made/readiness/settings.

#### Signature

```http
GET /business-made/readiness/job () -> JobRow
```

#### 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` | JobRow |
| `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/readiness/overrides

**List overrides and waivers**

`operationId: ReadinessController_overrides`

#### Signature

```http
GET /business-made/readiness/overrides (active: string, employee: string, location: string, requirement: string, page: string, pageSize: string) -> { data, total, page, pageSize }
```

#### 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. |
| `active` | query | string | — |  |
| `employee` | query | string | — |  |
| `location` | query | string | — |  |
| `requirement` | query | string | — |  |
| `page` | query | string | — |  |
| `pageSize` | query | string | — |  |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | { data, total, page, pageSize } |
| `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/readiness/overrides

**Record an override**

`operationId: ReadinessController_override`

Lets someone past a gate for a requirement until `expiresAt` (optionally for one shift). The approver is the caller and must be the location manager or HR, never the person or their supervisor alone. Written to the ledger as an `override` entry.

#### Signature

```http
POST /business-made/readiness/overrides (body) -> { id }
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `403` | APPROVER_NOT_ALLOWED | Only the location manager or HR can approve an override | The caller is not HR (Owner/ConfigAdmin/RootAdmin, an HR role, or named HR in leave settings) nor a manager at the person's location. The person's own supervisor alone is refused, as is the person themselves. | — |
| `400` | OVERRIDE_INVALID | The override is incomplete | gate, reasonCode, reason or a future expiresAt is missing. | The body carries `problems: [{ field, message }]`. |
| `404` | REQUIREMENT_NOT_FOUND | Requirement not found | No requirement has that id. | — |

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

### Request body

```json
{
  "employeeId": "EMP-1043",
  "requirementId": "RULE-1",
  "gate": "clockIn",
  "reasonCode": "renewal_booked",
  "reason": "Exam booked for Friday",
  "expiresAt": "2026-10-03T23:59:00Z"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | { id } |
| `400` | The override is incomplete — gate, reasonCode, reason or a future expiresAt is missing. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | Only the location manager or HR can approve an override — The caller is not HR (Owner/ConfigAdmin/RootAdmin, an HR role, or named HR in leave settings) nor a manager at the person's location. The person's own supervisor alone is refused, as is the person themselves. |
| `404` | Requirement not found — No requirement 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/readiness/waivers

**Waive a requirement for a person**

`operationId: ReadinessController_waive`

Same approver rule as overrides. Optional `expiresAt`; empty = until revoked.

#### Signature

```http
POST /business-made/readiness/waivers (body) -> { id }
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `403` | APPROVER_NOT_ALLOWED | Only the location manager or HR can approve an override | The caller is not HR (Owner/ConfigAdmin/RootAdmin, an HR role, or named HR in leave settings) nor a manager at the person's location. The person's own supervisor alone is refused, as is the person themselves. | — |
| `400` | WAIVER_INVALID | The waiver is incomplete | Required waiver fields are missing; the body lists `problems`. | — |

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

### Request body

```json
{
  "employeeId": "EMP-1043",
  "requirementId": "RULE-1",
  "reasonCode": "not_applicable",
  "reason": "Front of house only"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | { id } |
| `400` | The waiver is incomplete — Required waiver fields are missing; the body lists `problems`. |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | Only the location manager or HR can approve an override — The caller is not HR (Owner/ConfigAdmin/RootAdmin, an HR role, or named HR in leave settings) nor a manager at the person's location. The person's own supervisor alone is refused, as is the person themselves. |
| `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/readiness/waivers/{id}/revoke

**End a waiver**

`operationId: ReadinessController_revokeWaiver`

Writes a new status entry; the waiver entry is never changed.

#### Signature

```http
POST /business-made/readiness/waivers/{id}/revoke (id: string) -> { id, status }
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `403` | APPROVER_NOT_ALLOWED | Only the location manager or HR can approve an override | The caller is not HR (Owner/ConfigAdmin/RootAdmin, an HR role, or named HR in leave settings) nor a manager at the person's location. The person's own supervisor alone is refused, as is the person themselves. | — |
| `404` | WAIVER_NOT_FOUND | Waiver not found | No waiver ledger entry has that id. | — |

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

### Responses

| Status | Meaning |
| --- | --- |
| `201` | { id, status } |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | Only the location manager or HR can approve an override — The caller is not HR (Owner/ConfigAdmin/RootAdmin, an HR role, or named HR in leave settings) nor a manager at the person's location. The person's own supervisor alone is refused, as is the person themselves. |
| `404` | Waiver not found — No waiver ledger entry 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/readiness/evidence

**Record proof**

`operationId: ReadinessController_evidence`

Same as `ReadinessService.recordEvidence`. For forms, tasks and manual proof with a `requirementId`, the proof is written to the ledger; for courses, documents, acknowledgements and sign-offs the record itself is read.

#### Signature

```http
POST /business-made/readiness/evidence (body) -> { ok: true }
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | EVIDENCE_INVALID | employeeId and evidence { datatype, id } are required | A required field is missing. | — |
| `404` | EMPLOYEE_NOT_FOUND | Employee not found | No employee has that id. | — |

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
{
  "employeeId": "EMP-1043",
  "requirementId": "RULE-7",
  "kind": "form",
  "evidence": {
    "datatype": "gov_form",
    "id": "w4"
  }
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | { ok: true } |
| `400` | employeeId and evidence { datatype, id } are required — A required 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. |
| `404` | Employee not found — No employee 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/readiness/point-in-time

**Who worked, and were they compliant**

`operationId: ReadinessController_pointInTime`

The ledger joined with shifts and clock punches. `at` = one instant (every applicable requirement is listed); `from`+`to` = a window of up to 62 days (only overdue/expired rows unless `issuesOnly=false`; each row shows the worst status held during the worked interval). Unscheduled punches are included.

#### Signature

```http
GET /business-made/readiness/point-in-time (at: string, from: string, to: string, location: string, issuesOnly: string) -> { at\|from\|to, rows }
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | BAD_WINDOW | Pass `at`, or `from` and `to` (ISO date-times) | Neither form is given, `to` is before `from`, or the window is longer than 62 days. | — |

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. |
| `at` | query | string | — |  |
| `from` | query | string | — |  |
| `to` | query | string | — |  |
| `location` | query | string | — |  |
| `issuesOnly` | query | string | — |  |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | { at\|from\|to, rows } |
| `400` | Pass `at`, or `from` and `to` (ISO date-times) — Neither form is given, `to` is before `from`, or the window is longer than 62 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. |

Example response:

```json
{
  "at": "2026-03-14T19:00:00.000Z",
  "worked": 18,
  "nonCompliant": 1,
  "rows": [
    {
      "employeeId": "EMP-1043",
      "name": "Luis Ramirez",
      "shiftId": "sh-1",
      "start": "2026-03-14T16:00:00.000Z",
      "end": "2026-03-15T00:00:00.000Z",
      "station": "bar",
      "requirementId": "RULE-2",
      "title": "Alcohol server",
      "status": "expired",
      "override": {
        "approverName": "Maya",
        "reason": "renewal booked"
      }
    }
  ]
}
```

## GET /business-made/readiness/export.csv

**Inspector export (CSV)**

`operationId: ReadinessController_exportCsv`

One row per person and requirement at a location, filtered like the matrix (department, status, requirement, search, employee). With no `at` it is the live state; a past `at` is read from the ledger as it stood then. With `from` and `to` it exports the point-in-time rows (who worked, which requirement, its status, any override) instead. Includes overrides/waivers with approver and the rule citation.

#### Signature

```http
GET /business-made/readiness/export.csv (location: string, at: string, from: string, to: string, department: string, status: string, requirement: string, search: string, employee: string) -> CSV file
```

#### 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. |
| `location` | query | string | — |  |
| `at` | query | string | — |  |
| `from` | query | string | — |  |
| `to` | query | string | — |  |
| `department` | query | string | — |  |
| `status` | query | string | — |  |
| `requirement` | query | string | — |  |
| `search` | query | string | — |  |
| `employee` | query | string | — |  |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | CSV file |
| `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/readiness/packs

**Jurisdiction packs**

`operationId: ReadinessController_packs`

Rule templates, each with owner, review date, citation, and notes quoting the official source with its URL. `applied` = all its rules exist in this org; `updateAvailable` = the org has an older version. Harassment prevention: us-ca (5+ employees incl. contractors, 2h/1h within 6 months, every 2 years; seasonal/temporary within 30 days or 100 hours worked), us-ny (annual), us-ny-nyc (15+ employees, more than 80 hours in a calendar year and 90 days, interns and contractors, once per calendar year), us-il (every calendar year by 31 December; restaurants and bars supplement and week-one policy; Chicago 1h/2h plus 1h bystander every July–June year and week-one policy), us-ct (3+ employees, 2h within 6 months, every 10 years), us-me (15+ employees, within 1 year, supervisors additional), us-de (50+ employees, within 1 year then every 2 years, after 6 months of service). Food safety: us-ca (card 30 days/3 years, CFPM), us-ny-nyc (Food Protection Certificate), us-il (handler 30 days/3 years, CFPM, allergen, Chicago sanitation certificate), us-tx (handler 30 days/2 years, CFM 5 years), us-fl (60 days/3 years, CFPM 5 years), us-wa (14 days, 2 years), us-or (30 days/3 years), us-ut (30 days/3 years), us-nm (30 days/3 years, CFPM), us-az-maricopa (30 days/3 years, CFPM). Rules never carry content: the company attaches its own course, certification or policy.

#### Signature

```http
GET /business-made/readiness/packs () -> Packs
```

#### 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` | Packs |
| `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/readiness/packs/{packId}/apply

**Apply a jurisdiction pack**

`operationId: ReadinessController_applyPack`

Creates the pack rules for the org as drafts with no course attached (the company attaches its own content, narrows the scope where the notes say so, then activates each rule), skipping codes already present. `{ activate: true }` creates them active; `{ codes: [] }` picks some.

#### Signature

```http
POST /business-made/readiness/packs/{packId}/apply (packId: string, body) -> { packId, version, created, skipped, next }
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | PACK_NOT_FOUND | Unknown jurisdiction pack | No pack has that id; the body lists the `packs` there are. | — |

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. |
| `packId` | path | string | yes |  |

### Request body

### Responses

| Status | Meaning |
| --- | --- |
| `201` | { packId, version, created, skipped, next } |
| `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` | Unknown jurisdiction pack — No pack has that id; the body lists the `packs` there are. |
| `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. |

