# Business Made · Payroll

Part of AppEngine API Documentation. Full index: https://appengine.appmint.io/documentation.md
## POST /business-made/payroll/runs/{id}/parallel-check

**Compare a run against an incumbent provider**

`operationId: PayrollController_parallelCheck`

Compares a calculated run line by line against figures from your existing payroll provider — the parallel run every migration should do before cutting over.

Post the incumbent's numbers and the response reports where the two disagree. It changes nothing; it is purely a reconciliation.

#### Signature

```http
POST /business-made/payroll/runs/{id}/parallel-check (id: string, body) -> The comparison, showing any discrepancies
```

#### Access

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

#### Notes

- Read-only. Run this before the first live payroll on a new setup.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | PAYROLL_RUN_NOT_FOUND | Payroll run not found | No payroll run has that id. | The body carries `code` and the `payrollRunId`. |

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

#### See also

- `POST /business-made/payroll/runs/{id}/calculate`

### 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 | Payroll run id. |

### Request body

The incumbent provider's figures for the same period.

```json
{
  "incumbent": [
    {
      "employeeId": "EMP-4821",
      "gross": 3200,
      "netPay": 2410.55,
      "federalTax": 512
    }
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The comparison, showing any discrepancies |
| `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` | Payroll run not found — No payroll run 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/payroll/runs

**List payroll runs**

`operationId: PayrollController_getPayrollRuns`

Payroll runs across the org, with their status and pay period.

#### Signature

```http
GET /business-made/payroll/runs () -> Payroll runs
```

#### Access

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

#### Errors

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

#### See also

- `GET /business-made/payroll/runs/{id}`

### Parameters

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

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Payroll runs |
| `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/payroll/runs

**Create a payroll run**

`operationId: PayrollController_createPayrollRun`

Opens a payroll run for a pay period. Creating one calculates nothing and pays nobody — it is a container the rest of the lifecycle acts on.

Clear the pre-run blockers first: missing timesheets and employees without a payroll profile will otherwise surface at calculation.

#### Signature

```http
POST /business-made/payroll/runs (body) -> The created run
```

#### Access

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

#### Notes

- Check `GET /business-made/payroll/reports/blockers` before creating a run.
- Runs are posted on `payDate` — a future payDate creates a future-dated run, so use the real pay date, not the example verbatim.

#### Errors

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

#### See also

- `GET /business-made/payroll/reports/blockers`
- `POST /business-made/payroll/runs/{id}/calculate`

### 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 run to create.

```json
{
  "payPeriodStart": "2026-01-01",
  "payPeriodEnd": "2026-01-15",
  "payDate": "2026-01-20"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The created run |
| `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/payroll/runs/{id}

**Get a payroll run**

`operationId: PayrollController_getPayrollRun`

Fetches one run with its totals and current status.

#### Signature

```http
GET /business-made/payroll/runs/{id} (id: string) -> The payroll run
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | PAYROLL_RUN_NOT_FOUND | Payroll run not found | No payroll run has that id. | The body carries `code` and the `payrollRunId`. |

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

#### See also

- `POST /business-made/payroll/runs/{id}/calculate`

### 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 | Payroll run id. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The payroll run |
| `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` | Payroll run not found — No payroll run 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/payroll/runs/{id}

**Delete a payroll run**

`operationId: PayrollController_deletePayrollRun`

Deletes a run. For one already processed this destroys the record people were paid from — cancel instead, which keeps it.

#### Signature

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

#### Access

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

#### Notes

- Destroys the payroll audit trail for that period.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | PAYROLL_RUN_NOT_FOUND | Payroll run not found | No payroll run has that id. | The body carries `code` and the `payrollRunId`. |

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

#### See also

- `POST /business-made/payroll/runs/{id}/cancel`

### 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 | Payroll run id. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Deletion 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. |
| `404` | Payroll run not found — No payroll run 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/payroll/runs/update

**Update a payroll run**

`operationId: PayrollController_updatePayrollRun`

Updates a run's own fields. Recalculate afterwards if anything affecting pay changed — the stored totals are not refreshed automatically.

#### Signature

```http
POST /business-made/payroll/runs/update (body) -> The updated run
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | PAYROLL_RUN_NOT_FOUND | Payroll run not found | No payroll run has that id. | The body carries `code` and the `payrollRunId`. |

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

#### See also

- `POST /business-made/payroll/runs/{id}/calculate`

### 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 run to update.

```json
{
  "id": "PR-4821",
  "payDate": "2026-01-21"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated run |
| `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` | Payroll run not found — No payroll run 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/payroll/runs/{id}/calculate

**Calculate a payroll run**

`operationId: PayrollController_calculatePayrollRun`

Works out gross pay, deductions and taxes for everyone in the run, producing the figures for review.

Calculation moves no money and can be repeated — re-run it after correcting a timesheet or a deduction. Employees without a payroll profile cannot be calculated and will show up as blockers.

**State and local withholding follow the employee's signed state certificate.** The elections on `profile.data.stateTax` (written when the certificate is signed) are applied to the work state's table:
- An exempt claim withholds nothing through `exemptExpiresOn`; from the next day the rest of the certificate applies again (Arizona falls back to its 2.0% default).
- `extraWithholding` is added each period; `reducedWithholding` comes off; `specifiedWithholding` replaces the computed amount.
- Tables with `calculationType: "state_withholding"` also apply the certificate itself, as each state's published method says: allowances as a wage deduction (NY $1,000, MD $3,200) or a tax credit (CA $168.30), dollar exemption amounts (IN), estimated-deduction allowances (CA), the elected percentage (AZ), and the schedule the filing status or withholding code selects (CT-W4 codes A-F, NJ-W4 rate tables A-E). Shipped for 2026: CA, NY, AZ, MD, IN, CT, NJ, OH (Ohio switches formula for payrolls ending on or after Aug 1 2026).
- Legacy bracket/flat tables (CA/NY 2025, PA) keep computing exactly as before — only the shared adjustments above apply.

**Local tax** comes from the local lines on the certificate (`stateTax.localities`): NYC and Yonkers residence (IT-2104), Maryland county (MW507, or the .0225 nonresident rate), Indiana county (WH-4) and Ohio school district (IT 4). With no certificate on file, NYC and Yonkers are recognised from the home address city. Each local line is its own tax row carrying `locality` (e.g. `NY:NYC`, `MD:16`, `OH:SD8701`). A Maryland employee with no county on file gets a warning on the stub — only the state portion was withheld.

#### Signature

```http
POST /business-made/payroll/runs/{id}/calculate (id: string) -> The calculated run
```

#### Access

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

#### Notes

- Safe to repeat. Nothing is paid until `process`.
- An employee with no state elections gets the same state figure as before; only tables newly shipped for a year change what that year withholds.
- Stores `payReadiness` on the run and flags held entries in `payrollEntries` (see run preview). `totals.heldCount` counts them.
- Workforce Readiness: with no requirement rules this endpoint behaves exactly as before. `warn` never stops anything — it comes back as `readiness.warnings` in the response.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | PAYROLL_RUN_NOT_FOUND | Payroll run not found | No payroll run has that id. | The body carries `code` and the `payrollRunId`. |

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

#### See also

- `POST /business-made/payroll/runs/{id}/parallel-check`
- `POST /business-made/payroll/runs/{id}/approve`

### Parameters

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

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The calculated run |
| `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` | Payroll run not found — No payroll run 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/payroll/runs/{id}/approve

**Approve a payroll run**

`operationId: PayrollController_approvePayrollRun`

Signs off the calculated figures. Approval is the human check between calculation and payment — the last point at which a mistake is cheap to fix.

The approver is taken from the authenticated caller and recorded on the run.

#### Signature

```http
POST /business-made/payroll/runs/{id}/approve (id: string) -> The approved run
```

#### Access

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

#### Notes

- Approving still pays nobody. `process` does that.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | PAYROLL_RUN_NOT_FOUND | Payroll run not found | No payroll run has that id. | The body carries `code` and the `payrollRunId`. |

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

#### See also

- `POST /business-made/payroll/runs/{id}/process`

### 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 | Payroll run id. |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The approved run |
| `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` | Payroll run not found — No payroll run 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/payroll/runs/{id}/process

**Process a payroll run**

`operationId: PayrollController_processPayrollRun`

Executes the run — generates pay stubs and commits the payments.

**This is the irreversible step.** Everything before it can be recalculated or cancelled; once processed, money is moving and correcting it means an off-cycle adjustment rather than an edit.

Confirm the run is approved and the figures reviewed before calling it.

#### Signature

```http
POST /business-made/payroll/runs/{id}/process (id: string) -> The processed run
```

#### Access

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

#### Notes

- Irreversible. There is no unprocess.
- Generate the ACH file separately once processed — see `POST /business-made/payroll/runs/{runId}/ach/generate`.
- Re-checks pay readiness before paying: a W-4 filed since review releases a hold; a new block holds that pay item. Held entries stay on the run with reasons; the employee is notified.
- Workforce Readiness: with no requirement rules this endpoint behaves exactly as before. `warn` never stops anything — it comes back as `readiness.warnings` in the response.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | PAYROLL_RUN_NOT_FOUND | Payroll run not found | No payroll run has that id. | The body carries `code` and the `payrollRunId`. |

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

#### See also

- `POST /business-made/payroll/runs/{runId}/ach/generate`
- `GET /business-made/payroll/stubs/run/{payrollRunId}`

### 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 | Payroll run id. |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The processed run |
| `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` | Payroll run not found — No payroll run 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/payroll/runs/{id}/cancel

**Cancel a payroll run**

`operationId: PayrollController_cancelPayrollRun`

Cancels a run before it is processed, keeping the record and the reason. The way to abandon a run without destroying evidence that it existed.

#### Signature

```http
POST /business-made/payroll/runs/{id}/cancel (id: string, body) -> The cancelled run
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | PAYROLL_RUN_NOT_FOUND | Payroll run not found | No payroll run has that id. | The body carries `code` and the `payrollRunId`. |

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

#### See also

- `DELETE /business-made/payroll/runs/{id}`

### Parameters

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

### Request body

Why it was cancelled.

```json
{
  "reason": "Timesheets not approved in time"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The cancelled run |
| `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` | Payroll run not found — No payroll run 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/payroll/stubs

**List pay stubs**

`operationId: PayrollController_getPayStubs`

Pay stubs across the org. These contain pay and deduction detail — restrict access accordingly.

#### Signature

```http
GET /business-made/payroll/stubs () -> Pay stubs
```

#### Access

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

#### Notes

- Highly sensitive personal data.

#### Errors

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

#### See also

- `GET /business-made/payroll/stubs/employee/{employeeId}`

### Parameters

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

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Pay stubs |
| `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/payroll/stubs/{id}

**Get a pay stub**

`operationId: PayrollController_getPayStub`

Fetches one pay stub with its earnings, deductions and taxes.

#### Signature

```http
GET /business-made/payroll/stubs/{id} (id: string) -> The pay stub
```

#### Access

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

#### Errors

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

#### See also

- `GET /business-made/payroll/stubs/{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. |
| `id` | path | string | yes | Pay stub id. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The pay stub |
| `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/payroll/stubs/employee/{employeeId}

**Get an employee's pay stubs**

`operationId: PayrollController_getPayStubsByEmployee`

One employee's pay stub history — what a self-service payslip screen reads.

#### Signature

```http
GET /business-made/payroll/stubs/employee/{employeeId} (employeeId: string) -> The employee's pay stubs
```

#### Access

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

#### Notes

- Scope this tightly — an employee must see only their own.

#### Errors

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

#### See also

- `GET /business-made/payroll/stubs/run/{payrollRunId}`

### Parameters

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

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The employee's pay stubs |
| `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/payroll/stubs/run/{payrollRunId}

**Get pay stubs for a run**

`operationId: PayrollController_getPayStubsByPayrollRun`

Every stub produced by one payroll run — the reconciliation view after processing.

#### Signature

```http
GET /business-made/payroll/stubs/run/{payrollRunId} (payrollRunId: string) -> Pay stubs for the run
```

#### Access

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

#### Errors

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

#### See also

- `POST /business-made/payroll/runs/{runId}/stubs/pdf/generate`

### 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. |
| `payrollRunId` | path | string | yes | Payroll run id. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Pay stubs for the run |
| `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/payroll/employer-setup

**Get employer tax setup**

`operationId: PayrollController_getEmployerTaxSetup`

The org's employer tax registration — the identifiers filings are made under.

#### Signature

```http
GET /business-made/payroll/employer-setup () -> Employer tax setup
```

#### Access

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

#### Errors

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

#### See also

- `GET /business-made/payroll/employer-setup/readiness`

### 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` | Employer tax setup |
| `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/payroll/employer-setup

**Update employer tax setup**

`operationId: PayrollController_updateEmployerTaxSetup`

Updates the employer tax registration. These identifiers appear on statutory filings, so an error here propagates to every form filed afterwards.

#### Signature

```http
POST /business-made/payroll/employer-setup (body) -> The updated setup
```

#### Access

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

#### Notes

- Check readiness after changing anything.

#### Errors

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

#### See also

- `GET /business-made/payroll/employer-setup/readiness`

### 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 setup to store.

```json
{
  "ein": "12-3456789",
  "stateRegistrations": [
    {
      "state": "CA",
      "id": "123-4567-8"
    }
  ]
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The updated setup |
| `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/payroll/employer-setup/readiness

**Check filing readiness**

`operationId: PayrollController_assessEmployerTaxSetup`

Reports what is still missing before statutory filings would be valid — an unregistered state, an absent EIN, an incomplete deposit schedule.

Run this before the first payroll of a year, and after adding a state. A filing made without it is the kind of error that is discovered by a tax authority rather than by you.

#### Signature

```http
GET /business-made/payroll/employer-setup/readiness () -> What is missing for valid filings
```

#### Access

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

#### Errors

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

#### See also

- `POST /business-made/payroll/employer-setup`

### 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` | What is missing for valid filings |
| `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/payroll/profiles/migrate-tax-elections

**Migrate legacy tax elections**

`operationId: PayrollController_migrateTaxElections`

One-off migration that moves legacy flat tax elections onto the structured `data.federalTax` and `data.stateTax` fields.

An administrative operation for an existing installation — it rewrites tax elections across profiles, so run it once, deliberately, and check the results before the next payroll.

#### Signature

```http
POST /business-made/payroll/profiles/migrate-tax-elections (body) -> The migration result
```

#### Access

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

#### Notes

- Touches tax elections across every profile. Verify a sample before running payroll.

#### Errors

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

#### See also

- `GET /business-made/payroll/profiles`

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

```json
{}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The migration 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 /business-made/payroll/profiles

**List payroll profiles**

`operationId: PayrollController_getPayrollProfiles`

The payroll profiles in the org. A profile holds pay rate, tax elections, deductions and bank details — an employee without one cannot be paid.

#### Signature

```http
GET /business-made/payroll/profiles () -> Payroll profiles
```

#### Access

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

#### Errors

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

#### See also

- `GET /business-made/payroll/profiles/employee/{employeeId}`

### Parameters

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

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Payroll profiles |
| `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/payroll/profiles

**Create a payroll profile**

`operationId: PayrollController_createPayrollProfile`

Creates the payroll profile for an employee — pay rate, tax elections and pay frequency. This is what makes someone payable, and its absence is a pre-run blocker.

#### Signature

```http
POST /business-made/payroll/profiles (body) -> The created profile
```

#### Access

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

#### Errors

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

#### See also

- `POST /business-made/payroll/profiles/employee/{employeeId}/bank-accounts`

### 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 profile to create.

```json
{
  "employeeId": "EMP-4821",
  "payType": "salary",
  "annualSalary": 76800,
  "payFrequency": "semi-monthly",
  "federalTax": {
    "filingStatus": "single",
    "allowances": 1
  }
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The created 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 /business-made/payroll/profiles/{id}

**Get a payroll profile**

`operationId: PayrollController_getPayrollProfile`

Fetches one payroll profile.

#### Signature

```http
GET /business-made/payroll/profiles/{id} (id: string) -> The profile
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | PROFILE_NOT_FOUND | Payroll profile not found | The employee has no payroll profile. | Create one with `POST /business-made/payroll/profiles`. An employee without a profile cannot be paid — they appear in the pre-run blockers. |

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

#### See also

- `POST /business-made/payroll/profiles/update`

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

### 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. |
| `404` | Payroll profile not found — The employee has no payroll profile. |
| `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/payroll/profiles/{id}

**Delete a payroll profile**

`operationId: PayrollController_deletePayrollProfile`

Deletes a payroll profile. The employee becomes unpayable and will appear as a blocker on the next run — usually you want to terminate the employee instead.

#### Signature

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

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | PROFILE_NOT_FOUND | Payroll profile not found | The employee has no payroll profile. | Create one with `POST /business-made/payroll/profiles`. An employee without a profile cannot be paid — they appear in the pre-run blockers. |

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

#### See also

- `POST /business-made/employees/{id}/terminate`

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

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Deletion 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. |
| `404` | Payroll profile not found — The employee has no payroll profile. |
| `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/payroll/profiles/employee/{employeeId}

**Get an employee's payroll profile**

`operationId: PayrollController_getPayrollProfileByEmployee`

The payroll profile for one employee, looked up by employee rather than profile id.

#### Signature

```http
GET /business-made/payroll/profiles/employee/{employeeId} (employeeId: string) -> The profile
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | PROFILE_NOT_FOUND | Payroll profile not found | The employee has no payroll profile. | Create one with `POST /business-made/payroll/profiles`. An employee without a profile cannot be paid — they appear in the pre-run blockers. |

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

#### See also

- `POST /business-made/payroll/profiles`

### Parameters

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

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The 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. |
| `404` | Payroll profile not found — The employee has no payroll profile. |
| `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/payroll/profiles/update

**Update a payroll profile**

`operationId: PayrollController_updatePayrollProfile`

Updates a payroll profile. Changes apply to **future** runs — a run already calculated keeps the figures it was calculated with until recalculated.

#### Signature

```http
POST /business-made/payroll/profiles/update (body) -> The updated profile
```

#### Access

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

#### Notes

- Recalculate any open run after a pay change, or it will pay the old rate.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | PROFILE_NOT_FOUND | Payroll profile not found | The employee has no payroll profile. | Create one with `POST /business-made/payroll/profiles`. An employee without a profile cannot be paid — they appear in the pre-run blockers. |

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

#### See also

- `POST /business-made/payroll/runs/{id}/calculate`

### 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 profile to update.

```json
{
  "id": "PRF-4821",
  "annualSalary": 82000
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | 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. |
| `404` | Payroll profile not found — The employee has no payroll profile. |
| `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/payroll/profiles/employee/{employeeId}/deductions

**Add a deduction**

`operationId: PayrollController_addDeduction`

Adds a recurring deduction to an employee's profile — a pension contribution, a garnishment, a benefit premium. It applies from the next calculated run.

#### Signature

```http
POST /business-made/payroll/profiles/employee/{employeeId}/deductions (employeeId: string, body) -> The updated profile
```

#### Access

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

#### Notes

- `preTax` changes the taxable base — getting it wrong misstates tax withheld.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | PROFILE_NOT_FOUND | Payroll profile not found | The employee has no payroll profile. | Create one with `POST /business-made/payroll/profiles`. An employee without a profile cannot be paid — they appear in the pre-run blockers. |

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

#### See also

- `POST /business-made/payroll/profiles/employee/{employeeId}/deductions/{deductionId}`

### Parameters

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

### Request body

The deduction to add.

```json
{
  "type": "pension",
  "amount": 250,
  "frequency": "per-period",
  "preTax": true
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | 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. |
| `404` | Payroll profile not found — The employee has no payroll profile. |
| `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/payroll/profiles/employee/{employeeId}/deductions/{deductionId}

**Update a deduction**

`operationId: PayrollController_updateDeduction`

Updates one of an employee's deductions. Takes effect on the next calculation, not retrospectively.

#### Signature

```http
POST /business-made/payroll/profiles/employee/{employeeId}/deductions/{deductionId} (employeeId: string, deductionId: string, body) -> The updated profile
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | PROFILE_NOT_FOUND | Payroll profile not found | The employee has no payroll profile. | Create one with `POST /business-made/payroll/profiles`. An employee without a profile cannot be paid — they appear in the pre-run blockers. |

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

#### See also

- `DELETE /business-made/payroll/profiles/employee/{employeeId}/deductions/{deductionId}`

### Parameters

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

### Request body

Fields to change.

```json
{
  "amount": 300
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | 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. |
| `404` | Payroll profile not found — The employee has no payroll profile. |
| `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/payroll/profiles/employee/{employeeId}/deductions/{deductionId}

**Remove a deduction**

`operationId: PayrollController_removeDeduction`

Removes a deduction from an employee's profile. Historical stubs keep the deductions they were calculated with.

#### Signature

```http
DELETE /business-made/payroll/profiles/employee/{employeeId}/deductions/{deductionId} (employeeId: string, deductionId: string) -> The updated profile
```

#### Access

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

#### Notes

- A court-ordered garnishment should usually be stopped through its own end date rather than deleted.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | PROFILE_NOT_FOUND | Payroll profile not found | The employee has no payroll profile. | Create one with `POST /business-made/payroll/profiles`. An employee without a profile cannot be paid — they appear in the pre-run blockers. |

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

#### See also

- `POST /business-made/payroll/profiles/employee/{employeeId}/deductions`

### Parameters

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

### 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. |
| `404` | Payroll profile not found — The employee has no payroll profile. |
| `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/payroll/profiles/employee/{employeeId}/bank-accounts

**Add a bank account**

`operationId: PayrollController_addBankAccount`

Adds a bank account for direct deposit.

A new account should be **verified** before it is paid into — an unverified account is where payroll diversion fraud lands. Treat a change of bank details as a security event, not a routine edit.

#### Signature

```http
POST /business-made/payroll/profiles/employee/{employeeId}/bank-accounts (employeeId: string, body) -> The updated profile
```

#### Access

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

#### Notes

- Verify before the next run — see the verify endpoint.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | PROFILE_NOT_FOUND | Payroll profile not found | The employee has no payroll profile. | Create one with `POST /business-made/payroll/profiles`. An employee without a profile cannot be paid — they appear in the pre-run blockers. |

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

#### See also

- `POST /business-made/payroll/profiles/employee/{employeeId}/bank-accounts/{accountId}/verify`

### Parameters

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

### Request body

The account to add.

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

### Responses

| Status | Meaning |
| --- | --- |
| `201` | 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. |
| `404` | Payroll profile not found — The employee has no payroll profile. |
| `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/payroll/profiles/employee/{employeeId}/bank-accounts/{accountId}/verify

**Verify a bank account**

`operationId: PayrollController_verifyBankAccount`

Marks a bank account as verified, allowing direct deposit to it. This is the control that stops pay going to an account nobody checked.

#### Signature

```http
POST /business-made/payroll/profiles/employee/{employeeId}/bank-accounts/{accountId}/verify (employeeId: string, accountId: string) -> The updated profile
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | PROFILE_NOT_FOUND | Payroll profile not found | The employee has no payroll profile. | Create one with `POST /business-made/payroll/profiles`. An employee without a profile cannot be paid — they appear in the pre-run blockers. |

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

#### See also

- `DELETE /business-made/payroll/profiles/employee/{employeeId}/bank-accounts/{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. |
| `employeeId` | path | string | yes | Employee id. |
| `accountId` | path | string | yes | Bank account id. |

### Responses

| Status | Meaning |
| --- | --- |
| `201` | 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. |
| `404` | Payroll profile not found — The employee has no payroll profile. |
| `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/payroll/profiles/employee/{employeeId}/bank-accounts/{accountId}

**Remove a bank account**

`operationId: PayrollController_removeBankAccount`

Removes a bank account from a profile. Removing the only account leaves the employee with nowhere to be paid — add the replacement first.

#### Signature

```http
DELETE /business-made/payroll/profiles/employee/{employeeId}/bank-accounts/{accountId} (employeeId: string, accountId: string) -> The updated profile
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | PROFILE_NOT_FOUND | Payroll profile not found | The employee has no payroll profile. | Create one with `POST /business-made/payroll/profiles`. An employee without a profile cannot be paid — they appear in the pre-run blockers. |

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

#### See also

- `POST /business-made/payroll/profiles/employee/{employeeId}/bank-accounts`

### Parameters

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

### 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. |
| `404` | Payroll profile not found — The employee has no payroll profile. |
| `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/payroll/reports/summary/{year}

**Get an annual payroll summary**

`operationId: PayrollController_getPayrollSummary`

Payroll totals for a year — gross, taxes and deductions. The figures year-end filings are reconciled against.

#### Signature

```http
GET /business-made/payroll/reports/summary/{year} (year: string) -> The annual summary
```

#### Access

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

#### Errors

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

#### See also

- `GET /business-made/payroll/reports/w2/{employeeId}/{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 annual 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 /business-made/payroll/reports/w2/{employeeId}/{year}

**Get W-2 data for an employee**

`operationId: PayrollController_getEmployeeW2Data`

The W-2 figures for one employee and tax year, as calculated from their processed runs.

This is the data behind the form. Generating the filed document is a separate step through the payroll configuration controller.

#### Signature

```http
GET /business-made/payroll/reports/w2/{employeeId}/{year} (employeeId: string, year: string) -> W-2 data
```

#### Access

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

#### Notes

- Reconcile against the annual summary before filing — a discrepancy here becomes a corrected W-2 later.

#### Errors

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

#### See also

- `GET /business-made/payroll/reports/summary/{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. |
| `employeeId` | path | string | yes | Employee id. |
| `year` | path | string | yes | Tax year. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | W-2 data |
| `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/payroll/reports/upcoming-run

**Get the upcoming payroll run**

`operationId: PayrollController_getUpcomingRun`

The next scheduled run and its pay period — what the blocker list should be cleared against.

#### Signature

```http
GET /business-made/payroll/reports/upcoming-run (start?: string, end?: string, payDate?: string) -> The upcoming run
```

#### Access

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

#### Errors

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

#### See also

- `GET /business-made/payroll/reports/blockers`

### 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. |
| `start` | query | string | yes | Pay period start (YYYY-MM-DD). |
| `end` | query | string | yes | Pay period end. |
| `payDate` | query | string | yes |  |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The upcoming run |
| `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/payroll/reports/blockers

**Get pre-run blockers**

`operationId: PayrollController_getRunBlockers`

Everything standing between you and a clean payroll run: missing timesheets, submissions still awaiting approval, and employees with no payroll profile.

**Run this before every payroll.** Each blocker is something that would otherwise be discovered during calculation, or worse, produce a run that pays someone the wrong amount.

#### Signature

```http
GET /business-made/payroll/reports/blockers (start?: string, end?: string) -> Outstanding blockers
```

#### Access

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

#### Errors

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

#### See also

- `GET /business-made/timesheets/reports/missing`
- `POST /business-made/payroll/runs`

### 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. |
| `start` | query | string | yes | Pay period start (YYYY-MM-DD). |
| `end` | query | string | yes | Pay period end. |

### Responses

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

