# AI Build Reference - Verify Client API

This reference is written for AI agents and automation builders. It avoids narrative PRD language and states the contracts needed to build integrations.

## Stability

- Public API prefix: `/api/v1`.
- Public MCP endpoint: `/api/mcp`.
- MCP protected resource metadata: `/.well-known/oauth-protected-resource/mcp`.
- Route implementation: `api/routes/v1/`.
- MCP implementation: `api/mcp/`.
- Shared integration scopes: `shared/microapps.ts`.
- Scope enforcement: `api/microapps/microappScopes.ts` and selected first-party routes.

When this reference conflicts with implementation, treat implementation as source of truth and update this reference.

## Integration Model

Firm admins create API integrations in Firm -> Apps & API access. These records use the existing MicroApps registry:

```json
{
  "appType": "api_integration",
  "identifier": "case-management-system",
  "name": "Case Management System",
  "scopes": ["cases.create", "parties.manage", "client-care-letters.generate", "invites.send"],
  "allowedIpRanges": ["203.0.113.10/32"],
  "issueClientSecret": true
}
```

Creation returns a `clientSecret` once. Store it in the calling system's secret manager. The stable `clientId` is the returned `app.appId`.

## AI Consumption Contract

AI coding agents should use this order:

1. Read `docs/api/llms.txt` or `/integrations/developer-docs/llms.txt`.
2. Read this file for workflow rules and endpoint semantics.
3. Load `docs/api/api-integrations.openapi.yaml` or `/integrations/developer-docs/api-integrations.openapi.yaml` for schemas.
4. Inspect `api/routes/v1/*` only when a required behaviour is missing from the docs.

AI coding agents must not:

- Generate browser code containing `clientSecret`.
- Invent scopes, endpoints, response fields, or hidden query parameters.
- Persist bearer tokens past `expiresAt`.
- Treat `caseId`, `partyId`, or `inviteId` as parseable values.

## Authentication

### MCP OAuth For User-Authenticated AI Clients

Use MCP when ChatGPT, Codex, Claude, or another MCP client should act as the signed-in portal user instead of a headless API integration.

Endpoints:

```http
GET /.well-known/oauth-protected-resource/mcp
POST /api/mcp
```

Rules:

- Register the MCP server URL as `$VERIFY_CLIENT_BASE_URL/api/mcp`.
- Do not configure a static `Authorization` header when the client should trigger OAuth sign-in.
- The MCP endpoint responds to unauthenticated requests with `401` and `WWW-Authenticate: Bearer resource_metadata="..."`.
- Protected tools advertise OAuth security schemes with `vc.mcp.read` and, for future mutation tools, `vc.mcp.write`.
- Tool-level permission failures return `_meta["mcp/www_authenticate"]` so clients can restart sign-in or request the needed scope.
- The authenticated principal still goes through Verify Client role, organisation, and MicroApp scope checks. OAuth scope alone is not enough to access a case.

Current MCP tools:

| Tool           | Purpose                                              | Required Verify Client access                              |
| -------------- | ---------------------------------------------------- | ---------------------------------------------------------- |
| `whoami`       | Return the current principal and MCP capabilities    | Authenticated portal or system principal                   |
| `list_cases`   | List visible cases with optional status/demo filters | `case.list`; internal users must provide `orgId`           |
| `search_cases` | Search visible cases by matter/case/address fields   | `case.list`; internal users must provide `orgId`           |
| `get_case`     | Return a summarized case record                      | `case.read` plus organisation scope for the requested case |

Safety rules:

- MCP case tools return summarized matter, party, task, form, and document metadata only.
- Raw client workflow payloads, evidence keys, and large sensitive blobs are omitted.
- Cross-organisation access is rejected even when the caller knows a `caseId`.
- Future write tools should require `vc.mcp.write`, explicit confirmation, dry-run previews, and audit attribution.

Client setup examples:

```bash
codex mcp add verify-client --url "$VERIFY_CLIENT_BASE_URL/api/mcp"
codex mcp login verify-client
```

```bash
claude mcp add --transport http verify-client "$VERIFY_CLIENT_BASE_URL/api/mcp"
claude mcp login verify-client
```

### Create A System Token

Endpoint:

```http
POST /api/v1/microapps/system-tokens
Content-Type: application/json
```

This is a `POST` endpoint. Opening `/api/v1/microapps/system-tokens` in a browser or calling it with `GET` will not issue a token; use the curl example below or an equivalent server-side HTTP client.

Request:

```json
{
  "clientId": "00000000-0000-0000-0000-000000000000",
  "clientSecret": "stored-secret",
  "scopes": ["cases.create", "parties.manage", "client-care-letters.generate", "invites.send"],
  "expiresInSeconds": 3600
}
```

Rules:

- `clientId` is the integration `appId`.
- `clientSecret` is the one-time secret issued at creation or rotation.
- `scopes` is optional. If omitted, the token receives all scopes enabled on the integration.
- Requested scopes are intersected with integration scopes; no privilege escalation is possible.
- `expiresInSeconds` is clamped between 60 seconds and 30 days.
- `allowedIpRanges`, when configured, are enforced at token issuance and API use.

Response:

```json
{
  "ok": true,
  "accessToken": "<jwt>",
  "principal": {
    "principalType": "microApp",
    "tokenKind": "system",
    "appId": "00000000-0000-0000-0000-000000000000",
    "orgId": "org_123",
    "identifier": "case-management-system",
    "scopes": ["cases.create", "parties.manage", "client-care-letters.generate", "invites.send"]
  },
  "expiresAt": 1770000000
}
```

Use the token:

```http
Authorization: Bearer <accessToken>
Content-Type: application/json
```

Server-side curl example:

```bash
curl -X POST "$VERIFY_CLIENT_BASE_URL/api/v1/microapps/system-tokens" \
  -H "Content-Type: application/json" \
  -d '{
    "clientId": "00000000-0000-0000-0000-000000000000",
    "clientSecret": "'"$VERIFY_CLIENT_CLIENT_SECRET"'",
    "scopes": ["cases.create", "parties.manage", "client-care-letters.generate", "invites.send"],
    "expiresInSeconds": 3600
  }'
```

## Scopes

| Scope                          | Purpose                                        | Current endpoint                                                                                                                     |
| ------------------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `cases.create`                 | Create onboarding cases                        | `POST /api/v1/cases`                                                                                                                 |
| `parties.manage`               | Add/edit parties and choose the primary client | `POST /api/v1/cases/{caseId}/parties`, `PATCH /api/v1/cases/{caseId}/parties/{partyId}`, `PUT /api/v1/cases/{caseId}/primary-client` |
| `client-care-letters.generate` | Generate configured Client Care Letters        | `POST /api/v1/cases/{caseId}/client-care-letter/generate`                                                                            |
| `invites.send`                 | Send onboarding invite links and reminders     | `POST /api/v1/cases/{caseId}/invites`, `POST /api/v1/cases/{caseId}/parties/{partyId}/reminders`                                     |
| `cases.ids.list`               | List case IDs through MicroApps API            | `GET /api/v1/microapps/api/cases/ids`                                                                                                |
| `users.list`                   | List firm users through MicroApps API          | `GET /api/v1/microapps/api/users`                                                                                                    |
| `cases.read`                   | Read case and Client Care Letter status        | `GET /api/v1/cases/{caseId}/client-care-letter/status`                                                                               |
| `cases.list`                   | Reserved for first-party case list automation  | Inspect route support before use                                                                                                     |

## Supported Automation Flow

### 1. Create A Case With Parties

Required scope: `cases.create`

```http
POST /api/v1/cases
Authorization: Bearer <system-token>
Content-Type: application/json
```

Request:

```json
{
  "matterRef": "MAT-2026-001",
  "matterType": "Purchase",
  "tenure": "freehold",
  "propertyAddress": {
    "line1": "1 High Street",
    "city": "London",
    "postcode": "SW1A 1AA"
  },
  "purchasePropertyAddress": {
    "line1": "1 Purchase Street",
    "city": "London",
    "postcode": "SW1A 1AA"
  },
  "salePropertyAddress": {
    "line1": "9 Sale Road",
    "city": "London",
    "postcode": "SW1A 2AA"
  },
  "externalQuote": {
    "provider": "gc-crm",
    "quoteReference": "Q-123",
    "quoteUrl": "https://crm.example/quotes/Q-123",
    "quotePdfKey": "org/org_123/case/case_123/quotes/quote/Q-123.pdf",
    "termsUrl": "https://crm.example/terms/current",
    "termsPdfKey": "org/org_123/case/case_123/quotes/terms/terms.pdf",
    "totalAmountMinor": 120000,
    "depositAmountMinor": 25000,
    "depositPaid": true
  },
  "requireSof": true,
  "parties": [
    {
      "name": "Alex Client",
      "email": "alex@example.com",
      "mobile": "+447700900123",
      "role": "Buyer",
      "partyType": "individual",
      "isPrimaryContact": true
    }
  ]
}
```

Required fields:

- `matterRef`
- `matterType`

Optional fields:

- `propertyAddress.line1`
- `propertyAddress.city`
- `propertyAddress.postcode`
- `purchasePropertyAddress` / `salePropertyAddress`: use these for `Sale & Purchase` matters so the sale and onward purchase can be tracked separately. `propertyAddress` remains the compatibility primary address and should normally mirror the purchase address when supplied.
- `tenure`: `freehold`, `leasehold`, or `unsure`. Legacy `unknown`, `not sure`, and `commonhold` inputs are accepted and normalized to `unsure`. When supplied, the API resolves tenure-specific onboarding packs before falling back to the matter-type default. Sale-style matters also get the expected Law Society defaults: TA6 for freehold/unsure sale matters and TA6 plus TA7 for leasehold sale matters when those documents are not already in the configured pack.
- `externalQuote`: optional metadata from a CRM or third-party quote provider. Verify Client does not generate this quote; it stores the provider reference/link/PDF reference, Terms of Business link/PDF reference, and deposit values so they can be shown in client packs, letters, and evidence later. Hosted `quoteUrl` and `termsUrl` values are shown to the client as non-blocking "Quote" and "Terms of Business" items in Forms and documents.
- `requireSof` (ignored for matter types that do not support source of funds, such as `Sale`)
- `demoMode`
- `expiresAt`
- `fees` is retained for legacy integrations, but new integrations should use `externalQuote.totalAmountMinor`, `externalQuote.depositAmountMinor`, and `externalQuote.depositPaid`.
- `parties`

Matter-type defaults are resolved by the API. When a firm has configured an onboarding pack for the matter type, forms and official documents are assigned automatically to parties supplied during case creation and to parties added later through `POST /api/v1/cases/{caseId}/parties`. If `tenure` is supplied, tenure-specific packs such as `purchase:leasehold` are preferred before the generic `Purchase` pack. Empty tenure rows inherit the base matter-type pack. Integrations should send structured matter facts such as `matterType`, `tenure`, sale/purchase addresses, quote metadata, and parties; they should not duplicate the firm's pack-selection logic.

Source of Funds mode is resolved from the firm's onboarding settings. The firm default is `collect_only`: Verify Client asks the client for SOF information and stores it in the evidence pack, but does not force a firm review decision inside Verify Client. Firms can set a different default and then override individual matter-type or tenure pack rows with `firm_review_required` where SOF should remain a Verify Client review blocker, or `not_requested` where the client should not be asked for SOF.

To upload a third-party quote or Terms of Business PDF rather than hosting it elsewhere:

```http
POST /api/v1/cases/{caseId}/external-quote/presign
Authorization: Bearer <system-token>
Content-Type: application/json
```

```json
{
  "filename": "Q-123.pdf",
  "contentType": "application/pdf",
  "kind": "quote"
}
```

Upload the PDF to the returned `uploadUrl`, then save the returned `key` on `externalQuote.quotePdfKey` or `externalQuote.termsPdfKey`:

```http
PUT /api/v1/cases/{caseId}/external-quote
Authorization: Bearer <system-token>
Content-Type: application/json
```

```json
{
  "externalQuote": {
    "provider": "gc-crm",
    "quoteReference": "Q-123",
    "quotePdfKey": "org/org_123/case/case_123/quotes/quote/Q-123.pdf",
    "termsPdfKey": "org/org_123/case/case_123/quotes/terms/terms.pdf",
    "totalAmountMinor": 120000,
    "depositAmountMinor": 25000,
    "depositPaid": true
  }
}
```

Only PDF uploads are accepted. API integrations can use this narrow endpoint with the same `cases.create` scope they use to create the matter.

Party fields:

- `name` is required.
- `email` is optional.
- `mobile` is optional. When supplied, it must be a valid UK or international phone number and is stored in normalized international format.

- Required: `name`
- Optional: `email`, `mobile`, `role`, `partyType`, `company`, `relationshipToCompany`, `isPrimaryContact`
- `partyType` defaults to `individual`.
- For `partyType=company`, `company.companyName` defaults to party `name`.
- If no primary client is supplied, the API automatically marks the first party as the primary client. For multi-party matters where another party should be the Client Care Letter addressee, pass `isPrimaryContact: true` on exactly one party, or call `PUT /api/v1/cases/{caseId}/primary-client` after all parties have been created. Do not rely on guessed aliases such as `isPrimary`; the canonical stored field is `isPrimaryContact`, and the dedicated endpoint remains the clean way to change the final primary-client choice.

Client Care Letter contact fields are not normally required in API payloads. Generation uses the saved matter, parties, fees, and firm defaults. If the status endpoint reports missing CCL responsibility data, update the matter with `clientCareLetter.feeEarnerName`, `clientCareLetter.feeEarnerRoleTitle`, `clientCareLetter.supervisorName`, or `clientCareLetter.supervisorRoleTitle` only for that exception.

Response:

```json
{
  "caseId": "case_opaque_id",
  "status": "NotStarted",
  "parties": [
    {
      "partyId": "party_opaque_id",
      "name": "Alex Client",
      "email": "alex@example.com",
      "mobile": "+447700900123",
      "role": "Buyer",
      "partyType": "individual",
      "clientAccessStatus": "not_sent"
    }
  ]
}
```

Server-side curl example:

```bash
curl -X POST "$VERIFY_CLIENT_BASE_URL/api/v1/cases" \
  -H "Authorization: Bearer $VERIFY_CLIENT_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "matterRef": "MAT-2026-001",
    "matterType": "Purchase",
    "parties": [
      {
        "name": "Alex Client",
        "email": "alex@example.com",
        "role": "Buyer"
      }
    ]
  }'
```

### Duplicate Safeguards

Use a stable `matterRef` from the source system for every live creation attempt. Verify Client enforces `matterRef` uniqueness within the firm and rejects duplicate create requests with a `400` validation error.

Recommended create flow:

1. Check your own sync ledger for the source matter/reference.
2. If you keep a remote Verify Client ledger, refresh it with `GET /api/v1/microapps/api/cases/ids` before creating.
3. Call `POST /api/v1/cases` only when the local and remote checks are conclusive.
4. Treat a `400` duplicate matter reference response from `POST /api/v1/cases` as an existing case, not as a retryable server failure.

`GET /api/v1/microapps/api/cases/ids` is a scoped connectivity/sync endpoint, not a full-text search endpoint:

```http
GET /api/v1/microapps/api/cases/ids?limit=200&cursor=<nextCursor>
Authorization: Bearer <system-token>
```

Response:

```json
{
  "ok": true,
  "items": ["case_opaque_id"],
  "nextCursor": null
}
```

Remote duplicate-check failure handling:

- `200`: use the returned page and continue until `nextCursor` is `null` when a complete sync is required.
- `400`: fix the request, usually an invalid cursor; do not retry unchanged.
- `401`: credentials are missing, expired, malformed, or invalid. Refresh the system token or rotate/check credentials. Do not report this as duplicate uncertainty.
- `403`: the token is valid but lacks scope, the integration is disabled, or source IP/origin restrictions failed. A firm admin must correct configuration.
- `5xx` or network timeout: remote state is unknown. Block live creation unless an operator explicitly accepts local-ledger-only dedupe for that run.

### 2. Add A Party Later

Required scope: `parties.manage`

```http
POST /api/v1/cases/{caseId}/parties
Authorization: Bearer <system-token>
Content-Type: application/json
```

Request:

```json
{
  "name": "Jordan Client",
  "email": "jordan@example.com",
  "mobile": "+447700900456",
  "role": "Seller",
  "partyType": "individual"
}
```

Response: the created party record.

### 3. Edit A Party

Required scope: `parties.manage`

Use this when a source system needs to correct a client's name, email, mobile, role, or company metadata after the party has already been created. Supplied email addresses are trimmed/lowercased and must be unique within the case. Supplied mobile numbers are validated and stored in normalized international format.

```http
PATCH /api/v1/cases/{caseId}/parties/{partyId}
Authorization: Bearer <system-token>
Content-Type: application/json
```

Request:

```json
{
  "name": "Jordan Client",
  "email": "jordan.updated@example.com",
  "mobile": "+447700900456",
  "role": "Seller"
}
```

Response: the updated party record.

Failure handling:

- `400`: party does not exist, email duplicates another party on the case, or the supplied phone number is invalid.
- `401`: token is missing, expired, or invalid.
- `403`: token is valid but lacks `parties.manage`, the integration is disabled, or source restrictions failed.

### 4. Set The Primary Client For Multi-Party Matters

Required scope: `parties.manage`

Use this endpoint when the automatically selected first party is not the right primary client. It marks exactly one party as the primary client for Client Care Letter variables and invite readiness.

```http
PUT /api/v1/cases/{caseId}/primary-client
Authorization: Bearer <system-token>
Content-Type: application/json
```

Request:

```json
{
  "partyId": "party_opaque_id"
}
```

Response:

```json
{
  "ok": true,
  "party": {
    "partyId": "party_opaque_id",
    "name": "Alex Client",
    "isPrimaryContact": true
  }
}
```

Failure handling:

- `400`: `partyId` is missing or does not belong to the case.
- `401`: token is missing, expired, or invalid.
- `403`: token is valid but lacks `parties.manage`, the integration is disabled, or source restrictions failed.

### 5. Generate The Client Care Letter

Required scope: `client-care-letters.generate`

If the firm has configured a generated Client Care Letter for the matter type, generate it before sending the invite. This renders and saves the HTML document that the client will see during onboarding.

Check readiness:

```http
GET /api/v1/cases/{caseId}/client-care-letter/status
Authorization: Bearer <system-token>
```

Response:

```json
{
  "required": true,
  "mode": "generated",
  "readinessStatus": "ready_to_generate",
  "templateConfigured": true,
  "templateName": "Purchase Client Care Letter",
  "document": null,
  "missingFields": []
}
```

Generate:

```http
POST /api/v1/cases/{caseId}/client-care-letter/generate
Authorization: Bearer <system-token>
Content-Type: application/json
```

Response:

```json
{
  "ok": true,
  "document": {
    "documentId": "document_opaque_id",
    "type": "care_letter",
    "title": "Purchase Client Care Letter",
    "source": {
      "kind": "template"
    }
  },
  "status": {
    "readinessStatus": "ready",
    "document": {
      "documentId": "document_opaque_id",
      "sourceKind": "template"
    }
  }
}
```

Rules:

- Generation uses matter data plus firm defaults. The API should not send arbitrary template data or CCL content.
- If a required value is missing, update the matter with the missing field or, for responsibility wording only, a `clientCareLetter` contact override; then retry generation.
- Generated Client Care Letters are ready for invite as soon as the document is saved. No separate mark-ready call is required for generated letters.
- If the firm has selected uploaded mode for the case, upload/review must happen in the firm app before invites can be sent.

### 6. Send An Invite

Required scope: `invites.send`

```http
POST /api/v1/cases/{caseId}/invites
Authorization: Bearer <system-token>
Content-Type: application/json
```

Request:

```json
{
  "partyId": "party_opaque_id",
  "autoGenerateClientCareLetter": true,
  "sendSms": false
}
```

Optional invite fields:

- `emailSubject`
- `emailBody`
- `emailText`
- `emailHtml`
- `smsText`
- `sendSms`
- `autoGenerateClientCareLetter`

Rules:

- The party must have email or mobile.
- The firm must have a client access code configured.
- The case must have `matterRef`.
- If a Client Care Letter is required, it must already be ready. As an alternative, pass `autoGenerateClientCareLetter=true` and include `client-care-letters.generate` in the same system token.
- SMS is used only when `sendSms=true` and the party has mobile; otherwise email is preferred when available.

### 7. Send A Reminder

Required scope: `invites.send`

```http
POST /api/v1/cases/{caseId}/parties/{partyId}/reminders
Authorization: Bearer <system-token>
Content-Type: application/json
```

Request:

```json
{
  "preferredChannel": "auto"
}
```

Rules:

- Use this only after the original invite has been sent.
- The party must not have submitted or completed onboarding.
- `auto` prefers email when available, then SMS.
- Firm admins can configure the automatic cadence and reminder templates in Settings → Client communications → Onboarding reminders.
- Each automatic reminder rule can either use the shared reminder wording or its own email/SMS chaser wording for that specific cadence.
- Optional one-off override fields are available for exceptional cases: `emailSubject`, `emailText`, `emailHtml`, and `smsText`.

### Public Quote Handover Compatibility

Legacy or third-party quote systems can prepare onboarding with:

```http
POST /api/v1/client/quote-handover
Content-Type: application/json
```

Supported fields include `firmCode`, `quoteReference`, optional `caseReference`, client name/contact fields, `matterType` or `transactionType`, `tenure`, property address aliases, hosted quote links, Terms of Business links, quote total, and deposit details. When the matter does not already exist, Verify Client creates it with the same matter-type defaults used by the main case API. For example, `Sale` handovers do not create a Source of Funds task, while purchase-style matters do. Hosted quote and Terms of Business links are shown in the client Forms and documents checklist so the client can open them alongside forms, official documents, and the Client Care Letter.

### Client acknowledgement of generated letters

Generated Client Care Letters are delivered to the client as HTML during onboarding, but acknowledgement is stored as a signed PDF snapshot:

1. The client reviews the rendered HTML letter.
2. The client adds an electronic signature.
3. The client app renders the signed HTML letter to A4 PDF.
4. The client app uploads that PDF with `POST /api/v1/client/documents/{documentId}/presign-upload`.
5. The client app acknowledges with `POST /api/v1/client/documents/{documentId}/ack` and the uploaded `snapshotKey`.

For HTML template-based Client Care Letters, `/ack` rejects acknowledgements that do not provide a signed PDF snapshot under the document `snapshot/` prefix. Legacy unsigned generated PDFs do not satisfy this requirement.

### Evidence pack archive

Firm users and internal users can download the evidence pack PDF, or a full ZIP archive containing the PDF pack plus Client Care Letters, forms, official documents, and client uploads. When a matter includes third-party quote metadata, the PDF pack records the quote provider/reference, total, deposit status, hosted quote link, and Terms of Business link. The ZIP archive also includes a `Quote and Terms of Business` folder with a readable metadata text file and any stored `quotePdfKey` / `termsPdfKey` PDF files that the integration supplied.

```http
GET /api/v1/cases/{caseId}/evidence-packs/{packId}/zip
Authorization: Bearer <token>
```

The response returns a short-lived `downloadUrl`, `filename`, and `fileCount`. Filenames are grouped by matter reference with readable folders such as `Evidence pack`, `Client care letter`, `Forms`, `Official documents`, `Quote and Terms of Business`, and `Client uploads`.

## Errors

Common API error status codes:

| Status | Meaning                                                                                |
| ------ | -------------------------------------------------------------------------------------- |
| `400`  | Missing or invalid payload field                                                       |
| `401`  | Missing, expired, or invalid credentials                                               |
| `403`  | Valid credentials but missing permission, scope, active status, or source IP allowance |
| `404`  | Route not found                                                                        |
| `500`  | Server error                                                                           |

Error bodies use the shared API error shape from `api/lib/http.ts`; callers should treat `error.message` and `error.code` as optional.

Recommended client behaviour:

- `400`: fail the workflow and surface the validation issue to the integration operator.
- `401`: refresh credentials/token if expired; otherwise stop and rotate/check credentials.
- `403`: stop. The firm admin must enable the scope, integration status, or source IP range.
- `5xx`: retry with bounded exponential backoff and idempotency safeguards in the calling system. Do not create a live case if a required remote duplicate check has only failed with `5xx`; either wait for a successful remote check or record an explicit operator override.

## Auditability

System-token actions are audited as MicroApp actors:

```json
{
  "actorType": "microApp",
  "actorId": "<appId>",
  "meta": {
    "actorTokenKind": "system",
    "actorName": "<identifier>",
    "actorIdentifier": "<identifier>"
  }
}
```

Tracked metadata on the integration record includes:

- `lastUsedAt`
- `lastUsedIp`
- `lastUsedRoute`
- `lastTokenIssuedAt`
- `lastAuthFailureAt`
- `lastAuthFailureReason`
- `secretRotatedAt`

## Security Checklist For AI-Generated Integrations

- Do not put `clientSecret` in frontend code, logs, screenshots, source control, or prompts.
- Request only the scopes required for the operation.
- Cache system tokens only until expiry.
- Retry token issuance only on transient 5xx/network failures; do not retry invalid credentials indefinitely.
- Validate that `caseId` and `partyId` are present before continuing a multi-step flow.
- Treat all IDs as opaque strings.
- Prefer server-to-server calls from fixed egress IPs so firm admins can configure `allowedIpRanges`.
