# Staff Portal · Forms

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

**All my government forms**

`operationId: StaffFormsController_list`

One summary per form key — title, jurisdiction, status, `applies` (false = not required for me; hide it), `fillable` (a schema exists), signedAt, expiresOn, expiredAt, renewalRule, optional, message, source. The staff Tax forms screen lists these and opens each with GET /staff-portal/forms/{formKey}.

#### Signature

```http
GET /staff-portal/forms () -> { data: FormSummary[] }
```

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

## GET /staff-portal/forms/{formKey}

**My government form**

`operationId: StaffFormsController_get`

W-4 (2026); the work state’s withholding certificate — every state that taxes wages plus DC (see `STATE_WITHHOLDING_FORMS` in gov-forms/form-definitions.ts, each citing its official PDF). States with no wage income tax answer `not_required`; states that compute withholding from the federal W-4 and have no certificate of their own answer `federal_w4_used` with a message; a state whose own certificate is optional returns it with `optional: true`. Where a state accepts more than one certificate (NC-4 / NC-4EZ; NY IT-2104 / IT-2104-E for exemption) the view lists `alternatives: [{ variant, title, officialForm, current }]`; re-open with `?variant=<schemaVersion>` and send `variant` in the PUT body to fill the other one. A certificate signed for a previous work state is not shown for the new state. Also I-9 Section 1 (edition 01/20/25), and for contractors W-9 (Rev. March 2024) and the contractor agreement. Pre-filled from the employee record; SSN/TIN and document numbers are masked.

**Expiry.** A W-4 claiming exemption is good through February 15 of the year after it is signed (IRS Pub 15); state certificates whose exempt claim must be renewed each year carry their own date (e.g. MN W-4MN, NE W-4N, MD MW507 line 3: Feb 15). W-9 and the contractor agreement expire only if the org sets a renewal period (`PUT /business-made/gov-forms/renewal-settings`). The date is stamped on the document (`expirationDate`) when the employee signs, so readiness shows the requirement due soon ahead of it. The day after, the form becomes `expired`: readiness re-reads the evidence (the requirement goes due / overdue), `journeys.evidence` is emitted, and payroll readiness exceptions list it. An expired exempt W-4 switches the payroll profile to the IRS default — Single / MFS with no Step 2–4 entries (Pub 15 §9; an older non-exempt W-4 is not revived) — and says so. An expired state exemption turns `stateTax.exempt` off. Expiry runs on read (this endpoint, the exceptions list) and daily via `GovFormsService.expireDueForms(orgId)`.

#### Signature

```http
GET /staff-portal/forms/{formKey} (formKey: string, variant?: string) -> FormView
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | UNKNOWN_FORM | Unknown form "w5" | `formKey` is not a known form; the body lists `allowed`. | — |

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. |
| `formKey` | path | "w4" \| "state-withholding" \| "i9-section1" \| "w9" \| "contractor-agreement" | yes |  |
| `variant` | query | string | — | state-withholding only: schemaVersion of an alternative certificate listed in `alternatives`. |

### Responses

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

## PUT /staff-portal/forms/{formKey}

**Save or sign my form**

`operationId: StaffFormsController_put`

Saves answers (a masked value sent back means unchanged). With `signature` the form is validated for signing and signed; the server stamps signedAt, IP and user agent. A signed W-4 / state certificate updates the payroll profile’s tax elections, and every signed form is readiness evidence. Re-saving a signed tax form opens a new version; the signed one stays in force until the new one is signed. A signed I-9 Section 1 is locked.

#### Signature

```http
PUT /staff-portal/forms/{formKey} (formKey: string, body) -> FormView
```

#### Access

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

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `400` | FORM_INVALID | The form has problems | Validation failed; `errors` lists each problem. | — |
| `409` | I9_SECTION1_LOCKED | I-9 Section 1 is signed | Editing after signature. | — |
| `422` | FORM_NOT_REQUIRED | Not required | e.g. state withholding in TX/FL, W-4 for a contractor. | — |

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. |
| `formKey` | path | "w4" \| "state-withholding" \| "i9-section1" \| "w9" \| "contractor-agreement" | yes |  |

### Request body

```json
{
  "data": {
    "filingStatus": "single"
  },
  "signature": {
    "typedName": "Ana Ruiz",
    "consent": true
  }
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `200` | FormView |
| `400` | The form has problems — Validation failed; `errors` lists each problem. |
| `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. |
| `409` | I-9 Section 1 is signed — Editing after signature. |
| `422` | Not required — e.g. state withholding in TX/FL, W-4 for a contractor. |
| `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. |

