Developers

Kreto API reference

Classify notices, check payroll, work responses and more. Every operation below is generated from one OpenAPI specification. The Kreto SDK, CLI and MCP server are generated from the same spec; they are not published yet.

OpenAPI 3.1.0v1.0.072 resource groups501 operationsDownload openapi.json

What firms build with it.

  1. Bring notices in from your own intake.

    The Notices operations, starting with /api/notices/upload.

  2. Push payroll registers after each run.

    The Payroll operations, starting with /api/payroll/upload.

  3. Pull each client's deadlines and statuses into your own dashboard.

    The Compliance operations, starting with /api/compliance-calendar.

Authentication

UI clients sign in with a session and need no setup. Integrations use a Bearer token scoped to your firm. Create and manage keys in Settings, API access. Each request is isolated to the key’s firm by row-level security, and each key carries a list of scopes: an operation runs only if the key holds every scope it lists.

curl https://app.kreto.ai/api/notices \ -H "Authorization: Bearer krto_live_your_key_here"

Available scopes (18). Grant a key only the scopes its integration needs.

admin.readadmin.writeannual_reports.readannual_reports.writeaudit.readaudit.writeemployers.reademployers.writeforeign_qualifications.readforeign_qualifications.writehandbooks.readhandbooks.writenotices.readnotices.writepayroll.readpayroll.writestate_registration.readstate_registration.write

Agency documents7

GET/api/agency-documentsList an employer's agency documents

One row per agency document (its newest reading). The accountant sees each figure with its page and quote; the employer sees the status word only. Bearer scope: `notices.read`.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
entityIdquerystringrequired—

Responses

  • 200The employer's agency documentsAgencyDocumentList
    FieldTypeRequiredDescription
    agency_documentsarray<AgencyDocument>required—
  • 401UnauthorizedMiscUnauthorizedResponse
  • 404Not found — an absent, foreign or out-of-scope id (or the feature off) answers the same bodyMiscNotFoundResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerErrorWithDetails · AccountSetupIncompleteResponse)
POST/api/agency-documentsAdd an agency document

Add a state agency's rate notice, benefit-charge statement or other agency letter for an employer (multipart: `file`, `entity_id` — or a `notice_id` alone, its employer then — `kind` `rate_notice` | `benefit_charge_statement` | `other_agency_letter`). The accountant adds one for any employer it serves; the employer's own user (or key) for its own employer only, and is never shown a figure — the reading waits for the accountant's confirm. PDF, PNG, JPEG or TIFF under 25 MB, checked against the bytes. The same file for the same employer is one document (`deduplicated: true`). Kreto reads it in the background; nothing is used until an accountant confirms the figures. Bearer scope: `notices.write`.

Bearer scopesnotices.write

Request body objectrequired

FieldTypeRequiredDescription
filestring · binaryrequired—
entity_idstringoptional—
notice_idstringoptional—
kindenum: "rate_notice" | "benefit_charge_statement" | "other_agency_letter"required—

Responses

  • 202Added; the reading is queuedAgencyDocumentAdded
    FieldTypeRequiredDescription
    document_idstringrequired—
    deduplicatedbooleanrequired—
    job_idstringrequired—
  • 400`invalid_upload` — no file, or an unknown kindAgencyDocumentError
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403`CPA_ROLE_REQUIRED` — a role that is neither the accountant nor the employer's own userAgencyDocumentError
  • 404Not found — an absent, foreign or out-of-scope id (or the feature off) answers the same bodyMiscNotFoundResponse
  • 413`file_too_large`AgencyDocumentError
  • 415`unsupported_type` — not a PDF/PNG/JPEG/TIFF by its bytesAgencyDocumentError
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerErrorWithDetails · AccountSetupIncompleteResponse)
GET/api/agency-documents/{id}Read one agency-document reading

One reading, projected for the caller (figures for the accountant, the status word for the employer). Bearer scope: `notices.read`.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
idpathstringrequired—

Responses

  • 200The readingAgencyDocumentOne
    FieldTypeRequiredDescription
    agency_documentAgencyDocumentrequired—
  • 401UnauthorizedMiscUnauthorizedResponse
  • 404Not found — an absent, foreign or out-of-scope id (or the feature off) answers the same bodyMiscNotFoundResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerErrorWithDetails · AccountSetupIncompleteResponse)
POST/api/agency-documents/{id}/confirmConfirm an agency document's figures

The accountant confirms the figures Kreto read (rejecting any misread ones, or correcting one to the value printed on the page — Kreto's reading and quote are kept beside the correction). They are applied to the notice they are usable for — only when the document is dated the state's computation date for that rate year — as the agency's own figures; a figure that differs from one the employer stated is held for review. Bearer scope: `notices.write` (a firm key).

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstringrequired—

Request body AgencyConfirmBodyrequired

FieldTypeRequiredDescription
notice_idstringoptional—
rejectarray<object>optional—
editarray<object>optional—

Responses

  • 200Confirmed; each candidate notice's outcomeAgencyConfirmed
    FieldTypeRequiredDescription
    confirmedenum: truerequired—
    extraction_idstringrequired—
    acceptedarray<string>required—
    rejectedarray<string>required—
    editedarray<string>required—
    projectionsarray<object>required—
  • 400`figure_invalid` — every figure was rejected; a correction names a figure Kreto did not read, a rejected figure, or a negative amount; or a kept figure's quote was not found on the page and no value was typed for itAgencyDocumentError
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403`CPA_ROLE_REQUIRED` — only the accountant confirmsAgencyDocumentError
  • 404Not found — an absent, foreign or out-of-scope id (or the feature off) answers the same bodyMiscNotFoundResponse
  • 409`already_confirmed` | `extraction_not_ready`AgencyDocumentError
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerErrorWithDetails · AccountSetupIncompleteResponse)
GET/api/agency-documents/{id}/fileOpen an agency document's stored file

A 5-minute signed link to the stored file, so the accountant's confirm card can open it at the page a figure's quote was read from. Accountant session only: an API key never mints a file link, and the employer never gets one (the employer is never shown a figure).

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
idpathstringrequired—

Responses

  • 200A short-lived link to the fileAgencyDocumentFile
    FieldTypeRequiredDescription
    urlstringrequired—
    mime_typestringrequired—
    expires_in_secondsintegerrequired—
  • 401UnauthorizedMiscUnauthorizedResponse
  • 404Not found — an absent, foreign or out-of-scope id (or the feature off) answers the same bodyMiscNotFoundResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerErrorWithDetails · AccountSetupIncompleteResponse)
POST/api/agency-documents/{id}/rejectReject one figure Kreto read

Before confirming, the accountant rejects one figure with a reason from a closed list (`misread_value`, `wrong_line`, `wrong_period`, `not_on_document`, `illegible`, `other` — `other` needs a note). Kreto's reading and quote are kept; the figure is never applied to a notice, and re-opens only when the accountant types the value printed on the page in the confirm. Bearer scope: `notices.write` (a firm key).

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstringrequired—

Request body AgencyRejectBodyrequired

FieldTypeRequiredDescription
keystringrequired—
reasonenum: "misread_value" | "wrong_line" | "wrong_period" | "not_on_document" | …required—
notestringoptional—
notice_idstringoptional—

Responses

  • 200RejectedAgencyRejected
    FieldTypeRequiredDescription
    rejectedenum: truerequired—
    extraction_idstringrequired—
    keystringrequired—
    reasonenum: "misread_value" | "wrong_line" | "wrong_period" | "not_on_document" | …required—
  • 400`figure_invalid` — no reason (or one off the list), `other` without a note, a figure Kreto did not read, or one already rejectedAgencyDocumentError
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403`CPA_ROLE_REQUIRED` — only the accountant rejectsAgencyDocumentError
  • 404Not found — an absent, foreign or out-of-scope id (or the feature off) answers the same bodyMiscNotFoundResponse
  • 409`already_confirmed` | `extraction_not_ready` | `reading_changed`AgencyDocumentError
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerErrorWithDetails · AccountSetupIncompleteResponse)
POST/api/agency-documents/{id}/accept-allAccept every figure in one step

Confirms every figure of the reading at once — only when every figure was read with high confidence, its quote was found on the page, none was rejected and the reader's quote and date checks passed (the reading's `bulk_accept_available`). Otherwise refused as `bulk_requires_all_high`, naming each reason in `blocks` (the same sentences as the reading's `bulk_accept_blocks`); confirm figure by figure instead. Applied exactly like a confirm, with one audit entry per figure. Bearer scope: `notices.write` (a firm key).

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstringrequired—

Request body AgencyAcceptAllBodyrequired

FieldTypeRequiredDescription
notice_idstringoptional—

Responses

  • 200Confirmed; each candidate notice's outcomeAgencyConfirmed
    FieldTypeRequiredDescription
    confirmedenum: truerequired—
    extraction_idstringrequired—
    acceptedarray<string>required—
    rejectedarray<string>required—
    editedarray<string>required—
    projectionsarray<object>required—
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403`CPA_ROLE_REQUIRED` — only the accountant confirmsAgencyDocumentError
  • 404Not found — an absent, foreign or out-of-scope id (or the feature off) answers the same bodyMiscNotFoundResponse
  • 409`bulk_requires_all_high` (with `blocks`: each reason named) | `already_confirmed` | `extraction_not_ready`one of (AgencyBulkAcceptRefused · AgencyDocumentError)
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerErrorWithDetails · AccountSetupIncompleteResponse)

Agency portal downloads2

GET/api/portal-fetchList agency-portal downloads

The agency-portal downloads Kreto ran for one stored login (`credential_id`) or one employer (`entity_id`) — name exactly one — newest first, with the status word and, for the accountant, the next step. No files on a list row (read one run for those). Bearer scope: `admin.read` (a dedicated `portal_fetch.read` scope is owed).

Bearer scopesadmin.read

Parameters

NameInTypeRequiredDescription
credential_idquerystringoptional—
entity_idquerystringoptional—

Responses

  • 200The runs, newest firstPortalFetchList
    FieldTypeRequiredDescription
    planeenum: "cpa" | "employer"required—
    credentialobjectrequired—
    runsarray<PortalFetchRun>required—
  • 400`INVALID_INPUT` — neither or both ids namedPortalFetchError
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403A cookie role that is not the accountant's, or a Bearer key without `admin.read`PortalFetchError
  • 404Not found — an absent, foreign or out-of-scope id answers the same bodyMiscNotFoundResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerErrorWithDetails · AccountSetupIncompleteResponse)
GET/api/portal-fetch/{id}Read one agency-portal download

One run and its files: the status word, the next step for the accountant, and per file its kind, date and where Kreto put it. The employer sees dates and kinds only. Bearer scope: `admin.read` (a dedicated `portal_fetch.read` scope is owed).

Bearer scopesadmin.read

Parameters

NameInTypeRequiredDescription
idpathstringrequired—

Responses

  • 200The run and its filesPortalFetchOne
    FieldTypeRequiredDescription
    planeenum: "cpa" | "employer"required—
    runPortalFetchRunrequired—
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403A cookie role that is not the accountant's, or a Bearer key without `admin.read`PortalFetchError
  • 404Not found — an absent, foreign or out-of-scope id answers the same bodyMiscNotFoundResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerErrorWithDetails · AccountSetupIncompleteResponse)

Analytics1

GET/api/analytics/predictionsEntity risk predictions

Risk predictions for an entity: SUI rate-change forecast, ACA penalty risk (when FTE >= 40), and overall risk profile aggregating open/overdue notices + active state count. CPA-class roles only on cookie sessions; Bearer keys gate via `notices.read`.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
entity_idquerystring · uuidrequired—

Responses

  • 200Risk predictionsAnalyticsPredictionsResponse
    FieldTypeRequiredDescription
    entityobjectrequired—
    riskProfileobjectrequired—
    suiPredictionobjectrequired—
    acaRiskobjectrequired—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Forbidden — CPA access requiredMiscUnauthorizedResponse
  • 404Entity not foundMiscNotFoundResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)

API Keys4

GET/api/api-keysList API keys for the caller's tenant

Returns all API keys (active + revoked) for the authenticated CPA's tenant. Response NEVER includes plaintext or key_hash — only the publicly safe key_prefix + metadata.

Cookie session or Bearer token

Responses

  • 200List of API keysListApiKeysResponse
    FieldTypeRequiredDescription
    keysarray<PublicApiKey>required—
  • 401UnauthorizedApiKeysNotFoundResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth gate's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/api-keysIssue a new API key

Create a new API key for the authenticated CPA's tenant. Plaintext is returned ONCE in the response (`plaintext` field) — store it securely; subsequent GET calls return only the masked metadata. `scopes` is REQUIRED and must name at least one scope; an omitted, null or empty array is refused 400 with code `SCOPES_REQUIRED` — a key is never issued with every scope by default, and a key with no scopes authorizes nothing.

Cookie session or Bearer token

Request body CreateApiKeyBodyrequired

FieldTypeRequiredDescription
namestringrequired—
scopesarray<enum: "notices.read" | "employers.read" | "payroll.read" | "audit.read" | …>requiredREQUIRED, non-empty scope array (inbox 232). Omitted, null or [] is refused 400 SCOPES_REQUIRED — the server never defaults a key to every scope. A stored key with no scopes authorizes nothing.
expires_atstring · date-timeoptionalOptional expiry. Omitted or NULL = 90 days from mint (DEFAULT_TOKEN_LIFETIME_DAYS); a key never mints without one.

Responses

  • 201Key issued. Plaintext returned ONCE; store it now — it cannot be retrieved again.IssuedApiKey
    FieldTypeRequiredDescription
    idstringrequired—
    tenant_idstringrequired—
    key_prefixstringrequired—
    namestringrequired—
    scopesarray<enum: "notices.read" | "employers.read" | "payroll.read" | "audit.read" | …>required—
    created_by_user_idstringrequired—
    created_atstring · date-timerequired—
    last_used_atstring · date-timerequired—
    revoked_atstring · date-timerequired—
    expires_atstring · date-timerequired—
    plaintextstringrequired—
    warningenum: "This is the ONLY time the plaintext is shown. Store it securely now."required—
  • 400Validation error (Zod issues array in body), or `SCOPES_REQUIRED` when no scopes were requestedApiKeysValidationErrorResponse
  • 401Unauthorized — caller is not authenticated as a CPAApiKeysNotFoundResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth gate's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/api-keys/{id}Fetch a single API key's metadata
Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequired—

Responses

  • 200Key metadataobject
    FieldTypeRequiredDescription
    keyPublicApiKeyrequired—
  • 404Key not found in your tenantApiKeysNotFoundResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth gate's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
DELETE/api/api-keys/{id}Revoke an API key

Soft-delete via `revoked_at`. Idempotent — re-revoking returns the existing revocation timestamp. Optional body { reason: string } for audit context.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequired—

Request body object

FieldTypeRequiredDescription
reasonstringoptional—

Responses

  • 200Key revoked (or already revoked — idempotent)RevokeApiKeyResponse
    FieldTypeRequiredDescription
    revokedbooleanrequired—
    already_revokedbooleanrequired—
    revoked_atstring · date-timeoptional—
    keyPublicApiKeyoptional—
  • 404Key not found in your tenantApiKeysNotFoundResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth gate's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)

Audit1

GET/api/auditList audit events

Fetch tenant audit events, optionally filtered by entity_id. CPA-class roles only on cookie sessions; Bearer keys gate via `audit.read`.

Bearer scopesaudit.read

Parameters

NameInTypeRequiredDescription
limitqueryintegeroptional—
entity_idquerystring · uuidoptional—

Responses

  • 200Audit eventsAuditEventsResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    eventsarray<AuditEvent>required—
    totalintegerrequired—
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Forbidden — CPA access requiredMiscUnauthorizedResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)

Auth1

POST/api/auth/mfa/unenrollRemove one of the caller's MFA factors (step-up required)

Cookie session only; a Bearer API key is refused. The SESSION must be at assurance level aal2 (a TOTP challenge verified on this session) — otherwise 403 `STEP_UP_REQUIRED` and nothing is removed; complete the challenge and retry. The factor must be one of the session user's own factors; the body's `factorId` is the only field read (a user id in the body is ignored). A successful removal writes one `mfa_factor_unenrolled` audit event before the 200.

Cookie session or Bearer token

Request body MfaUnenrollRequestrequired

FieldTypeRequiredDescription
factorIdstring · uuidrequired—

Responses

  • 200Factor removedMfaUnenrollResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    factor_idstring · uuidrequired—
  • 400INVALID_BODY — body is not JSON or `factorId` is not a factor idMfaUnenrollError
  • 401No session (or a Bearer key was presented)MfaUnenrollError
  • 403STEP_UP_REQUIRED — the session is not aal2; or `mfa_required` from the cookie-API MFA gateMfaUnenrollError
  • 404FACTOR_NOT_FOUND — not one of the session user's factorsMfaUnenrollError
  • 500Server error — sanitized (`SanitizedServerError`), or a signed-in session with no profile gets `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)

Compliance22

POST/api/compliance/digestTrigger compliance digest

Manually trigger a weekly compliance digest email, or (body `{"mode":"preview"}`) build it without sending. Designed send refusals are returned in `skipped`, real failures in `errors`. CPA-class roles only on cookie sessions; Bearer keys gate via `admin.write` scope — sending mail on the tenant's behalf is a side effect, and the route ENFORCES admin.write (route.ts). SWEEP C16: the declaration said `admin.read` and was the half that was wrong.

Bearer scopesadmin.write

Request body ComplianceDigestRequest

FieldTypeRequiredDescription
modeenum: "send" | "preview"optional`send` (default when the body is omitted) emails the digest to the firm's CPA members and admins whose weekly digest is on. `preview` builds and returns the rendered email without sending anything.

Responses

  • 200Digest sent (mode send) or built (mode preview)ComplianceDigestResponse
  • 400Invalid bodyComplianceValidationErrorResponse
  • 401UnauthorizedComplianceUnauthorizedResponse
  • 403Forbidden — CPA access required / missing scopeComplianceUnauthorizedResponse
  • 429Rate limit exceededComplianceRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/compliance/score-historyGet compliance score history

Returns the most recent 12 score-history entries for the caller's tenant (or for a specific entity when entity_id is supplied). The series holds only scores recorded by the compliance monitor and starts at 2026-10-01T00:00:00Z; entries from the earlier scoring method are not returned, so a caller with no monitor-recorded score yet receives an empty history.

Bearer scopesaudit.read

Parameters

NameInTypeRequiredDescription
entity_idquerystring · uuidoptional—

Responses

  • 200Score historyComplianceScoreHistoryResponse
    FieldTypeRequiredDescription
    historyarray<ComplianceScoreHistoryEntry>required—
  • 400Validation errorComplianceValidationErrorResponse
  • 401UnauthorizedComplianceUnauthorizedResponse
  • 403Forbidden — missing required scopeComplianceUnauthorizedResponse
  • 429Rate limit exceededComplianceRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/compliance/roi-summaryGet YTD ROI summary

Returns YTD savings, penalties avoided, time saved, and notice count derived from `roi_events` for the caller's tenant.

Bearer scopesaudit.read

Responses

  • 200YTD ROI summaryComplianceRoiSummaryResponse
    FieldTypeRequiredDescription
    ytd_savingsintegerrequired—
    ytd_penalties_avoidedintegerrequired—
    ytd_time_saved_hoursintegerrequired—
    ytd_notices_processedintegerrequired—
  • 401UnauthorizedComplianceUnauthorizedResponse
  • 403Forbidden — missing required scopeComplianceUnauthorizedResponse
  • 429Rate limit exceededComplianceRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/compliance/intelligence-scoreGet compliance intelligence score

Returns the Compliance Intelligence Score either for a single entity (when entity_id is supplied) or aggregated across all of the caller's tenant's entities.

Bearer scopesaudit.read

Parameters

NameInTypeRequiredDescription
entity_idquerystring · uuidoptional—

Responses

  • 200Score (single or aggregate)one of (ComplianceSingleEntityScoreResponse · ComplianceIntelligenceScore)
  • 400Validation errorComplianceValidationErrorResponse
  • 401UnauthorizedComplianceUnauthorizedResponse
  • 403Forbidden — missing required scopeComplianceUnauthorizedResponse
  • 429Rate limit exceededComplianceRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/compliance-score/{entityId}Get single-entity score (legacy)

Legacy path: returns the Compliance Intelligence Score for a given entity. Tenant-scoped on the entity verification (KRT-1479 P4e.1 Senior-Dev-Override fix).

Bearer scopesaudit.read

Parameters

NameInTypeRequiredDescription
entityIdpathstring · uuidrequiredEmployer entity UUID

Responses

  • 200Compliance intelligence scoreobject
  • 400Validation errorComplianceValidationErrorResponse
  • 401UnauthorizedComplianceUnauthorizedResponse
  • 403Forbidden — missing required scopeComplianceUnauthorizedResponse
  • 404Entity not found in caller's tenantComplianceNotFoundResponse
  • 429Rate limit exceededComplianceRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/compliance/aca-summaryGet ACA summary

Returns ACA (Applicable Large Employer) status + penalty exposure per entity. Primary FTE source: payroll_pay_periods; fallback chain through payroll_records → onboarding range → entity.employee_count.

Bearer scopesaudit.read

Responses

  • 200ACA summary by entityComplianceAcaSummaryResponse
    FieldTypeRequiredDescription
    employeesintegerrequired—
    aleStatusenum: "applicable" | "not_applicable"required—
    dataQualityenum: "payroll" | "estimate"required—
    entitiesarray<ComplianceAcaEntity>required—
  • 401UnauthorizedComplianceUnauthorizedResponse
  • 403Forbidden — missing required scopeComplianceUnauthorizedResponse
  • 429Rate limit exceededComplianceRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/compliance/benefitsList benefits compliance

List benefits compliance records for all tenant entities for a given plan year (defaults to current year).

Bearer scopesaudit.read

Parameters

NameInTypeRequiredDescription
yearquerystringoptionalPlan year

Responses

  • 200Benefits listComplianceBenefitsListResponse
    FieldTypeRequiredDescription
    recordsarray<object>required—
    yearintegerrequired—
  • 401UnauthorizedComplianceUnauthorizedResponse
  • 403Forbidden — missing required scopeComplianceUnauthorizedResponse
  • 429Rate limit exceededComplianceRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/compliance/benefitsUpsert benefits compliance record

Upsert a benefits compliance record for an entity + plan year. Tenant-scoped on the entity verification (already correct pre-Phase-4).

Bearer scopesnotices.write

Request body ComplianceBenefitsPostBodyrequired

FieldTypeRequiredDescription
entity_idstring · uuidrequired—
plan_yearintegerrequired—
plan_typestringoptional—
carrier_namestringoptional—
plan_participantsintegeroptional—
rxdc_completedbooleanoptional—
rxdc_completed_datestring · dateoptional—
gag_clause_filedbooleanoptional—
gag_clause_filed_datestring · dateoptional—
gag_clause_confirmationstringoptional—
medicare_part_d_sentbooleanoptional—
medicare_part_d_sent_datestring · dateoptional—
chip_notice_sentbooleanoptional—
chip_notice_sent_datestring · dateoptional—
form_5500_requiredbooleanoptional—
form_5500_filedbooleanoptional—
form_5500_filed_datestring · dateoptional—
cobra_documentedbooleanoptional—

Responses

  • 200Record upsertedobject
    FieldTypeRequiredDescription
    recordBenefitsRecordrequired—
  • 400Validation errorComplianceValidationErrorResponse
  • 401UnauthorizedComplianceUnauthorizedResponse
  • 403Forbidden — missing required scopeComplianceUnauthorizedResponse
  • 404Entity not found in caller's tenantComplianceNotFoundResponse
  • 429Rate limit exceededComplianceRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/compliance/pfml-checkCheck PFML compliance gaps

PFML enrollment gap analysis for tenant entities. client_user role (cookie session) scoped to assigned entities; CPA-class + Bearer keys see all tenant entities.

Bearer scopesaudit.read

Responses

  • 200PFML gap analysisCompliancePfmlResponse
    FieldTypeRequiredDescription
    entitiesarray<CompliancePfmlResult>required—
    unavailableEntitiesarray<CompliancePfmlUnavailable>required—
    summaryobjectrequired—
  • 401UnauthorizedComplianceUnauthorizedResponse
  • 403Forbidden — missing required scopeComplianceUnauthorizedResponse
  • 429Rate limit exceededComplianceRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
  • 503The firm's entity list could not be read (no partial answer is served)one of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/compliance/wage-hour-riskGet wage & hour risk scores

Returns wage & hour risk scores per entity (state minimum wage check, overtime policy gaps, exempt-employee threshold, contractor risk).

Bearer scopesaudit.read

Responses

  • 200Risk scores per entityComplianceWageHourResponse
    FieldTypeRequiredDescription
    entitiesarray<ComplianceWageHourEntity>required—
    federalMinimumWagenumberrequired—
    overtimeSalaryThresholdnumberrequired—
    overtimeSalaryThresholdSourceobjectoptional—
    errorstringoptional—
  • 401UnauthorizedComplianceUnauthorizedResponse
  • 403Forbidden — missing required scopeComplianceUnauthorizedResponse
  • 429Rate limit exceededComplianceRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/compliance-monitorGet compliance monitor profiles

Returns compliance profiles for all employers (sorted worst-first). client_user role scoped to assigned entities; CPA-class + Bearer keys see all tenant entities.

Bearer scopesaudit.read

Parameters

NameInTypeRequiredDescription
entity_idquerystring · uuidoptionalFilter to a single entity (must be authorized)

Responses

  • 200Compliance profilesComplianceMonitorResponse
    FieldTypeRequiredDescription
    profilesarray<object>required—
  • 400Validation errorComplianceValidationErrorResponse
  • 401UnauthorizedComplianceUnauthorizedResponse
  • 403Forbidden — entity access denied / missing scopeComplianceUnauthorizedResponse
  • 429Rate limit exceededComplianceRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
PATCH/api/compliance/credential-alerts/{id}/acknowledgeAcknowledge credential-refresh alert

Dismiss a credential_refresh_alerts row (KRT-1288). 404 covers not-found / already-acknowledged / auto-resolved equally.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredAlert UUID

Responses

  • 200AcknowledgedComplianceCredentialAlertsAcknowledgeResponse
    FieldTypeRequiredDescription
    alertobjectrequired—
  • 400Validation errorComplianceValidationErrorResponse
  • 401UnauthorizedComplianceUnauthorizedResponse
  • 403Forbidden — CPA access required / missing scopeComplianceUnauthorizedResponse
  • 404Alert not found or already handledComplianceNotFoundResponse
  • 429Rate limit exceededComplianceRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
PATCH/api/compliance/recommendations/{id}/acknowledgeAcknowledge state-registration recommendation

Dismiss a state_registration_recommendations row (KRT-1216 PR-D).

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredRecommendation UUID

Responses

  • 200AcknowledgedComplianceRecommendationsAcknowledgeResponse
    FieldTypeRequiredDescription
    recommendationobjectrequired—
  • 400Validation errorComplianceValidationErrorResponse
  • 401UnauthorizedComplianceUnauthorizedResponse
  • 403A valid Bearer key without `notices.write`, a session that must complete an MFA step-up (`mfa_required`), or `role_forbidden` — a non-CPA cookie role (before any read), or a write refused on a row the caller can readComplianceRecommendationsAcknowledgeForbiddenResponse
  • 404No such recommendation in the caller's tenant (unknown or cross-tenant id — existence-opaque). The body keeps the route's historical marker text; an already-acknowledged row the caller can read is the 409, never this 404ComplianceNotFoundResponse
  • 409Already acknowledged (`already_acknowledged`) — before the write, or the write lost a race to another acknowledgerComplianceRecommendationsAcknowledgeConflictResponse
  • 429Rate limit exceededComplianceRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/compliance/payroll-tax-analysisGet payroll-tax analysis

Calculate expected federal payroll taxes (SS, Medicare, FUTA, 941, 940) per entity from the firm's uploaded pay-period payroll, else payroll_records, else none (dataSource=estimate, zero employees). Per-employee detail is stripped from response (PII).

Bearer scopesaudit.read

Parameters

NameInTypeRequiredDescription
entity_idquerystring · uuidoptional—
tax_yearqueryintegeroptionalAnalysis year (default 2026). Must have a cited Social Security wage base, else 400.

Responses

  • 200Payroll-tax analysisCompliancePayrollTaxAnalysisResponse
    FieldTypeRequiredDescription
    entitiesarray<CompliancePayrollTaxEntity>required—
    summaryobjectrequired—
  • 400Validation errorComplianceValidationErrorResponse
  • 401UnauthorizedComplianceUnauthorizedResponse
  • 403Forbidden — missing required scopeComplianceUnauthorizedResponse
  • 429Rate limit exceededComplianceRateLimitResponse
  • 500Server errorone of (SanitizedServerErrorWithCode · AccountSetupIncompleteResponse)
POST/api/compliance/payroll-tax-analysisAnalyze payroll-tax compliance

Compare expected payroll-tax liability against quarterly deposit input. Returns expected + compliance comparison + dataSource. Per-employee detail is stripped from response (PII).

Bearer scopesaudit.read

Request body CompliancePayrollTaxAnalysisPostBodyrequired

FieldTypeRequiredDescription
entity_idstring · uuidrequired—
tax_yearintegeroptionalAnalysis year (default 2026). Must have a cited Social Security wage base, else 400.
depositsarray<object>required—

Responses

  • 200Comparison resultCompliancePayrollTaxAnalysisPostResponse
    FieldTypeRequiredDescription
    expectedobjectrequired—
    complianceobjectrequired—
    taxYearintegerrequired—
    dataSourceenum: "finch" | "upload" | "payroll" | "estimate"required—
    uploadobjectrequiredUploaded pay rows counted and the first/last effective date they cover; null unless dataSource is upload.
    dataSourceNotestringrequiredPlain-language note on where the wages came from, for the CPA.
  • 400Validation errorComplianceValidationErrorResponse
  • 401UnauthorizedComplianceUnauthorizedResponse
  • 403Forbidden — missing required scopeComplianceUnauthorizedResponse
  • 404Entity not found in caller's tenantComplianceNotFoundResponse
  • 429Rate limit exceededComplianceRateLimitResponse
  • 500Server errorone of (SanitizedServerErrorWithCode · AccountSetupIncompleteResponse)
GET/api/compliance/alertsGet unified compliance alerts

Returns a unified compliance-alerts list across 5 sources (compliance_checks, urgent + overdue + needs-review notices, state_registration_recommendations, credential_refresh_alerts).

Bearer scopesaudit.read

Responses

  • 200Compliance alertsComplianceAlertsResponse
    FieldTypeRequiredDescription
    alertsarray<ComplianceAlert>required—
  • 401UnauthorizedComplianceUnauthorizedResponse
  • 403Forbidden — missing required scopeComplianceUnauthorizedResponse
  • 429Rate limit exceededComplianceRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/compliance-calendarGet compliance calendar

Returns notices + work_items + active licence renewals with deadlines, grouped into overdue / thisWeek / thisMonth / future buckets. Tenant-scoped (P4e.3 Senior-Dev-Override fix).

Bearer scopesaudit.read

Responses

  • 200Grouped deadline calendarComplianceCalendarResponse
    FieldTypeRequiredDescription
    overduearray<ComplianceCalendarItem>required—
    thisWeekarray<ComplianceCalendarItem>required—
    thisMontharray<ComplianceCalendarItem>required—
    futurearray<ComplianceCalendarItem>required—
    windowobjectrequiredThe window this surface answers from (G6 R1 A7). Unbounded: every open notice with a deadline, overdue included. `buckets` are the bucket boundaries inside that window, not a filter on it.
    summaryobjectrequired—
  • 401UnauthorizedComplianceUnauthorizedResponse
  • 403Forbidden — missing required scopeComplianceUnauthorizedResponse
  • 429Rate limit exceededComplianceRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/calendar/tokenList calendar subscriptions

Lists the caller's ACTIVE iCal subscriptions (not revoked, not expired). Never returns `token_hash` or anything the feed URL could be reconstructed from — the plaintext token is shown once at mint time and the row id is the only handle afterwards. Tenant- and user-scoped.

Bearer scopesadmin.write

Responses

  • 200Active subscriptions for the callerCalendarTokenSummaryResponse
    FieldTypeRequiredDescription
    tokensarray<object>required—
    limitintegerrequired—
  • 401Unauthorized — no session, or the key's creator is goneComplianceUnauthorizedResponse
  • 403Forbidden — CPA-class access required / missing scopeComplianceUnauthorizedResponse
  • 429Rate limit exceededComplianceRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
DELETE/api/calendar/tokenRevoke a calendar subscription

Revokes one iCal subscription (soft delete via `revoked_at`, appended to the hash-chained audit trail). The feed resolves tokens with `revoked_at IS NULL`, so this is what actually withdraws the capability, and it frees a slot against the active-token cap. 404 deliberately covers `not yours` / `no such id` / `already revoked` alike so ids are not enumerable.

Bearer scopesadmin.write

Parameters

NameInTypeRequiredDescription
idquerystring · uuidrequiredSubscription row id to revoke.

Responses

  • 200Subscription revokedCalendarTokenRevokeResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    revoked_idstring · uuidrequired—
  • 400Missing or malformed (non-UUID) subscription idComplianceValidationErrorResponse
  • 401Unauthorized — no session, or the key's creator is goneComplianceUnauthorizedResponse
  • 403Forbidden — CPA-class access required / missing scopeComplianceUnauthorizedResponse
  • 404Subscription not found or already revokedComplianceNotFoundResponse
  • 429Rate limit exceededComplianceRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/irs-correspondenceList IRS correspondence

Tenant-scoped IRS correspondence, newest first. A `client_user` is narrowed to its own entity memberships and fails CLOSED (empty page) with none. Free-text resolution/response bodies are not projected.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
entity_idquerystring · uuidoptional—
statusquerystringoptional—
notice_typequerystringoptional—
tax_yearquerystringoptional4-digit year. An unparseable value is IGNORED, not an error.
limitquerystringoptionalDefault 50, capped at 100.
offsetquerystringoptionalDefault 0, clamped non-negative.

Responses

  • 200One page of correspondence plus the exact pre-pagination total.IrsCorrespondenceListResponse
    FieldTypeRequiredDescription
    correspondencearray<IrsCorrespondenceListItem>required—
    totalintegerrequiredExact count BEFORE pagination. 0 both for an empty book and for a `client_user` with no entity memberships (which fails closed to an empty page).
  • 403Authenticated but out of scope — a `client_user` asking for an entity outside its memberships (`FORBIDDEN_ENTITY`), or a Bearer key that does not carry the declared scope.
  • 500A post-fetch tenant-scope leak was detected (defense-in-depth third wall) or the read failed. No partial data is returned.one of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/workers-comp/class-codesList workers-comp class codes

Class codes across the tenant's workers-comp policies, newest first. Entity / policy identity is lifted from a LEFT join on the parent policy. A `client_user` is narrowed to its own entities' policies and fails CLOSED (empty page) with none.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
entity_idquerystring · uuidoptional—
statequerystringoptional—
class_codequerystringoptional—
limitquerystringoptionalDefault 50, clamped to [1, 100].
offsetquerystringoptionalDefault 0, clamped non-negative.

Responses

  • 200One page of class codes plus the exact pre-pagination total.WorkersCompClassCodesResponse
    FieldTypeRequiredDescription
    classCodesarray<WorkersCompClassCodeListItem>required—
    totalintegerrequired—
  • 403Authenticated but out of scope — a `client_user` asking for an entity outside its memberships (`FORBIDDEN_ENTITY`), or a Bearer key that does not carry the declared scope.
  • 500A post-fetch tenant-scope leak was detected (defense-in-depth third wall) or the read failed. No partial data is returned.one of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/compliance/workers-compGet workers' comp summary

Tenant workers' comp summary. One row per employer: the live workers' comp NOTICES on file and the facts each one states (e-mod, class codes, audit period, carrier, policy, audited and deposit premium — each stated or null; `statedNotices: null` when the notices could not be read), plus expected losses from the class codes its ACTIVE policy states and the payroll on file over the experience period — never a guessed class code or rate. When a figure the arithmetic needs is not stated, the row is a named refusal (`dataSource: "pending_data"`, `missing_inputs`, no e-mod/premium); a row whose computation failed says so (`dataSource: "error"`, `error: "computation_failed"`, no figure). Tenant roll-ups are taken over rows that HAVE a figure and are null when none do. CPA-class callers only (firm-wide exposure across sibling employers).

Bearer scopesaudit.read

Responses

  • 200Workers' comp summary by employer (computed rows, named refusals and failed rows share one shape)WorkersCompSummaryResponse
    FieldTypeRequiredDescription
    experiencePeriodWorkersCompSummaryExperiencePeriodrequired—
    entitiesarray<WorkersCompSummaryEntity>required—
    tenantSummaryWorkersCompSummaryTenantSummaryrequired—
    noticesWorkersCompSummaryNoticeTallyrequired—
  • 401UnauthorizedComplianceUnauthorizedResponse
  • 403Forbidden — missing `audit.read` scope, or the caller is not CPA-class (a client_user session or an employer-minted / unattributable Bearer key)ComplianceUnauthorizedResponse
  • 429Rate limit exceededComplianceRateLimitResponse
  • 500Server error — a failed entities / policy / class-code read, a tenant-isolation check (any read, notices included), or a failed creator-role lookup (sanitized)one of (SanitizedServerError · AccountSetupIncompleteResponse)

Connections11

GET/api/connectionsList connections

Returns the unified connection list (data_connections + finch_connections) scoped to the caller's tenant. Sensitive config fields (encrypted refresh tokens, token expiries) are stripped.

Bearer scopespayroll.read

Responses

  • 200Connections listConnectionListResponse
  • 401UnauthorizedConnectionsUnauthorizedResponse
  • 403Forbidden — missing required scopeConnectionsUnauthorizedResponse
  • 429Rate limit exceededConnectionsRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/connections/{id}Trigger connection sync

Trigger a manual sync. Body: `{ action: "sync" }`. Enqueues a connection_sync job; the worker picks it up.

Bearer scopespayroll.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredConnection UUID

Request body ConnectionPostBodyrequired

FieldTypeRequiredDescription
actionenum: "sync"required—

Responses

  • 200Sync enqueuedConnectionSyncResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    messageenum: "Sync enqueued"required—
  • 400Validation error / wrong status / missing tokenConnectionsValidationErrorResponse
  • 401UnauthorizedConnectionsUnauthorizedResponse
  • 403Forbidden — missing required scopeConnectionsUnauthorizedResponse
  • 404Connection not found in caller's tenantConnectionsNotFoundResponse
  • 429Rate limit exceededConnectionsRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
DELETE/api/connections/{id}Disconnect connection

Disconnect a connection: revoke the Google OAuth token (best-effort) and mark the row as `revoked`. Sensitive config fields are cleared.

Bearer scopespayroll.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredConnection UUID

Responses

  • 200DisconnectedConnectionDisconnectResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    messageenum: "Disconnected"required—
  • 400Validation errorConnectionsValidationErrorResponse
  • 401UnauthorizedConnectionsUnauthorizedResponse
  • 403Forbidden — missing required scopeConnectionsUnauthorizedResponse
  • 404Connection not found in caller's tenantConnectionsNotFoundResponse
  • 429Rate limit exceededConnectionsRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/connections/finch/statusGet Finch connection status

Returns the Finch connection status + recent sync logs for an entity.

Bearer scopespayroll.read

Parameters

NameInTypeRequiredDescription
entity_idquerystring · uuidrequiredEntity UUID (required)

Responses

  • 200Status + sync logsFinchStatusResponse
    FieldTypeRequiredDescription
    connectedbooleanrequired—
    statusstringrequired—
    degradedbooleanoptional—
    degraded_reasonstringoptional—
    connectionobjectrequired—
    recent_syncsarray<FinchSyncLog>optional—
  • 400Validation errorConnectionsValidationErrorResponse
  • 401UnauthorizedConnectionsUnauthorizedResponse
  • 403Forbidden — missing required scopeConnectionsUnauthorizedResponse
  • 429Rate limit exceededConnectionsRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/connections/{id}/foldersList provider folders

List folders from the connected provider (Google Drive or Dropbox). Returns selectedFolderIds from current config so the UI can render the checkbox state.

Bearer scopespayroll.read

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredConnection UUID

Responses

  • 200Folder list + selected IDsConnectionFoldersGetResponse
    FieldTypeRequiredDescription
    foldersarray<ConnectionFolderEntry>required—
    selectedFolderIdsarray<string>required—
  • 400Validation errorConnectionsValidationErrorResponse
  • 401UnauthorizedConnectionsUnauthorizedResponse
  • 403Forbidden — missing required scopeConnectionsUnauthorizedResponse
  • 404Connection not found in caller's tenantConnectionsNotFoundResponse
  • 429Rate limit exceededConnectionsRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
PUT/api/connections/{id}/foldersSave selected folders

Save selected watched-folder IDs to the connection config.

Bearer scopespayroll.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredConnection UUID

Request body ConnectionFoldersPutBodyrequired

FieldTypeRequiredDescription
folderIdsarray<string>required—

Responses

  • 200SavedConnectionFoldersPutResponse
    FieldTypeRequiredDescription
    savedenum: truerequired—
    folderIdsarray<string>required—
  • 400Validation errorConnectionsValidationErrorResponse
  • 401UnauthorizedConnectionsUnauthorizedResponse
  • 403Forbidden — missing required scopeConnectionsUnauthorizedResponse
  • 404Connection not found in caller's tenantConnectionsNotFoundResponse
  • 429Rate limit exceededConnectionsRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/connections/{id}/watchList watched folders

List active watched folders for a connection.

Bearer scopespayroll.read

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredConnection UUID

Responses

  • 200Watched foldersConnectionWatchListResponse
    FieldTypeRequiredDescription
    foldersarray<object>required—
  • 400Validation errorConnectionsValidationErrorResponse
  • 401UnauthorizedConnectionsUnauthorizedResponse
  • 403Forbidden — missing required scopeConnectionsUnauthorizedResponse
  • 429Rate limit exceededConnectionsRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/connections/{id}/watchStart watching folder

Start watching a folder for inbound notice intake. Verifies that both the connection and target entity belong to the caller's tenant.

Bearer scopespayroll.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredConnection UUID

Request body ConnectionWatchPostBodyrequired

FieldTypeRequiredDescription
folder_idstringrequired—
folder_namestringrequired—
folder_pathstringoptional—
entity_idstring · uuidrequired—

Responses

  • 200Now watchingConnectionWatchPostResponse
    FieldTypeRequiredDescription
    watched_folderobjectrequired—
    entity_namestringrequired—
    messagestringrequired—
  • 400Validation errorConnectionsValidationErrorResponse
  • 401UnauthorizedConnectionsUnauthorizedResponse
  • 403Forbidden — missing required scopeConnectionsUnauthorizedResponse
  • 404Connection or entity not foundConnectionsNotFoundResponse
  • 429Rate limit exceededConnectionsRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
DELETE/api/connections/{id}/watchStop watching folder

Stop watching a folder (soft-delete via is_active=false).

Bearer scopespayroll.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredConnection UUID

Request body ConnectionWatchDeleteBodyrequired

FieldTypeRequiredDescription
watched_folder_idstring · uuidrequired—

Responses

  • 200Stoppedobject
    FieldTypeRequiredDescription
    successenum: truerequired—
  • 400Validation errorConnectionsValidationErrorResponse
  • 401UnauthorizedConnectionsUnauthorizedResponse
  • 403Forbidden — missing required scopeConnectionsUnauthorizedResponse
  • 429Rate limit exceededConnectionsRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/connections/finch/syncTrigger Finch sync

Trigger a manual Finch sync. Enqueues a finch_sync job; skips if another sync is already pending/running. Cookie sessions go through the Stripe subscription gate; Bearer keys bypass.

Bearer scopespayroll.write

Request body FinchSyncPostBodyrequired

FieldTypeRequiredDescription
entity_idstring · uuidrequired—

Responses

  • 200Sync enqueued or already in progressFinchSyncResponse
    FieldTypeRequiredDescription
    messagestringrequired—
    job_idstring · uuidoptional—
  • 400Validation error / connection inactiveConnectionsValidationErrorResponse
  • 401UnauthorizedConnectionsUnauthorizedResponse
  • 402Subscription required (cookie sessions only)ConnectionsUnauthorizedResponse
  • 403Forbidden — missing required scopeConnectionsUnauthorizedResponse
  • 404No Finch connection for entityConnectionsNotFoundResponse
  • 429Rate limit exceededConnectionsRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/connections/finch/statesDetected payroll work states for one entity

Distinct US work states detected on the entity's payroll records, used to seed state-registration recommendations. The entity is verified in the caller's tenant first; a `client_user` is additionally narrowed to its own entity. An unknown, foreign-tenant or sibling entity all answer the SAME 404 body — the refusal is not an existence oracle.

Bearer scopespayroll.read

Parameters

NameInTypeRequiredDescription
entity_idquerystring · uuidrequiredRequired. Must be a UUID naming an entity in the caller's tenant.

Responses

  • 200Detected states. The ONLY 200 shape — a failed payroll read answers 500, not an empty list.FinchDetectedStatesResponse
    FieldTypeRequiredDescription
    entity_idstring · uuidrequiredThe verified entity the states were derived from.
    statesarray<string>requiredDistinct, sorted, upper-cased 2-letter work states present on the entity's payroll records. `UNKNOWN` and non 2-letter values are dropped. EMPTY means exactly one thing — the entity has no synced work states — because a failed read now answers 500, not 200.
    countintegerrequired`states.length`.
  • 400`entity_id` missing or not a UUID.
  • 403Authenticated but out of scope — a `client_user` asking for an entity outside its memberships (`FORBIDDEN_ENTITY`), or a Bearer key that does not carry the declared scope.
  • 404Entity not found in the caller's tenant, or out of a `client_user`'s scope. One body for all three inputs.
  • 500The entity read failed, the `payroll_records` read failed (inbox 262 — NOT a 200 with an empty list), or a post-fetch tenant-scope leak was detected (defense-in-depth third wall). No partial data is returned.one of (SanitizedServerError · AccountSetupIncompleteResponse)

Dashboard2

GET/api/dashboard/summaryDashboard summary

Aggregated dashboard metrics (KRT-473): notice counts, task counts, fine amounts, and configured deadline window.

Bearer scopesnotices.read

Responses

  • 200Summary metricsDashboardSummaryResponse
    FieldTypeRequiredDescription
    noticesobjectrequired—
    tasksobjectrequired—
    finesobjectrequired—
    filing_timelinessobjectrequired—
    deadline_window_daysintegerrequired—
  • 401UnauthorizedMiscUnauthorizedResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)

Documents13

GET/api/documents/statsGet document counts

Returns document counts by status (total / pending / signed / expired) for the caller's tenant. Falls back to count-only when the documents.status column is missing.

Bearer scopesnotices.read

Responses

  • 200Stats by statusDocumentStatsResponse
    FieldTypeRequiredDescription
    totalintegerrequired—
    pendingintegerrequired—
    signedintegerrequired—
    expiredintegerrequired—
  • 401UnauthorizedDocumentUnauthorizedResponse
  • 403Forbidden — missing required scopeDocumentUnauthorizedResponse
  • 429Rate limit exceededDocumentRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/documents/collectionGet document-collection progress by entity

Returns document_requests grouped by entity, with progress counts (items_total / items_complete) and derived status (complete / overdue / in_progress).

Bearer scopesnotices.read

Responses

  • 200Grouped collection requestsDocumentCollectionResponse
    FieldTypeRequiredDescription
    requestsarray<DocumentCollectionGroup>required—
  • 401UnauthorizedDocumentUnauthorizedResponse
  • 403Forbidden — missing required scopeDocumentUnauthorizedResponse
  • 429Rate limit exceededDocumentRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/documentsList documents

List documents with optional filters (requestId, entityId, date range, includeDeleted) and pagination.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
requestIdquerystring · uuidoptional—
entityIdquerystring · uuidoptional—
startDatequerystring · date-timeoptional—
endDatequerystring · date-timeoptional—
limitquerystringoptional—
offsetquerystringoptional—
includeDeletedqueryenum: "true" | "false"optional—

Responses

  • 200Documents listDocumentListResponse
    FieldTypeRequiredDescription
    documentsarray<DocumentMetadata>required—
    pendingSignaturesobjectrequired—
    signedSignaturesobjectrequired—
    canResendSigningLinksbooleanrequired—
    signingEmailStatusAvailablebooleanrequired—
    countintegerrequired—
    limitintegerrequired—
    offsetintegerrequired—
  • 400Validation errorDocumentValidationErrorResponse
  • 401UnauthorizedDocumentUnauthorizedResponse
  • 403Forbidden — missing required scopeDocumentUnauthorizedResponse
  • 429Rate limit exceededDocumentRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/documents/{id}Get document detail

Get document metadata by id. document-engine internally scopes by RLS on the caller's session.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredDocument UUID

Responses

  • 200Document metadataDocumentDetailResponse
    FieldTypeRequiredDescription
    documentDocumentMetadatarequired—
  • 400Validation errorDocumentValidationErrorResponse
  • 401UnauthorizedDocumentUnauthorizedResponse
  • 403Forbidden — missing required scopeDocumentUnauthorizedResponse
  • 404Document not foundDocumentNotFoundResponse
  • 429Rate limit exceededDocumentRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
DELETE/api/documents/{id}Soft-delete document

Soft-delete a document with an optional reason. Returns 204 No Content on success. Already-deleted documents return 409 Conflict.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredDocument UUID

Request body DocumentDeleteBody

FieldTypeRequiredDescription
reasonstringoptional—

Responses

  • 204Soft-deleted
  • 400Validation errorDocumentValidationErrorResponse
  • 401UnauthorizedDocumentUnauthorizedResponse
  • 403A valid Bearer key without `notices.write`, a session that must complete an MFA step-up (`mfa_required`), or `role_forbidden` — a non-CPA cookie role, refused before any read or write (the same body for every id)NoticesWriteForbiddenResponse
  • 404Document not found / not accessibleDocumentNotFoundResponse
  • 409Document already deletedDocumentValidationErrorResponse
  • 429Rate limit exceededDocumentRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/documents/{id}/voidVoid document

Void a document with a reason. Voiding is a status transition, not a delete: the row, its storage object and its audit history all survive. Already-voided, completed and soft-deleted documents are refused with 409.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredDocument UUID

Request body DocumentVoidBodyrequired

FieldTypeRequiredDescription
reasonstringrequired—

Responses

  • 200VoidedDocumentVoidResponse
    FieldTypeRequiredDescription
    voidedenum: truerequired—
    idstring · uuidrequired—
  • 400Validation errorDocumentValidationErrorResponse
  • 401UnauthorizedDocumentUnauthorizedResponse
  • 403A valid Bearer key without `notices.write`, a session that must complete an MFA step-up (`mfa_required`), or `role_forbidden` — a non-CPA cookie role, refused before any read or write (the same body for every id)NoticesWriteForbiddenResponse
  • 404Not found or not accessible in this tenantDocumentNotFoundResponse
  • 409Document is already voided, completed, or deletedDocumentValidationErrorResponse
  • 429Rate limit exceededDocumentRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/documents/bulk/voidBulk void documents

Void up to 200 selected documents. Partial success by design: each id runs through the same per-document primitive, so non-transitionable ids are returned in `skipped[]` while the valid ids still void. Each void writes its own audit event.

Bearer scopesnotices.write

Request body DocumentBulkVoidBodyrequired

FieldTypeRequiredDescription
idsarray<string · uuid>required—
reasonstringrequired—

Responses

  • 200Per-item outcomeDocumentBulkVoidResponse
    FieldTypeRequiredDescription
    voidedintegerrequired—
    skippedarray<object>required—
    failedarray<object>required—
  • 400Validation errorDocumentValidationErrorResponse
  • 401UnauthorizedDocumentUnauthorizedResponse
  • 403Forbidden — CPA-level access requiredDocumentUnauthorizedResponse
  • 429Rate limit exceededDocumentRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/documents/request-emailSend document-request email

Send a document-request email to an employer contact via Resend. Sender is the caller's display_name + tenant name.

Bearer scopesnotices.write

Request body DocumentRequestEmailBodyrequired

FieldTypeRequiredDescription
entity_idstring · uuidrequired—
request_idstring · uuidrequired—
recipient_emailstring · emailrequired—
recipient_namestringoptional—

Responses

  • 200Email sent (`sent: true`), or NOT sent with no provider call (`sent: false, suppressed: true`): `reason: "customer_sends_disabled"` when customer sends are switched off on this deployment, no `reason` when the non-prod recipient allowlist suppressed the address.DocumentRequestEmailResponse
  • 400Validation errorDocumentValidationErrorResponse
  • 401UnauthorizedDocumentUnauthorizedResponse
  • 403Forbidden — missing required scopeDocumentUnauthorizedResponse
  • 404Entity or document request not found in caller's tenantDocumentNotFoundResponse
  • 409The document request has no upload portal yet (W28-D2-4-3) — generate the portal link before sending.DocumentValidationErrorResponse
  • 429Rate limit exceededDocumentRateLimitResponse
  • 500Server error / Resend API failureone of (SanitizedServerError · AccountSetupIncompleteResponse)
  • 503Email service not configuredDocumentUnauthorizedResponse
POST/api/documents/uploadUpload document

Upload a document (multipart/form-data) for an entity, optionally tied to a document_request. Idempotency-aware (x-idempotency-key header). Tags accept JSON array or comma-separated form.

Bearer scopesnotices.write

Request body DocumentUploadMultipartBodyrequired

FieldTypeRequiredDescription
filestringrequiredDocument binary
entityIdstring · uuidoptional—
requestIdstring · uuidoptional—
tagsstringoptionalJSON array or comma-separated tags

Responses

  • 200UploadedDocumentUploadResponse
    FieldTypeRequiredDescription
    successenum: truerequired—
    documentDocumentMetadatarequired—
    idempotent_replayenum: trueoptional—
  • 400Malformed form data, no `file`, a malformed `entityId` / `requestId`, a file too large / of a type not allowed / carrying unredactable sensitive data, or an idempotency refusal (a malformed key; `idempotency_body_unreadable`)DocumentUploadRefusal
  • 401UnauthorizedDocumentUnauthorizedResponse
  • 403Forbidden — missing required scopeDocumentUnauthorizedResponse
  • 404Entity or request not foundDocumentNotFoundResponse
  • 409The same content is already on file for the tenant, or `idempotency_key_conflict` (the key was used for a different request)DocumentUploadRefusal
  • 429Rate limit exceeded (uploads use the 10/min ceiling)DocumentRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
  • 503The idempotency check itself failed, so a retry could not be told from a fresh request — refused before the handler ran (retryable)SanitizedServerError
POST/api/documents/create-signing-requestCreate signing request link

Create a token-based signature request for a document and return a secure signing link suitable for email delivery. Token expires after 1-30 days.

Bearer scopesnotices.write

Request body DocumentCreateSigningRequestBodyrequired

FieldTypeRequiredDescription
document_idstring · uuidrequired—
entity_idstring · uuidoptionalThe employer entity the signature request is filed under. REQUIRED for an employer (client_user) caller — omitting it answers 400 `INPUT_VALIDATION_FAILED` naming `entity_id` — and it must be one of that caller's own entities: any other id answers the same 404 `Document not found` as a missing document. Optional for CPA callers, where a null leaves the request unlinked.
signer_namestringrequired—
signer_emailstring · emailrequired—
signer_titlestringoptional—
expires_daysintegeroptional—

Responses

  • 201Signing request createdDocumentCreateSigningRequestResponse
    FieldTypeRequiredDescription
    request_idstring · uuidrequired—
    secure_tokenstring · uuidrequired—
    signing_linkstringrequired—
    expires_atstring · date-timerequired—
  • 400Validation error — including an employer (client_user) caller that omitted `entity_id` (`code: INPUT_VALIDATION_FAILED`, issue path `entity_id`).DocumentCreateSigningRequestValidationError
  • 401UnauthorizedDocumentUnauthorizedResponse
  • 403Forbidden — missing required scopeDocumentUnauthorizedResponse
  • 404Document not found in caller's tenant — also the answer, byte-identical, for a document or body `entity_id` outside an employer caller's own entities (existence-opaque).DocumentNotFoundResponse
  • 409Refused before any write: the document is not a PDF, has no confirmed employer or belongs to a different employer than `entity_id`; it is a government form with no mapped signature line (owner ruling S2: "This form has no signature line mapped yet — Kreto cannot stamp it."); or — owner rule E1 — it is linked to a notice whose letter has not been approved by a CPA (`reason: letter_not_approved`).DocumentCreateSigningRequestRefusal
  • 429Rate limit exceededDocumentRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/documents/verifyVerify document via AI

Run AI-based document verification on an uploaded file (PDF/PNG/JPG/DOCX, ≤10 MB). Two audit events recorded (requested + completed). Returns documentType + confidence + verified + flags.

Bearer scopesnotices.write

Request body DocumentVerifyMultipartBodyrequired

FieldTypeRequiredDescription
filestringrequiredPDF/PNG/JPG/DOCX binary (max 10 MB)
expectedTypestringoptional—

Responses

  • 200Verification resultDocumentVerifyResult
    FieldTypeRequiredDescription
    okenum: truerequired—
    resultobjectrequired—
  • 400Validation error / unsupported MIME / unreadable fileDocumentValidationErrorResponse
  • 401UnauthorizedDocumentUnauthorizedResponse
  • 403Forbidden — missing required scopeDocumentUnauthorizedResponse
  • 413File too large (>10 MB)DocumentValidationErrorResponse
  • 429Rate limit exceededDocumentRateLimitResponse
  • 500Server error / verification pipeline failureone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/check-duplicateCheck upload duplicate

Check if an uploaded file with the given SHA-256 content hash already exists in the caller's tenant (KRT-203). The body is caller-dependent: CPA/system callers also get the matching upload's id, filename and upload time, while employer-plane callers (`client_user` cookie sessions and `client_user`-minted Bearer keys) get `isDuplicate` alone — the matched row can belong to a sibling employer, and its filename identifies that employer. Only `isDuplicate` is guaranteed present. Cookie sessions go through rate-limit middleware.

Bearer scopesnotices.read

Request body CheckDuplicateBodyrequired

FieldTypeRequiredDescription
contentHashstringrequired—

Responses

  • 200Duplicate check resultCheckDuplicateResponse
    FieldTypeRequiredDescription
    isDuplicatebooleanrequired—
    originalUploadIdstring · uuidoptional—
    originalFileNamestringoptional—
    uploadedAtstring · date-timeoptional—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/documents/bulk/exportExport documents as CSV

Export up to 1000 documents by id. Ids outside the caller's tenant (or outside a `client_user`'s entities) are simply absent from the file.

Bearer scopesnotices.read

Request body objectrequired

FieldTypeRequiredDescription
idsarray<string · uuid>requiredTarget ids, 1..1000. Over the cap the request is REFUSED, never silently truncated.

Responses

  • 200CSV attachment: `text/csv; charset=utf-8` with `Content-Disposition: attachment; filename="documents-export-<YYYY-MM-DD>.csv"`. Header row, in order: `document_id`, `filename`, `kind`, `status`, `entity_id`, `request_id`, `notice_id`, `uploaded_at`.string
  • 400Body failed validation — `ids` outside 1..1000, or a value that is not a UUID.
  • 403Authenticated but out of scope — a `client_user` asking outside its entity memberships, or a Bearer key that does not carry the declared scope.
  • 500The read failed, or a post-fetch tenant-scope leak was detected (defense-in-depth third wall). No partial file is written.one of (SanitizedServerError · AccountSetupIncompleteResponse)

Employer Invites1

POST/api/employer-invites/{token}/acceptAccept a CPA firm's employer invitation

The signed-in invitee accepts a live, pending invitation: their profile moves into the inviting firm's tenant as `client_user`, the entity link and the firm/employer relationship are recorded, and the invitation is marked accepted. Session cookie only (no Bearer). The 200 body reports per-stage `steps`. A standalone employer whose own workspace already holds data is refused with 409 `SELF_TENANT_HAS_DATA` and nothing changes.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
tokenpathstringrequiredThe plaintext invitation capability token.

Responses

  • 200Invitation accepted (see `steps` for per-stage outcome)EmployerInviteAcceptResponse
    FieldTypeRequiredDescription
    relationship_idstring · uuidrequired—
    entity_idstring · uuidrequired—
    stepsobjectrequiredOne status per non-fatal stage. `invite_status: failed` means the invitation is still pending and the link remains redeemable.
    messagestringrequired—
  • 401No sessionEmployerInviteUnauthorizedResponse
  • 403A precondition refused a live invitation: `SELF_ACCEPTANCE_NOT_ALLOWED`, `EMAIL_MISMATCH`, `ROLE_CONFLICT`, or the session's MFA step-up (`mfa_required`)one of (EmployerInviteRefusalResponse · EmployerInviteMfaRequiredResponse)
  • 404The one closed response: unknown, malformed, expired, revoked, or accepted-by-someone-else tokens are indistinguishable by designEmployerInviteErrorOnly
  • 409Either the invitee re-clicking their own already-accepted link (`{ error }`), or `SELF_TENANT_HAS_DATA` — the caller's standalone workspace holds notices/records the move would strand; nothing changedone of (EmployerInviteErrorOnly · EmployerInviteSelfTenantHasDataResponse)
  • 429Per-IP cap on the invitation-token planeEmployerInviteErrorOnly
  • 500Sanitized server errorSanitizedServerError

Employers18

POST/api/employersCreate employer

Create a new employer entity for the authenticated CPA's tenant. Validates the body against the multi-axis Zod schema (legal_form, engagement_type, payroll_provider). Returns 201 Created with the new entity ID + legal_name.

Cookie session or Bearer token

Request body CreateEmployerBodyrequired

FieldTypeRequiredDescription
legal_namestringrequired—
einstringoptional—
ein_last4stringoptional—
primary_statestringoptional—
addressstringoptional—
citystringoptional—
statestringoptional—
zipstringoptional—
contact_namestringoptional—
contact_emailstring · emailoptional—
contact_phonestringoptional—
employee_countintegeroptional—
legal_formenum: "llc" | "s_corp" | "sole_prop" | "partnership" | …optional—
engagement_typeenum: "full_managed" | "selective" | "employer_direct"optional—
payroll_providerenum: "adp" | "gusto" | "paychex" | "quickbooks" | …optional—

Responses

  • 201Employer createdCreateEmployerResponse
    FieldTypeRequiredDescription
    entityEntitySummaryrequired—
  • 400Validation errorValidationErrorResponse
  • 401UnauthorizedUnauthorizedResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
PATCH/api/employersUpdate employer

Update an entity's contact info or core identifiers. Tenant-scoped via explicit tenant_id filter (KRT-1191 audit fix).

Cookie session or Bearer token

Request body PatchEmployerBodyrequired

FieldTypeRequiredDescription
entity_idstring · uuidrequired—
contact_namestringoptional—
contact_emailstring · emailoptional—
contact_phonestringoptional—
legal_namestringoptional—
ein_last4stringoptional—
primary_statestringoptional—

Responses

  • 200Employer updatedPatchEmployerResponse
    FieldTypeRequiredDescription
    entityobjectrequired—
  • 400Validation error / no fields to updateValidationErrorResponse
  • 401UnauthorizedUnauthorizedResponse
  • 404Entity not found in caller's tenantNotFoundResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/employers/listList employers

List employers visible to the caller (RLS + explicit tenant filter). Each row carries notice_count, active_notices_count, overdue_notices_count, states_operated, and POA progress. Bearer-key callers need the `employers.read` scope (or NULL for full tenant scope).

Bearer scopesemployers.read

Parameters

NameInTypeRequiredDescription
include_deletedqueryenum: "true" | "false"optionalWhen `true`, include soft-deleted entities (CPA-class roles only).
todayquerystringoptionalThe reader's local calendar day (YYYY-MM-DD) the compliance monitor's overdue reads use for `compliance_score`. Defaults to the server's day.

Responses

  • 200Employer listEmployerListResponse
    FieldTypeRequiredDescription
    entitiesarray<EmployerListItem>required—
  • 401UnauthorizedUnauthorizedResponse
  • 403Forbidden — missing required scopeUnauthorizedResponse
  • 429Rate limit exceededRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (EmployersListServerError · AccountSetupIncompleteResponse)
GET/api/employers/{id}/detailGet employer detail

Return the full entity row with associated notices, POA records, and a derived I-9 compliance summary. All Supabase queries filter by tenant_id explicitly (KRT-1479 P4a Senior-Dev-Override fix).

Bearer scopesemployers.read

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredEmployer entity UUID

Responses

  • 200Employer detailEmployerDetailResponse
    FieldTypeRequiredDescription
    idstring · uuidrequired—
    tenant_idstring · uuidrequired—
    legal_namestringrequired—
    ein_last4stringrequired—
    primary_statestringrequired—
    entity_typestringrequired—
    contact_namestringrequired—
    contact_emailstringrequired—
    contact_phonestringrequired—
    created_atstring · date-timerequired—
    is_deletedbooleanoptional—
    deletion_datestring · date-timeoptional—
    deleted_atstring · date-timeoptional—
    deletion_scheduled_atstring · date-timeoptional—
    formation_datestring · dateoptional—
    noticesarray<NoticeSummary>required—
    poa_recordsarray<PoaRecord>required—
    i9_compliancearray<I9ComplianceEntry>required—
    penalties_ytdnumberrequired—
  • 400Validation errorValidationErrorResponse
  • 401UnauthorizedUnauthorizedResponse
  • 403Forbidden — missing required scopeUnauthorizedResponse
  • 404Entity not found in caller's tenantNotFoundResponse
  • 429Rate limit exceededRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/employers/update-einUpdate employer EIN

Set the last 4 digits of an entity's EIN, optionally update legal_name, and clear `needs_ein` flag on a referenced notice. Tenant-scoped.

Cookie session or Bearer token

Request body UpdateEinBodyrequired

FieldTypeRequiredDescription
entity_idstring · uuidrequired—
ein_last4stringoptional—
legal_namestringoptional—
notice_idstring · uuidoptional—

Responses

  • 200UpdatedSuccessResponse
    FieldTypeRequiredDescription
    successenum: truerequired—
  • 400Validation errorValidationErrorResponse
  • 401UnauthorizedUnauthorizedResponse
  • 404Entity not found in caller's tenantNotFoundResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
PATCH/api/employers/{id}/email-slugCustomize employer notice email

One-time customization of an entity's notice email slug. Once set, the slug is locked (`email_slug_customized=true`) and further changes require operator intervention.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredEmployer entity UUID

Request body EmailSlugBodyrequired

FieldTypeRequiredDescription
slugstringrequired—

Responses

  • 200Slug setEmailSlugResponse
    FieldTypeRequiredDescription
    successenum: truerequired—
    emailstringrequired—
  • 400Invalid slug format / already customizedValidationErrorResponse
  • 401UnauthorizedUnauthorizedResponse
  • 404Entity not found in caller's tenantNotFoundResponse
  • 409Slug taken by another entityValidationErrorResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/employers/{id}/deleteUndo employer soft-delete

Undo a soft-delete. Clears `deleted_at` + `deletion_scheduled_at`. Must be called within the 30-day grace window before /api/employers/cleanup permanently removes the entity row + cascade dependents.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredEmployer entity UUID

Responses

  • 200Deletion cancelledUndoDeleteResponse
    FieldTypeRequiredDescription
    successenum: truerequired—
    entity_idstring · uuidrequired—
  • 400Entity is not deletedValidationErrorResponse
  • 401UnauthorizedUnauthorizedResponse
  • 404Entity not found in caller's tenantNotFoundResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
DELETE/api/employers/{id}/deleteSoft-delete employer

Soft-delete an entity. Sets `deleted_at` + `deletion_scheduled_at` (now + 30 days). Permanent deletion is run by /api/employers/cleanup (internal cron). Reversible via POST to the same endpoint within the grace window.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredEmployer entity UUID

Responses

  • 200Deletion scheduledDeleteEmployerResponse
    FieldTypeRequiredDescription
    successenum: truerequired—
    scheduled_permanent_deletionstring · date-timerequired—
  • 401UnauthorizedUnauthorizedResponse
  • 404Entity not found in caller's tenantNotFoundResponse
  • 409Entity already scheduled for deletionValidationErrorResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/employers/{id}/separationsList an employer's separations

Separation events for one employer, newest first (capped at 100; totalCount is the exact total). A CPA reads the claimant's name; an employer user reads "Name withheld" in its place. No SSN or SSN hash is returned to anyone.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
idpathstringrequiredEmployer (entities) UUID

Responses

  • 200Separations — the CPA body, or the employer body (claimant name withheld)one of (EmployerSeparationsEmployer · EmployerSeparationsCpa)
  • 400The id is not a UUIDEmployerSeparationsInvalidId
  • 401UnauthorizedMiscUnauthorizedResponse
  • 404Employer not found (unknown, foreign and non-member ids alike)EmployerSeparationsNotFound
  • 409The employer was removedEmployerSeparationsRemoved
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/entities/{id}/payroll-name-confirmationRead the name confirmation

Read the standing name-identity confirmation for this employer (who confirmed, when) and the estimate label it gives. Cookie session only, CPA roles only.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
idpathstringrequiredEmployer (entities) UUID

Responses

  • 200The standing decisionPayrollNameConfirmationState
    FieldTypeRequiredDescription
    entity_idstringrequired—
    confirmedbooleanrequired—
    confirmed_bystringrequiredDisplay name of the confirming CPA; null when none stands
    confirmed_atstringrequiredISO timestamp of the standing confirmation; null when none
    labelstringrequiredThe estimate label for this employer's name-keyed figures, with 'confirmed by <CPA> on <date>' while a confirmation stands.
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Forbidden — CPA access required (employers are refused)MiscUnauthorizedResponse
  • 404Employer not found (unknown and foreign ids alike)PayrollNameConfirmationNotFound
  • 500Server error — including a refused audit append or confirmation readone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/entities/{id}/payroll-name-confirmationConfirm no two employees share a name

Record the CPA's confirmation that no two employees of this employer share a name, so a name-keyed payroll estimate may support a protest letter. Cookie session only, CPA roles only.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
idpathstringrequiredEmployer (entities) UUID

Responses

  • 200Confirmation recordedPayrollNameConfirmationResponse
    FieldTypeRequiredDescription
    entity_idstringrequired—
    confirmedbooleanrequired—
    labelstringrequiredThe estimate label every surface shows — with 'confirmed by <CPA> on <date>' after a confirmation. Still an estimate.
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Forbidden — CPA access required (employers are refused)MiscUnauthorizedResponse
  • 404Employer not found (unknown and foreign ids alike)PayrollNameConfirmationNotFound
  • 500Server error — including a refused audit append or confirmation readone of (SanitizedServerError · AccountSetupIncompleteResponse)
DELETE/api/entities/{id}/payroll-name-confirmationWithdraw the name confirmation

Withdraw the name-identity confirmation; the estimate's letters are held again. Cookie session only, CPA roles only.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
idpathstringrequiredEmployer (entities) UUID

Responses

  • 200Confirmation withdrawnPayrollNameConfirmationResponse
    FieldTypeRequiredDescription
    entity_idstringrequired—
    confirmedbooleanrequired—
    labelstringrequiredThe estimate label every surface shows — with 'confirmed by <CPA> on <date>' after a confirmation. Still an estimate.
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Forbidden — CPA access required (employers are refused)MiscUnauthorizedResponse
  • 404Employer not found (unknown and foreign ids alike)PayrollNameConfirmationNotFound
  • 500Server error — including a refused audit append or confirmation readone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/entities/{id}/payroll-start-dateRead mid-year start answers

Read the standing mid-year payroll start answers for this employer, per state and year. Cookie session only; CPA roles, or an employer user who is a member of this employer.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
idpathstringrequiredEmployer (entities) UUID

Responses

  • 200The standing answersPayrollStartDateState
    FieldTypeRequiredDescription
    entity_idstringrequired—
    decisionsarray<object>required—
    notesarray<object>requiredMid-year starts detected today on the payroll on file, one per state + year
  • 400The body is not a valid answer for this callerPayrollStartDateInvalidBody
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Forbidden — a role outside CPA roles and client_userMiscUnauthorizedResponse
  • 404Employer not found (unknown, foreign and non-member ids alike)PayrollStartDateNotFound
  • 500Server error — including a refused audit append or readone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/entities/{id}/payroll-start-dateAnswer the mid-year start question

Answer whether this employer started paying wages on the date its payroll on file starts. CPA: yes (confirm), no, reverse. Employer: yes (stated by the employer), no. A CPA's answer outranks the employer's. Cookie session only.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
idpathstringrequiredEmployer (entities) UUID

Request body PayrollStartDateAnswer

FieldTypeRequiredDescription
statestringrequired—
yearintegerrequired—
startstringrequired—
answerenum: "yes" | "no" | "reverse"requiredEmployers may answer yes or no; only a CPA may reverse

Responses

  • 200Answer recorded (or the CPA's answer stands)PayrollStartDateRecorded
    FieldTypeRequiredDescription
    entity_idstringrequired—
    recordedbooleanrequiredfalse when an employer answered a question its CPA has already answered (the CPA's answer governs; it is not disclosed)
    reruns_enqueuedintegeroptionalOpen notices queued for recalculation by this answer; null when the re-run could not be queued (absent when nothing was recorded)
    statusstringrequired—
  • 400The body is not a valid answer for this callerPayrollStartDateInvalidBody
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Forbidden — a role outside CPA roles and client_userMiscUnauthorizedResponse
  • 404Employer not found (unknown, foreign and non-member ids alike)PayrollStartDateNotFound
  • 500Server error — including a refused audit append or readone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/employers/bulk/archiveBulk archive or unarchive employers

Set `archived` on up to 200 employers. Soft-deleted employers are out of scope unless `include_deleted` is explicitly true.

Bearer scopesemployers.write

Request body objectrequired

FieldTypeRequiredDescription
idsarray<string · uuid>requiredTarget ids, 1..200. Duplicates are collapsed before any write.
archivedbooleanoptional—
include_deletedbooleanoptionalOpt in to mutating SOFT-DELETED employers. Absent or false (what every product surface sends) means a removed employer is not a target.

Responses

  • 200Per-id ledger. Partial success is a 200.BulkActionResult
    FieldTypeRequiredDescription
    appliedintegerrequiredIds that produced a real state change (and therefore exactly one audit row each).
    skippedarray<object>requiredIds deliberately not mutated. Present on a 200 — partial success is the contract, not an error.
    failedarray<object>requiredIds whose mutation raised. `probe_failed` means the tenant-scoped existence probe itself errored, so nothing was attempted.
  • 400Body failed validation (ids outside 1..200, or a bad uuid).
  • 403Authenticated but not CPA-class (`FORBIDDEN_ROLE`), or the Bearer key lacks the declared scope.
  • 500A tenant-scope leak was detected post-fetch (defense-in-depth third wall). No writes occurred.one of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/employers/bulk/assignBulk assign employers to a firm user

Assign up to 200 employers to one in-tenant user, or clear the assignee with `null`. The assignee is validated against `user_profiles` in-tenant first.

Bearer scopesemployers.write

Request body objectrequired

FieldTypeRequiredDescription
idsarray<string · uuid>requiredTarget ids, 1..200. Duplicates are collapsed before any write.
assigneestring · uuidrequired—
include_deletedbooleanoptionalOpt in to mutating SOFT-DELETED employers. Absent or false (what every product surface sends) means a removed employer is not a target.

Responses

  • 200Per-id ledger. Partial success is a 200.BulkActionResult
    FieldTypeRequiredDescription
    appliedintegerrequiredIds that produced a real state change (and therefore exactly one audit row each).
    skippedarray<object>requiredIds deliberately not mutated. Present on a 200 — partial success is the contract, not an error.
    failedarray<object>requiredIds whose mutation raised. `probe_failed` means the tenant-scoped existence probe itself errored, so nothing was attempted.
  • 400Body failed validation (ids outside 1..200, or a bad uuid).
  • 403Authenticated but not CPA-class (`FORBIDDEN_ROLE`), or the Bearer key lacks the declared scope.
  • 500A tenant-scope leak was detected post-fetch (defense-in-depth third wall). No writes occurred.one of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/employers/bulk/tagBulk add, remove or set employer tags

Apply up to 20 tags to up to 200 employers. `mode` defaults to `add`; `set` replaces the existing tag list.

Bearer scopesemployers.write

Request body objectrequired

FieldTypeRequiredDescription
idsarray<string · uuid>requiredTarget ids, 1..200. Duplicates are collapsed before any write.
tagsarray<string>required—
modeenum: "add" | "remove" | "set"optional—
include_deletedbooleanoptionalOpt in to mutating SOFT-DELETED employers. Absent or false (what every product surface sends) means a removed employer is not a target.

Responses

  • 200Per-id ledger. Partial success is a 200.BulkActionResult
    FieldTypeRequiredDescription
    appliedintegerrequiredIds that produced a real state change (and therefore exactly one audit row each).
    skippedarray<object>requiredIds deliberately not mutated. Present on a 200 — partial success is the contract, not an error.
    failedarray<object>requiredIds whose mutation raised. `probe_failed` means the tenant-scoped existence probe itself errored, so nothing was attempted.
  • 400Body failed validation (ids outside 1..200, or a bad uuid).
  • 403Authenticated but not CPA-class (`FORBIDDEN_ROLE`), or the Bearer key lacks the declared scope.
  • 500A tenant-scope leak was detected post-fetch (defense-in-depth third wall). No writes occurred.one of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/employers/bulk/exportExport employers as CSV

Export up to 1000 employers by id. The `status` cell carries ONE lifecycle vocabulary (`active` | `archived` | `removed`), so a soft-deleted employer cannot read as active. EINs are emitted masked (`***-NNNN`), never in full.

Bearer scopesemployers.read

Request body objectrequired

FieldTypeRequiredDescription
idsarray<string · uuid>requiredTarget ids, 1..1000. Over the cap the request is REFUSED, never silently truncated.
include_deletedbooleanoptionalCPA-only opt-in to including SOFT-DELETED employers. Absent or false (what every product surface sends) leaves them out.

Responses

  • 200CSV attachment: `text/csv; charset=utf-8` with `Content-Disposition: attachment; filename="employers-export-<YYYY-MM-DD>.csv"`. Header row, in order: `employer`, `ein_masked`, `primary_state`, `entity_type`, `compliance_score`, `tags`, `status`, `created`.string
  • 400Body failed validation — `ids` outside 1..1000, or a value that is not a UUID.
  • 403Authenticated but out of scope — a `client_user` asking outside its entity memberships, or a Bearer key that does not carry the declared scope.
  • 500The read failed, or a post-fetch tenant-scope leak was detected (defense-in-depth third wall). No partial file is written.one of (SanitizedServerError · AccountSetupIncompleteResponse)

Escalations3

POST/api/escalations/bulk/acknowledgeBulk acknowledge escalations

Acknowledge up to 200 escalations, keyed by notice id. An already-acknowledged escalation is skipped with `no_change`; a cross-tenant or unknown id is skipped with `not_found`. client_user is refused.

Bearer scopesnotices.write

Request body objectrequired

FieldTypeRequiredDescription
idsarray<string · uuid>requiredTarget ids, 1..200. Duplicates are collapsed before any write.

Responses

  • 200Per-id ledger. Partial success is a 200.BulkActionResult
    FieldTypeRequiredDescription
    appliedintegerrequiredIds that produced a real state change (and therefore exactly one audit row each).
    skippedarray<object>requiredIds deliberately not mutated. Present on a 200 — partial success is the contract, not an error.
    failedarray<object>requiredIds whose mutation raised. `probe_failed` means the tenant-scoped existence probe itself errored, so nothing was attempted.
  • 400Body failed validation (ids outside 1..200, or a bad uuid).
  • 403Authenticated but not CPA-class (`FORBIDDEN_ROLE`), or the Bearer key lacks the declared scope.
  • 500A tenant-scope leak was detected post-fetch (defense-in-depth third wall). No writes occurred.one of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/escalations/bulk/snoozeBulk snooze escalations

Snooze up to 200 escalations. `snooze_hours` defaults to the route's own default when omitted. client_user is refused.

Bearer scopesnotices.write

Request body objectrequired

FieldTypeRequiredDescription
idsarray<string · uuid>requiredTarget ids, 1..200. Duplicates are collapsed before any write.
snooze_hoursintegeroptional—

Responses

  • 200Per-id ledger. Partial success is a 200.BulkActionResult
    FieldTypeRequiredDescription
    appliedintegerrequiredIds that produced a real state change (and therefore exactly one audit row each).
    skippedarray<object>requiredIds deliberately not mutated. Present on a 200 — partial success is the contract, not an error.
    failedarray<object>requiredIds whose mutation raised. `probe_failed` means the tenant-scoped existence probe itself errored, so nothing was attempted.
  • 400Body failed validation (ids outside 1..200, or a bad uuid).
  • 403Authenticated but not CPA-class (`FORBIDDEN_ROLE`), or the Bearer key lacks the declared scope.
  • 500A tenant-scope leak was detected post-fetch (defense-in-depth third wall). No writes occurred.one of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/escalations/bulk/reassignBulk reassign escalations

Reassign up to 200 escalations to one in-tenant user, or clear the assignee with `null`. The assignee is validated in-tenant before any write.

Bearer scopesnotices.write

Request body objectrequired

FieldTypeRequiredDescription
idsarray<string · uuid>requiredTarget ids, 1..200. Duplicates are collapsed before any write.
assigneestring · uuidrequired—

Responses

  • 200Per-id ledger. Partial success is a 200.BulkActionResult
    FieldTypeRequiredDescription
    appliedintegerrequiredIds that produced a real state change (and therefore exactly one audit row each).
    skippedarray<object>requiredIds deliberately not mutated. Present on a 200 — partial success is the contract, not an error.
    failedarray<object>requiredIds whose mutation raised. `probe_failed` means the tenant-scoped existence probe itself errored, so nothing was attempted.
  • 400Body failed validation (ids outside 1..200, or a bad uuid).
  • 403Authenticated but not CPA-class (`FORBIDDEN_ROLE`), or the Bearer key lacks the declared scope.
  • 500A tenant-scope leak was detected post-fetch (defense-in-depth third wall). No writes occurred.one of (SanitizedServerError · AccountSetupIncompleteResponse)

Garnishments2

GET/api/garnishmentsList garnishment orders

List garnishment orders for the caller's tenant (compliance category #6 — wage garnishments). client_user is entity-scoped on cookie sessions; Bearer keys see full tenant scope.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
entity_idquerystring · uuidoptional—
statusquerystringoptional—
order_typequerystringoptional—
limitqueryintegeroptional—
offsetqueryintegeroptional—

Responses

  • 200GarnishmentsGarnishmentsListResponse
    FieldTypeRequiredDescription
    garnishmentsarray<GarnishmentOrder>required—
    totalintegerrequired—
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Forbidden — entity access denied (client_user scope)MiscUnauthorizedResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/garnishmentsCreate garnishment order

Create a garnishment order. Bearer keys gate via `notices.write`. client_user is entity-scoped on cookie sessions.

Bearer scopesnotices.write

Request body GarnishmentCreateBodyrequired

FieldTypeRequiredDescription
entity_idstring · uuidrequired—
notice_idstring · uuidoptional—
order_typestringoptional—
court_order_numberstringoptional—
case_numberstringoptional—
issuing_authoritystringoptional—
employee_identifierstringoptional—
employee_countintegeroptional—
garnishment_amountnumberoptional—
withholding_percentagenumberoptional—
max_cumulativenumberoptional—
priority_orderintegeroptional—
effective_datestringoptional—
expiration_datestringoptional—
response_deadlinestringoptional—

Responses

  • 201Garnishment createdGarnishmentCreateResponse
    FieldTypeRequiredDescription
    garnishmentGarnishmentOrderrequired—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Forbidden — entity access deniedMiscUnauthorizedResponse
  • 404Entity not found in caller's tenantMiscNotFoundResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)

Handbooks5

GET/api/handbooks/{id}Get one handbook with its sections and coverage gaps

The read behind the draft door: the handbook row, its pinned sections in document order, and the coverage gaps the generator recorded. Read-only — publishing is a separate approval route. A `client_user` (and any Bearer key minted by one) is narrowed to its own entity memberships: a handbook outside them answers the same 404 as an id that does not exist, never a 403. Gated per tenant by the `HANDBOOKS_ENABLED` flag; off answers a plain 404.

Bearer scopeshandbooks.read

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequired—

Responses

  • 200The handbook, its sections and its gaps.HandbookDetailResponse
    FieldTypeRequiredDescription
    handbookHandbookRowrequired—
    sectionsarray<HandbookSectionView>requiredPinned sections in document order. Pins whose module version no longer resolves are NOT dropped — they are counted in `unresolved_section_count`, because a shorter list would hide a hole in the document a reviewer is being asked to publish.
    gapsarray<HandbookGap>required—
    unresolved_section_countintegerrequired—
  • 400`id` is not a UUID (`VALIDATION_FAILED`).
  • 401No valid credentials: no cookie session and no Bearer key, or a Bearer key that is malformed, unknown, revoked or expired.
  • 403The auth wrapper's own refusals only: a valid Bearer key that does not carry `handbooks.read` (`Forbidden — missing required scope: handbooks.read` — never a 401), or a session that must complete an MFA step-up (`mfa_required`). Never an entity-scope decision — a `client_user` asking for a handbook outside its entity memberships is answered the 404 below (W47-HBAPI3, existence-opaque).HandbookDetailForbiddenResponse
  • 404No such handbook in the caller's tenant (`NOT_FOUND`) — and the SAME body for a foreign-tenant id, a sibling entity's handbook outside a `client_user`'s memberships, and a non-published handbook on the employer plane. Handbooks disabled for the tenant answers a plain `{ error: "Not found" }`.HandbookNotFoundResponse
  • 500The caller's entity scope could not be resolved (fail closed), or the read failed. No partial data is returned.one of (SanitizedServerErrorWithCode · AccountSetupIncompleteResponse)
POST/api/handbooks/{id}/publishPublish a draft handbook

Thin shim over the `publishHandbook` L2 method (APPROVAL_REQUIRED). The §0-2(b) blocking disclaimer checkbox is part of the contract: `disclaimerAccepted` must be `true`. CPA-class only; gated per tenant by the `HANDBOOKS_ENABLED` flag (off answers a plain 404).

Bearer scopeshandbooks.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequired—

Request body object

FieldTypeRequiredDescription
disclaimerAcceptedbooleanrequired—

Responses

  • 200Published: `{ handbook }` — the row as the publish wrote it (`status` `published`, `published_at` + `disclaimer_ack_at/by` stamped, `pdf_status` `queued`).HandbookPublishResponse
    FieldTypeRequiredDescription
    handbookHandbookRowrequired—
  • 400`id` is not a UUID, the body is not JSON, or `disclaimerAccepted` is not a boolean (`VALIDATION_FAILED`).
  • 401No valid credentials, or the API key's creator no longer exists (`AUTH_KEY_CREATOR_MISSING`).
  • 402Cookie session whose tenant has no active subscription. Bearer keys are not gated here.
  • 403A `client_user` session (`FORBIDDEN_ROLE`), a Bearer key without `handbooks.write`, or a session that must complete an MFA step-up.
  • 404No such handbook in the caller's tenant (`NOT_FOUND`). Handbooks disabled for the tenant answers a plain `{ error: "Not found" }`.
  • 409The draft cannot be published. `HANDBOOK_HAS_NO_SECTIONS`: it has no policy sections — `error` is the sentence for the CPA and `missing` names the jurisdictions and required topics the verified catalog lacks (the same code the /v1 twin answers). `ILLEGAL_TRANSITION`: any other illegal transition (the handbook is not a draft). Nothing is written on either arm.HandbookPublishRefusal
  • 422`disclaimerAccepted` is `false` — the blocking checkbox was not ticked (`DISCLAIMER_REQUIRED`).
  • 500The publish failed: the sanitized body `ctx.finalize` leaves with (`code` `INTERNAL_ERROR`, generic `error`, `requestId` when one was minted). A cookie session with no `user_profiles` row answers the `withApiAuth` wrapper's `AccountSetupIncompleteResponse` instead.one of (SanitizedServerErrorWithCode · AccountSetupIncompleteResponse)
POST/api/admin/handbook-modules/promotePromote a handbook module version or registry topic

Thin shim over `promoteHandbookModuleVersion` / `promoteStateRequiredTopic` (APPROVAL_REQUIRED, UI-only). The reviewer identity is the authenticated admin, never the body. The ladder is monotonic (unverified → human_reviewed → verified).

Cookie session or Bearer token

Request body one of (object · object)

application/json

Responses

  • 200Promoted: the row as the promote wrote it, with the ladder move.HandbookModulePromoteResponse
  • 400The body is not JSON, or it failed the method's input schema (`VALIDATION_FAILED`, with up to ten zod `details` when the schema rejected it).
  • 401No admin session (`requireAdminAPI`).
  • 403The session is not a system administrator, or must complete an MFA step-up (`requireAdminAPI`).
  • 404No such module version or registry row (`NOT_FOUND`).HandbookRecordNotFound
  • 409`PROVENANCE_REQUIRED`: the row has no `source_url`. `ILLEGAL_TRANSITION`: the version is emergency-retired, or the move is not up the ladder. Nothing is written.HandbookModulePromoteRefusal
  • 500Unexpected failure: the sanitized body `captureApiError` writes (generic `error`, `requestId` when one was minted). An admin session with no `user_profiles` row answers `requireAdminAPI`'s `AccountSetupIncompleteResponse` instead.one of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/admin/handbook-modules/retireEmergency-retire a handbook module version

Thin shim over `retireHandbookModuleVersion` (APPROVAL_REQUIRED, UI-only) — the §0-5d emergency pull. Reason mandatory; existing pins stay reconstructable.

Cookie session or Bearer token

Request body object

FieldTypeRequiredDescription
moduleVersionIdstring · uuidrequired—
reasonstringrequired—

Responses

  • 200Retired: `{ version }` with `retired_at` stamped.HandbookModuleRetireResponse
    FieldTypeRequiredDescription
    versionHandbookModuleVersionRecordrequired—
  • 400The body is not JSON, or it failed the method's input schema (`VALIDATION_FAILED`, with up to ten zod `details` when the schema rejected it).
  • 401No admin session (`requireAdminAPI`).
  • 403The session is not a system administrator, or must complete an MFA step-up (`requireAdminAPI`).
  • 404No such module version (`NOT_FOUND`).HandbookRecordNotFound
  • 409`ILLEGAL_TRANSITION`: the version is already retired. Nothing is written.HandbookIllegalTransitionRefusal
  • 500Unexpected failure: the sanitized body `captureApiError` writes (generic `error`, `requestId` when one was minted). An admin session with no `user_profiles` row answers `requireAdminAPI`'s `AccountSetupIncompleteResponse` instead.one of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/handbooks/{id}/acknowledgmentsSend a published handbook for employee acknowledgment

Thin shim over `sendHandbookAcknowledgments`. Writes one evidence row per reachable employee and mails the acknowledgment link; employees already sent are not re-mailed. CPA-class only; gated per tenant by the `HANDBOOKS_ENABLED` flag.

Bearer scopeshandbooks.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequired—

Responses

  • 200This round's outcome, counted per send.HandbookAcknowledgmentsSendResponse
    FieldTypeRequiredDescription
    creatednumberrequired—
    skippedExistingnumberrequired—
    cannotReacharray<object>required—
    emailFailuresnumberrequired—
    sendOutcomesobjectrequired—
  • 400`id` is not a UUID (`VALIDATION_FAILED`).
  • 401No valid credentials, or the API key's creator no longer exists (`AUTH_KEY_CREATOR_MISSING`).
  • 402Cookie session whose tenant has no active subscription. Bearer keys are not gated here.
  • 403A `client_user` session (`FORBIDDEN_ROLE`), a Bearer key without `handbooks.write`, or a session that must complete an MFA step-up.
  • 404No such handbook in the caller's tenant (`NOT_FOUND`). Handbooks disabled for the tenant answers a plain `{ error: "Not found" }`.HandbookAcknowledgmentsNotFound
  • 409`ILLEGAL_TRANSITION`: the handbook is not published — only published handbooks are sent. Nothing is written.HandbookIllegalTransitionRefusal
  • 500The send failed: the sanitized body `ctx.finalize` leaves with (`code` `INTERNAL_ERROR`, generic `error`, `requestId` when one was minted). A cookie session with no `user_profiles` row answers the `withApiAuth` wrapper's `AccountSetupIncompleteResponse` instead.one of (SanitizedServerErrorWithCode · AccountSetupIncompleteResponse)

I-912

POST/api/i9-compliance/uploadStore an I-9 document

Store a single I-9 document for an employee. Any authenticated cookie session may file — the CPA plane and the employer plane alike — and authorization is the caller's entity allow-set, not their role name; an employee outside it answers 404, never 403. Bearer keys gate via `notices.write`. Multipart body with `file` and `employee_id` (UUID) fields. The document is written to the private `i9-documents` bucket and linked to an `i9_records` row with a computed `retention_until`, as ONE compensated unit — a failure is a named 4xx/5xx and leaves nothing behind, never a success receipt. Every 200 carries `mode: "persisted"` and `persisted: true`. The response returns a path PREFIX, never an object key or a signed URL.

Bearer scopesnotices.write

Request body objectrequired

FieldTypeRequiredDescription
filestring · binaryrequired—
employee_idstringrequired—

Responses

  • 200Document stored — object and i9_records row both writtenI9UploadResponse
    FieldTypeRequiredDescription
    successenum: truerequired—
    modeenum: "persisted"required—
    persistedenum: truerequired—
    employee_idstringrequired—
    filenamestringrequired—
    record_idstringrequiredThe i9_records row the document is attached to.
    storage_path_prefixstringrequiredThe FOLDER the object was written to (<tenant_id>/<entity_id>/<i9_record_id>/) — never the object key and never a signed URL. Fetch documents through GET /api/i9-compliance/forms/{id}/documents.
    retention_untilstringrequired8 CFR 274a.2(b)(2)(i)(A): three years after hire or one year after termination, whichever is later.
    retention_provisionalbooleanrequiredTrue when the date was computed from an unknown (no termination date, or no hire date) and is therefore a FLOOR to be recomputed.
  • 400Validation errorI9ValidationErrorResponse
  • 401UnauthorizedI9UnauthorizedResponse
  • 403Forbidden — employer access required / missing scopeI9UnauthorizedResponse
  • 429Rate limit exceededI9RateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/i9-compliance/forms/{id}/documentsList (or mint a download for) one I-9 record's documents

Without `download`, answers SLOTS and counts only — original filenames are people's names and do not travel. With `download=pdf` or `download=<index>`, mints a short-lived signed URL for that one slot; the slot is the ONLY way a storage path is chosen, so a caller cannot name an arbitrary object. CPA-class roles only on cookie sessions.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequired—
downloadquerystringoptional`pdf`, or the zero-based index of a supporting document. Absent ⇒ the slot listing.

Responses

  • 200The slot listing, or the signed-URL grant when `download` is given.one of (I9DocumentSlotsResponse · I9DocumentDownloadResponse)
  • 400`id` is not a UUID, or `download` is neither `pdf` nor a valid index.
  • 401No valid credentials: no cookie session and no Bearer key, or a Bearer key that is malformed, unknown, revoked or expired.
  • 403A valid Bearer key that does not carry `notices.read` (`Forbidden — missing required scope: notices.read` — never a 401); a session that must complete an MFA step-up (`mfa_required`); or a caller that is not CPA-class — a non-CPA cookie session, or a Bearer key whose minting user is an employer or cannot be resolved (`Forbidden — CPA-level access required`).I9DocumentsForbiddenResponse
  • 404No such I-9 record in the caller's tenant, or no document in that slot.
  • 500The record read failed.one of (SanitizedServerError · AccountSetupIncompleteResponse)
  • 502The signed-URL mint failed.
POST/api/i9-compliance/forms/{id}/documentsUpload Section 2 supporting document

Upload a Section 2 supporting document for an I-9 record. CPA-class roles only on cookie sessions; Bearer keys gate via `notices.write`. Multipart body with `file`, `list` (a|b|c), and `index` (0..10).

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredI-9 record UUID

Request body objectrequired

FieldTypeRequiredDescription
filestring · binaryrequired—
listenum: "a" | "b" | "c"required—
indexintegerrequired—

Responses

  • 201Document uploadedI9RecordResponse
    FieldTypeRequiredDescription
    recordI9Recordrequired—
  • 400Validation errorI9ValidationErrorResponse
  • 401UnauthorizedI9UnauthorizedResponse
  • 403Forbidden — CPA access requiredI9UnauthorizedResponse
  • 404I-9 record not foundI9NotFoundResponse
  • 429Rate limit exceededI9RateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/i9-compliance/forms/{id}/pdfDownload I-9 PDF

Render the I-9 record as a flattened PDF. CPA-class roles only on cookie sessions; Bearer keys gate via `notices.read`.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredI-9 record UUID

Responses

  • 200PDF binaryI9PdfBinaryResponse
  • 401UnauthorizedI9UnauthorizedResponse
  • 403Forbidden — CPA access requiredI9UnauthorizedResponse
  • 404I-9 record not foundI9NotFoundResponse
  • 429Rate limit exceededI9RateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/i9-compliance/forms/{id}Get I-9 record

Get a single I-9 record. CPA-class roles only on cookie sessions; Bearer keys gate via `notices.read`.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredI-9 record UUID

Responses

  • 200I-9 recordI9RecordResponse
    FieldTypeRequiredDescription
    recordI9Recordrequired—
  • 400Validation errorI9ValidationErrorResponse
  • 401UnauthorizedI9UnauthorizedResponse
  • 403Forbidden — CPA access requiredI9UnauthorizedResponse
  • 404I-9 record not foundI9NotFoundResponse
  • 429Rate limit exceededI9RateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
PATCH/api/i9-compliance/forms/{id}Update I-9 Section 2

Update an I-9 record's Section 2 data (employer review). Validates List A is mutually exclusive with List B/C. CPA-class roles only on cookie sessions; Bearer keys gate via `notices.write`.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredI-9 record UUID

Request body I9Section2Bodyrequired

FieldTypeRequiredDescription
list_a_docsarray<I9DocumentEntry>optional—
list_b_docsarray<I9DocumentEntry>optional—
list_c_docsarray<I9DocumentEntry>optional—
reviewer_namestringrequired—
reviewer_titlestringrequired—
date_of_hirestringrequired—
date_of_completionstringrequired—

Responses

  • 200I-9 record updatedI9RecordResponse
    FieldTypeRequiredDescription
    recordI9Recordrequired—
  • 400Validation error / invalid document combinationI9ValidationErrorResponse
  • 401UnauthorizedI9UnauthorizedResponse
  • 403Forbidden — CPA access requiredI9UnauthorizedResponse
  • 404I-9 record not foundI9NotFoundResponse
  • 409Section 1 must be completed firstI9ValidationErrorResponse
  • 429Rate limit exceededI9RateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/i9-compliance/formsList I-9 records by employee

List I-9 records for a given employee. CPA-class roles only on cookie sessions; Bearer keys gate via `notices.read`.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
employee_idquerystring · uuidrequired—

Responses

  • 200I-9 recordsI9RecordsListResponse
    FieldTypeRequiredDescription
    recordsarray<I9Record>required—
  • 400Validation errorI9ValidationErrorResponse
  • 401UnauthorizedI9UnauthorizedResponse
  • 403Forbidden — CPA access requiredI9UnauthorizedResponse
  • 429Rate limit exceededI9RateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/i9-compliance/formsCreate I-9 record (Section 1)

Create an I-9 record (Section 1). Cross-field validation: LPR requires alien_registration_number; authorized_alien requires at least one of A-Number / I-94 / passport+country. CPA-class roles only on cookie sessions; Bearer keys gate via `notices.write`.

Bearer scopesnotices.write

Request body I9Section1CreateBodyrequired

FieldTypeRequiredDescription
employee_idstring · uuidrequired—
citizenship_statusI9CitizenshipStatusrequired—
section1_dataI9Section1Datarequired—
ssnstringoptional—

Responses

  • 201I-9 record createdI9RecordResponse
    FieldTypeRequiredDescription
    recordI9Recordrequired—
  • 400Validation errorI9ValidationErrorResponse
  • 401UnauthorizedI9UnauthorizedResponse
  • 403Forbidden — CPA access requiredI9UnauthorizedResponse
  • 404Employee not found in the caller's tenant (unknown and foreign-tenant ids answer identically)I9NotFoundResponse
  • 429Rate limit exceededI9RateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/i9-compliance/{employeeId}Get employee I-9 detail

Get a single employee's I-9 details + uploaded documents. Falls back to payroll_pay_periods when employeeId is an SSN hash. cookie sessions: client_user is entity-scoped; CPA roles see all. Bearer keys gate via `notices.read` (full tenant access).

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
employeeIdpathstringrequiredEmployee UUID or SSN hash

Responses

  • 200Employee + documentsI9EmployeeDetailResponse
    FieldTypeRequiredDescription
    employeeI9EmployeeI9required—
    documentsarray<I9DocumentRow>required—
    sourceenum: "employees" | "payroll_pay_periods"required—
  • 401UnauthorizedI9UnauthorizedResponse
  • 403Not authorized for this employee (entity-scope check)I9UnauthorizedResponse
  • 404Employee not foundI9NotFoundResponse
  • 429Rate limit exceededI9RateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/i9-compliance/bulk-uploadMatch I-9 documents to employees (preview only)

Bulk upload up to 50 I-9 documents. Extracts employee names from filenames and fuzzy-matches against the active roster (entity-scoped for client_user; full tenant for CPA / Bearer keys with `notices.write`). PREVIEW ONLY: returns matched + unmatched results and persists NOTHING — no storage object, no i9_records row, no audit entry. Every 200 carries `mode: "match_preview"` and `persisted: false`.

Bearer scopesnotices.write

Request body objectrequired

FieldTypeRequiredDescription
filesarray<string · binary>required—

Responses

  • 200Match resultsI9BulkUploadResponse
    FieldTypeRequiredDescription
    modeenum: "match_preview"required—
    persistedenum: falserequired—
    totalintegerrequired—
    matchedarray<I9BulkMatch>required—
    unmatchedarray<I9BulkUnmatched>required—
  • 400Validation errorI9ValidationErrorResponse
  • 401UnauthorizedI9UnauthorizedResponse
  • 429Rate limit exceededI9RateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/i9-complianceList I-9 employees

List I-9 employee records for the caller's tenant + expiry stats. Falls back to payroll_pay_periods when no employees rows exist. Bearer keys gate via `notices.read`.

Bearer scopesnotices.read

Responses

  • 200Employee list + expiry statsI9EmployeesListResponse
    FieldTypeRequiredDescription
    employeesarray<I9EmployeeListItem>required—
    expiry_statsobjectoptional—
    employment_statsobjectoptional—
    sourcestringoptional—
    messagestringoptional—
  • 401UnauthorizedI9UnauthorizedResponse
  • 429Rate limit exceededI9RateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/i9-complianceCreate I-9 employee

Create a new I-9 employee record. Employer-only on cookie sessions; Bearer keys gate via `notices.write`.

Bearer scopesnotices.write

Request body I9EmployeeCreateBodyrequired

FieldTypeRequiredDescription
employee_namestringrequired—
hire_datestringrequired—
entity_idstring · uuidrequired—
emailstring · emailoptional—
departmentstringoptional—

Responses

  • 201Employee createdI9EmployeeCreateResponse
    FieldTypeRequiredDescription
    employeeI9EmployeeListItemrequired—
  • 400Validation errorI9ValidationErrorResponse
  • 401UnauthorizedI9UnauthorizedResponse
  • 403Forbidden — employer access requiredI9UnauthorizedResponse
  • 404Entity not found in caller's tenantI9NotFoundResponse
  • 429Rate limit exceededI9RateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
  • 501Employees table not provisioned yetI9UnauthorizedResponse

KnowledgeBase1

GET/api/knowledge-baseQuery state compliance knowledge

Query the global state_compliance_knowledge table — Kreto-wide reference data covering state-by-state compliance rules, notice types, and deadlines. No tenant filter (read-only reference data). Bearer keys gate via `notices.read`.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
statequerystringoptional—
typequerystringoptional—
qquerystringoptional—
min_confidencequerynumberoptional—
limitqueryintegeroptional—
offsetqueryintegeroptional—

Responses

  • 200Knowledge entries + summaryKnowledgeBaseResponse
    FieldTypeRequiredDescription
    entriesarray<KnowledgeBaseEntry>required—
    totalintegerrequired—
    summaryobjectrequired—
  • 400Unparseable search termMiscUnauthorizedResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)

l2:agency-documents5

POST/v1/agency-documents.listList an employer's agency documents (state rate notices and charge statements Kreto read), one per document with its newest reading and its status. The accountant sees each figure with its page and quote; the employer sees the status word only. Tenant- and employer-scoped; read-only. AUTONOMOUS.

List an employer's agency documents (state rate notices and charge statements Kreto read), one per document with its newest reading and its status. The accountant sees each figure with its page and quote; the employer sees the status word only. Tenant- and employer-scoped; read-only. AUTONOMOUS.

Bearer scopesnotices.read

Request body AgencyDocumentsListInputrequired

FieldTypeRequiredDescription
entityIdstring · uuidrequired—

Responses

  • 200Successful invocationAgencyDocumentsListOutput
    FieldTypeRequiredDescription
    documentsarray<object>required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/agency-documents.getRead one agency-document reading: the form, the dates it states and its status; the accountant also sees each figure with its page, quote and confidence. Tenant- and employer-scoped; read-only. AUTONOMOUS.

Read one agency-document reading: the form, the dates it states and its status; the accountant also sees each figure with its page, quote and confidence. Tenant- and employer-scoped; read-only. AUTONOMOUS.

Bearer scopesnotices.read

Request body AgencyDocumentsGetInputrequired

FieldTypeRequiredDescription
extractionIdstring · uuidrequired—

Responses

  • 200Successful invocationAgencyDocumentsGetOutput
    FieldTypeRequiredDescription
    documentobjectrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/agency-documents.confirmThe accountant confirms an agency document's figures (each with its page and quote), rejecting any that were misread (each with a reason from a closed list; a rejected figure is kept on the reading but never used, and re-opens only by typing the printed value). Confirmed figures are applied to the notice they are usable for — only when the document's date is the state's computation date for that rate year — as the agency's own figures, never as Kreto-verified; a figure that differs from one the employer stated is held for the accountant's review. When every figure the rate check reads is on file, the rate decision re-runs through the job queue. A misread figure may be corrected to the value printed on the page; Kreto keeps its own reading and quote beside the accountant's value. Accountant-only, tenant- and employer-scoped. NOTIFY.

The accountant confirms an agency document's figures (each with its page and quote), rejecting any that were misread (each with a reason from a closed list; a rejected figure is kept on the reading but never used, and re-opens only by typing the printed value). Confirmed figures are applied to the notice they are usable for — only when the document's date is the state's computation date for that rate year — as the agency's own figures, never as Kreto-verified; a figure that differs from one the employer stated is held for the accountant's review. When every figure the rate check reads is on file, the rate decision re-runs through the job queue. A misread figure may be corrected to the value printed on the page; Kreto keeps its own reading and quote beside the accountant's value. Accountant-only, tenant- and employer-scoped. NOTIFY.

Bearer scopesnotices.write

Request body AgencyDocumentsConfirmInputrequired

FieldTypeRequiredDescription
extractionIdstring · uuidrequired—
noticeIdstring · uuidoptional—
rejectarray<object>optional—
editarray<object>optional—

Responses

  • 200Successful invocationAgencyDocumentsConfirmOutput
    FieldTypeRequiredDescription
    confirmedenum: truerequired—
    extractionIdstringrequired—
    acceptedarray<enum: "years_in_business" | "total_benefit_charges" | "total_contributions" | "average_annual_payroll" | …>required—
    rejectedarray<enum: "years_in_business" | "total_benefit_charges" | "total_contributions" | "average_annual_payroll" | …>required—
    editedarray<enum: "years_in_business" | "total_benefit_charges" | "total_contributions" | "average_annual_payroll" | …>required—
    projectionsarray<object>required—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/agency-documents.rejectFigureThe accountant rejects one figure Kreto read on an agency document, before confirming it, with a reason from a closed list (misread_value, wrong_line, wrong_period, not_on_document, illegible, other — `other` needs a short note). Kreto's reading and quote are kept; the figure is never applied to a notice, and it re-opens only when the accountant types the value printed on the page in the confirm. Accountant-only, tenant- and employer-scoped. NOTIFY.

The accountant rejects one figure Kreto read on an agency document, before confirming it, with a reason from a closed list (misread_value, wrong_line, wrong_period, not_on_document, illegible, other — `other` needs a short note). Kreto's reading and quote are kept; the figure is never applied to a notice, and it re-opens only when the accountant types the value printed on the page in the confirm. Accountant-only, tenant- and employer-scoped. NOTIFY.

Bearer scopesnotices.write

Request body AgencyDocumentsRejectFigureInputrequired

FieldTypeRequiredDescription
extractionIdstring · uuidrequired—
noticeIdstring · uuidoptional—
keyenum: "years_in_business" | "total_benefit_charges" | "total_contributions" | "average_annual_payroll" | …required—
reasonenum: "misread_value" | "wrong_line" | "wrong_period" | "not_on_document" | …required—
notestringoptional—

Responses

  • 200Successful invocationAgencyDocumentsRejectFigureOutput
    FieldTypeRequiredDescription
    rejectedenum: truerequired—
    extractionIdstringrequired—
    keyenum: "years_in_business" | "total_benefit_charges" | "total_contributions" | "average_annual_payroll" | …required—
    reasonenum: "misread_value" | "wrong_line" | "wrong_period" | "not_on_document" | …required—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/agency-documents.acceptAllThe accountant confirms every figure of an agency document in one step — offered only when every figure was read with high confidence, its quote was found on the page, none was rejected and the reader's quote and date checks passed; otherwise refused as `bulk_requires_all_high` with each reason named (confirm figure by figure instead). Applied exactly like a confirm, with one audit entry per figure. Accountant-only, tenant- and employer-scoped. NOTIFY.

The accountant confirms every figure of an agency document in one step — offered only when every figure was read with high confidence, its quote was found on the page, none was rejected and the reader's quote and date checks passed; otherwise refused as `bulk_requires_all_high` with each reason named (confirm figure by figure instead). Applied exactly like a confirm, with one audit entry per figure. Accountant-only, tenant- and employer-scoped. NOTIFY.

Bearer scopesnotices.write

Request body AgencyDocumentsAcceptAllInputrequired

FieldTypeRequiredDescription
extractionIdstring · uuidrequired—
noticeIdstring · uuidoptional—

Responses

  • 200Successful invocationAgencyDocumentsAcceptAllOutput
    FieldTypeRequiredDescription
    confirmedenum: truerequired—
    extractionIdstringrequired—
    acceptedarray<enum: "years_in_business" | "total_benefit_charges" | "total_contributions" | "average_annual_payroll" | …>required—
    rejectedarray<enum: "years_in_business" | "total_benefit_charges" | "total_contributions" | "average_annual_payroll" | …>required—
    editedarray<enum: "years_in_business" | "total_benefit_charges" | "total_contributions" | "average_annual_payroll" | …>required—
    projectionsarray<object>required—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 409Conflict. `bulk_requires_all_high`: Accept all is withheld — `blocks` names each reason, one sentence each (the same entries as the reading's `bulk_accept_blocks`). Any other code: the reading is already confirmed or not read yet (the envelope names it).one of (object · L2ErrorEnvelope)
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

l2:annual-reports12

POST/v1/annual-reports.listAnnualReportRulesList the state annual-report catalog (global config): per-(state, entity_kind) SOS annual/biennial report rules with the verification ladder status of every cell and honest per-status counts. Cells below the display gate (human_reviewed) must never be rendered as authoritative to customers.

List the state annual-report catalog (global config): per-(state, entity_kind) SOS annual/biennial report rules with the verification ladder status of every cell and honest per-status counts. Cells below the display gate (human_reviewed) must never be rendered as authoritative to customers.

Bearer scopesannual_reports.read

Request body AnnualReportsListAnnualReportRulesInputrequired

FieldTypeRequiredDescription
stateCodestringoptional—
entityKindenum: "corporation" | "llc" | "nonprofit" | "lp" | …optional—
verificationStatusenum: "unverified" | "human_reviewed" | "verified"optional—

Responses

  • 200Successful invocationAnnualReportsListAnnualReportRulesOutput
    FieldTypeRequiredDescription
    rulesarray<object>required—
    countsobjectrequired—
    filteredCountnumberrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `annual_reports.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/annual-reports.getAnnualReportRuleFetch one annual-report catalog cell with the (state, entity_kind) → (state, 'default') fallback. Returns the human-readable due-rule narrative alongside the raw rule. Null rule = no catalog coverage yet.

Fetch one annual-report catalog cell with the (state, entity_kind) → (state, 'default') fallback. Returns the human-readable due-rule narrative alongside the raw rule. Null rule = no catalog coverage yet.

Bearer scopesannual_reports.read

Request body AnnualReportsGetAnnualReportRuleInputrequired

FieldTypeRequiredDescription
stateCodestringrequired—
entityKindenum: "corporation" | "llc" | "nonprofit" | "lp" | …optional—

Responses

  • 200Successful invocationAnnualReportsGetAnnualReportRuleOutput
    FieldTypeRequiredDescription
    ruleobjectrequired—
    usedDefaultFallbackbooleanrequired—
    dueNarrativestringrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `annual_reports.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/annual-reports.proposeAnnualReportRuleResearch-agent write path for the annual-report catalog. Upserts one (state, entity_kind) cell as verification_status='unverified' with mandatory provenance (source_url + retrieved_at). REFUSES to touch a cell already at human_reviewed/verified — verified cells are never downgraded (AC-22 clone); corrections go through the review queue.

Research-agent write path for the annual-report catalog. Upserts one (state, entity_kind) cell as verification_status='unverified' with mandatory provenance (source_url + retrieved_at). REFUSES to touch a cell already at human_reviewed/verified — verified cells are never downgraded (AC-22 clone); corrections go through the review queue.

Bearer scopesannual_reports.write

Request body AnnualReportsProposeAnnualReportRuleInputrequired

FieldTypeRequiredDescription
stateCodestringrequired—
entityKindenum: "corporation" | "llc" | "nonprofit" | "lp" | …optional—
agencyCodestringrequired—
agencyNamestringrequired—
portalUrlstring · urioptional—
formNamestringoptional—
filingFeeUsdnumberoptional—
frequencyenum: "annual" | "biennial" | "decennial" | "none"required—
dueRuleone of (object · object · object · object · object)required—
parityenum: "odd" | "even" | "formation_year"optional—
gracePeriodDaysintegeroptional—
lateFeeNotesstringoptional—
sourceUrlstring · urirequired—
retrievedAtstring · date-timerequired—
notesstringoptional—

Responses

  • 200Successful invocationAnnualReportsProposeAnnualReportRuleOutput
    FieldTypeRequiredDescription
    ruleobjectrequired—
    createdbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `annual_reports.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/annual-reports.promoteAnnualReportRulePromote an annual-report catalog cell along the verification ladder (unverified → human_reviewed → verified). APPROVAL_REQUIRED and UI-only: the handler rejects any non-human principal (AC-20 clone) and refuses cells with no source_url provenance (CWE-345).

Promote an annual-report catalog cell along the verification ladder (unverified → human_reviewed → verified). APPROVAL_REQUIRED and UI-only: the handler rejects any non-human principal (AC-20 clone) and refuses cells with no source_url provenance (CWE-345).

Bearer scopesannual_reports.write

Request body AnnualReportsPromoteAnnualReportRuleInputrequired

FieldTypeRequiredDescription
stateCodestringrequired—
entityKindenum: "corporation" | "llc" | "nonprofit" | "lp" | …optional—
targetStatusenum: "human_reviewed" | "verified"required—
reasonstringrequired—

Responses

  • 200Successful invocationAnnualReportsPromoteAnnualReportRuleOutput
    FieldTypeRequiredDescription
    ruleobjectrequired—
    fromStatusenum: "unverified" | "human_reviewed" | "verified"required—
    toStatusenum: "unverified" | "human_reviewed" | "verified"required—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden. `SYSTEM_ADMIN_REQUIRED`: the caller is not a Kreto system administrator on an interactive session — the global catalog every firm pins (handbook modules, state filing / foreign-qualification / annual-report / e-delivery rules) is staff-only, exactly as on the REST admin door. Checked before any catalog read, so the answer is the same whether or not the row exists. `STEP_UP_REQUIRED`: the dispatcher's APPROVAL_REQUIRED gate (an API key, or no fresh re-authentication). Nothing is written.one of (L2SystemAdminRequiredRefusal · L2ErrorEnvelope)
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/annual-reports.listAnnualReportsList the tenant's annual-report tracker rows (per entity × state × filing period), filterable by entity, state, status and period. Paginated (default 25). Tenant-scoped via the invocation context.

List the tenant's annual-report tracker rows (per entity × state × filing period), filterable by entity, state, status and period. Paginated (default 25). Tenant-scoped via the invocation context.

Bearer scopesannual_reports.read

Request body AnnualReportsListAnnualReportsInputrequired

FieldTypeRequiredDescription
entityIdstringoptional—
stateCodestringoptional—
statusenum: "pending" | "prepared" | "submitted" | "approved" | …optional—
periodLabelstringoptional—
limitintegeroptional—
offsetintegeroptional—

Responses

  • 200Successful invocationAnnualReportsListAnnualReportsOutput
    FieldTypeRequiredDescription
    reportsarray<object>required—
    totalnumberrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `annual_reports.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/annual-reports.getAnnualReportByEntityFetch every annual-report tracker row for one entity (optionally one state), all periods — history plus in-flight. Tenant-scoped.

Fetch every annual-report tracker row for one entity (optionally one state), all periods — history plus in-flight. Tenant-scoped.

Bearer scopesannual_reports.read

Request body AnnualReportsGetAnnualReportByEntityInputrequired

FieldTypeRequiredDescription
entityIdstringrequired—
stateCodestringoptional—

Responses

  • 200Successful invocationAnnualReportsGetAnnualReportByEntityOutput
    FieldTypeRequiredDescription
    reportsarray<object>required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `annual_reports.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/annual-reports.computeAnnualReportScheduleDry-run compute of an entity's annual-report schedule for one period year (default: current year): per candidate state, the computed due date OR an explicit non-computed disposition (needs_input / needs_research / below_display_gate / not_required / not_due_this_period). Writes nothing — the sweep's cycle roller persists rows; this method answers 'what would be due?'.

Dry-run compute of an entity's annual-report schedule for one period year (default: current year): per candidate state, the computed due date OR an explicit non-computed disposition (needs_input / needs_research / below_display_gate / not_required / not_due_this_period). Writes nothing — the sweep's cycle roller persists rows; this method answers 'what would be due?'.

Bearer scopesannual_reports.read

Request body AnnualReportsComputeAnnualReportScheduleInputrequired

FieldTypeRequiredDescription
entityIdstringrequired—
periodYearintegeroptional—

Responses

  • 200Successful invocationAnnualReportsComputeAnnualReportScheduleOutput
    FieldTypeRequiredDescription
    entityIdstringrequired—
    periodYearnumberrequired—
    itemsarray<object>required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `annual_reports.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/annual-reports.createAnnualReportCreate one manual annual-report tracker row (source='manual') for an entity × state × period. Computes the due date from the display-gated catalog when possible; otherwise the row starts manual_required with a NULL due date ('Kreto needs your input'). Duplicate periods are rejected by the per-period UNIQUE.

Create one manual annual-report tracker row (source='manual') for an entity × state × period. Computes the due date from the display-gated catalog when possible; otherwise the row starts manual_required with a NULL due date ('Kreto needs your input'). Duplicate periods are rejected by the per-period UNIQUE.

Bearer scopesannual_reports.write

Request body AnnualReportsCreateAnnualReportInputrequired

FieldTypeRequiredDescription
entityIdstringrequired—
stateCodestringrequired—
periodLabelstringrequired—
anchorKindenum: "formation" | "foreign_qualification" | "manual"optional—
anchorDatestringoptional—
notesstringoptional—

Responses

  • 200Successful invocationAnnualReportsCreateAnnualReportOutput
    FieldTypeRequiredDescription
    reportobjectrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `annual_reports.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/annual-reports.updateAnnualReportMetadataEdit tracker metadata on one annual-report row: anchor date/kind (recomputes the due date from the display-gated catalog and flips manual_required → pending when a date becomes computable), assignee and notes. Writes a structured audit event capturing before/after.

Edit tracker metadata on one annual-report row: anchor date/kind (recomputes the due date from the display-gated catalog and flips manual_required → pending when a date becomes computable), assignee and notes. Writes a structured audit event capturing before/after.

Bearer scopesannual_reports.write

Request body AnnualReportsUpdateAnnualReportMetadataInputrequired

FieldTypeRequiredDescription
annualReportIdstringrequired—
anchorDatestringoptional—
anchorKindenum: "formation" | "foreign_qualification" | "manual"optional—
assigneestringoptional—
notesstringoptional—
reasonstringrequired—

Responses

  • 200Successful invocationAnnualReportsUpdateAnnualReportMetadataOutput
    FieldTypeRequiredDescription
    reportobjectrequired—
    changedFieldsarray<string>required—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `annual_reports.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/annual-reports.prepareAnnualReportPopulate an annual report's prepared form payload (officers, principal address, the customer's OWN registered agent — Kreto is not the RA) and transition pending/manual_required → prepared. APPROVAL_REQUIRED: symmetric with prepareRegistrationForm; a human reviews before any filing. PII in form_payload is scrubbed from audit rows.

Populate an annual report's prepared form payload (officers, principal address, the customer's OWN registered agent — Kreto is not the RA) and transition pending/manual_required → prepared. APPROVAL_REQUIRED: symmetric with prepareRegistrationForm; a human reviews before any filing. PII in form_payload is scrubbed from audit rows.

Bearer scopesannual_reports.write

Request body AnnualReportsPrepareAnnualReportInputrequired

FieldTypeRequiredDescription
annualReportIdstringrequired—
formPayloadobjectrequired—

Responses

  • 200Successful invocationAnnualReportsPrepareAnnualReportOutput
    FieldTypeRequiredDescription
    reportobjectrequired—
    fromStatusenum: "pending" | "prepared" | "submitted" | "approved" | …required—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `annual_reports.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/annual-reports.requestConciergeFilingRecord the '$149 File it for me' concierge request on a prepared annual report: stamps filing_requested_at/by (design D3 — an annotation on 'prepared', not a status) and charges the $149 as a standalone Stripe invoice (created + finalized, so it bills every customer; owner 2026-07-10; fail-closed when STRIPE_SECRET_KEY is unset; double-bill safe). APPROVAL_REQUIRED: this moves money, so the v1 dispatcher blocks non-interactive API-key / MCP callers (KRT-1657) — it requires an interactive human session. The click by an authenticated CPA/owner IS the recorded filing authorization. The state filing fee is separate and paid to the state.

Record the '$149 File it for me' concierge request on a prepared annual report: stamps filing_requested_at/by (design D3 — an annotation on 'prepared', not a status) and charges the $149 as a standalone Stripe invoice (created + finalized, so it bills every customer; owner 2026-07-10; fail-closed when STRIPE_SECRET_KEY is unset; double-bill safe). APPROVAL_REQUIRED: this moves money, so the v1 dispatcher blocks non-interactive API-key / MCP callers (KRT-1657) — it requires an interactive human session. The click by an authenticated CPA/owner IS the recorded filing authorization. The state filing fee is separate and paid to the state.

Bearer scopesannual_reports.write

Request body AnnualReportsRequestConciergeFilingInputrequired

FieldTypeRequiredDescription
annualReportIdstringrequired—

Responses

  • 200Successful invocationAnnualReportsRequestConciergeFilingOutput
    FieldTypeRequiredDescription
    reportobjectrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `annual_reports.write`.L2ErrorEnvelope
  • 409Not available (code `STATE_FILING_NOT_AVAILABLE`): Kreto does not file annual reports yet. Nothing is written, nothing is charged, nothing is enqueued and the row's status does not change.L2StateFilingNotAvailableRefusal
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/annual-reports.transitionAnnualReportStatusApply a matrix-gated status transition to an annual-report tracker row (exact esr 7-status machine). Supports the mark-filed extras (confirmationNumber + filedAt on → submitted) and the confirmedImmediately chain (submitted → approved in one audited call for instant-confirm states). Every hop writes a hash-chained audit_events row.

Apply a matrix-gated status transition to an annual-report tracker row (exact esr 7-status machine). Supports the mark-filed extras (confirmationNumber + filedAt on → submitted) and the confirmedImmediately chain (submitted → approved in one audited call for instant-confirm states). Every hop writes a hash-chained audit_events row.

Bearer scopesannual_reports.write

Request body AnnualReportsTransitionAnnualReportStatusInputrequired

FieldTypeRequiredDescription
annualReportIdstringrequired—
newStatusenum: "pending" | "prepared" | "submitted" | "approved" | …required—
reasonstringrequired—
confirmationNumberstringoptional—
filedAtstring · date-timeoptional—
confirmedImmediatelybooleanoptional—
lastErrorstringoptional—

Responses

  • 200Successful invocationAnnualReportsTransitionAnnualReportStatusOutput
    FieldTypeRequiredDescription
    reportobjectrequired—
    fromStatusenum: "pending" | "prepared" | "submitted" | "approved" | …required—
    toStatusenum: "pending" | "prepared" | "submitted" | "approved" | …required—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `annual_reports.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

l2:audit1

POST/v1/audit.getRecentForRegistrationReturn recent service_audit_log rows linked to a specific state_registration row (via linked_resource_ids.registrationId). Tenant-scoped. AUTONOMOUS: append-only audit-table reads have no side effects.

Return recent service_audit_log rows linked to a specific state_registration row (via linked_resource_ids.registrationId). Tenant-scoped. AUTONOMOUS: append-only audit-table reads have no side effects.

Bearer scopesaudit.read

Request body AuditGetRecentForRegistrationInputrequired

FieldTypeRequiredDescription
registrationIdstringrequired—
limitintegeroptional—

Responses

  • 200Successful invocationAuditGetRecentForRegistrationOutput
    FieldTypeRequiredDescription
    eventsarray<object>required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `audit.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

l2:business-license5

POST/v1/business-license.listLicensesList the business licenses and local permits tracked for the tenant (optionally filtered to one entity), soft-deleted rows excluded, ordered by soonest expiration. Read-only. Kreto tracks licenses the employer already holds — it does not discover, obtain, or renew them.

List the business licenses and local permits tracked for the tenant (optionally filtered to one entity), soft-deleted rows excluded, ordered by soonest expiration. Read-only. Kreto tracks licenses the employer already holds — it does not discover, obtain, or renew them.

Bearer scopesstate_registration.read

Request body BusinessLicenseListLicensesInputrequired

FieldTypeRequiredDescription
entityIdstringoptional—

Responses

  • 200Successful invocationBusinessLicenseListLicensesOutput
    FieldTypeRequiredDescription
    licensesarray<object>required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/business-license.createLicenseRecord a business license or local permit the employer already holds (manual tracker entry). Verifies the target entity belongs to the caller's tenant, inserts the row, and appends a hash-chained business_license.created audit event. NOTIFY classification: agents can call but the action is logged for human review.

Record a business license or local permit the employer already holds (manual tracker entry). Verifies the target entity belongs to the caller's tenant, inserts the row, and appends a hash-chained business_license.created audit event. NOTIFY classification: agents can call but the action is logged for human review.

Bearer scopesstate_registration.write

Request body BusinessLicenseCreateLicenseInputrequired

FieldTypeRequiredDescription
entityIdstringrequired—
licenseNamestringrequired—
licenseTypeenum: "state_business" | "city_business" | "county_business" | "professional" | …required—
licenseNumberstringoptional—
issuingAuthoritystringrequired—
statestringoptional—
jurisdictionstringoptional—
statusenum: "active" | "expired" | "pending" | "suspended" | …optional—
issuedDatestringoptional—
expirationDatestringoptional—
renewalDatestringoptional—
renewalFeenumberoptional—
autoRenewbooleanoptional—
notesstringoptional—
noExpirybooleanoptional—

Responses

  • 200Successful invocationBusinessLicenseCreateLicenseOutput
    FieldTypeRequiredDescription
    licenseobjectrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/business-license.updateLicenseEdit a tracked business license. Absent key = unchanged, null = clear (where the column is nullable). Appends a hash-chained business_license.updated audit event carrying per-field old → new. No-op submits write nothing. NOTIFY classification: agents can call but the action is logged for human review.

Edit a tracked business license. Absent key = unchanged, null = clear (where the column is nullable). Appends a hash-chained business_license.updated audit event carrying per-field old → new. No-op submits write nothing. NOTIFY classification: agents can call but the action is logged for human review.

Bearer scopesstate_registration.write

Request body BusinessLicenseUpdateLicenseInputrequired

FieldTypeRequiredDescription
licenseIdstringrequired—
licenseNamestringoptional—
licenseTypeenum: "state_business" | "city_business" | "county_business" | "professional" | …optional—
licenseNumberstringoptional—
issuingAuthoritystringoptional—
statestringoptional—
jurisdictionstringoptional—
statusenum: "active" | "expired" | "pending" | "suspended" | …optional—
issuedDatestringoptional—
expirationDatestringoptional—
renewalDatestringoptional—
renewalFeenumberoptional—
autoRenewbooleanoptional—
notesstringoptional—

Responses

  • 200Successful invocationBusinessLicenseUpdateLicenseOutput
    FieldTypeRequiredDescription
    licenseobjectrequired—
    changedFieldsarray<string>required—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/business-license.recordRenewalRecord a license renewal the employer/CPA already completed with the issuing authority: moves the license's expiration date, recomputes status, and appends a license_renewal_history row plus a hash-chained business_license.renewed audit event. Kreto never performs the renewal itself. NOTIFY classification: agents can call but the action is logged for human review.

Record a license renewal the employer/CPA already completed with the issuing authority: moves the license's expiration date, recomputes status, and appends a license_renewal_history row plus a hash-chained business_license.renewed audit event. Kreto never performs the renewal itself. NOTIFY classification: agents can call but the action is logged for human review.

Bearer scopesstate_registration.write

Request body BusinessLicenseRecordRenewalInputrequired

FieldTypeRequiredDescription
licenseIdstringrequired—
newExpirationDatestringrequired—
renewalCostnumberoptional—
confirmationNumberstringoptional—
notesstringoptional—

Responses

  • 200Successful invocationBusinessLicenseRecordRenewalOutput
    FieldTypeRequiredDescription
    licenseobjectrequired—
    historyRecordedbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/business-license.deleteLicenseSoft-delete a tracked business license (sets deleted_at; the row stays for audit). Pending renewal-alert idempotency rows CASCADE on hard delete only — soft-deleted rows simply stop matching the reminder scan. Appends a hash-chained business_license.deleted audit event. NOTIFY classification: agents can call but the action is logged for human review.

Soft-delete a tracked business license (sets deleted_at; the row stays for audit). Pending renewal-alert idempotency rows CASCADE on hard delete only — soft-deleted rows simply stop matching the reminder scan. Appends a hash-chained business_license.deleted audit event. NOTIFY classification: agents can call but the action is logged for human review.

Bearer scopesstate_registration.write

Request body BusinessLicenseDeleteLicenseInputrequired

FieldTypeRequiredDescription
licenseIdstringrequired—

Responses

  • 200Successful invocationBusinessLicenseDeleteLicenseOutput
    FieldTypeRequiredDescription
    licenseIdstringrequired—
    deletedAtstringrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

l2:catalog-watch3

POST/v1/catalog-watch.watchSourceCheck one watched catalog source now: Claude reads the cited agency page (server-side web fetch), code compares the figures with the catalog cell, and any difference is written as a PROPOSAL for the admin Review Queue. Never writes a catalog row. Kreto staff (system_admin session) only.

Check one watched catalog source now: Claude reads the cited agency page (server-side web fetch), code compares the figures with the catalog cell, and any difference is written as a PROPOSAL for the admin Review Queue. Never writes a catalog row. Kreto staff (system_admin session) only.

Bearer scopesadmin.write

Request body CatalogWatchWatchSourceInputrequired

FieldTypeRequiredDescription
watchIdstringrequired—

Responses

  • 200Successful invocationCatalogWatchWatchSourceOutput
    FieldTypeRequiredDescription
    watchIdstringrequired—
    statusstringrequired—
    modelstringrequired—
    findingsRaisedintegerrequired—
    findingIdsarray<string>required—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/catalog-watch.listWatchFindingsList catalog-watch findings (proposals) for the admin Review Queue, newest first, with open/approved/dismissed counts and how many watched sources are failing. Kreto staff (system_admin session) only.

List catalog-watch findings (proposals) for the admin Review Queue, newest first, with open/approved/dismissed counts and how many watched sources are failing. Kreto staff (system_admin session) only.

Bearer scopesadmin.read

Request body CatalogWatchListWatchFindingsInputrequired

FieldTypeRequiredDescription
statusenum: "open" | "approved" | "dismissed"optional—
familyenum: "filing_rule" | "filing_prereq" | "foreign_qual_rule" | "compliance_obligation"optional—
stateCodestringoptional—
limitintegeroptional—

Responses

  • 200Successful invocationCatalogWatchListWatchFindingsOutput
    FieldTypeRequiredDescription
    findingsarray<object>required—
    countsobjectrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/catalog-watch.resolveWatchFindingA Kreto staff member approves or dismisses one catalog-watch finding. Approve records the decision and returns the cell's EXISTING catalog edit page — it writes no catalog row. Dismiss needs a reason. Both append a hash-chained audit event. UI-only (APPROVAL_REQUIRED).

A Kreto staff member approves or dismisses one catalog-watch finding. Approve records the decision and returns the cell's EXISTING catalog edit page — it writes no catalog row. Dismiss needs a reason. Both append a hash-chained audit event. UI-only (APPROVAL_REQUIRED).

Bearer scopesadmin.write

Request body CatalogWatchResolveWatchFindingInputrequired

FieldTypeRequiredDescription
findingIdstringrequired—
decisionenum: "approve" | "dismiss"required—
reasonstringoptional—

Responses

  • 200Successful invocationCatalogWatchResolveWatchFindingOutput
    FieldTypeRequiredDescription
    findingobjectrequired—
    editHrefstringrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

l2:cpa4

POST/v1/cpa.listEmployersList the CPA's linked employers (entities) with per-employer notice counts. Paginated via `limit` (1-100, default 25) and zero-indexed `offset`. Optional case-insensitive `search` filters on legal_name. LIVE, non-archived employers only by default — pass `include_deleted` for removed (purge-scheduled) employers or `include_archived` for archived ones; every row carries `lifecycle` (live | archived | removed), and `total` counts the same set the rows come from. Tenant-scoped. Returns aggregates only — no PII (no signer info, no employee names, no notice subjects).

List the CPA's linked employers (entities) with per-employer notice counts. Paginated via `limit` (1-100, default 25) and zero-indexed `offset`. Optional case-insensitive `search` filters on legal_name. LIVE, non-archived employers only by default — pass `include_deleted` for removed (purge-scheduled) employers or `include_archived` for archived ones; every row carries `lifecycle` (live | archived | removed), and `total` counts the same set the rows come from. Tenant-scoped. Returns aggregates only — no PII (no signer info, no employee names, no notice subjects).

Bearer scopesemployers.read

Request body CpaListEmployersInputrequired

FieldTypeRequiredDescription
limitintegeroptional—
offsetintegeroptional—
searchstringoptional—
include_deletedbooleanoptional—
include_archivedbooleanoptional—

Responses

  • 200Successful invocationCpaListEmployersOutput
    FieldTypeRequiredDescription
    employersarray<object>required—
    totalintegerrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `employers.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/cpa.summarizeComplianceAggregate the CPA tenant's compliance posture: notice counts by status, per-employer rollup, fines (total/recovered/outstanding cents), and response-time average. Tenant-scoped. Returns aggregates only — no PII.

Aggregate the CPA tenant's compliance posture: notice counts by status, per-employer rollup, fines (total/recovered/outstanding cents), and response-time average. Tenant-scoped. Returns aggregates only — no PII.

Bearer scopesemployers.read

Request body CpaSummarizeComplianceInputrequired

application/json

Responses

  • 200Successful invocationCpaSummarizeComplianceOutput
    FieldTypeRequiredDescription
    generated_atstringrequired—
    tenant_idstringrequired—
    noticesobjectrequired—
    per_entityarray<object>required—
    finesobjectrequired—
    response_timeobjectrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `employers.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/cpa.queryAuditEventsQuery the tenant's audit event timeline. Paginated via `limit` (1-100, default 25) and zero-indexed `offset`. Optional filters: `entity_id` (UUID), `notice_id` (UUID), `event_type` (exact match e.g. 'notice_created'), `since`/`until` (ISO datetimes). Tenant-scoped. `detail` is PII-redacted (SSN/EIN/bank patterns replaced) and capped at 500 chars. Metadata + integrity-chain columns are NOT returned.

Query the tenant's audit event timeline. Paginated via `limit` (1-100, default 25) and zero-indexed `offset`. Optional filters: `entity_id` (UUID), `notice_id` (UUID), `event_type` (exact match e.g. 'notice_created'), `since`/`until` (ISO datetimes). Tenant-scoped. `detail` is PII-redacted (SSN/EIN/bank patterns replaced) and capped at 500 chars. Metadata + integrity-chain columns are NOT returned.

Bearer scopesaudit.read

Request body CpaQueryAuditEventsInputrequired

FieldTypeRequiredDescription
limitintegeroptional—
offsetintegeroptional—
entity_idstringoptional—
notice_idstringoptional—
event_typestringoptional—
sincestring · date-timeoptional—
untilstring · date-timeoptional—

Responses

  • 200Successful invocationCpaQueryAuditEventsOutput
    FieldTypeRequiredDescription
    eventsarray<object>required—
    totalintegerrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `audit.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/cpa.summarizeObligationsAggregate compliance-obligations rollup across the CPA's employer book. Paginated via `limit` (1-100, default 25) and zero-indexed `offset`; optional case-insensitive `search` on legal_name. For each employer: applicable / low-confidence / pending-data obligation counts plus the earliest computable due date; firm-wide totals included. Tenant-scoped. Returns aggregates only — no PII (no employee data, no wages, no notice subjects).

Aggregate compliance-obligations rollup across the CPA's employer book. Paginated via `limit` (1-100, default 25) and zero-indexed `offset`; optional case-insensitive `search` on legal_name. For each employer: applicable / low-confidence / pending-data obligation counts plus the earliest computable due date; firm-wide totals included. Tenant-scoped. Returns aggregates only — no PII (no employee data, no wages, no notice subjects).

Bearer scopesemployers.read

Request body CpaSummarizeObligationsInputrequired

FieldTypeRequiredDescription
limitintegeroptional—
offsetintegeroptional—
searchstringoptional—

Responses

  • 200Successful invocationCpaSummarizeObligationsOutput
    FieldTypeRequiredDescription
    per_entityarray<object>required—
    totalsobjectrequired—
    totalintegerrequired—
    generated_atstringrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `employers.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

l2:cpa-team-invitations6

POST/v1/cpa-team-invitations.createInvitationCreate a firm team-invitation row, generate a single-use accept token, and email it to the invitee. The accept link is never returned to the caller (ruling 97(a)); the response reports whether the email was sent. Only firm_admin/system_admin may call.

Create a firm team-invitation row, generate a single-use accept token, and email it to the invitee. The accept link is never returned to the caller (ruling 97(a)); the response reports whether the email was sent. Only firm_admin/system_admin may call.

Bearer scopesadmin.write

Request body CpaTeamInvitationsCreateInvitationInputrequired

FieldTypeRequiredDescription
emailstring · emailrequired—
invited_roleenum: "cpa_member" | "staff"required—

Responses

  • 200Successful invocationCpaTeamInvitationsCreateInvitationOutput
    FieldTypeRequiredDescription
    invitationobjectrequired—
    email_sentbooleanrequired—
    email_failure_reasonenum: "suppressed_non_prod" | "rate_limited" | "rejected" | "unavailable" | …optional—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/cpa-team-invitations.listInvitationsList firm team invitations for the caller's tenant. Never returns plaintext tokens or token hashes.

List firm team invitations for the caller's tenant. Never returns plaintext tokens or token hashes.

Bearer scopesadmin.read

Request body CpaTeamInvitationsListInvitationsInputrequired

FieldTypeRequiredDescription
statusenum: "pending" | "accepted" | "revoked" | "expired"optional—

Responses

  • 200Successful invocationCpaTeamInvitationsListInvitationsOutput
    FieldTypeRequiredDescription
    invitationsarray<object>required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/cpa-team-invitations.revokeInvitationRevoke a pending firm invitation. Idempotent for already-revoked/accepted rows (returns current status). Only firm_admin/system_admin may revoke.

Revoke a pending firm invitation. Idempotent for already-revoked/accepted rows (returns current status). Only firm_admin/system_admin may revoke.

Bearer scopesadmin.write

Request body CpaTeamInvitationsRevokeInvitationInputrequired

FieldTypeRequiredDescription
invitation_idstringrequired—

Responses

  • 200Successful invocationCpaTeamInvitationsRevokeInvitationOutput
    FieldTypeRequiredDescription
    idstringrequired—
    statusstringrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/cpa-team-invitations.resendInvitationRe-send a pending firm invitation's e-mail. Mints a NEW single-use accept token and invalidates the previous one, so the earlier link stops working. The accept link is never returned to the caller (ruling 97(a)); the response reports whether the e-mail was sent. Expired, accepted and revoked invitations cannot be resent. Only firm_admin/system_admin may call.

Re-send a pending firm invitation's e-mail. Mints a NEW single-use accept token and invalidates the previous one, so the earlier link stops working. The accept link is never returned to the caller (ruling 97(a)); the response reports whether the e-mail was sent. Expired, accepted and revoked invitations cannot be resent. Only firm_admin/system_admin may call.

Bearer scopesadmin.write

Request body CpaTeamInvitationsResendInvitationInputrequired

FieldTypeRequiredDescription
invitation_idstringrequired—

Responses

  • 200Successful invocationCpaTeamInvitationsResendInvitationOutput
    FieldTypeRequiredDescription
    idstringrequired—
    statusstringrequired—
    expires_atstringrequired—
    email_sentbooleanrequired—
    email_failure_reasonenum: "suppressed_non_prod" | "rate_limited" | "rejected" | "unavailable" | …optional—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/cpa-team-invitations.acceptInvitationAccept a firm team invitation. Verifies token, expiry, self-acceptance and email-mismatch guards. Grants user_profiles.role + a portal_roles row on success. The caller MUST be signed in as the invited email.

Accept a firm team invitation. Verifies token, expiry, self-acceptance and email-mismatch guards. Grants user_profiles.role + a portal_roles row on success. The caller MUST be signed in as the invited email.

Bearer scopesadmin.write

Request body CpaTeamInvitationsAcceptInvitationInputrequired

FieldTypeRequiredDescription
tokenstringrequired—

Responses

  • 200Successful invocationCpaTeamInvitationsAcceptInvitationOutput
    FieldTypeRequiredDescription
    invitation_idstringrequired—
    invited_rolestringrequired—
    current_rolestringrequired—
    role_grantedbooleanrequired—
    existing_role_preservedbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/cpa-team-invitations.getInvitationByTokenPublic lookup by plaintext token (hashes on read; never returns hash or plaintext). Used by /accept-cpa-invite/[token] for render.

Public lookup by plaintext token (hashes on read; never returns hash or plaintext). Used by /accept-cpa-invite/[token] for render.

Bearer scopesadmin.read

Request body CpaTeamInvitationsGetInvitationByTokenInputrequired

FieldTypeRequiredDescription
tokenstringrequired—

Responses

  • 200Successful invocationCpaTeamInvitationsGetInvitationByTokenOutput
    FieldTypeRequiredDescription
    invitationobjectrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

l2:credential-vault15

POST/v1/credential-vault.createCredentialCreate a tax account credential. Plaintext password / mfaSecret fields are written to Supabase Vault (vault.secrets) and the returned secret IDs are stored on tax_account_credentials. APPROVAL_REQUIRED — agents must not invoke this without HITL.

Create a tax account credential. Plaintext password / mfaSecret fields are written to Supabase Vault (vault.secrets) and the returned secret IDs are stored on tax_account_credentials. APPROVAL_REQUIRED — agents must not invoke this without HITL.

Bearer scopesadmin.write

Request body CredentialVaultCreateCredentialInputrequired

FieldTypeRequiredDescription
employerEntityIdstringrequired—
agencyIdstringrequired—
accountNumberstringoptional—
loginUsernamestringoptional—
passwordstringoptional—
mfaSecretstringoptional—
backupCodesstringoptional—
notesstringoptional—
otpChannelenum: "none" | "totp" | "email" | "sms"optional—

Responses

  • 200Successful invocationCredentialVaultCreateCredentialOutput
    FieldTypeRequiredDescription
    idstringrequired—
    tenant_idstringrequired—
    employer_entity_idstringrequired—
    agency_idstringrequired—
    account_numberstringrequired—
    login_usernamestringrequired—
    has_passwordbooleanrequired—
    has_mfa_secretbooleanrequired—
    has_backup_codesbooleanrequired—
    last_login_atstringrequired—
    last_password_change_atstringrequired—
    refresh_required_bystringrequired—
    statusenum: "active" | "revoked" | "refresh_required" | "locked"required—
    notesstringrequired—
    created_atstringrequired—
    updated_atstringrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/credential-vault.getCredentialFetch a credential by id, including plaintext password and mfaSecret decrypted from Supabase Vault. APPROVAL_REQUIRED. The returned plaintext fields are named `password` / `mfaSecret` (camelCase, matching framework SENSITIVE_KEYS) so the audit row's outputs JSONB shows them as [redacted] while the live caller receives the actual plaintext (R13 mitigation).

Fetch a credential by id, including plaintext password and mfaSecret decrypted from Supabase Vault. APPROVAL_REQUIRED. The returned plaintext fields are named `password` / `mfaSecret` (camelCase, matching framework SENSITIVE_KEYS) so the audit row's outputs JSONB shows them as [redacted] while the live caller receives the actual plaintext (R13 mitigation).

Bearer scopesadmin.write

Request body CredentialVaultGetCredentialInputrequired

FieldTypeRequiredDescription
credentialIdstringrequired—
purposestringrequired—

Responses

  • 200Successful invocationCredentialVaultGetCredentialOutput
    FieldTypeRequiredDescription
    idstringrequired—
    tenant_idstringrequired—
    employer_entity_idstringrequired—
    agency_idstringrequired—
    account_numberstringrequired—
    login_usernamestringrequired—
    has_passwordbooleanrequired—
    has_mfa_secretbooleanrequired—
    has_backup_codesbooleanrequired—
    last_login_atstringrequired—
    last_password_change_atstringrequired—
    refresh_required_bystringrequired—
    statusenum: "active" | "revoked" | "refresh_required" | "locked"required—
    notesstringrequired—
    created_atstringrequired—
    updated_atstringrequired—
    passwordstringrequired—
    mfaSecretstringrequired—
    backupCodesstringrequired—
    health_check_auto_flipbooleanoptional—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/credential-vault.getCredentialByEntityResolve and fetch a credential by (tenantId, entityId, agencyId) lookup, returning plaintext password and mfaSecret. The 3-tuple is index-served by tac_unique_active_per_agency. Throws if no active credential matches. APPROVAL_REQUIRED. Plaintext fields are camelCase-named (`password`, `mfaSecret`) so the audit row's outputs JSONB shows them as [redacted] (R13 mitigation).

Resolve and fetch a credential by (tenantId, entityId, agencyId) lookup, returning plaintext password and mfaSecret. The 3-tuple is index-served by tac_unique_active_per_agency. Throws if no active credential matches. APPROVAL_REQUIRED. Plaintext fields are camelCase-named (`password`, `mfaSecret`) so the audit row's outputs JSONB shows them as [redacted] (R13 mitigation).

Bearer scopesadmin.write

Request body CredentialVaultGetCredentialByEntityInputrequired

FieldTypeRequiredDescription
entityIdstringrequired—
agencyIdstringrequired—
purposestringrequired—

Responses

  • 200Successful invocationCredentialVaultGetCredentialByEntityOutput
    FieldTypeRequiredDescription
    idstringrequired—
    tenant_idstringrequired—
    employer_entity_idstringrequired—
    agency_idstringrequired—
    account_numberstringrequired—
    login_usernamestringrequired—
    has_passwordbooleanrequired—
    has_mfa_secretbooleanrequired—
    has_backup_codesbooleanrequired—
    last_login_atstringrequired—
    last_password_change_atstringrequired—
    refresh_required_bystringrequired—
    statusenum: "active" | "revoked" | "refresh_required" | "locked"required—
    notesstringrequired—
    created_atstringrequired—
    updated_atstringrequired—
    passwordstringrequired—
    mfaSecretstringrequired—
    backupCodesstringrequired—
    health_check_auto_flipbooleanoptional—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/credential-vault.listCredentialsList credentials in the caller's tenant. Returns metadata only (no plaintext, no vault secret_ids). AUTONOMOUS — agents may call freely. Pass `entityId` to scope to a single employer; pass `statuses` to widen beyond the default `active` filter.

List credentials in the caller's tenant. Returns metadata only (no plaintext, no vault secret_ids). AUTONOMOUS — agents may call freely. Pass `entityId` to scope to a single employer; pass `statuses` to widen beyond the default `active` filter.

Bearer scopesadmin.read

Request body CredentialVaultListCredentialsInputrequired

FieldTypeRequiredDescription
entityIdstringoptional—
statusesarray<enum: "active" | "revoked" | "refresh_required" | "locked">optional—

Responses

  • 200Successful invocationCredentialVaultListCredentialsOutput
    FieldTypeRequiredDescription
    credentialsarray<object>required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/credential-vault.listCredentialsExpiringSoonList active credentials whose refresh_required_by falls within the next `daysAhead` days. Used by the auto-refresh job (plan §6 D4). AUTONOMOUS.

List active credentials whose refresh_required_by falls within the next `daysAhead` days. Used by the auto-refresh job (plan §6 D4). AUTONOMOUS.

Bearer scopesadmin.read

Request body CredentialVaultListCredentialsExpiringSoonInputrequired

FieldTypeRequiredDescription
daysAheadintegerrequired—

Responses

  • 200Successful invocationCredentialVaultListCredentialsExpiringSoonOutput
    FieldTypeRequiredDescription
    credentialsarray<object>required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/credential-vault.updateCredentialUpdate credential metadata and/or rotate plaintext password / mfaSecret. APPROVAL_REQUIRED. Plaintext rotations go through vault.update_secret; the secret_id on the credential row is preserved so audit trails remain stable.

Update credential metadata and/or rotate plaintext password / mfaSecret. APPROVAL_REQUIRED. Plaintext rotations go through vault.update_secret; the secret_id on the credential row is preserved so audit trails remain stable.

Bearer scopesadmin.write

Request body CredentialVaultUpdateCredentialInputrequired

FieldTypeRequiredDescription
credentialIdstringrequired—
patchobjectrequired—

Responses

  • 200Successful invocationCredentialVaultUpdateCredentialOutput
    FieldTypeRequiredDescription
    idstringrequired—
    tenant_idstringrequired—
    employer_entity_idstringrequired—
    agency_idstringrequired—
    account_numberstringrequired—
    login_usernamestringrequired—
    has_passwordbooleanrequired—
    has_mfa_secretbooleanrequired—
    has_backup_codesbooleanrequired—
    last_login_atstringrequired—
    last_password_change_atstringrequired—
    refresh_required_bystringrequired—
    statusenum: "active" | "revoked" | "refresh_required" | "locked"required—
    notesstringrequired—
    created_atstringrequired—
    updated_atstringrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/credential-vault.revokeCredentialSet status='revoked' on a credential AND overwrite the plaintext in Supabase Vault with a tombstone marker. The vault secret_ids stay on the credential row so the audit trail can resolve which credential a historical event referenced, but the actual plaintext is unrecoverable after revoke. Aligns with GDPR Art. 17 / CCPA right-to-erasure. APPROVAL_REQUIRED. PR #466 review Bug 4.

Set status='revoked' on a credential AND overwrite the plaintext in Supabase Vault with a tombstone marker. The vault secret_ids stay on the credential row so the audit trail can resolve which credential a historical event referenced, but the actual plaintext is unrecoverable after revoke. Aligns with GDPR Art. 17 / CCPA right-to-erasure. APPROVAL_REQUIRED. PR #466 review Bug 4.

Bearer scopesadmin.write

Request body CredentialVaultRevokeCredentialInputrequired

FieldTypeRequiredDescription
credentialIdstringrequired—

Responses

  • 200Successful invocationCredentialVaultRevokeCredentialOutput
    FieldTypeRequiredDescription
    idstringrequired—
    tenant_idstringrequired—
    employer_entity_idstringrequired—
    agency_idstringrequired—
    account_numberstringrequired—
    login_usernamestringrequired—
    has_passwordbooleanrequired—
    has_mfa_secretbooleanrequired—
    has_backup_codesbooleanrequired—
    last_login_atstringrequired—
    last_password_change_atstringrequired—
    refresh_required_bystringrequired—
    statusenum: "active" | "revoked" | "refresh_required" | "locked"required—
    notesstringrequired—
    created_atstringrequired—
    updated_atstringrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/credential-vault.markRefreshRequiredFlip a credential to status='refresh_required' and set refresh_required_by=now(). Lighter-risk than other write methods because no plaintext is touched — just state. NOTIFY.

Flip a credential to status='refresh_required' and set refresh_required_by=now(). Lighter-risk than other write methods because no plaintext is touched — just state. NOTIFY.

Bearer scopesadmin.write

Request body CredentialVaultMarkRefreshRequiredInputrequired

FieldTypeRequiredDescription
credentialIdstringrequired—

Responses

  • 200Successful invocationCredentialVaultMarkRefreshRequiredOutput
    FieldTypeRequiredDescription
    idstringrequired—
    tenant_idstringrequired—
    employer_entity_idstringrequired—
    agency_idstringrequired—
    account_numberstringrequired—
    login_usernamestringrequired—
    has_passwordbooleanrequired—
    has_mfa_secretbooleanrequired—
    has_backup_codesbooleanrequired—
    last_login_atstringrequired—
    last_password_change_atstringrequired—
    refresh_required_bystringrequired—
    statusenum: "active" | "revoked" | "refresh_required" | "locked"required—
    notesstringrequired—
    created_atstringrequired—
    updated_atstringrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/credential-vault.getCurrentOtpReturn the current RFC-6238 TOTP code for a credential's stored authenticator (MFA) seed, plus seconds remaining in the period. Returns { code: null, secondsRemaining: 0 } when the credential has no stored MFA seed. APPROVAL_REQUIRED — it reads decrypted credential auth material from the vault.

Return the current RFC-6238 TOTP code for a credential's stored authenticator (MFA) seed, plus seconds remaining in the period. Returns { code: null, secondsRemaining: 0 } when the credential has no stored MFA seed. APPROVAL_REQUIRED — it reads decrypted credential auth material from the vault.

Bearer scopesadmin.write

Request body CredentialVaultGetCurrentOtpInputrequired

FieldTypeRequiredDescription
credentialIdstringrequired—

Responses

  • 200Successful invocationCredentialVaultGetCurrentOtpOutput
    FieldTypeRequiredDescription
    codestringrequired—
    secondsRemainingintegerrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/credential-vault.getHealthCheckPolicyRead the per-agency health-check status-mutation policy for the caller's tenant. AUTONOMOUS — read-only config. Returns statusMutationEnabled=false (record-only, the safe default) when no policy row exists for the agency.

Read the per-agency health-check status-mutation policy for the caller's tenant. AUTONOMOUS — read-only config. Returns statusMutationEnabled=false (record-only, the safe default) when no policy row exists for the agency.

Bearer scopesadmin.read

Request body CredentialVaultGetHealthCheckPolicyInputrequired

FieldTypeRequiredDescription
agencyIdstringrequired—

Responses

  • 200Successful invocationCredentialVaultGetHealthCheckPolicyOutput
    FieldTypeRequiredDescription
    agencyIdstringrequired—
    statusMutationEnabledbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/credential-vault.setHealthCheckPolicyEnable/disable health-check status auto-flip for one agency in the caller's tenant. NOTIFY — a config write that should be human-visible. Enable only for portals whose invalidSignal coverage is proven (un-hardened portals stay record-only). Upserts the per-(tenant, agency) policy row.

Enable/disable health-check status auto-flip for one agency in the caller's tenant. NOTIFY — a config write that should be human-visible. Enable only for portals whose invalidSignal coverage is proven (un-hardened portals stay record-only). Upserts the per-(tenant, agency) policy row.

Bearer scopesadmin.write

Request body CredentialVaultSetHealthCheckPolicyInputrequired

FieldTypeRequiredDescription
agencyIdstringrequired—
statusMutationEnabledbooleanrequired—

Responses

  • 200Successful invocationCredentialVaultSetHealthCheckPolicyOutput
    FieldTypeRequiredDescription
    agencyIdstringrequired—
    statusMutationEnabledbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/credential-vault.useUse a stored credential BY REFERENCE to drive an authenticated portal login: dispatches the portal-execution job and returns the job id only. The plaintext secret is decrypted exclusively inside the portal worker and never appears in this method's output. Use when a filing or protest flow needs an authenticated portal session.

Use a stored credential BY REFERENCE to drive an authenticated portal login: dispatches the portal-execution job and returns the job id only. The plaintext secret is decrypted exclusively inside the portal worker and never appears in this method's output. Use when a filing or protest flow needs an authenticated portal session.

Bearer scopesadmin.read

Request body CredentialVaultUseInputrequired

FieldTypeRequiredDescription
credentialIdstringrequired—
purposestringrequired—
dedupeTokenstringrequired—

Responses

  • 200Successful invocationCredentialVaultUseOutput
    FieldTypeRequiredDescription
    jobIdstringrequired—
    deduplicatedbooleanrequired—
    credentialIdstringrequired—
    agencyIdstringrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/credential-vault.rotateRotate a stored portal password BY REFERENCE: dispatches the transactional rotation job (login, change-password, verify-login in one governed session) and returns the job id only. The new secret is generated inside the worker and never appears anywhere outside the vault and the portal DOM. Use when a credential is suspected compromised or a portal's password-age policy requires a change.

Rotate a stored portal password BY REFERENCE: dispatches the transactional rotation job (login, change-password, verify-login in one governed session) and returns the job id only. The new secret is generated inside the worker and never appears anywhere outside the vault and the portal DOM. Use when a credential is suspected compromised or a portal's password-age policy requires a change.

Bearer scopesadmin.write

Request body CredentialVaultRotateInputrequired

FieldTypeRequiredDescription
credentialIdstringrequired—
reasonstringoptional—

Responses

  • 200Successful invocationCredentialVaultRotateOutput
    FieldTypeRequiredDescription
    jobIdstringrequired—
    deduplicatedbooleanrequired—
    credentialIdstringrequired—
    agencyIdstringrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/credential-vault.healthReport a credential's login health from the latest lockout-aware scheduled probe (the hourly health cron is the only thing that actually logs in — this read never touches the portal, so it can never contribute to a lockout). Use to surface which credentials need attention before relying on one for a filing. 'uncertain' means no probe evidence — never treat it as healthy.

Report a credential's login health from the latest lockout-aware scheduled probe (the hourly health cron is the only thing that actually logs in — this read never touches the portal, so it can never contribute to a lockout). Use to surface which credentials need attention before relying on one for a filing. 'uncertain' means no probe evidence — never treat it as healthy.

Bearer scopesadmin.read

Request body CredentialVaultHealthInputrequired

FieldTypeRequiredDescription
credentialIdstringrequired—

Responses

  • 200Successful invocationCredentialVaultHealthOutput
    FieldTypeRequiredDescription
    credentialIdstringrequired—
    statusstringrequired—
    healthenum: "pass" | "fail" | "uncertain"required—
    lastHealthCheckAtstringrequired—
    lastHealthCheckOutcomestringrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/credential-vault.revealReveal a credential's plaintext to an authorized human screen. Requires a typed reason and passes the account's reveal-visibility policy for VAULT_V1 tenants; the hash-chained evidence row is written BEFORE decryption and the reveal is refused if it cannot be. APPROVAL_REQUIRED — UI-only, never exposed via MCP or CLI.

Reveal a credential's plaintext to an authorized human screen. Requires a typed reason and passes the account's reveal-visibility policy for VAULT_V1 tenants; the hash-chained evidence row is written BEFORE decryption and the reveal is refused if it cannot be. APPROVAL_REQUIRED — UI-only, never exposed via MCP or CLI.

Bearer scopesadmin.write

Request body CredentialVaultRevealInputrequired

FieldTypeRequiredDescription
credentialIdstringrequired—
reasonstringoptional—
ipAddressstringoptional—
userAgentstringoptional—

Responses

  • 200Successful invocationCredentialVaultRevealOutput
    FieldTypeRequiredDescription
    idstringrequired—
    tenant_idstringrequired—
    employer_entity_idstringrequired—
    agency_idstringrequired—
    account_numberstringrequired—
    login_usernamestringrequired—
    has_passwordbooleanrequired—
    has_mfa_secretbooleanrequired—
    has_backup_codesbooleanrequired—
    last_login_atstringrequired—
    last_password_change_atstringrequired—
    refresh_required_bystringrequired—
    statusenum: "active" | "revoked" | "refresh_required" | "locked"required—
    notesstringrequired—
    created_atstringrequired—
    updated_atstringrequired—
    passwordstringrequired—
    mfaSecretstringrequired—
    backupCodesstringrequired—
    health_check_auto_flipbooleanoptional—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

l2:documents2

POST/v1/documents.listSigningRequestsList a notice's LIVE e-signature requests, newest first: pending requests that have not expired (on a live document) and signed ones. Returns ids, status and dates only — never the signer's name, email or the signing link. Use an id here with documents.cancelSigningRequest.

List a notice's LIVE e-signature requests, newest first: pending requests that have not expired (on a live document) and signed ones. Returns ids, status and dates only — never the signer's name, email or the signing link. Use an id here with documents.cancelSigningRequest.

Bearer scopesnotices.read

Request body DocumentsListSigningRequestsInputrequired

FieldTypeRequiredDescription
noticeIdstringrequired—

Responses

  • 200Successful invocationDocumentsListSigningRequestsOutput
    FieldTypeRequiredDescription
    requestsarray<object>required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/documents.cancelSigningRequestWithdraw one PENDING, unexpired e-signature request in the caller's firm. The signing link stops working at once (it then answers exactly like an expired or unknown link), and the notice's signature-requested fact is re-derived — cleared when no other request holds it, so the draft can be regenerated or revised again. A signed, expired or already cancelled request is refused (409, with the reason). Recoverable by sending a new request.

Withdraw one PENDING, unexpired e-signature request in the caller's firm. The signing link stops working at once (it then answers exactly like an expired or unknown link), and the notice's signature-requested fact is re-derived — cleared when no other request holds it, so the draft can be regenerated or revised again. A signed, expired or already cancelled request is refused (409, with the reason). Recoverable by sending a new request.

Bearer scopesnotices.write

Request body DocumentsCancelSigningRequestInputrequired

FieldTypeRequiredDescription
requestIdstringrequired—

Responses

  • 200Successful invocationDocumentsCancelSigningRequestOutput
    FieldTypeRequiredDescription
    okenum: truerequired—
    requestIdstringrequired—
    statusenum: "cancelled"required—
    noticeIdstringrequired—
    signatureRequestedAtstringrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

l2:edelivery10

POST/v1/edelivery.listEdeliveryRulesBrowse the e-delivery catalog (state_edelivery_rules, global config — NO tenant filter). Optional filters: stateCode, requirementType, verificationStatus. Returns matching rows plus HONEST per-status counts ({unverified, human_reviewed, verified}) — never a 'done/complete' claim. AUTONOMOUS, read-only.

Browse the e-delivery catalog (state_edelivery_rules, global config — NO tenant filter). Optional filters: stateCode, requirementType, verificationStatus. Returns matching rows plus HONEST per-status counts ({unverified, human_reviewed, verified}) — never a 'done/complete' claim. AUTONOMOUS, read-only.

Bearer scopesnotices.read

Request body EdeliveryListEdeliveryRulesInputrequired

FieldTypeRequiredDescription
stateCodestringoptional—
requirementTypeenum: "sui_registration" | "withholding_registration" | "workers_comp" | "pfml" | …optional—
verificationStatusenum: "unverified" | "human_reviewed" | "verified"optional—

Responses

  • 200Successful invocationEdeliveryListEdeliveryRulesOutput
    FieldTypeRequiredDescription
    rowsarray<object>required—
    countsobjectrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/edelivery.getEdeliveryRuleRead one e-delivery catalog cell (state_edelivery_rules, global config — NO tenant filter): the rule row plus its typed ordered enrollment steps. Cell absent → honest null + [] (never invented data). AUTONOMOUS, read-only.

Read one e-delivery catalog cell (state_edelivery_rules, global config — NO tenant filter): the rule row plus its typed ordered enrollment steps. Cell absent → honest null + [] (never invented data). AUTONOMOUS, read-only.

Bearer scopesnotices.read

Request body EdeliveryGetEdeliveryRuleInputrequired

FieldTypeRequiredDescription
stateCodestringrequired—
requirementTypeenum: "sui_registration" | "withholding_registration" | "workers_comp" | "pfml" | …required—

Responses

  • 200Successful invocationEdeliveryGetEdeliveryRuleOutput
    FieldTypeRequiredDescription
    ruleobjectrequired—
    stepsarray<object>required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/edelivery.proposeEdeliveryRuleResearch one (state, requirement) cell's PAPERLESS/e-delivery facts via the Claude + web_search agent and upsert an UNVERIFIED proposal onto state_edelivery_rules BY PK (global config — NO tenant filter). The write ALWAYS leaves verification_status='unverified' and NEVER sets verified/human_reviewed (agents may never verify); REFUSES to write at all unless the proposal carries complete provenance (source_url + retrieved_at + a verbatim quote + an evidence tier); idempotent-by-PK. MUST NOT downgrade a human-verified cell: returns the existing row + the fresh proposal so a human can compare. PII-free. NOTIFY: agents may call; a human reviews before promotion.

Research one (state, requirement) cell's PAPERLESS/e-delivery facts via the Claude + web_search agent and upsert an UNVERIFIED proposal onto state_edelivery_rules BY PK (global config — NO tenant filter). The write ALWAYS leaves verification_status='unverified' and NEVER sets verified/human_reviewed (agents may never verify); REFUSES to write at all unless the proposal carries complete provenance (source_url + retrieved_at + a verbatim quote + an evidence tier); idempotent-by-PK. MUST NOT downgrade a human-verified cell: returns the existing row + the fresh proposal so a human can compare. PII-free. NOTIFY: agents may call; a human reviews before promotion.

Bearer scopesnotices.write

Request body EdeliveryProposeEdeliveryRuleInputrequired

FieldTypeRequiredDescription
stateCodestringrequired—
requirementTypeenum: "sui_registration" | "withholding_registration" | "workers_comp" | "pfml" | …required—

Responses

  • 200Successful invocationEdeliveryProposeEdeliveryRuleOutput
    FieldTypeRequiredDescription
    rowobjectrequired—
    proposalobjectrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/edelivery.promoteEdeliveryRuleHuman gate: promote an e-delivery cell ONE RUNG along the verification ladder (unverified → human_reviewed → verified) — a transition that skips a rung, or runs backwards, is refused. APPROVAL_REQUIRED, UI-only — excluded from MCP/CLI, and the handler REJECTS any non-human principal before any write. Verifying requires provenance (source_url + retrieved_at). Stamps reviewed_by / last_reviewed_at, and on verify also verified_at / verified_by.

Human gate: promote an e-delivery cell ONE RUNG along the verification ladder (unverified → human_reviewed → verified) — a transition that skips a rung, or runs backwards, is refused. APPROVAL_REQUIRED, UI-only — excluded from MCP/CLI, and the handler REJECTS any non-human principal before any write. Verifying requires provenance (source_url + retrieved_at). Stamps reviewed_by / last_reviewed_at, and on verify also verified_at / verified_by.

Bearer scopesnotices.write

Request body EdeliveryPromoteEdeliveryRuleInputrequired

FieldTypeRequiredDescription
stateCodestringrequired—
requirementTypeenum: "sui_registration" | "withholding_registration" | "workers_comp" | "pfml" | …required—
toStatusenum: "human_reviewed" | "verified"required—
reasonstringoptional—

Responses

  • 200Successful invocationEdeliveryPromoteEdeliveryRuleOutput
    FieldTypeRequiredDescription
    stateCodestringrequired—
    requirementTypeenum: "sui_registration" | "withholding_registration" | "workers_comp" | "pfml" | …required—
    fromStatusenum: "unverified" | "human_reviewed" | "verified"required—
    toStatusenum: "unverified" | "human_reviewed" | "verified"required—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden. `SYSTEM_ADMIN_REQUIRED`: the caller is not a Kreto system administrator on an interactive session — the global catalog every firm pins (handbook modules, state filing / foreign-qualification / annual-report / e-delivery rules) is staff-only, exactly as on the REST admin door. Checked before any catalog read, so the answer is the same whether or not the row exists. `STEP_UP_REQUIRED`: the dispatcher's APPROVAL_REQUIRED gate (an API key, or no fresh re-authentication). Nothing is written.one of (L2SystemAdminRequiredRefusal · L2ErrorEnvelope)
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/edelivery.listEdeliveryEnrollmentsList the per-entity e-delivery enrollment checklist (tenant+entity scoped), idempotently seeding missing (state, requirement) items from the employer's footprint (employer_state_registrations + states_operated; v1 scope = SUI + withholding). Each item carries its catalog cell's facts FAIL-CLOSED: enrollment steps are attached ONLY when the cell is verified — unverified facts never render as authoritative. AUTONOMOUS, read-mostly.

List the per-entity e-delivery enrollment checklist (tenant+entity scoped), idempotently seeding missing (state, requirement) items from the employer's footprint (employer_state_registrations + states_operated; v1 scope = SUI + withholding). Each item carries its catalog cell's facts FAIL-CLOSED: enrollment steps are attached ONLY when the cell is verified — unverified facts never render as authoritative. AUTONOMOUS, read-mostly.

Bearer scopesnotices.read

Request body EdeliveryListEdeliveryEnrollmentsInputrequired

FieldTypeRequiredDescription
entityIdstringrequired—

Responses

  • 200Successful invocationEdeliveryListEdeliveryEnrollmentsOutput
    FieldTypeRequiredDescription
    entityIdstringrequired—
    intakeEmailstringrequired—
    forwardingOnbooleanrequired—
    itemsarray<object>required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/edelivery.listEdeliveryEnrollmentsReadonlyRead an entity's e-delivery enrollment checklist WITHOUT seeding (the employer-portal spectator view). Returns the persisted items the CPA/Kreto-ops maintains, each with its catalog facts FAIL-CLOSED: authoritative steps only on verified cells, unverified steps under `draftSteps` for the 'unverified draft' banner. AUTONOMOUS, read-only.

Read an entity's e-delivery enrollment checklist WITHOUT seeding (the employer-portal spectator view). Returns the persisted items the CPA/Kreto-ops maintains, each with its catalog facts FAIL-CLOSED: authoritative steps only on verified cells, unverified steps under `draftSteps` for the 'unverified draft' banner. AUTONOMOUS, read-only.

Bearer scopesnotices.read

Request body EdeliveryListEdeliveryEnrollmentsReadonlyInputrequired

FieldTypeRequiredDescription
entityIdstringrequired—

Responses

  • 200Successful invocationEdeliveryListEdeliveryEnrollmentsReadonlyOutput
    FieldTypeRequiredDescription
    entityIdstringrequired—
    intakeEmailstringrequired—
    forwardingOnbooleanrequired—
    itemsarray<object>required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/edelivery.setEnrollmentStatusSet one e-delivery checklist item's status (not_started | enrolled) with human attribution (completed_by = the session user on enroll). Tenant+entity scoped. Appends a hash-chained audit_events row — enrollment status is a compliance-posture claim. NOTIFY. The customer performed the enrollment in the agency portal; Kreto only records it.

Set one e-delivery checklist item's status (not_started | enrolled) with human attribution (completed_by = the session user on enroll). Tenant+entity scoped. Appends a hash-chained audit_events row — enrollment status is a compliance-posture claim. NOTIFY. The customer performed the enrollment in the agency portal; Kreto only records it.

Bearer scopesnotices.write

Request body EdeliverySetEnrollmentStatusInputrequired

FieldTypeRequiredDescription
entityIdstringrequired—
itemIdstringoptional—
stateCodestringoptional—
requirementTypeenum: "sui_registration" | "withholding_registration" | "workers_comp" | "pfml" | …optional—
statusenum: "not_started" | "enrolled"required—

Responses

  • 200Successful invocationEdeliverySetEnrollmentStatusOutput
    FieldTypeRequiredDescription
    itemIdstringrequired—
    stateCodestringrequired—
    requirementTypeenum: "sui_registration" | "withholding_registration" | "workers_comp" | "pfml" | …required—
    statusenum: "not_started" | "enrolled"required—
    completedAtstringrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/edelivery.listFetchQueueList the tenant's OPEN Portal-pickup tasks: inbound agency notification emails ('you have new correspondence — log in') that carried no document (inbound_emails processing_status='notification_only', unresolved). Optional entityId filter. The row IS the task — a notice is only ever created when the fetched document arrives. AUTONOMOUS, read-only.

List the tenant's OPEN Portal-pickup tasks: inbound agency notification emails ('you have new correspondence — log in') that carried no document (inbound_emails processing_status='notification_only', unresolved). Optional entityId filter. The row IS the task — a notice is only ever created when the fetched document arrives. AUTONOMOUS, read-only.

Bearer scopesnotices.read

Request body EdeliveryListFetchQueueInputrequired

FieldTypeRequiredDescription
entityIdstringoptional—

Responses

  • 200Successful invocationEdeliveryListFetchQueueOutput
    FieldTypeRequiredDescription
    tasksarray<object>required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/edelivery.resolveFetchTaskResolve one Portal-pickup task (inbound_emails processing_status='notification_only'): stamps resolved_at/resolved_by and optionally back-links the notice created when the customer fetched the document (the noticeId must belong to the caller's tenant). Tenant-scoped. NOTIFY.

Resolve one Portal-pickup task (inbound_emails processing_status='notification_only'): stamps resolved_at/resolved_by and optionally back-links the notice created when the customer fetched the document (the noticeId must belong to the caller's tenant). Tenant-scoped. NOTIFY.

Bearer scopesnotices.write

Request body EdeliveryResolveFetchTaskInputrequired

FieldTypeRequiredDescription
taskIdstringrequired—
noticeIdstringoptional—

Responses

  • 200Successful invocationEdeliveryResolveFetchTaskOutput
    FieldTypeRequiredDescription
    taskIdstringrequired—
    resolvedAtstringrequired—
    noticeIdstringrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/edelivery.enrollViaKretoOpsKreto ops performs an agency-portal paperless enrollment ON THE CUSTOMER'S BEHALF under an explicit consent artifact + stored portal credentials. APPROVAL_REQUIRED, UI-only — excluded from MCP/CLI, and the handler REJECTS any non-human principal before any write. Records an IMMUTABLE per-enrollment consent (who consented, when, IP/UA, scope) and flips the checklist item to enrolled, attributed to ops. HONESTY: customer-facing copy stays 'the customer performs' until this flow is enabled + verified.

Kreto ops performs an agency-portal paperless enrollment ON THE CUSTOMER'S BEHALF under an explicit consent artifact + stored portal credentials. APPROVAL_REQUIRED, UI-only — excluded from MCP/CLI, and the handler REJECTS any non-human principal before any write. Records an IMMUTABLE per-enrollment consent (who consented, when, IP/UA, scope) and flips the checklist item to enrolled, attributed to ops. HONESTY: customer-facing copy stays 'the customer performs' until this flow is enabled + verified.

Bearer scopesnotices.write

Request body EdeliveryEnrollViaKretoOpsInputrequired

FieldTypeRequiredDescription
entityIdstringrequired—
stateCodestringrequired—
requirementTypeenum: "sui_registration" | "withholding_registration" | "workers_comp" | "pfml" | …required—
consentedByUserIdstringrequired—
consentIpstringoptional—
consentUserAgentstringoptional—

Responses

  • 200Successful invocationEdeliveryEnrollViaKretoOpsOutput
    FieldTypeRequiredDescription
    consentIdstringrequired—
    itemIdstringrequired—
    stateCodestringrequired—
    requirementTypeenum: "sui_registration" | "withholding_registration" | "workers_comp" | "pfml" | …required—
    statusenum: "not_started" | "enrolled"required—
    enrolledViaOpsbooleanrequired—
    reusedExistingConsentbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

l2:employee-roster6

POST/v1/employee-roster.getActiveRosterList employees on the active roster for the caller's tenant. Defaults to employment_status='active'; pass `statuses` to widen (e.g. ['active','terminated','leave'] for the I-9 list). Pass `entityIds` to narrow to a subset of entities the caller has access to. If `asOf` is supplied, joins payroll_pay_periods to detect employees who had a paycheck within 60 days of asOf and weren't terminated before it.

List employees on the active roster for the caller's tenant. Defaults to employment_status='active'; pass `statuses` to widen (e.g. ['active','terminated','leave'] for the I-9 list). Pass `entityIds` to narrow to a subset of entities the caller has access to. If `asOf` is supplied, joins payroll_pay_periods to detect employees who had a paycheck within 60 days of asOf and weren't terminated before it.

Bearer scopespayroll.read

Request body EmployeeRosterGetActiveRosterInputrequired

FieldTypeRequiredDescription
asOfstring · date-timeoptional—
entityIdsarray<string>optional—
statusesarray<enum: "active" | "terminated" | "leave">optional—

Responses

  • 200Successful invocationEmployeeRosterGetActiveRosterOutput
    FieldTypeRequiredDescription
    employeesarray<object>required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `payroll.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/employee-roster.getEmployeeFetch a single employee by id, scoped to the caller's tenant. Returns null if no row matches. Per README §6 Q5, no asOf is supported on this method; per-employee historical context comes from payroll_pay_periods joins, not effective-dating columns.

Fetch a single employee by id, scoped to the caller's tenant. Returns null if no row matches. Per README §6 Q5, no asOf is supported on this method; per-employee historical context comes from payroll_pay_periods joins, not effective-dating columns.

Bearer scopespayroll.read

Request body EmployeeRosterGetEmployeeInputrequired

FieldTypeRequiredDescription
employeeIdstringrequired—

Responses

  • 200Successful invocationEmployeeRosterGetEmployeeOutput
    FieldTypeRequiredDescription
    idstringrequired—
    entity_idstringrequired—
    ssn_last4stringrequired—
    full_namestringrequired—
    emailstringrequired—
    departmentstringrequired—
    hire_datestringrequired—
    termination_datestringrequired—
    employment_statusenum: "active" | "terminated" | "leave"required—
    work_statestringrequired—
    home_statestringrequired—
    salary_typeenum: "hourly" | "salary" | "unknown"required—
    hourly_ratenumberrequired—
    annual_salarynumberrequired—
    i9_section1_completebooleanrequired—
    i9_section2_completebooleanrequired—
    i9_reverification_neededbooleanrequired—
    work_auth_typestringrequired—
    work_auth_expirystringrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `payroll.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/employee-roster.getEmployeesByStateList employees whose canonical work_state matches the supplied two-letter code (e.g. 'OH'). asOf is optional; without it, the active roster filtered by work_state is returned. With asOf, the asOf logic from getActiveRoster applies first, then the work_state filter narrows the result.

List employees whose canonical work_state matches the supplied two-letter code (e.g. 'OH'). asOf is optional; without it, the active roster filtered by work_state is returned. With asOf, the asOf logic from getActiveRoster applies first, then the work_state filter narrows the result.

Bearer scopespayroll.read

Request body EmployeeRosterGetEmployeesByStateInputrequired

FieldTypeRequiredDescription
statestringrequired—
asOfstring · date-timeoptional—

Responses

  • 200Successful invocationEmployeeRosterGetEmployeesByStateOutput
    FieldTypeRequiredDescription
    employeesarray<object>required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `payroll.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/employee-roster.countEmployeesCount active employees on the caller's tenant. Pass `entityId` to scope to a single employer (PEPM Source 3 pattern). Replaces the ad-hoc count query in src/lib/billing/pepm-quantity.ts — migrated in Part F (KRT-1159). asOf is supported via the same join-with-payroll_pay_periods logic getActiveRoster uses.

Count active employees on the caller's tenant. Pass `entityId` to scope to a single employer (PEPM Source 3 pattern). Replaces the ad-hoc count query in src/lib/billing/pepm-quantity.ts — migrated in Part F (KRT-1159). asOf is supported via the same join-with-payroll_pay_periods logic getActiveRoster uses.

Bearer scopespayroll.read

Request body EmployeeRosterCountEmployeesInputrequired

FieldTypeRequiredDescription
asOfstring · date-timeoptional—
entityIdstringoptional—

Responses

  • 200Successful invocationEmployeeRosterCountEmployeesOutput
    FieldTypeRequiredDescription
    countintegerrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `payroll.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/employee-roster.getStateChangesDetect work_state transitions across employees by walking payroll_pay_periods paycheck history since the supplied date. Returns one StateChangeEvent per detected transition (e.g. an employee moving MA→KS mid-quarter). The trigger source for the Phase 1+ state-registration agent.

Detect work_state transitions across employees by walking payroll_pay_periods paycheck history since the supplied date. Returns one StateChangeEvent per detected transition (e.g. an employee moving MA→KS mid-quarter). The trigger source for the Phase 1+ state-registration agent.

Bearer scopespayroll.read

Request body EmployeeRosterGetStateChangesInputrequired

FieldTypeRequiredDescription
sincestring · date-timerequired—

Responses

  • 200Successful invocationEmployeeRosterGetStateChangesOutput
    FieldTypeRequiredDescription
    changesarray<object>required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `payroll.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/employee-roster.getRosterWithSsnHashRoster rows for a single entity, with `ssn_hash` exposed in addition to the public EmployeeRosterRow fields. ELEVATED risk: ssn_hash is the cross-payroll-graph join key but is NOT part of the public PII contract. Use only from cross-reference / audit-defense consumers. Filters by employment_status='active' by default; with `asOf`, applies the same payroll_pay_periods liveness heuristic getActiveRoster uses. Excludes rows whose ssn_hash is NULL (those rows can't be joined against the payroll fact graph anyway).

Roster rows for a single entity, with `ssn_hash` exposed in addition to the public EmployeeRosterRow fields. ELEVATED risk: ssn_hash is the cross-payroll-graph join key but is NOT part of the public PII contract. Use only from cross-reference / audit-defense consumers. Filters by employment_status='active' by default; with `asOf`, applies the same payroll_pay_periods liveness heuristic getActiveRoster uses. Excludes rows whose ssn_hash is NULL (those rows can't be joined against the payroll fact graph anyway).

Bearer scopesadmin.read

Request body EmployeeRosterGetRosterWithSsnHashInputrequired

FieldTypeRequiredDescription
entityIdstringrequired—
asOfstring · date-timeoptional—

Responses

  • 200Successful invocationEmployeeRosterGetRosterWithSsnHashOutput
    FieldTypeRequiredDescription
    employeesarray<object>required—
  • 400Validation error (input schema rejected). risk_level=elevated.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

l2:employers2

POST/v1/employers.setEinStore an employer's FULL federal EIN: plaintext to Supabase Vault, a keyed HMAC to entities.ein_hash for per-firm duplicate refusal, and a server-derived ein_last4. Refuses (409-equivalent) when another live employer in the same tenant already holds that EIN. APPROVAL_REQUIRED and therefore UI-ONLY — deliberately absent from MCP and the CLI until step-up auth (KRT-1554) ships, because this is a PII-ingress write.

Store an employer's FULL federal EIN: plaintext to Supabase Vault, a keyed HMAC to entities.ein_hash for per-firm duplicate refusal, and a server-derived ein_last4. Refuses (409-equivalent) when another live employer in the same tenant already holds that EIN. APPROVAL_REQUIRED and therefore UI-ONLY — deliberately absent from MCP and the CLI until step-up auth (KRT-1554) ships, because this is a PII-ingress write.

Bearer scopesemployers.write

Request body EmployersSetEinInputrequired

FieldTypeRequiredDescription
entityIdstringrequired—
einstringrequired—

Responses

  • 200Successful invocationEmployersSetEinOutput
    FieldTypeRequiredDescription
    entityIdstringrequired—
    einLast4stringrequired—
    hasFullEinenum: truerequired—
    replacedbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `employers.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/employers.einStatusRead-only: does this employer have a FULL EIN on file, and what are its last four digits? Never returns the number, the hash, or the vault pointer — only a boolean and the same four digits already shown on the employer roster. This is the question the 8821 and state-registration readiness flows actually ask.

Read-only: does this employer have a FULL EIN on file, and what are its last four digits? Never returns the number, the hash, or the vault pointer — only a boolean and the same four digits already shown on the employer roster. This is the question the 8821 and state-registration readiness flows actually ask.

Bearer scopesemployers.read

Request body EmployersEinStatusInputrequired

FieldTypeRequiredDescription
entityIdstringrequired—

Responses

  • 200Successful invocationEmployersEinStatusOutput
    FieldTypeRequiredDescription
    entityIdstringrequired—
    hasFullEinbooleanrequired—
    einLast4stringrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `employers.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

l2:foreign-qualification13

POST/v1/foreign-qualification.listForeignQualRulesList the state foreign-qualification catalog (global config): per-(state, entity_kind) SOS certificate-of-authority rules — agency, form, state filing fee, good-standing prerequisite and doing-business notes — with the verification ladder status of every cell and honest per-status counts. Cells below the display gate (human_reviewed) must never be rendered as authoritative to customers.

List the state foreign-qualification catalog (global config): per-(state, entity_kind) SOS certificate-of-authority rules — agency, form, state filing fee, good-standing prerequisite and doing-business notes — with the verification ladder status of every cell and honest per-status counts. Cells below the display gate (human_reviewed) must never be rendered as authoritative to customers.

Bearer scopesforeign_qualifications.read

Request body ForeignQualificationListForeignQualRulesInputrequired

FieldTypeRequiredDescription
stateCodestringoptional—
entityKindenum: "corporation" | "llc" | "nonprofit" | "lp" | …optional—
verificationStatusenum: "unverified" | "human_reviewed" | "verified"optional—

Responses

  • 200Successful invocationForeignQualificationListForeignQualRulesOutput
    FieldTypeRequiredDescription
    rulesarray<object>required—
    countsobjectrequired—
    filteredCountnumberrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `foreign_qualifications.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/foreign-qualification.getForeignQualRuleFetch one foreign-qualification catalog cell with the (state, entity_kind) → (state, 'default') fallback. Null rule = no catalog coverage yet. Advisory context only — doing_business_notes never assert an obligation.

Fetch one foreign-qualification catalog cell with the (state, entity_kind) → (state, 'default') fallback. Null rule = no catalog coverage yet. Advisory context only — doing_business_notes never assert an obligation.

Bearer scopesforeign_qualifications.read

Request body ForeignQualificationGetForeignQualRuleInputrequired

FieldTypeRequiredDescription
stateCodestringrequired—
entityKindenum: "corporation" | "llc" | "nonprofit" | "lp" | …optional—

Responses

  • 200Successful invocationForeignQualificationGetForeignQualRuleOutput
    FieldTypeRequiredDescription
    ruleobjectrequired—
    usedDefaultFallbackbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `foreign_qualifications.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/foreign-qualification.proposeForeignQualRuleResearch-agent write path for the foreign-qualification catalog. Upserts one (state, entity_kind) cell as verification_status='unverified' with mandatory provenance (source_url + retrieved_at). REFUSES to touch a cell already at human_reviewed/verified — verified cells are never downgraded (AC-22 clone); corrections go through the review queue.

Research-agent write path for the foreign-qualification catalog. Upserts one (state, entity_kind) cell as verification_status='unverified' with mandatory provenance (source_url + retrieved_at). REFUSES to touch a cell already at human_reviewed/verified — verified cells are never downgraded (AC-22 clone); corrections go through the review queue.

Bearer scopesforeign_qualifications.write

Request body ForeignQualificationProposeForeignQualRuleInputrequired

FieldTypeRequiredDescription
stateCodestringrequired—
entityKindenum: "corporation" | "llc" | "nonprofit" | "lp" | …optional—
agencyCodestringrequired—
agencyNamestringrequired—
portalUrlstring · urioptional—
formNamestringoptional—
filingFeeUsdnumberoptional—
expediteFeeNotesstringoptional—
goodStandingRequiredbooleanoptional—
goodStandingMaxAgeDaysintegeroptional—
registeredAgentRequiredbooleanoptional—
nameCheckNotesstringoptional—
doingBusinessNotesstringoptional—
penaltyNotesstringoptional—
processingTimeNotesstringoptional—
sourceUrlstring · urirequired—
retrievedAtstring · date-timerequired—
notesstringoptional—

Responses

  • 200Successful invocationForeignQualificationProposeForeignQualRuleOutput
    FieldTypeRequiredDescription
    ruleobjectrequired—
    createdbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `foreign_qualifications.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/foreign-qualification.promoteForeignQualRulePromote a foreign-qualification catalog cell along the verification ladder (unverified → human_reviewed → verified). APPROVAL_REQUIRED and UI-only: the handler rejects any non-human principal (AC-20 clone) and refuses cells with no source_url provenance (CWE-345).

Promote a foreign-qualification catalog cell along the verification ladder (unverified → human_reviewed → verified). APPROVAL_REQUIRED and UI-only: the handler rejects any non-human principal (AC-20 clone) and refuses cells with no source_url provenance (CWE-345).

Bearer scopesforeign_qualifications.write

Request body ForeignQualificationPromoteForeignQualRuleInputrequired

FieldTypeRequiredDescription
stateCodestringrequired—
entityKindenum: "corporation" | "llc" | "nonprofit" | "lp" | …optional—
targetStatusenum: "human_reviewed" | "verified"required—
reasonstringrequired—

Responses

  • 200Successful invocationForeignQualificationPromoteForeignQualRuleOutput
    FieldTypeRequiredDescription
    ruleobjectrequired—
    fromStatusenum: "unverified" | "human_reviewed" | "verified"required—
    toStatusenum: "unverified" | "human_reviewed" | "verified"required—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden. `SYSTEM_ADMIN_REQUIRED`: the caller is not a Kreto system administrator on an interactive session — the global catalog every firm pins (handbook modules, state filing / foreign-qualification / annual-report / e-delivery rules) is staff-only, exactly as on the REST admin door. Checked before any catalog read, so the answer is the same whether or not the row exists. `STEP_UP_REQUIRED`: the dispatcher's APPROVAL_REQUIRED gate (an API key, or no fresh re-authentication). Nothing is written.one of (L2SystemAdminRequiredRefusal · L2ErrorEnvelope)
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/foreign-qualification.listForeignQualificationsList the tenant's foreign-qualification tracker rows (once per entity × state), filterable by entity, state, status and obligation assessment. Advisory rows are unconfirmed legal judgments — callers must exclude them from compliance/on-track counts (design D5/D6). Paginated (default 25). Tenant-scoped via the invocation context.

List the tenant's foreign-qualification tracker rows (once per entity × state), filterable by entity, state, status and obligation assessment. Advisory rows are unconfirmed legal judgments — callers must exclude them from compliance/on-track counts (design D5/D6). Paginated (default 25). Tenant-scoped via the invocation context.

Bearer scopesforeign_qualifications.read

Request body ForeignQualificationListForeignQualificationsInputrequired

FieldTypeRequiredDescription
entityIdstringoptional—
stateCodestringoptional—
statusenum: "pending" | "prepared" | "submitted" | "approved" | …optional—
obligationAssessmentenum: "advisory" | "confirmed" | "not_required"optional—
excludeAdvisorybooleanoptional—
limitintegeroptional—
offsetintegeroptional—

Responses

  • 200Successful invocationForeignQualificationListForeignQualificationsOutput
    FieldTypeRequiredDescription
    qualificationsarray<object>required—
    totalnumberrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `foreign_qualifications.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/foreign-qualification.getForeignQualificationsByEntityFetch every foreign-qualification tracker row for one entity (optionally one state) — advisory, confirmed, dismissed and recorded qualifications. Tenant-scoped.

Fetch every foreign-qualification tracker row for one entity (optionally one state) — advisory, confirmed, dismissed and recorded qualifications. Tenant-scoped.

Bearer scopesforeign_qualifications.read

Request body ForeignQualificationGetForeignQualificationsByEntityInputrequired

FieldTypeRequiredDescription
entityIdstringrequired—
stateCodestringoptional—

Responses

  • 200Successful invocationForeignQualificationGetForeignQualificationsByEntityOutput
    FieldTypeRequiredDescription
    qualificationsarray<object>required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `foreign_qualifications.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/foreign-qualification.detectForeignQualNeedsDry-run compute of an entity's foreign-qualification detection signal (design §4): per candidate state, whether the advisory signal fires (operating evidence outside the formation state with no qualification on record) or an explicit disposition (already_tracked / not_foreign / no_signal / needs_input when the formation state is unknown). Writes NOTHING — the sweep's detection roll persists advisory rows; this method answers 'what would be flagged?'. ADVISORY POSTURE: a firing signal is never an obligation assertion — a human CPA confirms.

Dry-run compute of an entity's foreign-qualification detection signal (design §4): per candidate state, whether the advisory signal fires (operating evidence outside the formation state with no qualification on record) or an explicit disposition (already_tracked / not_foreign / no_signal / needs_input when the formation state is unknown). Writes NOTHING — the sweep's detection roll persists advisory rows; this method answers 'what would be flagged?'. ADVISORY POSTURE: a firing signal is never an obligation assertion — a human CPA confirms.

Bearer scopesforeign_qualifications.read

Request body ForeignQualificationDetectForeignQualNeedsInputrequired

FieldTypeRequiredDescription
entityIdstringrequired—

Responses

  • 200Successful invocationForeignQualificationDetectForeignQualNeedsOutput
    FieldTypeRequiredDescription
    resultobjectrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `foreign_qualifications.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/foreign-qualification.createForeignQualificationCreate one manual foreign-qualification tracker row (source='manual') for an entity × state. Defaults to obligation_assessment='advisory'; creating a CONFIRMED row is a human CPA judgment and rejects agent principals (design D5). Duplicate states are rejected by the once-per-state UNIQUE.

Create one manual foreign-qualification tracker row (source='manual') for an entity × state. Defaults to obligation_assessment='advisory'; creating a CONFIRMED row is a human CPA judgment and rejects agent principals (design D5). Duplicate states are rejected by the once-per-state UNIQUE.

Bearer scopesforeign_qualifications.write

Request body ForeignQualificationCreateForeignQualificationInputrequired

FieldTypeRequiredDescription
entityIdstringrequired—
stateCodestringrequired—
obligationAssessmentenum: "advisory" | "confirmed"optional—
assessmentNotestringoptional—
notesstringoptional—

Responses

  • 200Successful invocationForeignQualificationCreateForeignQualificationOutput
    FieldTypeRequiredDescription
    qualificationobjectrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `foreign_qualifications.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/foreign-qualification.recordExistingQualificationRecord an ALREADY-HELD foreign qualification (design D12 backfill): the row lands status='approved', obligation_assessment='confirmed', source='manual' with the state-issued qualification_date + sos_file_number. This is the authoritative annual-reports foreign anchor (design D9) and permanently suppresses detection advisories for the state (once-per-state UNIQUE).

Record an ALREADY-HELD foreign qualification (design D12 backfill): the row lands status='approved', obligation_assessment='confirmed', source='manual' with the state-issued qualification_date + sos_file_number. This is the authoritative annual-reports foreign anchor (design D9) and permanently suppresses detection advisories for the state (once-per-state UNIQUE).

Bearer scopesforeign_qualifications.write

Request body ForeignQualificationRecordExistingQualificationInputrequired

FieldTypeRequiredDescription
entityIdstringrequired—
stateCodestringrequired—
qualificationDatestringrequired—
sosFileNumberstringoptional—
notesstringoptional—

Responses

  • 200Successful invocationForeignQualificationRecordExistingQualificationOutput
    FieldTypeRequiredDescription
    qualificationobjectrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `foreign_qualifications.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/foreign-qualification.updateForeignQualificationMetadataEdit tracker metadata on one foreign-qualification row: the obligation assessment (confirm the obligation — unlocks prepare — or dismiss as not_required with a REQUIRED assessment note; both are human-only CPA judgments that reject agent principals, design D5), assignee and notes. Writes a structured audit event capturing before/after.

Edit tracker metadata on one foreign-qualification row: the obligation assessment (confirm the obligation — unlocks prepare — or dismiss as not_required with a REQUIRED assessment note; both are human-only CPA judgments that reject agent principals, design D5), assignee and notes. Writes a structured audit event capturing before/after.

Bearer scopesforeign_qualifications.write

Request body ForeignQualificationUpdateForeignQualificationMetadataInputrequired

FieldTypeRequiredDescription
foreignQualificationIdstringrequired—
obligationAssessmentenum: "confirmed" | "not_required"optional—
assessmentNotestringoptional—
assigneestringoptional—
notesstringoptional—
reasonstringrequired—

Responses

  • 200Successful invocationForeignQualificationUpdateForeignQualificationMetadataOutput
    FieldTypeRequiredDescription
    qualificationobjectrequired—
    changedFieldsarray<string>required—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `foreign_qualifications.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/foreign-qualification.prepareForeignQualificationPopulate a foreign qualification's prepared form payload (officers, principal address, home-state formation details, the customer's OWN foreign-state registered agent — Kreto is not the RA — and the good-standing checklist) and transition pending/manual_required → prepared. APPROVAL_REQUIRED and UI-only. HARD GUARDS (design D5/D7): refused unless obligation_assessment='confirmed' (a human CPA judgment) and the catalog cell is at/above the display gate; a required good-standing certificate must be recorded as obtained. PII in form_payload is scrubbed from audit rows.

Populate a foreign qualification's prepared form payload (officers, principal address, home-state formation details, the customer's OWN foreign-state registered agent — Kreto is not the RA — and the good-standing checklist) and transition pending/manual_required → prepared. APPROVAL_REQUIRED and UI-only. HARD GUARDS (design D5/D7): refused unless obligation_assessment='confirmed' (a human CPA judgment) and the catalog cell is at/above the display gate; a required good-standing certificate must be recorded as obtained. PII in form_payload is scrubbed from audit rows.

Bearer scopesforeign_qualifications.write

Request body ForeignQualificationPrepareForeignQualificationInputrequired

FieldTypeRequiredDescription
foreignQualificationIdstringrequired—
formPayloadobjectrequired—

Responses

  • 200Successful invocationForeignQualificationPrepareForeignQualificationOutput
    FieldTypeRequiredDescription
    qualificationobjectrequired—
    fromStatusenum: "pending" | "prepared" | "submitted" | "approved" | …required—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `foreign_qualifications.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/foreign-qualification.requestForeignQualConciergeFilingRecord the '$149 File it for me' concierge request on a prepared foreign qualification: stamps filing_requested_at/by (design D3 — an annotation on 'prepared', not a status) and charges the $149 as a standalone Stripe invoice (created + finalized, so it bills every customer; owner 2026-07-10; fail-closed when STRIPE_SECRET_KEY is unset; double-bill safe). APPROVAL_REQUIRED: this moves money, so the v1 dispatcher blocks non-interactive API-key / MCP callers (KRT-1657) — it requires an interactive human session. Concierge gate (design D7): the catalog cell must be 'verified'. The click by an authenticated CPA/owner IS the recorded filing authorization. The state's own filing fee is separate and paid to the state.

Record the '$149 File it for me' concierge request on a prepared foreign qualification: stamps filing_requested_at/by (design D3 — an annotation on 'prepared', not a status) and charges the $149 as a standalone Stripe invoice (created + finalized, so it bills every customer; owner 2026-07-10; fail-closed when STRIPE_SECRET_KEY is unset; double-bill safe). APPROVAL_REQUIRED: this moves money, so the v1 dispatcher blocks non-interactive API-key / MCP callers (KRT-1657) — it requires an interactive human session. Concierge gate (design D7): the catalog cell must be 'verified'. The click by an authenticated CPA/owner IS the recorded filing authorization. The state's own filing fee is separate and paid to the state.

Bearer scopesforeign_qualifications.write

Request body ForeignQualificationRequestForeignQualConciergeFilingInputrequired

FieldTypeRequiredDescription
foreignQualificationIdstringrequired—

Responses

  • 200Successful invocationForeignQualificationRequestForeignQualConciergeFilingOutput
    FieldTypeRequiredDescription
    qualificationobjectrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `foreign_qualifications.write`.L2ErrorEnvelope
  • 409Not available (code `STATE_FILING_NOT_AVAILABLE`): Kreto does not file foreign qualifications yet. Nothing is written, nothing is charged, nothing is enqueued and the row's status does not change.L2StateFilingNotAvailableRefusal
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/foreign-qualification.transitionForeignQualificationStatusApply a matrix-gated status transition to a foreign-qualification tracker row (exact ear/esr 7-status machine). Supports the mark-filed extras (confirmationNumber + filedAt on → submitted), the approval record (qualificationDate + sosFileNumber on → approved — this becomes the authoritative annual-reports foreign anchor, design D9), the confirmedImmediately chain, and the manual withdrawal record (withdrawnAt on → expired; the sweep never writes expired). Every hop writes a hash-chained audit_events row.

Apply a matrix-gated status transition to a foreign-qualification tracker row (exact ear/esr 7-status machine). Supports the mark-filed extras (confirmationNumber + filedAt on → submitted), the approval record (qualificationDate + sosFileNumber on → approved — this becomes the authoritative annual-reports foreign anchor, design D9), the confirmedImmediately chain, and the manual withdrawal record (withdrawnAt on → expired; the sweep never writes expired). Every hop writes a hash-chained audit_events row.

Bearer scopesforeign_qualifications.write

Request body ForeignQualificationTransitionForeignQualificationStatusInputrequired

FieldTypeRequiredDescription
foreignQualificationIdstringrequired—
newStatusenum: "pending" | "prepared" | "submitted" | "approved" | …required—
reasonstringrequired—
confirmationNumberstringoptional—
filedAtstring · date-timeoptional—
qualificationDatestringoptional—
sosFileNumberstringoptional—
confirmedImmediatelybooleanoptional—
acknowledgedNoQualificationDatebooleanoptional—
withdrawnAtstringoptional—
lastErrorstringoptional—

Responses

  • 200Successful invocationForeignQualificationTransitionForeignQualificationStatusOutput
    FieldTypeRequiredDescription
    qualificationobjectrequired—
    fromStatusenum: "pending" | "prepared" | "submitted" | "approved" | …required—
    toStatusenum: "pending" | "prepared" | "submitted" | "approved" | …required—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `foreign_qualifications.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

l2:handbooks13

POST/v1/handbooks.listHandbookModulesList the handbook policy-module catalog (global config): per (jurisdiction × topic) modules with each module's CURRENT version and its per-version verification-ladder status, plus honest per-status counts. Assembly gate is verified-only (§0-1b): anything below 'verified' must never be rendered on a tenant surface as usable text.

List the handbook policy-module catalog (global config): per (jurisdiction × topic) modules with each module's CURRENT version and its per-version verification-ladder status, plus honest per-status counts. Assembly gate is verified-only (§0-1b): anything below 'verified' must never be rendered on a tenant surface as usable text.

Bearer scopeshandbooks.read

Request body HandbooksListHandbookModulesInputrequired

FieldTypeRequiredDescription
jurisdictionstringoptional—
categoryenum: "federal_core" | "state_required" | "state_recommended"optional—
verificationStatusenum: "unverified" | "human_reviewed" | "verified"optional—

Responses

  • 200Successful invocationHandbooksListHandbookModulesOutput
    FieldTypeRequiredDescription
    modulesarray<object>required—
    countsobjectrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `handbooks.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/handbooks.getHandbookModuleFetch one handbook policy module by (jurisdiction, topic) with its FULL version history (each version carries its own ladder status — verifying v3 says nothing about v4). Null module = no catalog coverage; the caller renders a coverage gap, never a blank.

Fetch one handbook policy module by (jurisdiction, topic) with its FULL version history (each version carries its own ladder status — verifying v3 says nothing about v4). Null module = no catalog coverage; the caller renders a coverage gap, never a blank.

Bearer scopeshandbooks.read

Request body HandbooksGetHandbookModuleInputrequired

FieldTypeRequiredDescription
jurisdictionstringrequired—
topicstringrequired—

Responses

  • 200Successful invocationHandbooksGetHandbookModuleOutput
    FieldTypeRequiredDescription
    moduleobjectrequired—
    versionsarray<object>required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `handbooks.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/handbooks.proposeHandbookModuleResearch-agent write path for the policy-module catalog. Creates the (jurisdiction, topic) module identity if missing and appends a NEW version row at verification_status='unverified' with mandatory provenance. NEVER touches an existing verified version (AC-22 clone) — a law change is a new version, promoted only by a human (AC-20 wall). Inputs are public (jurisdiction, topic) facts — PII-free by construction.

Research-agent write path for the policy-module catalog. Creates the (jurisdiction, topic) module identity if missing and appends a NEW version row at verification_status='unverified' with mandatory provenance. NEVER touches an existing verified version (AC-22 clone) — a law change is a new version, promoted only by a human (AC-20 wall). Inputs are public (jurisdiction, topic) facts — PII-free by construction.

Bearer scopeshandbooks.write

Request body HandbooksProposeHandbookModuleInputrequired

FieldTypeRequiredDescription
jurisdictionstringrequired—
topicstringrequired—
titlestringrequired—
categoryenum: "federal_core" | "state_required" | "state_recommended"required—
minEmployeeCountintegeroptional—
applicabilityobjectoptional—
displayOrderintegeroptional—
bodyMdstringrequired—
changeSummarystringoptional—
lawEffectiveDatestringoptional—
statuteCitationsarray<object>required—
draftedByenum: "research_agent" | "human"optional—
draftProvenanceobjectoptional—
sourceUrlstring · urirequired—
retrievedAtstring · date-timerequired—
notesstringoptional—

Responses

  • 200Successful invocationHandbooksProposeHandbookModuleOutput
    FieldTypeRequiredDescription
    moduleobjectrequired—
    versionobjectrequired—
    moduleCreatedbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `handbooks.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/handbooks.proposeStateRequiredTopicResearch-agent write path for the §3A2 completeness registry — which topics a state REQUIRES at what headcount (a fee-catalog-class checkable fact). Upserts one (jurisdiction, topic) row as 'unverified' with mandatory provenance. REFUSES to touch a row already at human_reviewed/verified (no-downgrade AC-22 clone). The §4 gap computation only trusts VERIFIED registry rows.

Research-agent write path for the §3A2 completeness registry — which topics a state REQUIRES at what headcount (a fee-catalog-class checkable fact). Upserts one (jurisdiction, topic) row as 'unverified' with mandatory provenance. REFUSES to touch a row already at human_reviewed/verified (no-downgrade AC-22 clone). The §4 gap computation only trusts VERIFIED registry rows.

Bearer scopeshandbooks.write

Request body HandbooksProposeStateRequiredTopicInputrequired

FieldTypeRequiredDescription
jurisdictionstringrequired—
topicstringrequired—
requirementKindenum: "required" | "recommended"required—
minEmployeeCountintegeroptional—
applicabilityobjectoptional—
statuteCitationsarray<object>required—
sourceUrlstring · urirequired—
retrievedAtstring · date-timerequired—
notesstringoptional—

Responses

  • 200Successful invocationHandbooksProposeStateRequiredTopicOutput
    FieldTypeRequiredDescription
    topicRowobjectrequired—
    createdbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `handbooks.write`.L2ErrorEnvelope
  • 409Reviewed cell locked (code `REVIEWED_CELL_LOCKED`): the registry row is already human-reviewed or verified — an agent proposal may not overwrite it. Nothing is written.L2HandbookReviewedCellLockedRefusal
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/handbooks.promoteHandbookModuleVersionPromote one policy-module VERSION along the verification ladder (unverified → human_reviewed → verified). APPROVAL_REQUIRED and UI-only: rejects any non-human principal (AC-20 wall), refuses missing source_url provenance. The promotion UI shows the §0-1(b) honest definition of what the click asserts. On 'verified': the prior verified version gets superseded_at and current_version_id advances (§7 step 3). 'human_reviewed' remains admin-queue-only — it renders on NO tenant surface (§0-1b).

Promote one policy-module VERSION along the verification ladder (unverified → human_reviewed → verified). APPROVAL_REQUIRED and UI-only: rejects any non-human principal (AC-20 wall), refuses missing source_url provenance. The promotion UI shows the §0-1(b) honest definition of what the click asserts. On 'verified': the prior verified version gets superseded_at and current_version_id advances (§7 step 3). 'human_reviewed' remains admin-queue-only — it renders on NO tenant surface (§0-1b).

Bearer scopeshandbooks.write

Request body HandbooksPromoteHandbookModuleVersionInputrequired

FieldTypeRequiredDescription
moduleVersionIdstringrequired—
targetStatusenum: "human_reviewed" | "verified"required—
reasonstringrequired—

Responses

  • 200Successful invocationHandbooksPromoteHandbookModuleVersionOutput
    FieldTypeRequiredDescription
    versionobjectrequired—
    fromStatusenum: "unverified" | "human_reviewed" | "verified"required—
    toStatusenum: "unverified" | "human_reviewed" | "verified"required—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden. `SYSTEM_ADMIN_REQUIRED`: the caller is not a Kreto system administrator on an interactive session — the global catalog every firm pins (handbook modules, state filing / foreign-qualification / annual-report / e-delivery rules) is staff-only, exactly as on the REST admin door. Checked before any catalog read, so the answer is the same whether or not the row exists. `STEP_UP_REQUIRED`: the dispatcher's APPROVAL_REQUIRED gate (an API key, or no fresh re-authentication). Nothing is written.one of (L2SystemAdminRequiredRefusal · L2ErrorEnvelope)
  • 404Not found (code `NOT_FOUND`): the row the input names is not in the caller's tenant (or the catalog), or handbooks are disabled for the tenant. Nothing is written.L2HandbookNotFoundRefusal
  • 409`PROVENANCE_REQUIRED`: the row has no `source_url` — provenance is mandatory before promotion. `ILLEGAL_TRANSITION`: the version is emergency-retired, or the move is not up the verification ladder (the ladder is monotonic; no downgrade). Nothing is written.one of (L2HandbookProvenanceRequiredRefusal · L2HandbookIllegalTransitionRefusal)
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/handbooks.promoteStateRequiredTopicPromote a §3A2 completeness-registry row along the verification ladder. APPROVAL_REQUIRED and UI-only (AC-20 wall — rejects non-human principals; refuses missing source_url). Only VERIFIED registry rows feed the honest-gaps computation.

Promote a §3A2 completeness-registry row along the verification ladder. APPROVAL_REQUIRED and UI-only (AC-20 wall — rejects non-human principals; refuses missing source_url). Only VERIFIED registry rows feed the honest-gaps computation.

Bearer scopeshandbooks.write

Request body HandbooksPromoteStateRequiredTopicInputrequired

FieldTypeRequiredDescription
jurisdictionstringrequired—
topicstringrequired—
targetStatusenum: "human_reviewed" | "verified"required—
reasonstringrequired—

Responses

  • 200Successful invocationHandbooksPromoteStateRequiredTopicOutput
    FieldTypeRequiredDescription
    topicRowobjectrequired—
    fromStatusenum: "unverified" | "human_reviewed" | "verified"required—
    toStatusenum: "unverified" | "human_reviewed" | "verified"required—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden. `SYSTEM_ADMIN_REQUIRED`: the caller is not a Kreto system administrator on an interactive session — the global catalog every firm pins (handbook modules, state filing / foreign-qualification / annual-report / e-delivery rules) is staff-only, exactly as on the REST admin door. Checked before any catalog read, so the answer is the same whether or not the row exists. `STEP_UP_REQUIRED`: the dispatcher's APPROVAL_REQUIRED gate (an API key, or no fresh re-authentication). Nothing is written.one of (L2SystemAdminRequiredRefusal · L2ErrorEnvelope)
  • 404Not found (code `NOT_FOUND`): the row the input names is not in the caller's tenant (or the catalog), or handbooks are disabled for the tenant. Nothing is written.L2HandbookNotFoundRefusal
  • 409`PROVENANCE_REQUIRED`: the row has no `source_url` — provenance is mandatory before promotion. `ILLEGAL_TRANSITION`: the move is not up the verification ladder (the ladder is monotonic; no downgrade). Nothing is written.one of (L2HandbookProvenanceRequiredRefusal · L2HandbookIllegalTransitionRefusal)
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/handbooks.retireHandbookModuleVersionEMERGENCY version-level retire (§0-5d): a repealed/amended/enjoined statute pulls a verified version from assembly IMMEDIATELY, without waiting for a replacement to clear review — the ONE sanctioned exemption from no-downgrade. APPROVAL_REQUIRED, human-only, reason mandatory. Existing pins are unaffected (reconstructability holds) but published handbooks pinning this version get flagged by checkHandbookDrift.

EMERGENCY version-level retire (§0-5d): a repealed/amended/enjoined statute pulls a verified version from assembly IMMEDIATELY, without waiting for a replacement to clear review — the ONE sanctioned exemption from no-downgrade. APPROVAL_REQUIRED, human-only, reason mandatory. Existing pins are unaffected (reconstructability holds) but published handbooks pinning this version get flagged by checkHandbookDrift.

Bearer scopeshandbooks.write

Request body HandbooksRetireHandbookModuleVersionInputrequired

FieldTypeRequiredDescription
moduleVersionIdstringrequired—
reasonstringrequired—

Responses

  • 200Successful invocationHandbooksRetireHandbookModuleVersionOutput
    FieldTypeRequiredDescription
    versionobjectrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden. `SYSTEM_ADMIN_REQUIRED`: the caller is not a Kreto system administrator on an interactive session — the global catalog every firm pins (handbook modules, state filing / foreign-qualification / annual-report / e-delivery rules) is staff-only, exactly as on the REST admin door. Checked before any catalog read, so the answer is the same whether or not the row exists. `STEP_UP_REQUIRED`: the dispatcher's APPROVAL_REQUIRED gate (an API key, or no fresh re-authentication). Nothing is written.one of (L2SystemAdminRequiredRefusal · L2ErrorEnvelope)
  • 404Not found (code `NOT_FOUND`): the row the input names is not in the caller's tenant (or the catalog), or handbooks are disabled for the tenant. Nothing is written.L2HandbookNotFoundRefusal
  • 409Illegal transition (code `ILLEGAL_TRANSITION`): the version is already retired. Nothing is written.L2HandbookIllegalTransitionRefusal
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/handbooks.listHandbooksList the tenant's handbook instances (per entity, versioned), filterable by entity and status. Paginated (default 25). Tenant-scoped via the invocation context. Each draft carries `publish_refusal`: the `missing` set publishHandbook would refuse it with (no policy sections), or null when it may publish.

List the tenant's handbook instances (per entity, versioned), filterable by entity and status. Paginated (default 25). Tenant-scoped via the invocation context. Each draft carries `publish_refusal`: the `missing` set publishHandbook would refuse it with (no policy sections), or null when it may publish.

Bearer scopeshandbooks.read

Request body HandbooksListHandbooksInputrequired

FieldTypeRequiredDescription
entityIdstringoptional—
statusenum: "draft" | "published" | "superseded" | "archived"optional—
limitintegeroptional—
offsetintegeroptional—

Responses

  • 200Successful invocationHandbooksListHandbooksOutput
    FieldTypeRequiredDescription
    handbooksarray<object>required—
    totalnumberrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `handbooks.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/handbooks.generateHandbookDraftGenerate a handbook DRAFT for an entity: deterministic module selection (selectHandbookModules) from live operating states + per-state headcount, assembly gate = verified-only, honest coverage gaps computed against the §3A2 registry (uncovered states gap UNCONDITIONALLY), generation-time staleness warnings (§0-5c). Pins exact module-version ids. Publishing is a separate APPROVAL_REQUIRED step with the blocking disclaimer checkbox.

Generate a handbook DRAFT for an entity: deterministic module selection (selectHandbookModules) from live operating states + per-state headcount, assembly gate = verified-only, honest coverage gaps computed against the §3A2 registry (uncovered states gap UNCONDITIONALLY), generation-time staleness warnings (§0-5c). Pins exact module-version ids. Publishing is a separate APPROVAL_REQUIRED step with the blocking disclaimer checkbox.

Bearer scopeshandbooks.write

Request body HandbooksGenerateHandbookDraftInputrequired

FieldTypeRequiredDescription
entityIdstringrequired—
titlestringoptional—
companyInputsobjectoptional—

Responses

  • 200Successful invocationHandbooksGenerateHandbookDraftOutput
    FieldTypeRequiredDescription
    handbookobjectrequired—
    selectionobjectrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `handbooks.write`.L2ErrorEnvelope
  • 404Not found (code `NOT_FOUND`): the row the input names is not in the caller's tenant (or the catalog), or handbooks are disabled for the tenant. Nothing is written.L2HandbookNotFoundRefusal
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/handbooks.publishHandbookPublish a draft handbook. APPROVAL_REQUIRED and UI-only: the §0-2(b) blocking disclaimer checkbox is part of the contract — refuses disclaimerAccepted !== true, stamps disclaimer_ack_at/by, records the exact checkbox copy in the hash-chained audit trail, freezes the pins + states_snapshot, supersedes the prior published version, and queues the PDF render (onboarding-docs queue posture — never a half-written published row).

Publish a draft handbook. APPROVAL_REQUIRED and UI-only: the §0-2(b) blocking disclaimer checkbox is part of the contract — refuses disclaimerAccepted !== true, stamps disclaimer_ack_at/by, records the exact checkbox copy in the hash-chained audit trail, freezes the pins + states_snapshot, supersedes the prior published version, and queues the PDF render (onboarding-docs queue posture — never a half-written published row).

Bearer scopeshandbooks.write

Request body HandbooksPublishHandbookInputrequired

FieldTypeRequiredDescription
handbookIdstringrequired—
disclaimerAcceptedbooleanrequired—

Responses

  • 200Successful invocationHandbooksPublishHandbookOutput
    FieldTypeRequiredDescription
    handbookobjectrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `handbooks.write`.L2ErrorEnvelope
  • 404Not found (code `NOT_FOUND`): the row the input names is not in the caller's tenant (or the catalog), or handbooks are disabled for the tenant. Nothing is written.L2HandbookNotFoundRefusal
  • 409The draft cannot be published — the same two refusals the REST door (`POST /api/handbooks/{id}/publish`) answers, in the L2 envelope. `HANDBOOK_HAS_NO_SECTIONS`: it has no policy sections; `missing` names the jurisdictions and required topics the verified catalog lacks. `ILLEGAL_TRANSITION`: the handbook is not a draft. Nothing is written.L2HandbookPublishRefusal
  • 422Disclaimer required (code `DISCLAIMER_REQUIRED`): `disclaimerAccepted` is not `true` — the blocking attorney-review checkbox was not ticked. Nothing is written.L2HandbookDisclaimerRequiredRefusal
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/handbooks.sendHandbookAcknowledgmentsCreate one acknowledgment EVIDENCE row per active roster employee of the published handbook's entity — snapshotting name/email/work_state at send (§3E) — and email each a tokenized link (signature_requests clone). Employees without email land in an honest 'cannot reach' list. Idempotent: existing (handbook, employee) rows are skipped.

Create one acknowledgment EVIDENCE row per active roster employee of the published handbook's entity — snapshotting name/email/work_state at send (§3E) — and email each a tokenized link (signature_requests clone). Employees without email land in an honest 'cannot reach' list. Idempotent: existing (handbook, employee) rows are skipped.

Bearer scopeshandbooks.write

Request body HandbooksSendHandbookAcknowledgmentsInputrequired

FieldTypeRequiredDescription
handbookIdstringrequired—

Responses

  • 200Successful invocationHandbooksSendHandbookAcknowledgmentsOutput
    FieldTypeRequiredDescription
    creatednumberrequired—
    skippedExistingnumberrequired—
    cannotReacharray<object>required—
    emailFailuresnumberrequired—
    sendOutcomesobjectrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `handbooks.write`.L2ErrorEnvelope
  • 404Not found (code `NOT_FOUND`): the row the input names is not in the caller's tenant (or the catalog), or handbooks are disabled for the tenant. Nothing is written.L2HandbookNotFoundRefusal
  • 409Illegal transition (code `ILLEGAL_TRANSITION`): the handbook is not published — only published handbooks are sent for acknowledgment. Nothing is written.L2HandbookIllegalTransitionRefusal
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/handbooks.getHandbookAckStatusThe acknowledgment tracking matrix for one handbook: per-employee sent/viewed/acknowledged status with honest counts. secure_token is never returned. Tenant-scoped.

The acknowledgment tracking matrix for one handbook: per-employee sent/viewed/acknowledged status with honest counts. secure_token is never returned. Tenant-scoped.

Bearer scopeshandbooks.read

Request body HandbooksGetHandbookAckStatusInputrequired

FieldTypeRequiredDescription
handbookIdstringrequired—

Responses

  • 200Successful invocationHandbooksGetHandbookAckStatusOutput
    FieldTypeRequiredDescription
    acksarray<object>required—
    countsobjectrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `handbooks.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/handbooks.checkHandbookDriftFlag every published handbook of an entity that is stale on either axis (§7 step 4): pinned-module drift (pins a superseded or emergency-retired version) or selection-input drift (a deterministic re-run of selectHandbookModules against LIVE operating states + headcount diverges from states_snapshot — new state registered, threshold crossed). Read-only; never auto-republishes (§0-5e).

Flag every published handbook of an entity that is stale on either axis (§7 step 4): pinned-module drift (pins a superseded or emergency-retired version) or selection-input drift (a deterministic re-run of selectHandbookModules against LIVE operating states + headcount diverges from states_snapshot — new state registered, threshold crossed). Read-only; never auto-republishes (§0-5e).

Bearer scopeshandbooks.read

Request body HandbooksCheckHandbookDriftInputrequired

FieldTypeRequiredDescription
entityIdstringrequired—

Responses

  • 200Successful invocationHandbooksCheckHandbookDriftOutput
    FieldTypeRequiredDescription
    entityIdstringrequired—
    flagsarray<object>required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `handbooks.read`.L2ErrorEnvelope
  • 404Not found (code `NOT_FOUND`): the row the input names is not in the caller's tenant (or the catalog), or handbooks are disabled for the tenant. Nothing is written.L2HandbookNotFoundRefusal
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

l2:i9-records6

POST/v1/i9-records.createI9RecordRecord Section 1 of an I-9 TRANSCRIBED from a form the employee already completed and signed (section1Data.transcribed_from_signed_original must be true — staff never attest Section 1 themselves). SSN plaintext is encrypted via the public.app_create_secret vault wrapper; only the returned secret UUID is stored on the row. Dual-writes employees.i9_section1_complete = true. APPROVAL_REQUIRED.

Record Section 1 of an I-9 TRANSCRIBED from a form the employee already completed and signed (section1Data.transcribed_from_signed_original must be true — staff never attest Section 1 themselves). SSN plaintext is encrypted via the public.app_create_secret vault wrapper; only the returned secret UUID is stored on the row. Dual-writes employees.i9_section1_complete = true. APPROVAL_REQUIRED.

Bearer scopespayroll.write

Request body I9RecordsCreateI9RecordInputrequired

FieldTypeRequiredDescription
employeeIdstringrequired—
citizenshipStatusenum: "us_citizen" | "noncitizen_national" | "lpr" | "authorized_alien"required—
section1Dataobjectrequired—
ssnstringoptional—

Responses

  • 200Successful invocationI9RecordsCreateI9RecordOutput
    FieldTypeRequiredDescription
    idstringrequired—
    tenant_idstringrequired—
    employee_idstringrequired—
    citizenship_statusenum: "us_citizen" | "noncitizen_national" | "lpr" | "authorized_alien" | …required—
    section1_completed_atstringrequired—
    section2_completed_atstringrequired—
    has_ssnbooleanrequired—
    ssn_last_4stringrequired—
    retention_untilstringrequired—
    retention_provisionalbooleanrequired—
    is_archivedbooleanrequired—
    has_pdfbooleanrequired—
    supporting_docs_countintegerrequired—
    created_atstringrequired—
    updated_atstringrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `payroll.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/i9-records.getI9RecordFetch a single I-9 record by id, including decrypted SSN plaintext for the CPA detail view. APPROVAL_REQUIRED. The `ssn` field is redacted from audit outputs while the live caller receives the actual value (R13 mitigation, mirrors credential-vault.getCredential).

Fetch a single I-9 record by id, including decrypted SSN plaintext for the CPA detail view. APPROVAL_REQUIRED. The `ssn` field is redacted from audit outputs while the live caller receives the actual value (R13 mitigation, mirrors credential-vault.getCredential).

Bearer scopespayroll.write

Request body I9RecordsGetI9RecordInputrequired

FieldTypeRequiredDescription
i9RecordIdstringrequired—

Responses

  • 200Successful invocationI9RecordsGetI9RecordOutput
    FieldTypeRequiredDescription
    idstringrequired—
    tenant_idstringrequired—
    employee_idstringrequired—
    citizenship_statusenum: "us_citizen" | "noncitizen_national" | "lpr" | "authorized_alien" | …required—
    section1_completed_atstringrequired—
    section2_completed_atstringrequired—
    has_ssnbooleanrequired—
    ssn_last_4stringrequired—
    retention_untilstringrequired—
    retention_provisionalbooleanrequired—
    is_archivedbooleanrequired—
    has_pdfbooleanrequired—
    supporting_docs_countintegerrequired—
    created_atstringrequired—
    updated_atstringrequired—
    ssnstringrequired—
    section1_dataobjectrequired—
    section2_dataobjectrequired—
    list_a_docsarray<object>required—
    list_b_docsarray<object>required—
    list_c_docsarray<object>required—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `payroll.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/i9-records.updateI9RecordSection2Update an i9_record with Section 2 employer-review data. Validates Section 1 completion + USCIS document combination (List A XOR List B+C). Dual-writes employees.i9_section2_complete = true. APPROVAL_REQUIRED.

Update an i9_record with Section 2 employer-review data. Validates Section 1 completion + USCIS document combination (List A XOR List B+C). Dual-writes employees.i9_section2_complete = true. APPROVAL_REQUIRED.

Bearer scopespayroll.write

Request body I9RecordsUpdateI9RecordSection2Inputrequired

FieldTypeRequiredDescription
i9RecordIdstringrequired—
section2Dataobjectrequired—
reviewerUserIdstringoptional—

Responses

  • 200Successful invocationI9RecordsUpdateI9RecordSection2Output
    FieldTypeRequiredDescription
    idstringrequired—
    tenant_idstringrequired—
    employee_idstringrequired—
    citizenship_statusenum: "us_citizen" | "noncitizen_national" | "lpr" | "authorized_alien" | …required—
    section1_completed_atstringrequired—
    section2_completed_atstringrequired—
    has_ssnbooleanrequired—
    ssn_last_4stringrequired—
    retention_untilstringrequired—
    retention_provisionalbooleanrequired—
    is_archivedbooleanrequired—
    has_pdfbooleanrequired—
    supporting_docs_countintegerrequired—
    created_atstringrequired—
    updated_atstringrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `payroll.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/i9-records.listI9RecordsList I-9 records in the caller's tenant. Returns metadata only (no plaintext SSN, no full section data). The `ssn_last_4` field is left null on this path — fetch via getI9Record for last-4 display. AUTONOMOUS.

List I-9 records in the caller's tenant. Returns metadata only (no plaintext SSN, no full section data). The `ssn_last_4` field is left null on this path — fetch via getI9Record for last-4 display. AUTONOMOUS.

Bearer scopespayroll.read

Request body I9RecordsListI9RecordsInputrequired

FieldTypeRequiredDescription
employeeIdstringoptional—
includeArchivedbooleanoptional—

Responses

  • 200Successful invocationI9RecordsListI9RecordsOutput
    FieldTypeRequiredDescription
    recordsarray<object>required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `payroll.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/i9-records.addI9SupportingDocumentUpload a supporting document (passport image, driver's license, SSN card, etc.) for an I-9 record. Append-only: every upload is stored at its own key in the service-role-only i9-documents bucket (<tenant-id>/<record-id>/list_<a|b|c>_doc_<n>-<version>.<ext>) with its own i9_record_documents row; an earlier upload in the same slot is never removed or overwritten. i9_records.supporting_docs_paths lists the newest path per slot. APPROVAL_REQUIRED.

Upload a supporting document (passport image, driver's license, SSN card, etc.) for an I-9 record. Append-only: every upload is stored at its own key in the service-role-only i9-documents bucket (<tenant-id>/<record-id>/list_<a|b|c>_doc_<n>-<version>.<ext>) with its own i9_record_documents row; an earlier upload in the same slot is never removed or overwritten. i9_records.supporting_docs_paths lists the newest path per slot. APPROVAL_REQUIRED.

Bearer scopespayroll.write

Request body I9RecordsAddI9SupportingDocumentInputrequired

FieldTypeRequiredDescription
i9RecordIdstringrequired—
listenum: "a" | "b" | "c"required—
indexintegerrequired—
filenamestringrequired—
mimeTypestringrequired—
fileBytesstring · binaryrequired—

Responses

  • 200Successful invocationI9RecordsAddI9SupportingDocumentOutput
    FieldTypeRequiredDescription
    idstringrequired—
    tenant_idstringrequired—
    employee_idstringrequired—
    citizenship_statusenum: "us_citizen" | "noncitizen_national" | "lpr" | "authorized_alien" | …required—
    section1_completed_atstringrequired—
    section2_completed_atstringrequired—
    has_ssnbooleanrequired—
    ssn_last_4stringrequired—
    retention_untilstringrequired—
    retention_provisionalbooleanrequired—
    is_archivedbooleanrequired—
    has_pdfbooleanrequired—
    supporting_docs_countintegerrequired—
    created_atstringrequired—
    updated_atstringrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `payroll.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/i9-records.generateI9PdfRender a USCIS Form I-9 PDF from i9_records data and store it as a new version at i9-documents/<tenant-id>/<id>/form-<version>.pdf with its own i9_record_documents row (kind generated); i9_records.pdf_path points at the newest. Regenerating adds a version and never overwrites or removes an earlier one. SSN is decrypted internally and rendered into the PDF; never returned to the caller. APPROVAL_REQUIRED.

Render a USCIS Form I-9 PDF from i9_records data and store it as a new version at i9-documents/<tenant-id>/<id>/form-<version>.pdf with its own i9_record_documents row (kind generated); i9_records.pdf_path points at the newest. Regenerating adds a version and never overwrites or removes an earlier one. SSN is decrypted internally and rendered into the PDF; never returned to the caller. APPROVAL_REQUIRED.

Bearer scopespayroll.write

Request body I9RecordsGenerateI9PdfInputrequired

FieldTypeRequiredDescription
i9RecordIdstringrequired—

Responses

  • 200Successful invocationI9RecordsGenerateI9PdfOutput
    FieldTypeRequiredDescription
    i9RecordIdstringrequired—
    byte_sizeintegerrequired—
    document_idstringrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `payroll.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

l2:location-detection4

POST/v1/location-detection.recordLocationChangeRecord a detected or operator-entered work-state transition for an employee. Idempotent on (employee_id, effective_date) — re-recording the same date returns the existing row's id with isDuplicate=true. NOTIFY — operator decision OR payroll-detection adapter call; the follow-up confirmLocationChange gates SUI/PFL jurisdiction routing.

Record a detected or operator-entered work-state transition for an employee. Idempotent on (employee_id, effective_date) — re-recording the same date returns the existing row's id with isDuplicate=true. NOTIFY — operator decision OR payroll-detection adapter call; the follow-up confirmLocationChange gates SUI/PFL jurisdiction routing.

Bearer scopespayroll.write

Request body LocationDetectionRecordLocationChangeInputrequired

FieldTypeRequiredDescription
employeeIdstringrequired—
toStatestringrequired—
fromStatestringoptional—
effectiveDatestringrequired—
sourceenum: "payroll_detection" | "operator_manual" | "finch_sync" | "gusto_sync" | …required—
metadataobjectoptional—

Responses

  • 200Successful invocationLocationDetectionRecordLocationChangeOutput
    FieldTypeRequiredDescription
    idstringrequired—
    employeeIdstringrequired—
    toStatestringrequired—
    effectiveDatestringrequired—
    isDuplicatebooleanrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `payroll.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/location-detection.listLocationChangesReturn tenant-scoped work-state transition history. Optional `entityId` / `employeeId` filters narrow the result; `onlyConfirmed` filters to operator-confirmed rows. Ordered most-recent effective_date first. AUTONOMOUS — read-only.

Return tenant-scoped work-state transition history. Optional `entityId` / `employeeId` filters narrow the result; `onlyConfirmed` filters to operator-confirmed rows. Ordered most-recent effective_date first. AUTONOMOUS — read-only.

Bearer scopespayroll.read

Request body LocationDetectionListLocationChangesInputrequired

FieldTypeRequiredDescription
entityIdstringoptional—
employeeIdstringoptional—
onlyConfirmedbooleanoptional—
limitintegeroptional—

Responses

  • 200Successful invocationLocationDetectionListLocationChangesOutput
    FieldTypeRequiredDescription
    changesarray<object>required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `payroll.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/location-detection.confirmLocationChangeOperator confirms a detected work-state transition is real (not a payroll-data typo). Sets is_confirmed=true + confirmed_at + confirmed_by. Once confirmed, the locationDetection agent treats the row as the canonical state-of-record for the employee from effective_date forward. NOTIFY — operator override.

Operator confirms a detected work-state transition is real (not a payroll-data typo). Sets is_confirmed=true + confirmed_at + confirmed_by. Once confirmed, the locationDetection agent treats the row as the canonical state-of-record for the employee from effective_date forward. NOTIFY — operator override.

Bearer scopespayroll.write

Request body LocationDetectionConfirmLocationChangeInputrequired

FieldTypeRequiredDescription
idstringrequired—

Responses

  • 200Successful invocationLocationDetectionConfirmLocationChangeOutput
    FieldTypeRequiredDescription
    idstringrequired—
    isConfirmedenum: truerequired—
    confirmedAtstringrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `payroll.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/location-detection.getLatestLocationByEmployeeReturn the most recent confirmed work-state transition for an employee (as-of optional date; defaults to now). Returns {toState: null} when no confirmed rows exist. AUTONOMOUS — read-only.

Return the most recent confirmed work-state transition for an employee (as-of optional date; defaults to now). Returns {toState: null} when no confirmed rows exist. AUTONOMOUS — read-only.

Bearer scopespayroll.read

Request body LocationDetectionGetLatestLocationByEmployeeInputrequired

FieldTypeRequiredDescription
employeeIdstringrequired—
asOfstringoptional—

Responses

  • 200Successful invocationLocationDetectionGetLatestLocationByEmployeeOutput
    FieldTypeRequiredDescription
    employeeIdstringrequired—
    toStatestringrequired—
    effectiveDatestringrequired—
    isConfirmedbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `payroll.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

l2:mailbox9

POST/v1/mailbox.listInboundList inbound mail for an employer entity. Returns received mail rows (newest first) with sender info, status, OCR text, and notice linkage. Tenant-scoped; RLS enforced with a defense-in-depth cross-tenant assertion. AUTONOMOUS — read-only, recoverable, no agency impact.

List inbound mail for an employer entity. Returns received mail rows (newest first) with sender info, status, OCR text, and notice linkage. Tenant-scoped; RLS enforced with a defense-in-depth cross-tenant assertion. AUTONOMOUS — read-only, recoverable, no agency impact.

Bearer scopesnotices.read

Request body MailboxListInboundInputrequired

FieldTypeRequiredDescription
entityIdstring · uuidrequired—

Responses

  • 200Successful invocationMailboxListInboundOutput
    FieldTypeRequiredDescription
    itemsarray<object>required—
    totalintegerrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/mailbox.discardHeldDiscard a piece of inbound mail HELD for a removed employer: status held -> failed, never processed into a notice. Firm plane only (refused for any entity-restricted caller). 409 MAIL_NOT_HELD when the piece is not held. The scan stays in storage; the employer plane never sees it. NOTIFY — firm triage write, audited.

Discard a piece of inbound mail HELD for a removed employer: status held -> failed, never processed into a notice. Firm plane only (refused for any entity-restricted caller). 409 MAIL_NOT_HELD when the piece is not held. The scan stays in storage; the employer plane never sees it. NOTIFY — firm triage write, audited.

Bearer scopesnotices.write

Request body MailboxDiscardHeldInputrequired

FieldTypeRequiredDescription
entityIdstring · uuidrequired—
mailIdstring · uuidrequired—
reasonenum: "employer_removed"required—

Responses

  • 200Successful invocationMailboxDiscardHeldOutput
    FieldTypeRequiredDescription
    idstring · uuidrequired—
    statusenum: "failed"required—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/mailbox.laneSummaryCount the CPA mailbox lanes (approve / input / active / done) over every live notice of the firm, through the one lane ladder. Firm users only: an entity-scoped caller gets 403 MAILBOX_CPA_ONLY before any read; a firm whose CPA_MAILBOX flag is off gets 403 FEATURE_DISABLED. `capped` true means the population hit the row cap and every count is a lower bound. `overdueByLane` counts past-deadline notices per lane over the same population. `attribution.unconfirmed` counts notices, not employers. `asOf` is this read's own instant; a listLane total read later may drift from it. AUTONOMOUS — read-only.

Count the CPA mailbox lanes (approve / input / active / done) over every live notice of the firm, through the one lane ladder. Firm users only: an entity-scoped caller gets 403 MAILBOX_CPA_ONLY before any read; a firm whose CPA_MAILBOX flag is off gets 403 FEATURE_DISABLED. `capped` true means the population hit the row cap and every count is a lower bound. `overdueByLane` counts past-deadline notices per lane over the same population. `attribution.unconfirmed` counts notices, not employers. `asOf` is this read's own instant; a listLane total read later may drift from it. AUTONOMOUS — read-only.

Bearer scopesnotices.read

Request body MailboxLaneSummaryInputrequired

FieldTypeRequiredDescription
entityIdstring · uuidoptionalNarrow to the notices attached to this employer (entity id). An id that is not one of the firm's employers returns the empty slice.
ownerenum: "mine" | "unassigned"optional'mine' = notices the caller owns; 'unassigned' = OPEN notices nobody owns (the same set as the work queue's Unassigned count). Omitted = every owner.

Responses

  • 200Successful invocationMailboxLaneSummaryOutput
    FieldTypeRequiredDescription
    lanesobjectrequired—
    overdueByLaneobjectrequiredPer lane, how many notices are past their governing deadline (the Overdue chip's predicate: junk, terminal and recorded-verdict notices are never overdue). Counted over the same firm-wide population as `lanes`, so each value is <= that lane's count.
    notANoticeInActiveintegerrequired—
    populationintegerrequired—
    cappedbooleanrequired—
    windowobjectrequired—
    attributionobjectrequired—
    asOfstringrequiredISO-8601 instant of this read (one clock call per read). laneSummary and listLane are separate reads, each with its own asOf: a listLane total may drift from the summary's lane count when the firm changed in between — re-read laneSummary to reconcile.
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/mailbox.listLaneList one CPA mailbox lane, paged after classification (deadline asc nulls last, then id). Rows are the /api/notices CPA row shape. `total` is the lane's count and equals laneSummary's for the same state — the two are separate reads with their own `asOf`, so a page total may drift from the summary tab until laneSummary is re-read. Firm users only: an entity-scoped caller gets 403 MAILBOX_CPA_ONLY before any read; a firm whose CPA_MAILBOX flag is off gets 403 FEATURE_DISABLED. AUTONOMOUS — read-only.

List one CPA mailbox lane, paged after classification (deadline asc nulls last, then id). Rows are the /api/notices CPA row shape. `total` is the lane's count and equals laneSummary's for the same state — the two are separate reads with their own `asOf`, so a page total may drift from the summary tab until laneSummary is re-read. Firm users only: an entity-scoped caller gets 403 MAILBOX_CPA_ONLY before any read; a firm whose CPA_MAILBOX flag is off gets 403 FEATURE_DISABLED. AUTONOMOUS — read-only.

Bearer scopesnotices.read

Request body MailboxListLaneInputrequired

FieldTypeRequiredDescription
laneenum: "approve" | "input" | "active" | "done"required—
offsetintegeroptional—
limitintegeroptional—
entityIdstring · uuidoptionalNarrow to the notices attached to this employer (entity id). An id that is not one of the firm's employers returns the empty slice.
ownerenum: "mine" | "unassigned"optional'mine' = notices the caller owns; 'unassigned' = OPEN notices nobody owns (the same set as the work queue's Unassigned count). Omitted = every owner.

Responses

  • 200Successful invocationMailboxListLaneOutput
    FieldTypeRequiredDescription
    rowsarray<object>required—
    totalintegerrequired—
    cappedbooleanrequired—
    asOfstringrequiredISO-8601 instant of this read (one clock call per read). laneSummary and listLane are separate reads, each with its own asOf: a listLane total may drift from the summary's lane count when the firm changed in between — re-read laneSummary to reconcile.
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/mailbox.listOtherMailList the firm's OTHER MAIL: scanned letters and forwarded email that did not become a notice (held for a removed employer, not a notice, could not be read, no attachment, looked like junk), newest first, one page of 50 per source. Sender shown as a domain or PII-floored name; no body. emailChecked=false means forwarded email could not be read. Firm plane only (403 OTHER_MAIL_FIRM_ONLY otherwise). AUTONOMOUS — read-only.

List the firm's OTHER MAIL: scanned letters and forwarded email that did not become a notice (held for a removed employer, not a notice, could not be read, no attachment, looked like junk), newest first, one page of 50 per source. Sender shown as a domain or PII-floored name; no body. emailChecked=false means forwarded email could not be read. Firm plane only (403 OTHER_MAIL_FIRM_ONLY otherwise). AUTONOMOUS — read-only.

Bearer scopesnotices.read

Request body MailboxListOtherMailInputrequired

FieldTypeRequiredDescription
pageintegeroptional—

Responses

  • 200Successful invocationMailboxListOtherMailOutput
    FieldTypeRequiredDescription
    itemsarray<object>required—
    letterTotalintegerrequired—
    emailTotalintegerrequired—
    emailCheckedbooleanrequired—
    heldTotalintegerrequired—
    unknownAddressEmailTotalintegerrequired—
    unknownAddressResendToobjectrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/mailbox.releaseHeldRelease a scanned letter HELD for a removed employer: held -> received and a mailbox_ingest job, so it becomes a notice through the normal funnel. The employer must be restored first (409 EMPLOYER_STILL_REMOVED). 409 MAIL_NOT_OTHER_MAIL when the piece is not held (one body for unknown, foreign and wrong-state ids). Firm plane only. NOTIFY — firm triage write, audited.

Release a scanned letter HELD for a removed employer: held -> received and a mailbox_ingest job, so it becomes a notice through the normal funnel. The employer must be restored first (409 EMPLOYER_STILL_REMOVED). 409 MAIL_NOT_OTHER_MAIL when the piece is not held (one body for unknown, foreign and wrong-state ids). Firm plane only. NOTIFY — firm triage write, audited.

Bearer scopesnotices.write

Request body MailboxReleaseHeldInputrequired

FieldTypeRequiredDescription
mailIdstring · uuidrequired—

Responses

  • 200Successful invocationMailboxReleaseHeldOutput
    FieldTypeRequiredDescription
    idstring · uuidrequired—
    statusenum: "received"required—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/mailbox.markAsNotice'This is a notice' for a scanned letter that did not become one (not a notice, or could not be read): -> received and a mailbox_ingest job, so it becomes a notice through the normal funnel. Never writes notices directly. 409 MAIL_NOT_OTHER_MAIL when the piece is not in such a state (one body for unknown, foreign and wrong-state ids). Firm plane only. NOTIFY — firm triage write, audited.

'This is a notice' for a scanned letter that did not become one (not a notice, or could not be read): -> received and a mailbox_ingest job, so it becomes a notice through the normal funnel. Never writes notices directly. 409 MAIL_NOT_OTHER_MAIL when the piece is not in such a state (one body for unknown, foreign and wrong-state ids). Firm plane only. NOTIFY — firm triage write, audited.

Bearer scopesnotices.write

Request body MailboxMarkAsNoticeInputrequired

FieldTypeRequiredDescription
mailIdstring · uuidrequired—

Responses

  • 200Successful invocationMailboxMarkAsNoticeOutput
    FieldTypeRequiredDescription
    idstring · uuidrequired—
    statusenum: "received"required—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/mailbox.dismissOtherMail'Dismiss' for mail that did not become a notice. source='letter' (default): a scanned letter (not a notice, or could not be read); stamps dismissed_at, which starts the 7-day clock on the original scan; 'This is a notice' (mailbox.markAsNotice) undoes it. source='email': a forwarded email of this firm with no notice (no attachment, attachment not read, looked like junk, could not be read); stamps dismissed_at, which starts the 7-day clock on the email text. The status is unchanged. 409 MAIL_NOT_OTHER_MAIL when the row is not such mail or is already dismissed (one body for unknown, foreign, unknown-address and wrong-state ids). clear=true with source='email' is 'Keep this email': clears dismissed_at on a dismissed email (409 when it is not dismissed; clear on a letter is 409, use markAsNotice). Firm plane only. NOTIFY — firm triage write, audited.

'Dismiss' for mail that did not become a notice. source='letter' (default): a scanned letter (not a notice, or could not be read); stamps dismissed_at, which starts the 7-day clock on the original scan; 'This is a notice' (mailbox.markAsNotice) undoes it. source='email': a forwarded email of this firm with no notice (no attachment, attachment not read, looked like junk, could not be read); stamps dismissed_at, which starts the 7-day clock on the email text. The status is unchanged. 409 MAIL_NOT_OTHER_MAIL when the row is not such mail or is already dismissed (one body for unknown, foreign, unknown-address and wrong-state ids). clear=true with source='email' is 'Keep this email': clears dismissed_at on a dismissed email (409 when it is not dismissed; clear on a letter is 409, use markAsNotice). Firm plane only. NOTIFY — firm triage write, audited.

Bearer scopesnotices.write

Request body MailboxDismissOtherMailInputrequired

FieldTypeRequiredDescription
mailIdstring · uuidrequired—
sourceenum: "letter" | "email"optional—
clearbooleanoptional—

Responses

  • 200Successful invocationMailboxDismissOtherMailOutput
    FieldTypeRequiredDescription
    idstring · uuidrequired—
    dismissedAtstringrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/mailbox.listOverdueList every overdue notice of the CPA mailbox, whichever lane holds it (approve / input / active), deadline asc nulls last, then id — the same firm-wide population and overdue predicate laneSummary counts `overdueByLane` with. Unpaged; `capped` true means the population hit the row cap. Firm users only: an entity-scoped caller gets 403 MAILBOX_CPA_ONLY before any read; a firm whose CPA_MAILBOX flag is off gets 403 FEATURE_DISABLED. AUTONOMOUS — read-only.

List every overdue notice of the CPA mailbox, whichever lane holds it (approve / input / active), deadline asc nulls last, then id — the same firm-wide population and overdue predicate laneSummary counts `overdueByLane` with. Unpaged; `capped` true means the population hit the row cap. Firm users only: an entity-scoped caller gets 403 MAILBOX_CPA_ONLY before any read; a firm whose CPA_MAILBOX flag is off gets 403 FEATURE_DISABLED. AUTONOMOUS — read-only.

Bearer scopesnotices.read

Request body MailboxListOverdueInputrequired

FieldTypeRequiredDescription
entityIdstring · uuidoptionalNarrow to the notices attached to this employer (entity id). An id that is not one of the firm's employers returns the empty slice.
ownerenum: "mine" | "unassigned"optional'mine' = notices the caller owns; 'unassigned' = OPEN notices nobody owns (the same set as the work queue's Unassigned count). Omitted = every owner.

Responses

  • 200Successful invocationMailboxListOverdueOutput
    FieldTypeRequiredDescription
    rowsarray<object>requiredEvery notice past its governing deadline (the Overdue chip's predicate) in the approve / input / active lanes, deadline asc nulls last, then id. A notice of a removed employer is not listed.
    cappedbooleanrequired—
    asOfstringrequiredISO-8601 instant of this read (one clock call per read). laneSummary and listLane are separate reads, each with its own asOf: a listLane total may drift from the summary's lane count when the firm changed in between — re-read laneSummary to reconcile.
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

l2:mfa7

POST/v1/mfa.getMatrixRead the Kreto-curated MFA capability row for a portal: supported second-factor methods, the preferred waterfall rung, whether the portal's identity layer blocks VoIP numbers, and the Google-Voice canary state. Use when enrolling a credential to pick the right MFA strategy for that portal.

Read the Kreto-curated MFA capability row for a portal: supported second-factor methods, the preferred waterfall rung, whether the portal's identity layer blocks VoIP numbers, and the Google-Voice canary state. Use when enrolling a credential to pick the right MFA strategy for that portal.

Bearer scopesadmin.read

Request body MfaGetMatrixInputrequired

FieldTypeRequiredDescription
agencyIdstringrequired—

Responses

  • 200Successful invocationMfaGetMatrixOutput
    FieldTypeRequiredDescription
    entryobjectrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/mfa.resolveStrategyResolve which MFA rung the portal worker will drive for this account + portal: the account's per-portal override wins, else the curated matrix, else freeze_required (an unknown MFA method freezes the credential and requires a human — AC-13). A resolved 'sms' rung degrades to route_to_human while the relay is dormant.

Resolve which MFA rung the portal worker will drive for this account + portal: the account's per-portal override wins, else the curated matrix, else freeze_required (an unknown MFA method freezes the credential and requires a human — AC-13). A resolved 'sms' rung degrades to route_to_human while the relay is dormant.

Bearer scopesadmin.read

Request body MfaResolveStrategyInputrequired

FieldTypeRequiredDescription
agencyIdstringrequired—

Responses

  • 200Successful invocationMfaResolveStrategyOutput
    FieldTypeRequiredDescription
    strategyone of (enum: "totp" | "email" | "sms" | "route_to_human" | … · enum: "freeze_required")required—
    sourceenum: "override" | "matrix" | "unresolved"required—
    voipBlockedbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/mfa.listHandoffsList the account's route-to-human MFA handoffs, newest first: any pending request (a portal session is waiting for a human to supply a code right now) plus the recent ledger (fulfilled / expired / escalated). Code values are never returned — codes are write-only.

List the account's route-to-human MFA handoffs, newest first: any pending request (a portal session is waiting for a human to supply a code right now) plus the recent ledger (fulfilled / expired / escalated). Code values are never returned — codes are write-only.

Bearer scopesadmin.read

Request body MfaListHandoffsInputrequired

application/json

Responses

  • 200Successful invocationMfaListHandoffsOutput
    FieldTypeRequiredDescription
    handoffsarray<object>required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/mfa.submitHandoffCodeSupply the MFA code a waiting portal session asked for (route-to-human). Fulfills the pending handoff so the portal worker can inject the code; refused once the session window has lapsed. UI-only.

Supply the MFA code a waiting portal session asked for (route-to-human). Fulfills the pending handoff so the portal worker can inject the code; refused once the session window has lapsed. UI-only.

Bearer scopesadmin.write

Request body MfaSubmitHandoffCodeInputrequired

FieldTypeRequiredDescription
handoffIdstringrequired—
codestringrequired—

Responses

  • 200Successful invocationMfaSubmitHandoffCodeOutput
    FieldTypeRequiredDescription
    fulfilledbooleanrequired—
    windowLapsedbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/mfa.listRelayNumbersList the account's leased receive-only relay numbers (one per credential, per the locked per-client model) and whether the SMS relay is currently enabled. Released numbers stay listed for the audit trail (AC-19).

List the account's leased receive-only relay numbers (one per credential, per the locked per-client model) and whether the SMS relay is currently enabled. Released numbers stay listed for the audit trail (AC-19).

Bearer scopesadmin.read

Request body MfaListRelayNumbersInputrequired

application/json

Responses

  • 200Successful invocationMfaListRelayNumbersOutput
    FieldTypeRequiredDescription
    numbersarray<object>required—
    smsEnabledbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/mfa.leaseRelayNumberLease a receive-only relay number for one credential (the SMS rung of the waterfall). Dormant: refuses with sms_disabled until OTP_SMS_ENABLED and a provider account exist. UI-only.

Lease a receive-only relay number for one credential (the SMS rung of the waterfall). Dormant: refuses with sms_disabled until OTP_SMS_ENABLED and a provider account exist. UI-only.

Bearer scopesadmin.write

Request body MfaLeaseRelayNumberInputrequired

FieldTypeRequiredDescription
credentialIdstringrequired—

Responses

  • 200Successful invocationMfaLeaseRelayNumberOutput
    FieldTypeRequiredDescription
    numberobjectrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/mfa.releaseRelayNumberRelease a leased relay number (AC-19: credential revocation releases its relay number; the row is kept for the audit trail). UI-only.

Release a leased relay number (AC-19: credential revocation releases its relay number; the row is kept for the audit trail). UI-only.

Bearer scopesadmin.write

Request body MfaReleaseRelayNumberInputrequired

FieldTypeRequiredDescription
numberIdstringrequired—

Responses

  • 200Successful invocationMfaReleaseRelayNumberOutput
    FieldTypeRequiredDescription
    releasedbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

l2:notice-intake13

POST/v1/notice-intake.ingestEmailWrap the existing /api/webhooks/inbound-email logic into a typed service method. Slug-based routing → domain fallback → unmatched queue. AUTONOMOUS — system-driven, idempotent at the inbound_emails row level.

Wrap the existing /api/webhooks/inbound-email logic into a typed service method. Slug-based routing → domain fallback → unmatched queue. AUTONOMOUS — system-driven, idempotent at the inbound_emails row level.

Bearer scopesnotices.write

Request body NoticeIntakeIngestEmailInputrequired

FieldTypeRequiredDescription
resendPayloadobjectrequired—

Responses

  • 200Successful invocationNoticeIntakeIngestEmailOutput
    FieldTypeRequiredDescription
    inboundEmailIdstring · uuidrequired—
    matchMethodenum: "slug" | "domain" | "none"required—
    matchedTenantIdstring · uuidrequired—
    matchedEntityIdstring · uuidrequired—
    unmatchedbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/notice-intake.classifyNoticeWrap agentClassify with a typed service-method surface. Reads the notice's raw_text + state, calls the LLM classifier, persists notice_type_classified + classification_confidence + notice_facts. AUTONOMOUS — read-modify, recoverable.

Wrap agentClassify with a typed service-method surface. Reads the notice's raw_text + state, calls the LLM classifier, persists notice_type_classified + classification_confidence + notice_facts. AUTONOMOUS — read-modify, recoverable.

Bearer scopesnotices.read

Request body NoticeIntakeClassifyNoticeInputrequired

FieldTypeRequiredDescription
noticeIdstring · uuidrequired—

Responses

  • 200Successful invocationNoticeIntakeClassifyNoticeOutput
    FieldTypeRequiredDescription
    noticeIdstring · uuidrequired—
    noticeTypestringrequired—
    confidencenumberrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/notice-intake.ingestNoticeWrap agentIngest with a typed service-method surface. Runs OCR / text-extraction (PDF text layer -> Claude Vision OCR) + SHA-256 dedup, persists raw_text + sha256_hash + pipeline_status. AUTONOMOUS — read-modify, recoverable.

Wrap agentIngest with a typed service-method surface. Runs OCR / text-extraction (PDF text layer -> Claude Vision OCR) + SHA-256 dedup, persists raw_text + sha256_hash + pipeline_status. AUTONOMOUS — read-modify, recoverable.

Bearer scopesnotices.write

Request body NoticeIntakeIngestNoticeInputrequired

FieldTypeRequiredDescription
noticeIdstring · uuidrequired—

Responses

  • 200Successful invocationNoticeIntakeIngestNoticeOutput
    FieldTypeRequiredDescription
    noticeIdstring · uuidrequired—
    pipelineStatusstringrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/notice-intake.validateNoticeWrap agentValidate with a typed service-method surface. Runs SUI / rate verification + payroll cross-reference, flags protest + potential savings, persists pipeline_status + protest_recommended. AUTONOMOUS — read-modify, recoverable.

Wrap agentValidate with a typed service-method surface. Runs SUI / rate verification + payroll cross-reference, flags protest + potential savings, persists pipeline_status + protest_recommended. AUTONOMOUS — read-modify, recoverable.

Bearer scopesnotices.read

Request body NoticeIntakeValidateNoticeInputrequired

FieldTypeRequiredDescription
noticeIdstring · uuidrequired—

Responses

  • 200Successful invocationNoticeIntakeValidateNoticeOutput
    FieldTypeRequiredDescription
    noticeIdstring · uuidrequired—
    pipelineStatusstringrequired—
    protestRecommendedbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/notice-intake.setNoticeDeadlineWrap agentDeadline with a typed service-method surface. Computes the response deadline + protest clock from the notice's classification + per-state rules, persists deadline + protest_deadline + pipeline_status. AUTONOMOUS — read-modify, recoverable.

Wrap agentDeadline with a typed service-method surface. Computes the response deadline + protest clock from the notice's classification + per-state rules, persists deadline + protest_deadline + pipeline_status. AUTONOMOUS — read-modify, recoverable.

Bearer scopesnotices.write

Request body NoticeIntakeSetNoticeDeadlineInputrequired

FieldTypeRequiredDescription
noticeIdstring · uuidrequired—

Responses

  • 200Successful invocationNoticeIntakeSetNoticeDeadlineOutput
    FieldTypeRequiredDescription
    noticeIdstring · uuidrequired—
    pipelineStatusstringrequired—
    deadlinestringrequired—
    protestDeadlinestringrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/notice-intake.reclassifyNoticeOperator-driven re-classification. Re-runs the classifier on the notice as it stands (its pipeline status is NOT reset). When the classifier's type differs from the stored one, the change is chained old → new and ONE continuation job is queued — the same job the CPA's set-type door queues — which re-runs the later steps in order and stops at the next human gate; the response names that job. Same type: nothing is queued. Refused (409 TYPE_CORRECTION_REFUSED, nothing written) once a filing, an outside response, a signature request or an approval is on record, or the notice is closed, resolved, dismissed, rejected, verified, agency-responded, SIDES-exempt, failed or OCR-only. NOTIFY — operator override, audit-visible. Reason field NOT redacted (operator-authored; if PII leaks, it's the operator's responsibility — flagged in README).

Operator-driven re-classification. Re-runs the classifier on the notice as it stands (its pipeline status is NOT reset). When the classifier's type differs from the stored one, the change is chained old → new and ONE continuation job is queued — the same job the CPA's set-type door queues — which re-runs the later steps in order and stops at the next human gate; the response names that job. Same type: nothing is queued. Refused (409 TYPE_CORRECTION_REFUSED, nothing written) once a filing, an outside response, a signature request or an approval is on record, or the notice is closed, resolved, dismissed, rejected, verified, agency-responded, SIDES-exempt, failed or OCR-only. NOTIFY — operator override, audit-visible. Reason field NOT redacted (operator-authored; if PII leaks, it's the operator's responsibility — flagged in README).

Bearer scopesnotices.write

Request body NoticeIntakeReclassifyNoticeInputrequired

FieldTypeRequiredDescription
noticeIdstring · uuidrequired—
reasonstringrequired—

Responses

  • 200Successful invocationNoticeIntakeReclassifyNoticeOutput
    FieldTypeRequiredDescription
    noticeIdstring · uuidrequired—
    noticeTypestringrequired—
    confidencenumberrequired—
    cascadedAgentsarray<string>required—
    continuationobjectrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/notice-intake.routeNoticeAssign a notice to an employer entity. Mirrors entity-match-resolve route's 'confirmed' action. Pure data update — does NOT advance pipeline_status, does NOT cascade to downstream agents. NOTIFY — operator override, audit-visible.

Assign a notice to an employer entity. Mirrors entity-match-resolve route's 'confirmed' action. Pure data update — does NOT advance pipeline_status, does NOT cascade to downstream agents. NOTIFY — operator override, audit-visible.

Bearer scopesnotices.write

Request body NoticeIntakeRouteNoticeInputrequired

FieldTypeRequiredDescription
noticeIdstring · uuidrequired—
entityIdstring · uuidrequired—

Responses

  • 200Successful invocationNoticeIntakeRouteNoticeOutput
    FieldTypeRequiredDescription
    noticeIdstring · uuidrequired—
    previousEntityIdstring · uuidrequired—
    newEntityIdstring · uuidrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/notice-intake.flagJunkMark a notice as junk. Sets is_junk=true + pipeline_status=DISMISSED. Refused (409 NOTICE_PAST_TRIAGE, nothing written) once the notice is approved, filed, actioned, resolved or dismissed. NOTIFY — recoverable (unflagJunk restores the prior pipeline_status); no compliance impact from a wrong call. Compliance-impact gate is at submitProtest (Phase 3).

Mark a notice as junk. Sets is_junk=true + pipeline_status=DISMISSED. Refused (409 NOTICE_PAST_TRIAGE, nothing written) once the notice is approved, filed, actioned, resolved or dismissed. NOTIFY — recoverable (unflagJunk restores the prior pipeline_status); no compliance impact from a wrong call. Compliance-impact gate is at submitProtest (Phase 3).

Bearer scopesnotices.write

Request body NoticeIntakeFlagJunkInputrequired

FieldTypeRequiredDescription
noticeIdstring · uuidrequired—
reasonenum: "marketing" | "portal_automation" | "bounce_notification" | "newsletter" | …required—
reasonDetailstringoptional—

Responses

  • 200Successful invocationNoticeIntakeFlagJunkOutput
    FieldTypeRequiredDescription
    noticeIdstring · uuidrequired—
    isJunkenum: truerequired—
    pipelineStatusenum: "DISMISSED"required—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/notice-intake.unflagJunkReverse a junk flag. Sets is_junk=false and restores pipeline_status to its value before the flag (from the flag's audit before-image); PENDING when no flag audit row exists, reported as restoreSource=default_pending. Refused (409 NOTICE_NOT_JUNK) on a notice that is not flagged. NOTIFY — operator override; the reason field is the operator's audit-rationale (10–500 chars).

Reverse a junk flag. Sets is_junk=false and restores pipeline_status to its value before the flag (from the flag's audit before-image); PENDING when no flag audit row exists, reported as restoreSource=default_pending. Refused (409 NOTICE_NOT_JUNK) on a notice that is not flagged. NOTIFY — operator override; the reason field is the operator's audit-rationale (10–500 chars).

Bearer scopesnotices.write

Request body NoticeIntakeUnflagJunkInputrequired

FieldTypeRequiredDescription
noticeIdstring · uuidrequired—
reasonstringrequired—

Responses

  • 200Successful invocationNoticeIntakeUnflagJunkOutput
    FieldTypeRequiredDescription
    noticeIdstring · uuidrequired—
    isJunkenum: falserequired—
    pipelineStatusenum: "PENDING" | "INGESTED" | "CLASSIFIED" | "VALIDATED" | …required—
    restoreSourceenum: "flag_audit" | "default_pending"required—
    readingRequeuedbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/notice-intake.draftNoticeWrap agentDraft with a typed service-method surface. Generates the protest-letter draft for the notice (persists the draft row + `protest_draft_generated_at`) and returns a typed acknowledgment. Advances `pipeline_status` to 'READY_FOR_REVIEW' UNLESS the payroll gate holds the notice at 'AWAITING_PAYROLL' — a notice that needs payroll evidence and has none keeps its phase and the draft is written BLOCKED. NOTIFY — read-modify, a human reviews the draft before any external send.

Wrap agentDraft with a typed service-method surface. Generates the protest-letter draft for the notice (persists the draft row + `protest_draft_generated_at`) and returns a typed acknowledgment. Advances `pipeline_status` to 'READY_FOR_REVIEW' UNLESS the payroll gate holds the notice at 'AWAITING_PAYROLL' — a notice that needs payroll evidence and has none keeps its phase and the draft is written BLOCKED. NOTIFY — read-modify, a human reviews the draft before any external send.

Bearer scopesnotices.write

Request body NoticeIntakeDraftNoticeInputrequired

FieldTypeRequiredDescription
noticeIdstring · uuidrequired—

Responses

  • 200Successful invocationNoticeIntakeDraftNoticeOutput
    FieldTypeRequiredDescription
    noticeIdstring · uuidrequired—
    hasDraftbooleanrequired—
    draftLengthintegerrequired—
    pipelineStatusstringrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/notice-intake.approveNoticeOperator marks the generated draft as approved + ready to file. Records approved_at + approved_by + cpa_action='approved'. Does NOT transition pipeline_status or send anything externally. NOTIFY — reversible; fileNotice is the external-impact gate.

Operator marks the generated draft as approved + ready to file. Records approved_at + approved_by + cpa_action='approved'. Does NOT transition pipeline_status or send anything externally. NOTIFY — reversible; fileNotice is the external-impact gate.

Bearer scopesnotices.write

Request body NoticeIntakeApproveNoticeInputrequired

FieldTypeRequiredDescription
noticeIdstring · uuidrequired—
notestringoptional—

Responses

  • 200Successful invocationNoticeIntakeApproveNoticeOutput
    FieldTypeRequiredDescription
    noticeIdstring · uuidrequired—
    approvedAtstringrequired—
    cpaActionenum: "approved"required—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/notice-intake.fileNoticeOperator records that the approved response has been transmitted to the agency. Stores filed_at + filed_by + filing_method + confirmation_number, advances status to 'filed' (via the trigger-mandated two-step transition) and pipeline_status to ACTIONED. Requires an approval that is EVIDENCED on the notice's audit trail, not merely asserted by cpa_action (the approve-before-file invariant). NOTIFY — records external act; Kreto itself does NOT mail/fax/post in this op.

Operator records that the approved response has been transmitted to the agency. Stores filed_at + filed_by + filing_method + confirmation_number, advances status to 'filed' (via the trigger-mandated two-step transition) and pipeline_status to ACTIONED. Requires an approval that is EVIDENCED on the notice's audit trail, not merely asserted by cpa_action (the approve-before-file invariant). NOTIFY — records external act; Kreto itself does NOT mail/fax/post in this op.

Bearer scopesnotices.write

Request body NoticeIntakeFileNoticeInputrequired

FieldTypeRequiredDescription
noticeIdstring · uuidrequired—
filingMethodenum: "mail" | "fax" | "online_portal" | "email" | …required—
notestringoptional—
confirmationNumberstringoptional—

Responses

  • 200Successful invocationNoticeIntakeFileNoticeOutput
    FieldTypeRequiredDescription
    noticeIdstring · uuidrequired—
    filedAtstringrequired—
    filingMethodenum: "mail" | "fax" | "online_portal" | "email" | …required—
    pipelineStatusenum: "ACTIONED"required—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/notice-intake.escalateNoticeFlag a notice for senior-CPA review. Sets is_escalated=true + escalated_at + escalated_by + escalation_reason. Does NOT change pipeline_status (escalation is orthogonal). Idempotent — re-calling updates the reason + timestamp. NOTIFY — flag-only; no external impact.

Flag a notice for senior-CPA review. Sets is_escalated=true + escalated_at + escalated_by + escalation_reason. Does NOT change pipeline_status (escalation is orthogonal). Idempotent — re-calling updates the reason + timestamp. NOTIFY — flag-only; no external impact.

Bearer scopesnotices.write

Request body NoticeIntakeEscalateNoticeInputrequired

FieldTypeRequiredDescription
noticeIdstring · uuidrequired—
reasonstringrequired—

Responses

  • 200Successful invocationNoticeIntakeEscalateNoticeOutput
    FieldTypeRequiredDescription
    noticeIdstring · uuidrequired—
    isEscalatedenum: truerequired—
    escalatedAtstringrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

l2:notices10

POST/v1/notices.submitDraftFeedbackRecord thumbs-up/down feedback on a notice response draft. Asserts the notice and draft both belong to the caller's tenant, inserts a draft_feedback row, and emits a product-analytics event. NOTIFY — operator-driven, audit-visible.

Record thumbs-up/down feedback on a notice response draft. Asserts the notice and draft both belong to the caller's tenant, inserts a draft_feedback row, and emits a product-analytics event. NOTIFY — operator-driven, audit-visible.

Bearer scopesnotices.write

Request body NoticesSubmitDraftFeedbackInputrequired

FieldTypeRequiredDescription
noticeIdstring · uuidrequired—
draftIdstring · uuidrequired—
ratingenum: "thumbs_up" | "thumbs_down"required—
commentstringoptional—

Responses

  • 200Successful invocationNoticesSubmitDraftFeedbackOutput
    FieldTypeRequiredDescription
    successenum: truerequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/notices.resolveEntityMatchResolve a notice's fuzzy entity match: confirm an existing entity, or create a new entity under the notice's tenant and assign it. Tenant-scoped with defense-in-depth assertions. NOTIFY — operator-driven, audit-visible.

Resolve a notice's fuzzy entity match: confirm an existing entity, or create a new entity under the notice's tenant and assign it. Tenant-scoped with defense-in-depth assertions. NOTIFY — operator-driven, audit-visible.

Bearer scopesnotices.write

Request body NoticesResolveEntityMatchInputrequired

application/json

Responses

  • 200Successful invocationNoticesResolveEntityMatchOutput
    FieldTypeRequiredDescription
    successenum: truerequired—
    entityIdstring · uuidrequired—
    actionenum: "confirmed" | "create_new"required—
    entityNamestringoptional—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/notices.linkLink an agency-response notice to its parent (original) notice and record the response type (acceptance/counter/additional_info_request/rejection). Sets parent_notice_id + agency_response_type on the response notice. Both notices must belong to the caller's tenant. AUTONOMOUS — internal edge write, recoverable, no agency impact.

Link an agency-response notice to its parent (original) notice and record the response type (acceptance/counter/additional_info_request/rejection). Sets parent_notice_id + agency_response_type on the response notice. Both notices must belong to the caller's tenant. AUTONOMOUS — internal edge write, recoverable, no agency impact.

Bearer scopesnotices.write

Request body NoticesLinkInputrequired

FieldTypeRequiredDescription
noticeIdstring · uuidrequired—
parentNoticeIdstring · uuidrequired—
agencyResponseTypeenum: "acceptance" | "counter" | "additional_info_request" | "rejection"required—

Responses

  • 200Successful invocationNoticesLinkOutput
    FieldTypeRequiredDescription
    successenum: truerequired—
    noticeIdstring · uuidrequired—
    parentNoticeIdstring · uuidrequired—
    agencyResponseTypeenum: "acceptance" | "counter" | "additional_info_request" | "rejection"required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/notices.deadlineSweepPreview the deadline escalations that would fire for your tenant — which compliance-notice deadlines are escalating and who would be notified. Read-only plan over the same ladder + candidate filter the hourly cron uses; sends nothing and writes nothing (the cron does the actual emails/push/Slack). Use to surface what deadlines need attention now. NOTIFY — surfaces work for human attention, no external impact.

Preview the deadline escalations that would fire for your tenant — which compliance-notice deadlines are escalating and who would be notified. Read-only plan over the same ladder + candidate filter the hourly cron uses; sends nothing and writes nothing (the cron does the actual emails/push/Slack). Use to surface what deadlines need attention now. NOTIFY — surfaces work for human attention, no external impact.

Bearer scopesnotices.write

Request body NoticesDeadlineSweepInputrequired

FieldTypeRequiredDescription
entityIdstring · uuidoptional—
horizonDaysintegeroptional—

Responses

  • 200Successful invocationNoticesDeadlineSweepOutput
    FieldTypeRequiredDescription
    asOfstringrequired—
    scannedNoticesintegerrequired—
    escalationsarray<object>required—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/notices.crossReferenceQueue a cross-reference of a notice against the employer's payroll (re-runs the discrepancy measurement). Refused with 409 ENTITY_UNCONFIRMED while the notice's employer is unconfirmed. Idempotent per notice per day. Tenant- and employer-scoped. NOTIFY — recomputation on the job queue, audited, no agency impact.

Queue a cross-reference of a notice against the employer's payroll (re-runs the discrepancy measurement). Refused with 409 ENTITY_UNCONFIRMED while the notice's employer is unconfirmed. Idempotent per notice per day. Tenant- and employer-scoped. NOTIFY — recomputation on the job queue, audited, no agency impact.

Bearer scopesnotices.write

Request body NoticesCrossReferenceInputrequired

FieldTypeRequiredDescription
noticeIdstring · uuidrequired—

Responses

  • 200Successful invocationNoticesCrossReferenceOutput
    FieldTypeRequiredDescription
    enqueuedenum: truerequired—
    noticeIdstring · uuidrequired—
    jobIdstring · uuidrequired—
    deduplicatedbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/notices.getDecisionCardRead a notice's decision card: four rows (Read · Checked · Proposed · Decision), each with a state, labelled figures and a link to the page that holds the evidence. Read-only — approve, decline and file stay at their existing doors. Tenant- and employer-scoped; an employer caller gets the employer projection. AUTONOMOUS.

Read a notice's decision card: four rows (Read · Checked · Proposed · Decision), each with a state, labelled figures and a link to the page that holds the evidence. Read-only — approve, decline and file stay at their existing doors. Tenant- and employer-scoped; an employer caller gets the employer projection. AUTONOMOUS.

Bearer scopesnotices.read

Request body NoticesGetDecisionCardInputrequired

FieldTypeRequiredDescription
noticeIdstring · uuidrequired—

Responses

  • 200Successful invocationNoticesGetDecisionCardOutput
    FieldTypeRequiredDescription
    noticeIdstring · uuidrequired—
    planeenum: "cpa" | "employer"required—
    currentenum: "read" | "checked" | "proposed" | "decision"required—
    figureHeldbooleanrequired—
    rowsobjectrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/notices.getExperienceRequestRead a notice's open experience-data request — the experience-statement figures the employer is asked for, the tax year and the state — and whether the caller may answer it (only the employer's owner). `request` is null when none is open. Tenant- and employer-scoped; read-only. AUTONOMOUS.

Read a notice's open experience-data request — the experience-statement figures the employer is asked for, the tax year and the state — and whether the caller may answer it (only the employer's owner). `request` is null when none is open. Tenant- and employer-scoped; read-only. AUTONOMOUS.

Bearer scopesnotices.read

Request body NoticesGetExperienceRequestInputrequired

FieldTypeRequiredDescription
noticeIdstring · uuidrequired—

Responses

  • 200Successful invocationNoticesGetExperienceRequestOutput
    FieldTypeRequiredDescription
    requestobjectrequired—
    canAnswerbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/notices.submitExperienceFiguresThe employer's owner states the experience-statement figures an open experience-data request asks for (only the asked keys; average annual payroll above 0). Stored as employer-stated notice facts — never as a Kreto-verified figure — then the rate decision re-runs through the job queue. Replaying the same answer is deduplicated; a different answer to an answered request is refused (409). Owner-only, tenant- and employer-scoped. NOTIFY — the CPA sees the result.

The employer's owner states the experience-statement figures an open experience-data request asks for (only the asked keys; average annual payroll above 0). Stored as employer-stated notice facts — never as a Kreto-verified figure — then the rate decision re-runs through the job queue. Replaying the same answer is deduplicated; a different answer to an answered request is refused (409). Owner-only, tenant- and employer-scoped. NOTIFY — the CPA sees the result.

Bearer scopesnotices.write

Request body NoticesSubmitExperienceFiguresInputrequired

FieldTypeRequiredDescription
noticeIdstring · uuidrequired—
requestIdstringrequired—
figuresobjectrequired—

Responses

  • 200Successful invocationNoticesSubmitExperienceFiguresOutput
    FieldTypeRequiredDescription
    answeredenum: truerequired—
    requestIdstringrequired—
    deduplicatedbooleanrequired—
    rerunobjectrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/notices.confirmRateWithAgencyClose a range-held SUI rate notice (the rate is outside Kreto's recorded range for the state and no protest is recommended) because the agency confirmed the rate: no letter is sent, the notice closes as 'Confirmed with the agency' through the lifecycle writer, and the close is recorded on the audit chain with the confirming user. Refused (409) when the notice is not range-held, already closed, or has an approved protest. A repeated call on a confirmed notice replays. Tenant- and employer-scoped; a person must call it. NOTIFY.

Close a range-held SUI rate notice (the rate is outside Kreto's recorded range for the state and no protest is recommended) because the agency confirmed the rate: no letter is sent, the notice closes as 'Confirmed with the agency' through the lifecycle writer, and the close is recorded on the audit chain with the confirming user. Refused (409) when the notice is not range-held, already closed, or has an approved protest. A repeated call on a confirmed notice replays. Tenant- and employer-scoped; a person must call it. NOTIFY.

Bearer scopesnotices.write

Request body NoticesConfirmRateWithAgencyInputrequired

FieldTypeRequiredDescription
noticeIdstring · uuidrequired—

Responses

  • 200Successful invocationNoticesConfirmRateWithAgencyOutput
    FieldTypeRequiredDescription
    closedenum: truerequired—
    replayedbooleanrequired—
    verdictenum: "agency_confirmed"required—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/notices.requestExperienceFiguresAsk the employer's owner for the experience-statement figures that check an in-range SUI rate increase Kreto could only check against the state's range. Opens the same experience-data request Kreto fires on its own (one email to the employer's owners, the request on the employer's notice). Idempotent: an open request is returned, never re-sent. Refused (409) when the figures are already on file, when the rate does not need them, or when the employer has no owner account. CPA only; tenant- and employer-scoped. NOTIFY.

Ask the employer's owner for the experience-statement figures that check an in-range SUI rate increase Kreto could only check against the state's range. Opens the same experience-data request Kreto fires on its own (one email to the employer's owners, the request on the employer's notice). Idempotent: an open request is returned, never re-sent. Refused (409) when the figures are already on file, when the rate does not need them, or when the employer has no owner account. CPA only; tenant- and employer-scoped. NOTIFY.

Bearer scopesnotices.write

Request body NoticesRequestExperienceFiguresInputrequired

FieldTypeRequiredDescription
noticeIdstring · uuidrequired—

Responses

  • 200Successful invocationNoticesRequestExperienceFiguresOutput
    FieldTypeRequiredDescription
    requestedenum: truerequired—
    deduplicatedbooleanrequired—
    requestobjectrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

l2:notifications2

POST/v1/notifications.listList the account's vault notifications, newest first: pending time-critical items (awaiting a human acknowledgement) and the informational ledger (rotations completed, credentials frozen, health changes). Content is reference-and-action only — never a secret, code, or amount.

List the account's vault notifications, newest first: pending time-critical items (awaiting a human acknowledgement) and the informational ledger (rotations completed, credentials frozen, health changes). Content is reference-and-action only — never a secret, code, or amount.

Bearer scopesadmin.read

Request body NotificationsListInputrequired

FieldTypeRequiredDescription
statusenum: "pending" | "acknowledged" | "expired"optional—

Responses

  • 200Successful invocationNotificationsListOutput
    FieldTypeRequiredDescription
    notificationsarray<object>required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/notifications.ackAcknowledge a time-critical notification, stopping its escalation. Escalate-until-ack means until a HUMAN acks — UI-only by design.

Acknowledge a time-critical notification, stopping its escalation. Escalate-until-ack means until a HUMAN acks — UI-only by design.

Bearer scopesadmin.write

Request body NotificationsAckInputrequired

FieldTypeRequiredDescription
notificationIdstringrequired—

Responses

  • 200Successful invocationNotificationsAckOutput
    FieldTypeRequiredDescription
    acknowledgedbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

l2:payments5

POST/v1/payments.remitRequest a payment from a stored payment/bank credential. Creates an approval request ONLY — money never moves without a human approving this exact request (the hard floor no configuration can relax). The responsible humans are notified WITHOUT the amount; they first see the figure on the approval screen.

Request a payment from a stored payment/bank credential. Creates an approval request ONLY — money never moves without a human approving this exact request (the hard floor no configuration can relax). The responsible humans are notified WITHOUT the amount; they first see the figure on the approval screen.

Bearer scopesadmin.write

Request body PaymentsRemitInputrequired

FieldTypeRequiredDescription
credentialIdstringrequired—
payeestringrequired—
amountCentsintegerrequired—
currencystringoptional—
memostringoptional—

Responses

  • 200Successful invocationPaymentsRemitOutput
    FieldTypeRequiredDescription
    requestobjectrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/payments.listList the account's payment approval requests, newest first. Use to surface what is awaiting human approval; approving is UI-only.

List the account's payment approval requests, newest first. Use to surface what is awaiting human approval; approving is UI-only.

Bearer scopesadmin.read

Request body PaymentsListInputrequired

FieldTypeRequiredDescription
statusenum: "requested" | "approved" | "rejected" | "expired"optional—

Responses

  • 200Successful invocationPaymentsListOutput
    FieldTypeRequiredDescription
    requestsarray<object>required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/payments.approveApprove a pending payment request — the money-movement hard floor. Requires a human actor and a typed amount that matches the request exactly; the approval (approver identity + amount) is sealed into the tamper-evident audit chain and becomes the authorization. UI-only; no configuration can bypass this step.

Approve a pending payment request — the money-movement hard floor. Requires a human actor and a typed amount that matches the request exactly; the approval (approver identity + amount) is sealed into the tamper-evident audit chain and becomes the authorization. UI-only; no configuration can bypass this step.

Bearer scopesadmin.write

Request body PaymentsApproveInputrequired

FieldTypeRequiredDescription
approvalIdstringrequired—
confirmAmountCentsintegerrequired—

Responses

  • 200Successful invocationPaymentsApproveOutput
    FieldTypeRequiredDescription
    requestobjectrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/payments.rejectReject a pending payment request. The rejection is recorded on the audit chain. UI-only.

Reject a pending payment request. The rejection is recorded on the audit chain. UI-only.

Bearer scopesadmin.write

Request body PaymentsRejectInputrequired

FieldTypeRequiredDescription
approvalIdstringrequired—

Responses

  • 200Successful invocationPaymentsRejectOutput
    FieldTypeRequiredDescription
    requestobjectrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/payments.executeExecute an APPROVED payment. DORMANT: Kreto has no payment rails — this refuses payment_rails_unavailable for every request, approved or not. When an ACH/provider integration lands it slots in here; the approval floor above it does not change. UI-only.

Execute an APPROVED payment. DORMANT: Kreto has no payment rails — this refuses payment_rails_unavailable for every request, approved or not. When an ACH/provider integration lands it slots in here; the approval floor above it does not change. UI-only.

Bearer scopesadmin.write

Request body PaymentsExecuteInputrequired

FieldTypeRequiredDescription
approvalIdstringrequired—

Responses

  • 200Successful invocationPaymentsExecuteOutput
    FieldTypeRequiredDescription
    executedenum: falserequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

l2:payroll4

POST/v1/payroll.connectInitiate a payroll connection for an entity. Returns 'connected' (already linked), 'consent_required' (the employer must accept the KRT-768 consent in the UI first — never recorded on their behalf), or 'pending' (consent present; a pending connection intent is recorded and the employer completes the provider OAuth in the UI). Use to unblock a notice that needs payroll. NOTIFY — records the intent; the actual link is the employer's OAuth action.

Initiate a payroll connection for an entity. Returns 'connected' (already linked), 'consent_required' (the employer must accept the KRT-768 consent in the UI first — never recorded on their behalf), or 'pending' (consent present; a pending connection intent is recorded and the employer completes the provider OAuth in the UI). Use to unblock a notice that needs payroll. NOTIFY — records the intent; the actual link is the employer's OAuth action.

Bearer scopespayroll.write

Request body PayrollConnectInputrequired

FieldTypeRequiredDescription
entityIdstring · uuidrequired—
providerenum: "gusto" | "adp_run" | "paychex" | "paylocity" | …required—

Responses

  • 200Successful invocationPayrollConnectOutput
    FieldTypeRequiredDescription
    entityIdstring · uuidrequired—
    providerenum: "gusto" | "adp_run" | "paychex" | "paylocity" | …required—
    statusenum: "connected" | "consent_required" | "pending"required—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `payroll.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/payroll.gateStatusAnswer 'what is Kreto waiting on for this notice?' — the AWAITING_PAYROLL gate's state for one notice, with NO side effect. Returns the same discriminants payroll.releaseGate uses ('released' = the gate would open now, 'needs_connection' = no active payroll connection, 'needs_input' = connected but the notice's state + tax year are not covered yet, 'not_gated' = the notice is not held at the payroll gate, 'unreadable' = the payroll on file could not be read just now; this is not an answer about whether payroll is on file), plus whether a connection is active, the tax year coverage was checked for, and the named coverage gap when the probe could name one. Both the employer panel and the release itself resolve through this same verdict, so the banner and the decision cannot disagree. AUTONOMOUS — pure read. Refuses with NOTICE_NOT_FOUND (404) for unknown, other-tenant or out-of-scope notices and FEATURE_DISABLED (403) when FF_PAYROLL_GATE_AUTO_RELEASE is off.

Answer 'what is Kreto waiting on for this notice?' — the AWAITING_PAYROLL gate's state for one notice, with NO side effect. Returns the same discriminants payroll.releaseGate uses ('released' = the gate would open now, 'needs_connection' = no active payroll connection, 'needs_input' = connected but the notice's state + tax year are not covered yet, 'not_gated' = the notice is not held at the payroll gate, 'unreadable' = the payroll on file could not be read just now; this is not an answer about whether payroll is on file), plus whether a connection is active, the tax year coverage was checked for, and the named coverage gap when the probe could name one. Both the employer panel and the release itself resolve through this same verdict, so the banner and the decision cannot disagree. AUTONOMOUS — pure read. Refuses with NOTICE_NOT_FOUND (404) for unknown, other-tenant or out-of-scope notices and FEATURE_DISABLED (403) when FF_PAYROLL_GATE_AUTO_RELEASE is off.

Bearer scopespayroll.read

Request body PayrollGateStatusInputrequired

FieldTypeRequiredDescription
noticeIdstring · uuidrequired—

Responses

  • 200Successful invocationPayrollGateStatusOutput
    FieldTypeRequiredDescription
    noticeIdstring · uuidrequired—
    entityIdstring · uuidrequired—
    reasonenum: "released" | "needs_connection" | "needs_input" | "not_gated" | …required—
    gatedbooleanrequired—
    releasablebooleanrequired—
    connectedbooleanrequired—
    payrollOnFilebooleanrequired—
    hasStoredPayrollbooleanrequired—
    payrollReadOkbooleanrequired—
    providerstringrequired—
    coverageYearintegerrequired—
    coveragePeriodstringrequired—
    coverageGapstringrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `payroll.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/payroll.listGatedNoticesList an employer entity's notices currently held at the AWAITING_PAYROLL gate, each with its own gate verdict ('released' | 'needs_connection' | 'needs_input' | 'unreadable' — 'unreadable' = the payroll on file could not be read just now; this is not an answer about whether payroll is on file), the tax year checked and any named coverage gap — the 'Notices waiting on this payroll (N)' list on the employer's payroll page. Also reports whether an active payroll connection exists for the entity. Capped at 20 notices per call (`truncated` says when there are more); the remainder release on the entity's next payroll sync. AUTONOMOUS — pure read. Refuses with ENTITY_NOT_FOUND (404) for entities outside the caller's scope and FEATURE_DISABLED (403) when FF_PAYROLL_GATE_AUTO_RELEASE is off.

List an employer entity's notices currently held at the AWAITING_PAYROLL gate, each with its own gate verdict ('released' | 'needs_connection' | 'needs_input' | 'unreadable' — 'unreadable' = the payroll on file could not be read just now; this is not an answer about whether payroll is on file), the tax year checked and any named coverage gap — the 'Notices waiting on this payroll (N)' list on the employer's payroll page. Also reports whether an active payroll connection exists for the entity. Capped at 20 notices per call (`truncated` says when there are more); the remainder release on the entity's next payroll sync. AUTONOMOUS — pure read. Refuses with ENTITY_NOT_FOUND (404) for entities outside the caller's scope and FEATURE_DISABLED (403) when FF_PAYROLL_GATE_AUTO_RELEASE is off.

Bearer scopespayroll.read

Request body PayrollListGatedNoticesInputrequired

FieldTypeRequiredDescription
entityIdstring · uuidrequired—

Responses

  • 200Successful invocationPayrollListGatedNoticesOutput
    FieldTypeRequiredDescription
    entityIdstring · uuidrequired—
    connectedbooleanrequired—
    payrollOnFilebooleanrequired—
    payrollReadOkbooleanrequired—
    providerstringrequired—
    noticesarray<object>required—
    countintegerrequired—
    truncatedbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `payroll.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/payroll.releaseGateRelease the AWAITING_PAYROLL gate on one notice using payroll already on file — the employer's explicit 'Use my connected payroll' action, for an employer who connected long ago and has no sync event to ride. Probes the entity's payroll connection for real, re-runs the SAME coverage verdict the worker uses, and on 'released' enqueues the shared PROCESS_PAYROLL_VERIFICATION job and appends a hash-chained payroll_gate_released event naming the actor and trigger. Idempotent: a second call inside the same minute coalesces onto the first call's job and returns the same jobId with deduplicated=true and no second audit row. Writes NO pipeline_status — the transition out of AWAITING_PAYROLL stays the property of the verification handler. NOTIFY — it acts and then tells the CPA; it is not a sign-off, and it cannot open a gate the evidence does not open. Refuses with PAYROLL_GATE_NOT_GATED (409) when the notice is not at the gate, PAYROLL_GATE_NEEDS_CONNECTION / PAYROLL_GATE_NEEDS_INPUT (422) when the payroll evidence is missing, PAYROLL_GATE_UNREADABLE (409) when the payroll on file could not be read just now, NOTICE_NOT_FOUND (404) for unknown, other-tenant or out-of-scope notices, and FEATURE_DISABLED (403) when FF_PAYROLL_GATE_AUTO_RELEASE is off.

Release the AWAITING_PAYROLL gate on one notice using payroll already on file — the employer's explicit 'Use my connected payroll' action, for an employer who connected long ago and has no sync event to ride. Probes the entity's payroll connection for real, re-runs the SAME coverage verdict the worker uses, and on 'released' enqueues the shared PROCESS_PAYROLL_VERIFICATION job and appends a hash-chained payroll_gate_released event naming the actor and trigger. Idempotent: a second call inside the same minute coalesces onto the first call's job and returns the same jobId with deduplicated=true and no second audit row. Writes NO pipeline_status — the transition out of AWAITING_PAYROLL stays the property of the verification handler. NOTIFY — it acts and then tells the CPA; it is not a sign-off, and it cannot open a gate the evidence does not open. Refuses with PAYROLL_GATE_NOT_GATED (409) when the notice is not at the gate, PAYROLL_GATE_NEEDS_CONNECTION / PAYROLL_GATE_NEEDS_INPUT (422) when the payroll evidence is missing, PAYROLL_GATE_UNREADABLE (409) when the payroll on file could not be read just now, NOTICE_NOT_FOUND (404) for unknown, other-tenant or out-of-scope notices, and FEATURE_DISABLED (403) when FF_PAYROLL_GATE_AUTO_RELEASE is off.

Bearer scopespayroll.write

Request body PayrollReleaseGateInputrequired

FieldTypeRequiredDescription
noticeIdstring · uuidrequired—

Responses

  • 200Successful invocationPayrollReleaseGateOutput
    FieldTypeRequiredDescription
    noticeIdstring · uuidrequired—
    entityIdstring · uuidrequired—
    releasedbooleanrequired—
    reasonenum: "released" | "needs_connection" | "needs_input" | "not_gated" | …required—
    jobIdstringrequired—
    deduplicatedbooleanrequired—
    coverageYearintegerrequired—
    coverageGapstringrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `payroll.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

l2:poa-filing3

POST/v1/poa-filing.generate8821Generate an IRS Form 8821 (Tax Information Authorization) naming Kreto Inc. as the taxpayer's information designee. Fills the official IRS PDF template with the taxpayer's entity data and selected tax matters / years, uploads the unsigned PDF to Supabase Storage, creates a draft poa_authorizations row and a signature_requests row, returns the signing URL. The taxpayer (authorized signer on the entity) signs via the existing signing flow; finalization writes the signed PDF URL back to the poa_authorizations row.

Generate an IRS Form 8821 (Tax Information Authorization) naming Kreto Inc. as the taxpayer's information designee. Fills the official IRS PDF template with the taxpayer's entity data and selected tax matters / years, uploads the unsigned PDF to Supabase Storage, creates a draft poa_authorizations row and a signature_requests row, returns the signing URL. The taxpayer (authorized signer on the entity) signs via the existing signing flow; finalization writes the signed PDF URL back to the poa_authorizations row.

Bearer scopesstate_registration.write

Request body PoaFilingGenerate8821Inputrequired

FieldTypeRequiredDescription
employerEntityIdstringrequired—
taxMattersarray<enum: "income" | "employment" | "excise" | "estate" | …>required—
taxYearsarray<integer>required—
signerEmailstring · emailoptional—
signerTitlestringoptional—

Responses

  • 200Successful invocationPoaFilingGenerate8821Output
    FieldTypeRequiredDescription
    poaAuthorizationIdstringrequired—
    signatureRequestIdstringrequired—
    signingUrlstringrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/poa-filing.listPendingFilingsAdmin-only: list signed IRS Form 8821 POAs awaiting filing or in post-filing IRS states. Cross-tenant (Kreto staff console). Returns submission_status, signer info, filing_date, and CAF number for each row. Use to triage what 8821s are ready to upload to irs.gov/Submit8821, or to audit recent filings.

Admin-only: list signed IRS Form 8821 POAs awaiting filing or in post-filing IRS states. Cross-tenant (Kreto staff console). Returns submission_status, signer info, filing_date, and CAF number for each row. Use to triage what 8821s are ready to upload to irs.gov/Submit8821, or to audit recent filings.

Bearer scopesadmin.read

Request body PoaFilingListPendingFilingsInputrequired

FieldTypeRequiredDescription
statusenum: "signed" | "submitted" | "accepted" | "rejected"optionalFilter by submission_status. Omit to receive every non-draft row.
limitintegeroptional—
offsetintegeroptional—

Responses

  • 200Successful invocationPoaFilingListPendingFilingsOutput
    FieldTypeRequiredDescription
    rowsarray<object>required—
    totalintegerrequired—
  • 400Validation error (input schema rejected). risk_level=elevated.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/poa-filing.transitionPoaFilingStatusAdmin-only: advance the IRS 8821 filing state machine for a single POA. Allowed: signed→submitted (Kreto staff uploaded to IRS portal), submitted→accepted (record CAF number from IRS ack), submitted→ rejected (record IRS rejection reason). Any other transition errors. Use to mirror IRS portal interactions back into Kreto state.

Admin-only: advance the IRS 8821 filing state machine for a single POA. Allowed: signed→submitted (Kreto staff uploaded to IRS portal), submitted→accepted (record CAF number from IRS ack), submitted→ rejected (record IRS rejection reason). Any other transition errors. Use to mirror IRS portal interactions back into Kreto state.

Bearer scopesadmin.write

Request body PoaFilingTransitionPoaFilingStatusInputrequired

FieldTypeRequiredDescription
poaAuthorizationIdstringrequired—
toStatusenum: "submitted" | "accepted" | "rejected"required—
cafNumberstringoptional—
rejectionReasonstringoptional—

Responses

  • 200Successful invocationPoaFilingTransitionPoaFilingStatusOutput
    FieldTypeRequiredDescription
    poaAuthorizationIdstringrequired—
    submissionStatusenum: "signed" | "submitted" | "accepted" | "rejected" | …required—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

l2:policy2

POST/v1/policy.getGet the mailbox automation policy for the caller's tenant (or a specific entity): the per-action execution-mode matrix + step-up threshold + outbound posture, and the deadline-escalation ladder. Returns conservative code defaults (isDefault=true) when no policy row exists. AUTONOMOUS — read-only.

Get the mailbox automation policy for the caller's tenant (or a specific entity): the per-action execution-mode matrix + step-up threshold + outbound posture, and the deadline-escalation ladder. Returns conservative code defaults (isDefault=true) when no policy row exists. AUTONOMOUS — read-only.

Bearer scopesadmin.read

Request body PolicyGetInputrequired

FieldTypeRequiredDescription
entityIdstring · uuidoptional—

Responses

  • 200Successful invocationPolicyGetOutput
    FieldTypeRequiredDescription
    policyobjectrequired—
    isDefaultbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/policy.updateSet the mailbox automation policy for the caller's tenant (or a specific entity): the per-action execution-mode matrix + step-up threshold + outbound posture, and the deadline-escalation ladder. Upserts the single (tenant, entity) row and seals an audit event. NOTIFY — a human-made control-plane change; it never auto-executes anything itself.

Set the mailbox automation policy for the caller's tenant (or a specific entity): the per-action execution-mode matrix + step-up threshold + outbound posture, and the deadline-escalation ladder. Upserts the single (tenant, entity) row and seals an audit event. NOTIFY — a human-made control-plane change; it never auto-executes anything itself.

Bearer scopesadmin.write

Request body PolicyUpdateInputrequired

FieldTypeRequiredDescription
entityIdstring · uuidoptional—
byActionTypeobjectrequired—
escalationobjectrequired—

Responses

  • 200Successful invocationPolicyUpdateOutput
    FieldTypeRequiredDescription
    policyobjectrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

l2:portal-fetch3

POST/v1/portal-fetch.requestAsk Kreto to download an employer's documents (rate notices, benefit-charge statements, agency letters) from a state agency portal. Runs the one eligibility predicate (flag, vault lock, firm + employer consent, state authorization, login, MFA path, portal config, terms, cooldown) and, only when eligible, queues one portal_fetch run (single attempt, one per employer + agency + day). A refusal returns its reason code and sentence and writes nothing. NOTIFY: no portal settings are ever changed.

Ask Kreto to download an employer's documents (rate notices, benefit-charge statements, agency letters) from a state agency portal. Runs the one eligibility predicate (flag, vault lock, firm + employer consent, state authorization, login, MFA path, portal config, terms, cooldown) and, only when eligible, queues one portal_fetch run (single attempt, one per employer + agency + day). A refusal returns its reason code and sentence and writes nothing. NOTIFY: no portal settings are ever changed.

Bearer scopesadmin.write

Request body PortalFetchRequestInputrequired

FieldTypeRequiredDescription
entityIdstring · uuidrequired—
agencyIdstringrequired—
kindsarray<enum: "rate_notice" | "benefit_charge_statement" | "correspondence">optional—

Responses

  • 200Successful invocationPortalFetchRequestOutput
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/portal-fetch.listList the agency-portal downloads Kreto ran for one stored login (`credentialId`) or one employer (`entityId`) — name exactly one — newest first: the status word, when it ran and how many new documents it found. The accountant also sees the trigger, the kinds asked for and the sentence that says what to do next; the employer sees dates and kinds only. Never a document title or a storage path. Tenant- and employer-scoped; read-only. AUTONOMOUS.

List the agency-portal downloads Kreto ran for one stored login (`credentialId`) or one employer (`entityId`) — name exactly one — newest first: the status word, when it ran and how many new documents it found. The accountant also sees the trigger, the kinds asked for and the sentence that says what to do next; the employer sees dates and kinds only. Never a document title or a storage path. Tenant- and employer-scoped; read-only. AUTONOMOUS.

Bearer scopesadmin.read

Request body PortalFetchListInputrequired

FieldTypeRequiredDescription
credentialIdstring · uuidoptional—
entityIdstring · uuidoptional—

Responses

  • 200Successful invocationPortalFetchListOutput
    FieldTypeRequiredDescription
    planeenum: "cpa" | "employer"required—
    credentialobjectrequired—
    runsarray<object>required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/portal-fetch.getRead one agency-portal download and its files: the status word and, for the accountant, the next step (a person's step when the portal needed one, the login sentence when sign-in failed, the reason when Kreto stopped), and per file its kind, date, where Kreto put it and the notice or document it became. The employer sees dates and kinds only. Never a document title or a storage path. Tenant- and employer-scoped; read-only. AUTONOMOUS.

Read one agency-portal download and its files: the status word and, for the accountant, the next step (a person's step when the portal needed one, the login sentence when sign-in failed, the reason when Kreto stopped), and per file its kind, date, where Kreto put it and the notice or document it became. The employer sees dates and kinds only. Never a document title or a storage path. Tenant- and employer-scoped; read-only. AUTONOMOUS.

Bearer scopesadmin.read

Request body PortalFetchGetInputrequired

FieldTypeRequiredDescription
fetchIdstring · uuidrequired—

Responses

  • 200Successful invocationPortalFetchGetOutput
    FieldTypeRequiredDescription
    planeenum: "cpa" | "employer"required—
    runobjectrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

l2:regulation4

POST/v1/regulation.getRuleThe one live regulation-catalog value for (family, key, jurisdiction) on an as-of date, with its rule id, status, checked-by level and source quote — or {withheld: reason} (no_rule, ambiguous, enum:<code>). No fallback, no 'latest', no neighbouring year; proposals and superseded rows are never returned. Global reference data, identical for every tenant. Example: SUI_PROTEST_WINDOW / SUI_RATE_NOTICE / AZ / 2026-03-02.

The one live regulation-catalog value for (family, key, jurisdiction) on an as-of date, with its rule id, status, checked-by level and source quote — or {withheld: reason} (no_rule, ambiguous, enum:<code>). No fallback, no 'latest', no neighbouring year; proposals and superseded rows are never returned. Global reference data, identical for every tenant. Example: SUI_PROTEST_WINDOW / SUI_RATE_NOTICE / AZ / 2026-03-02.

Bearer scopesadmin.read

Request body RegulationGetRuleInputrequired

FieldTypeRequiredDescription
familystringrequired—
keystringrequired—
jurisdictionone of (string · object)required—
asOfstringrequired—
attributesobjectoptional—

Responses

  • 200Successful invocationRegulationGetRuleOutput
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/regulation.listProposalsPending regulation-catalog proposals (oldest first), optionally one family: the old/new value inputs a reviewer decides on, with the source quote and link. Read-only; Kreto staff (system_admin session) only.

Pending regulation-catalog proposals (oldest first), optionally one family: the old/new value inputs a reviewer decides on, with the source quote and link. Read-only; Kreto staff (system_admin session) only.

Bearer scopesadmin.read

Request body RegulationListProposalsInputrequired

FieldTypeRequiredDescription
familystringoptional—
limitintegeroptional—

Responses

  • 200Successful invocationRegulationListProposalsOutput
    FieldTypeRequiredDescription
    proposalsarray<object>required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/regulation.proposeRegulationRulePut ONE candidate regulation-catalog value (a contract proposal row) in the human review queue: writes a single state_compliance_obligation_proposals row with decision='pending' and NEVER writes a rule row — only a human decision (decideRegulationProposal) can make a rule live. Global reference data, no tenant filter, no employer PII. NOTIFY: agents, watchers and vendor adapters may call it; Kreto staff may call it as proposed_by_kind='staff'.

Put ONE candidate regulation-catalog value (a contract proposal row) in the human review queue: writes a single state_compliance_obligation_proposals row with decision='pending' and NEVER writes a rule row — only a human decision (decideRegulationProposal) can make a rule live. Global reference data, no tenant filter, no employer PII. NOTIFY: agents, watchers and vendor adapters may call it; Kreto staff may call it as proposed_by_kind='staff'.

Bearer scopesadmin.write

Request body RegulationProposeRegulationRuleInputrequired

FieldTypeRequiredDescription
rule_familystringrequired—
rule_keystringrequired—
jurisdiction_levelenum: "federal" | "state" | "county" | "city" | …required—
state_codestringrequired—
jurisdiction_namestringrequired—
value_typeenum: "number" | "percent" | "days" | "money" | …required—
valueobjectrequired—
applies_toobjectrequired—
effective_fromstringrequired—
proposed_by_kindenum: "watcher" | "cpa_sheet_import" | "vendor_adapter" | "legislation_watch" | …required—
proposal_typeenum: "value" | "watch" | "level_raise"required—
source_kindstringrequired—
source_urlstring · urirequired—
source_quotestringrequired—
source_hashstringrequired—
excerptstringrequired—
supersedes_rule_idstringrequired—
impactobjectrequired—

Responses

  • 200Successful invocationRegulationProposeRegulationRuleOutput
    FieldTypeRequiredDescription
    proposalIdstringrequired—
    obligationCodestringrequired—
    decisionenum: "pending"required—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/regulation.decideRegulationProposalHuman gate: approve or reject ONE pending regulation-catalog proposal with a named approver and a reason. Approval of a value proposal makes the new rule live and closes (or, for a same-period correction, supersedes) the row it replaces in one transaction. APPROVAL_REQUIRED, UI-only — excluded from MCP/CLI; refuses any non-human principal, any non-system_admin session, and any approver whose class may not decide the family (deadline + money families: CPA or owner; others: staff).

Human gate: approve or reject ONE pending regulation-catalog proposal with a named approver and a reason. Approval of a value proposal makes the new rule live and closes (or, for a same-period correction, supersedes) the row it replaces in one transaction. APPROVAL_REQUIRED, UI-only — excluded from MCP/CLI; refuses any non-human principal, any non-system_admin session, and any approver whose class may not decide the family (deadline + money families: CPA or owner; others: staff).

Bearer scopesadmin.write

Request body RegulationDecideRegulationProposalInputrequired

FieldTypeRequiredDescription
proposalIdstringrequired—
decisionenum: "approved" | "rejected"required—
reasonstringrequired—

Responses

  • 200Successful invocationRegulationDecideRegulationProposalOutput
    FieldTypeRequiredDescription
    proposalIdstringrequired—
    decisionenum: "approved" | "rejected"required—
    proposalTypestringrequired—
    ruleIdstringrequired—
    supersededRuleIdstringrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

l2:resolutions11

POST/v1/resolutions.getGet a single resolution draft by id for the caller's tenant. Returns the draft letter, status, and the agentic resolution metadata (needs_input/needs_connection/decision_context/trace). Tenant-scoped with a defense-in-depth cross-tenant assertion. AUTONOMOUS — read-only.

Get a single resolution draft by id for the caller's tenant. Returns the draft letter, status, and the agentic resolution metadata (needs_input/needs_connection/decision_context/trace). Tenant-scoped with a defense-in-depth cross-tenant assertion. AUTONOMOUS — read-only.

Bearer scopesnotices.read

Request body ResolutionsGetInputrequired

FieldTypeRequiredDescription
resolutionIdstring · uuidrequired—

Responses

  • 200Successful invocationResolutionsGetOutput
    FieldTypeRequiredDescription
    resolutionobjectrequired—
    responseOriginenum: "kreto" | "outside" | nullrequired—
    outsideResponderenum: "cpa_firm" | "employer" | "outside_counsel" | "other" | …required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/resolutions.listList all resolution draft versions for a notice (newest version first) for the caller's tenant. Use to surface a notice's proposed responses and their approval status. Tenant-scoped with a defense-in-depth cross-tenant assertion. AUTONOMOUS — read-only.

List all resolution draft versions for a notice (newest version first) for the caller's tenant. Use to surface a notice's proposed responses and their approval status. Tenant-scoped with a defense-in-depth cross-tenant assertion. AUTONOMOUS — read-only.

Bearer scopesnotices.read

Request body ResolutionsListInputrequired

FieldTypeRequiredDescription
noticeIdstring · uuidrequired—

Responses

  • 200Successful invocationResolutionsListOutput
    FieldTypeRequiredDescription
    itemsarray<object>required—
    totalintegerrequired—
    runobjectoptional—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/resolutions.getApprovalLetterGet the current (head) response letter for a notice, as the document the authorising party reads before approving it: the body, its version and status, and whether it is filable. Readable by the notice's own employer entity principal or a viewer with sensitive-data access; every other caller gets the same 404 as a notice that does not exist. Tenant-, entity- and live-scoped. AUTONOMOUS — read-only.

Get the current (head) response letter for a notice, as the document the authorising party reads before approving it: the body, its version and status, and whether it is filable. Readable by the notice's own employer entity principal or a viewer with sensitive-data access; every other caller gets the same 404 as a notice that does not exist. Tenant-, entity- and live-scoped. AUTONOMOUS — read-only.

Bearer scopesnotices.read

Request body ResolutionsGetApprovalLetterInputrequired

FieldTypeRequiredDescription
noticeIdstring · uuidrequired—

Responses

  • 200Successful invocationResolutionsGetApprovalLetterOutput
    FieldTypeRequiredDescription
    letterobjectrequired—
    crossReferenceFormulaGapbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/resolutions.reconcileAndDraftReconcile an actionable notice against the entity's connected payroll and draft a resolution. Returns needs_connection (no payroll linked) or needs_input (payroll missing for the period) synchronously; otherwise enqueues the reconcile+draft worker job and returns 'queued' (read the draft + line-level trace via resolutions.get/list). AUTONOMOUS — computes + queues; a human reviews the draft before anything files.

Reconcile an actionable notice against the entity's connected payroll and draft a resolution. Returns needs_connection (no payroll linked) or needs_input (payroll missing for the period) synchronously; otherwise enqueues the reconcile+draft worker job and returns 'queued' (read the draft + line-level trace via resolutions.get/list). AUTONOMOUS — computes + queues; a human reviews the draft before anything files.

Bearer scopesnotices.read

Request body ResolutionsReconcileAndDraftInputrequired

FieldTypeRequiredDescription
noticeIdstring · uuidrequired—

Responses

  • 200Successful invocationResolutionsReconcileAndDraftOutput
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.read`.L2ErrorEnvelope
  • 409Conflict. `FACTS_UNCONFIRMED`: the CPA has not confirmed what Kreto read (employer, notice type, deadline) — POST /api/notices/{id}/confirm-facts first; the same refusal POST /api/notices/{id}/draft answers. Asked after the employer refusal (`ENTITY_UNCONFIRMED` speaks first). Nothing is written and nothing is enqueued. Any other code: the notice cannot be drafted yet (the envelope names it).one of (L2FactsUnconfirmedRefusal · L2ErrorEnvelope)
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/resolutions.supplyInputSupply the missing fields a draft is blocked on, clearing needs_input (AC-6). Records the values so the reconcile can re-run, and surfaces the draft for review. NOTIFY — writes a human-reviewed update; files nothing.

Supply the missing fields a draft is blocked on, clearing needs_input (AC-6). Records the values so the reconcile can re-run, and surfaces the draft for review. NOTIFY — writes a human-reviewed update; files nothing.

Bearer scopesnotices.write

Request body ResolutionsSupplyInputInputrequired

FieldTypeRequiredDescription
resolutionIdstring · uuidrequired—
inputsobjectrequired—

Responses

  • 200Successful invocationResolutionsSupplyInputOutput
    FieldTypeRequiredDescription
    resolutionobjectrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/resolutions.recordOutcomeRecord an agency response on a resolution — acceptance confirms the matter, anything else reopens it (AC-12) — and seal the transition into the SHA-256 audit chain. AUTONOMOUS — deterministic classification of the agency's reply; no human gate.

Record an agency response on a resolution — acceptance confirms the matter, anything else reopens it (AC-12) — and seal the transition into the SHA-256 audit chain. AUTONOMOUS — deterministic classification of the agency's reply; no human gate.

Bearer scopesnotices.write

Request body ResolutionsRecordOutcomeInputrequired

FieldTypeRequiredDescription
resolutionIdstring · uuidrequired—
agencyResponseTypeenum: "acceptance" | "counter" | "additional_info_request" | "rejection"required—

Responses

  • 200Successful invocationResolutionsRecordOutcomeOutput
    FieldTypeRequiredDescription
    resolutionobjectrequired—
    outcomeenum: "confirmed" | "reopened"required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/resolutions.approveApprove a resolution draft (transitions draft -> approved). Requires a typed confirmation phrase ("APPROVE"). APPROVAL_REQUIRED — human gate; files/moves nothing by itself (execute is the external-impact step). UI-only: not exposed via MCP/CLI.

Approve a resolution draft (transitions draft -> approved). Requires a typed confirmation phrase ("APPROVE"). APPROVAL_REQUIRED — human gate; files/moves nothing by itself (execute is the external-impact step). UI-only: not exposed via MCP/CLI.

Bearer scopesnotices.write

Request body ResolutionsApproveInputrequired

FieldTypeRequiredDescription
resolutionIdstring · uuidrequired—
confirmationPhrasestringrequiredTyped confirmation — must equal "APPROVE" (AC-9 typed-confirm).
payrollOverrideReasonstringoptionalWritten justification for approving without a completed payroll cross-reference. Recorded in the hash-chained audit trail.

Responses

  • 200Successful invocationResolutionsApproveOutput
    FieldTypeRequiredDescription
    resolutionobjectrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 409Conflict. `draft_superseded`: the draft id was replaced by a newer version (one live draft per notice) — nothing is written; approve `live_draft_id` instead. Any other code: the draft or notice is not in an approvable state (the L2 envelope names it).one of (L2DraftSupersededRefusal · L2LetterRefusalEnvelope)
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/resolutions.rejectReject a resolution draft (transitions draft -> rejected). Records the reason; redraft=true signals the redraft loop, otherwise the matter is finalized. APPROVAL_REQUIRED — human gate; UI-only, not MCP/CLI-exposed.

Reject a resolution draft (transitions draft -> rejected). Records the reason; redraft=true signals the redraft loop, otherwise the matter is finalized. APPROVAL_REQUIRED — human gate; UI-only, not MCP/CLI-exposed.

Bearer scopesnotices.write

Request body ResolutionsRejectInputrequired

FieldTypeRequiredDescription
resolutionIdstring · uuidrequired—
reasonstringoptional—
redraftbooleanoptional—

Responses

  • 200Successful invocationResolutionsRejectOutput
    FieldTypeRequiredDescription
    resolutionobjectrequired—
    redraftJobobjectoptional—
    redraftQueueErrorenum: "requeue_failed"optional—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/resolutions.executeExecute an approved resolution: record the agency filing (operator-attested) and advance the notice to ACTIONED, sealing a hash-chained audit receipt. Requires the draft to be 'approved' (post-approval invariant) and not already executed. APPROVAL_REQUIRED — UI/session-only (not MCP/CLI); Kreto records the filing, it does not auto-transmit.

Execute an approved resolution: record the agency filing (operator-attested) and advance the notice to ACTIONED, sealing a hash-chained audit receipt. Requires the draft to be 'approved' (post-approval invariant) and not already executed. APPROVAL_REQUIRED — UI/session-only (not MCP/CLI); Kreto records the filing, it does not auto-transmit.

Bearer scopesnotices.write

Request body ResolutionsExecuteInputrequired

FieldTypeRequiredDescription
resolutionIdstring · uuidrequired—
filingMethodenum: "mail" | "fax" | "online_portal" | "email" | …required—
notestringoptional—
confirmationNumberstringrequired—

Responses

  • 200Successful invocationResolutionsExecuteOutput
    FieldTypeRequiredDescription
    resolutionobjectrequired—
    filingobjectoptional—
    sealedAtstringoptional—
    employerReportobjectoptional—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 409Conflict. `DRAFT_NOT_FILABLE`: the head letter is not a filable document — `unfilable_reason` names why (`unfilled_fields` / `unfilled_field_count` for unfilled slots). Any other code: the notice or letter is not in a fileable state (the envelope names it).L2LetterRefusalEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/resolutions.recordOutsideResponseRecord that the agency was answered OUTSIDE Kreto (by the CPA firm, the employer or outside counsel): moves the notice to filed with response_origin='outside', stamps the attested date, party and channel, retires any live Kreto draft, and seals one audit row. APPROVAL_REQUIRED — a human attestation that an obligation was met; UI/session-only.

Record that the agency was answered OUTSIDE Kreto (by the CPA firm, the employer or outside counsel): moves the notice to filed with response_origin='outside', stamps the attested date, party and channel, retires any live Kreto draft, and seals one audit row. APPROVAL_REQUIRED — a human attestation that an obligation was met; UI/session-only.

Bearer scopesnotices.write

Request body ResolutionsRecordOutsideResponseInputrequired

FieldTypeRequiredDescription
noticeIdstringrequired—
responderenum: "cpa_firm" | "employer" | "outside_counsel" | "other"required—
channelenum: "mail" | "fax" | "online_portal" | "email" | …required—
respondedOnstringrequired—
documentIdstringoptional—
confirmationNumberstringoptional—
notestringoptional—

Responses

  • 200Successful invocationResolutionsRecordOutsideResponseOutput
    FieldTypeRequiredDescription
    noticeIdstringrequired—
    lifecycleStateenum: "filed"required—
    responseOriginenum: "outside"required—
    outsideResponderenum: "cpa_firm" | "employer" | "outside_counsel" | "other"required—
    filedAtstringrequired—
    retiredDraftIdstringrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/resolutions.withdrawApprovalWithdraw the approval of a notice's letter that has NOT been sent or filed: the notice returns to drafting, the approved letter of record is kept as its version marked 'withdrawn' (the next draft is the next version), and one hash-chained audit row records who, when and the required reason — all in one transaction. Refused (409) once the letter is filed, reported filed, answered outside Kreto, sent for signature, or the notice is closed; 404 for an unknown or foreign notice. APPROVAL_REQUIRED — a human act; interactive CPA-firm sessions only.

Withdraw the approval of a notice's letter that has NOT been sent or filed: the notice returns to drafting, the approved letter of record is kept as its version marked 'withdrawn' (the next draft is the next version), and one hash-chained audit row records who, when and the required reason — all in one transaction. Refused (409) once the letter is filed, reported filed, answered outside Kreto, sent for signature, or the notice is closed; 404 for an unknown or foreign notice. APPROVAL_REQUIRED — a human act; interactive CPA-firm sessions only.

Bearer scopesnotices.write

Request body ResolutionsWithdrawApprovalInputrequired

FieldTypeRequiredDescription
noticeIdstringrequired—
reasonstringrequired—

Responses

  • 200Successful invocationResolutionsWithdrawApprovalOutput
    FieldTypeRequiredDescription
    noticeIdstringrequired—
    lifecycleStatestringrequired—
    withdrawnDraftIdstringrequired—
    withdrawnDraftVersionintegerrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

l2:signature-consents5

POST/v1/signature-consents.captureConsentPersist an employer officer's signature + scoped consent for state filings. Use when an officer has signed on a canvas and explicitly authorized Kreto to apply the captured signature to per-state PDFs (Middesk-match signature-warehouse model). Idempotent: returns existing un-revoked consent for the same (officer, scope) instead of duplicating. Returns only the consent's id, scope and signed_at — never the signature image, IP, user agent or consent text.

Persist an employer officer's signature + scoped consent for state filings. Use when an officer has signed on a canvas and explicitly authorized Kreto to apply the captured signature to per-state PDFs (Middesk-match signature-warehouse model). Idempotent: returns existing un-revoked consent for the same (officer, scope) instead of duplicating. Returns only the consent's id, scope and signed_at — never the signature image, IP, user agent or consent text.

Bearer scopesstate_registration.write

Request body SignatureConsentsCaptureConsentInputrequired

FieldTypeRequiredDescription
entityIdstringrequired—
officerMemberIdstringrequired—
scopeenum: "sos_and_payroll_tax" | "state_registration_only" | "all_state_filings"required—
signatureDataUrlstringrequired—
ipstringrequired—
userAgentstringrequired—

Responses

  • 200Successful invocationSignatureConsentsCaptureConsentOutput
    FieldTypeRequiredDescription
    consentobjectrequired—
    isNewCapturebooleanrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/signature-consents.getActiveConsentCheck whether an entity has an active (un-revoked) signature consent for a scope, optionally for one officer. Returns only the consent's id, scope and signed_at — never the signature image, IP, user agent or consent text. Returns null if no active consent exists — caller should prompt the officer to sign first.

Check whether an entity has an active (un-revoked) signature consent for a scope, optionally for one officer. Returns only the consent's id, scope and signed_at — never the signature image, IP, user agent or consent text. Returns null if no active consent exists — caller should prompt the officer to sign first.

Bearer scopesstate_registration.read

Request body SignatureConsentsGetActiveConsentInputrequired

FieldTypeRequiredDescription
entityIdstringrequired—
scopeenum: "sos_and_payroll_tax" | "state_registration_only" | "all_state_filings"required—
officerMemberIdstringoptional—

Responses

  • 200Successful invocationSignatureConsentsGetActiveConsentOutput
    FieldTypeRequiredDescription
    consentobjectrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/signature-consents.revokeConsentSoft-revoke a signature consent so it cannot be applied to future filings. Past filings already stamped with the signature retain their stamps (audit immutability). Use when an officer leaves the company or the customer wants to withdraw authorization. Returns only the consent's id, scope, signed_at and revoked_at.

Soft-revoke a signature consent so it cannot be applied to future filings. Past filings already stamped with the signature retain their stamps (audit immutability). Use when an officer leaves the company or the customer wants to withdraw authorization. Returns only the consent's id, scope, signed_at and revoked_at.

Bearer scopesstate_registration.write

Request body SignatureConsentsRevokeConsentInputrequired

FieldTypeRequiredDescription
consentIdstringrequired—
revocationReasonstringrequired—

Responses

  • 200Successful invocationSignatureConsentsRevokeConsentOutput
    FieldTypeRequiredDescription
    consentobjectrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/signature-consents.attestSignatoryAuthorityThe employer's officer confirms, from their own login, that they are an owner/partner/member/corporate officer with authority to bind the entity — setting entity_members.is_business_signatory on their OWN row. Refuses for any other user's row. This is the only writer of that flag; a CPA cannot attest on an officer's behalf (use requestSignatoryAttestation to ask).

The employer's officer confirms, from their own login, that they are an owner/partner/member/corporate officer with authority to bind the entity — setting entity_members.is_business_signatory on their OWN row. Refuses for any other user's row. This is the only writer of that flag; a CPA cannot attest on an officer's behalf (use requestSignatoryAttestation to ask).

Bearer scopesstate_registration.write

Request body SignatureConsentsAttestSignatoryAuthorityInputrequired

FieldTypeRequiredDescription
entityIdstringrequired—
officerMemberIdstringrequired—
titlestringrequired—
attestationAcceptedenum: truerequired—
attestationTextShownstringoptional—
ipstringrequired—
userAgentstringrequired—

Responses

  • 200Successful invocationSignatureConsentsAttestSignatoryAuthorityOutput
    FieldTypeRequiredDescription
    officerMemberIdstringrequired—
    isBusinessSignatoryenum: truerequired—
    authorityBasisenum: "officer_self"required—
    attestationTextstringrequired—
    attestationTextHashstringrequired—
    titlestringrequired—
    attestedAtstringrequired—
    alreadyAttestedbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/signature-consents.requestSignatoryAttestationAsk an employer officer to confirm their authority to sign (the CPA nomination) — or, when they already are a signatory, to sign the registration. Emails the officer a link to their own login. NEVER sets entity_members.is_business_signatory — the nomination stays pending until the officer attests themselves.

Ask an employer officer to confirm their authority to sign (the CPA nomination) — or, when they already are a signatory, to sign the registration. Emails the officer a link to their own login. NEVER sets entity_members.is_business_signatory — the nomination stays pending until the officer attests themselves.

Bearer scopesstate_registration.write

Request body SignatureConsentsRequestSignatoryAttestationInputrequired

FieldTypeRequiredDescription
entityIdstringrequired—
officerMemberIdstringrequired—
notestringoptional—
stateCodestringoptional—

Responses

  • 200Successful invocationSignatureConsentsRequestSignatoryAttestationOutput
    FieldTypeRequiredDescription
    officerMemberIdstringrequired—
    isBusinessSignatorybooleanrequired—
    requestedenum: truerequired—
    emailSentbooleanrequired—
    noContactOnFilebooleanrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

l2:state-coverage1

POST/v1/state-coverage.getByStateFetch one US state's compliance coverage for an employer: all registrations (with state account numbers), open notices (not in terminal status), and open recommendations. Use when an agent or the UI drawer needs deep detail on a single state. Example agent question: 'What's our SUI registration status in California?' Returns empty arrays for any signal that has no data — never throws on missing rows.

Fetch one US state's compliance coverage for an employer: all registrations (with state account numbers), open notices (not in terminal status), and open recommendations. Use when an agent or the UI drawer needs deep detail on a single state. Example agent question: 'What's our SUI registration status in California?' Returns empty arrays for any signal that has no data — never throws on missing rows.

Bearer scopesstate_registration.read

Request body StateCoverageGetByStateInputrequired

FieldTypeRequiredDescription
employerEntityIdstringrequired—
stateCodestringrequired—

Responses

  • 200Successful invocationStateCoverageGetByStateOutput
    FieldTypeRequiredDescription
    stateCodestringrequired—
    registrationsarray<object>required—
    openNoticesarray<object>required—
    recommendationsarray<object>required—
  • 400Validation error (input schema rejected). risk_level=elevated.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

l2:state-kb1

POST/v1/state-kb.getStateDetailOne US state's full compliance ruleset from Kreto's public reference catalog: the state agency and its portal, SUI tax rules for a given tax year, rate schedules, surcharges, notice types, protest rules, POA rules, statutes and the data-quality record. Global reference data — identical for every tenant, and no employer data is read. Use to ground reasoning about a notice before recommending an action. Example: 'what is Ohio's SUI protest deadline and which form files it?'

One US state's full compliance ruleset from Kreto's public reference catalog: the state agency and its portal, SUI tax rules for a given tax year, rate schedules, surcharges, notice types, protest rules, POA rules, statutes and the data-quality record. Global reference data — identical for every tenant, and no employer data is read. Use to ground reasoning about a notice before recommending an action. Example: 'what is Ohio's SUI protest deadline and which form files it?'

Bearer scopesnotices.read

Request body StateKbGetStateDetailInputrequired

FieldTypeRequiredDescription
stateCodestringrequired—
taxYearintegeroptional—

Responses

  • 200Successful invocationStateKbGetStateDetailOutput
    FieldTypeRequiredDescription
    state_codestringrequired—
    state_namestringrequired—
    agency_namestringrequired—
    agency_abbreviationstringrequired—
    agency_urlstringrequired—
    employer_portal_urlstringrequired—
    phonestringrequired—
    state_tax_rulesarray<object>required—
    state_rate_schedulesarray<object>required—
    state_surchargesarray<object>required—
    state_notice_typesarray<object>required—
    state_protest_rulesarray<object>required—
    state_poa_rulesobjectrequired—
    state_statutesarray<object>required—
    state_data_qualityobjectrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `notices.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

l2:state-registration26

POST/v1/state-registration.detectNewStateRequirementGiven a StateChangeEvent (an employee moved from one state to another), return the requirements that apply for the destination state. Reads the TS rules registry; does NOT write to DB. AUTONOMOUS — agents may call freely; the trigger pipeline lives in PR-D's worker handler.

Given a StateChangeEvent (an employee moved from one state to another), return the requirements that apply for the destination state. Reads the TS rules registry; does NOT write to DB. AUTONOMOUS — agents may call freely; the trigger pipeline lives in PR-D's worker handler.

Bearer scopesstate_registration.read

Request body StateRegistrationDetectNewStateRequirementInputrequired

FieldTypeRequiredDescription
stateChangeEventobjectrequired—

Responses

  • 200Successful invocationStateRegistrationDetectNewStateRequirementOutput
    FieldTypeRequiredDescription
    requirementsarray<object>required—
    reasoningstringrequired—
    stateSupportedbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/state-registration.listRequiredAccountsList required state-agency accounts for a given state. When employerEntityId is supplied, also returns the subset of requirements already registered (status NOT IN rejected/expired) for that employer. Reads from rules registry. AUTONOMOUS.

List required state-agency accounts for a given state. When employerEntityId is supplied, also returns the subset of requirements already registered (status NOT IN rejected/expired) for that employer. Reads from rules registry. AUTONOMOUS.

Bearer scopesstate_registration.read

Request body StateRegistrationListRequiredAccountsInputrequired

FieldTypeRequiredDescription
stateCodestringrequired—
employerEntityIdstringoptional—

Responses

  • 200Successful invocationStateRegistrationListRequiredAccountsOutput
    FieldTypeRequiredDescription
    stateCodestringrequired—
    stateSupportedbooleanrequired—
    accountsarray<object>required—
    alreadyRegisteredarray<enum: "sui_registration" | "withholding_registration" | "workers_comp" | "pfml" | …>required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/state-registration.prepareRegistrationFormValidate a registration form payload against the per-state Zod schema and persist (upsert) a row in employer_state_registrations with status='prepared'. dryRun=true returns the validated payload without persisting. APPROVAL_REQUIRED — Phase 0 metadata only (HITL gate in Phase 3).

Validate a registration form payload against the per-state Zod schema and persist (upsert) a row in employer_state_registrations with status='prepared'. dryRun=true returns the validated payload without persisting. APPROVAL_REQUIRED — Phase 0 metadata only (HITL gate in Phase 3).

Bearer scopesstate_registration.write

Request body StateRegistrationPrepareRegistrationFormInputrequired

FieldTypeRequiredDescription
entityIdstringrequired—
stateCodestringrequired—
requirementTypeenum: "sui_registration" | "withholding_registration" | "workers_comp" | "pfml" | …required—
formPayloadobjectoptional—
dryRunbooleanoptional—
sourceenum: "manual" | "mosey" | "portal"optional—
sourceExternalIdstringoptional—

Responses

  • 200Successful invocationStateRegistrationPrepareRegistrationFormOutput
    FieldTypeRequiredDescription
    registrationobjectrequired—
    validatedPayloadobjectoptional—
    dryRunbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/state-registration.submitRegistrationSubmit a prepared registration to the state portal via Browserbase. The most consequential method in Phase E — actually files with a government agency. APPROVAL_REQUIRED (Phase 0 metadata-only; HITL gate in Phase 3). Dispatches via the StatePortalAdapter registry; throws NotImplementedError for states without a wired adapter.

Submit a prepared registration to the state portal via Browserbase. The most consequential method in Phase E — actually files with a government agency. APPROVAL_REQUIRED (Phase 0 metadata-only; HITL gate in Phase 3). Dispatches via the StatePortalAdapter registry; throws NotImplementedError for states without a wired adapter.

Bearer scopesstate_registration.write

Request body StateRegistrationSubmitRegistrationInputrequired

FieldTypeRequiredDescription
registrationIdstringrequired—
dryRunbooleanoptional—

Responses

  • 200Successful invocationStateRegistrationSubmitRegistrationOutput
    FieldTypeRequiredDescription
    registrationobjectrequired—
    dryRunbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/state-registration.checkRegistrationStatusReturn the current status of a registration. PR-A reads DB last-known state only (pollSource='db_only'). PR-B/C wires actual portal polling via Browserbase (pollSource='portal_polled'). AUTONOMOUS.

Return the current status of a registration. PR-A reads DB last-known state only (pollSource='db_only'). PR-B/C wires actual portal polling via Browserbase (pollSource='portal_polled'). AUTONOMOUS.

Bearer scopesstate_registration.read

Request body StateRegistrationCheckRegistrationStatusInputrequired

FieldTypeRequiredDescription
registrationIdstringrequired—

Responses

  • 200Successful invocationStateRegistrationCheckRegistrationStatusOutput
    FieldTypeRequiredDescription
    registrationobjectrequired—
    pollSourceenum: "db_only" | "portal_polled"required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/state-registration.getRegistrationByEntityLook up the in-flight registration for (tenantId, entityId, stateCode, requirementType). Returns null when no row matches (rejected/expired excluded). AUTONOMOUS.

Look up the in-flight registration for (tenantId, entityId, stateCode, requirementType). Returns null when no row matches (rejected/expired excluded). AUTONOMOUS.

Bearer scopesstate_registration.read

Request body StateRegistrationGetRegistrationByEntityInputrequired

FieldTypeRequiredDescription
entityIdstringrequired—
stateCodestringrequired—
requirementTypeenum: "sui_registration" | "withholding_registration" | "workers_comp" | "pfml" | …required—

Responses

  • 200Successful invocationStateRegistrationGetRegistrationByEntityOutput
    FieldTypeRequiredDescription
    registrationobjectrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/state-registration.listRegistrationsPaginated read of employer_state_registrations rows for the tracking UI. Tenant-scoped via ctx.tenantId; optional entity / status / state / requirement_type / source filters. Returns at most pageSize rows per call (default 25, max 100).

Paginated read of employer_state_registrations rows for the tracking UI. Tenant-scoped via ctx.tenantId; optional entity / status / state / requirement_type / source filters. Returns at most pageSize rows per call (default 25, max 100).

Bearer scopesstate_registration.read

Request body StateRegistrationListRegistrationsInputrequired

FieldTypeRequiredDescription
entityIdstringoptional—
statusenum: "pending" | "prepared" | "awaiting_approval" | "preflight_failed" | …optional—
stateCodestringoptional—
requirementTypeenum: "sui_registration" | "withholding_registration" | "workers_comp" | "pfml" | …optional—
sourceenum: "manual" | "mosey" | "portal"optional—
pageintegeroptional—
pageSizeintegeroptional—

Responses

  • 200Successful invocationStateRegistrationListRegistrationsOutput
    FieldTypeRequiredDescription
    registrationsarray<object>required—
    pageintegerrequired—
    pageSizeintegerrequired—
    totalintegerrequired—
    hasMorebooleanrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/state-registration.transitionStatusApply a manual CPA-driven status transition to an existing registration row. Validates the (from, to) pair against the per-state allowedTransitions matrix (with universal default fallback). Writes a structured audit event capturing the transition + reason. NOTIFY classification: agents can call but the action is logged for human review.

Apply a manual CPA-driven status transition to an existing registration row. Validates the (from, to) pair against the per-state allowedTransitions matrix (with universal default fallback). Writes a structured audit event capturing the transition + reason. NOTIFY classification: agents can call but the action is logged for human review.

Bearer scopesstate_registration.write

Request body StateRegistrationTransitionStatusInputrequired

FieldTypeRequiredDescription
registrationIdstringrequired—
newStatusenum: "pending" | "prepared" | "awaiting_approval" | "preflight_failed" | …required—
reasonstringrequired—
stateAccountNumberstringoptional—
filedAtstring · date-timeoptional—

Responses

  • 200Successful invocationStateRegistrationTransitionStatusOutput
    FieldTypeRequiredDescription
    registrationobjectrequired—
    fromStatusenum: "pending" | "prepared" | "awaiting_approval" | "preflight_failed" | …required—
    toStatusenum: "pending" | "prepared" | "awaiting_approval" | "preflight_failed" | …required—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/state-registration.updateRegistrationMetadataManually edit the tracker metadata fields (assigned_rate as a decimal fraction, assigned_deposit_frequency, effective_date, next_renewal_at) on an existing registration row. Absent key = unchanged, null = clear. Appends a human-readable notes line and a hash-chained audit_events row carrying per-field old → new + the required reason. `source` is never changed (creation provenance). NOTIFY classification: agents can call but the action is logged for human review. On a sui_registration row, a rate or effective-date edit (or a stated rateYear) also records the SUI rate of record for that year, atomically with the edit.

Manually edit the tracker metadata fields (assigned_rate as a decimal fraction, assigned_deposit_frequency, effective_date, next_renewal_at) on an existing registration row. Absent key = unchanged, null = clear. Appends a human-readable notes line and a hash-chained audit_events row carrying per-field old → new + the required reason. `source` is never changed (creation provenance). NOTIFY classification: agents can call but the action is logged for human review. On a sui_registration row, a rate or effective-date edit (or a stated rateYear) also records the SUI rate of record for that year, atomically with the edit.

Bearer scopesstate_registration.write

Request body StateRegistrationUpdateRegistrationMetadataInputrequired

FieldTypeRequiredDescription
registrationIdstringrequired—
assignedRatenumberoptional—
assignedDepositFrequencyenum: "weekly" | "biweekly" | "semimonthly" | "monthly" | …optional—
effectiveDatestringoptional—
nextRenewalAtstringoptional—
rateYearintegeroptional—
reasonstringrequired—

Responses

  • 200Successful invocationStateRegistrationUpdateRegistrationMetadataOutput
    FieldTypeRequiredDescription
    registrationobjectrequired—
    changedFieldsarray<string>required—
    rateOfRecordobjectoptional—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/state-registration.recordPortalResultInbound write path for state portal scrapers. Transitions a registration row to approved or rejected based on the portal's outcome, sets state_account_number on success, captures last_error on failure, and emits a recommendation row that the alert cron uses to email/push the CPA team. APPROVAL_REQUIRED per /goal Q8 decision — customer + external-facing writes are NOT MCP-exposable; the audit row carries the elevated risk level so Phase 3 HITL gates these calls. Auth: server-to-server via API key with scope state_registration.write.

Inbound write path for state portal scrapers. Transitions a registration row to approved or rejected based on the portal's outcome, sets state_account_number on success, captures last_error on failure, and emits a recommendation row that the alert cron uses to email/push the CPA team. APPROVAL_REQUIRED per /goal Q8 decision — customer + external-facing writes are NOT MCP-exposable; the audit row carries the elevated risk level so Phase 3 HITL gates these calls. Auth: server-to-server via API key with scope state_registration.write.

Bearer scopesstate_registration.write

Request body StateRegistrationRecordPortalResultInputrequired

FieldTypeRequiredDescription
registrationIdstringrequired—
outcomeenum: "approved" | "rejected"required—
stateAccountNumberstringoptional—
decidedAtstring · date-timeoptional—
lastErrorstringoptional—

Responses

  • 200Successful invocationStateRegistrationRecordPortalResultOutput
    FieldTypeRequiredDescription
    registrationobjectrequired—
    recommendationIdstringrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/state-registration.generateDe1FilingGenerate a draft California Form DE 1 (covers SUI + Withholding + SDI/PFL on a single CA EDD form) filled with the employer's intake data and stamped with the captured officer signature. Creates a state_filings parent row + 3 employer_state_registrations rows (SUI/Withholding/PFML) all linked via filing_id. NOTIFY risk: this is a draft for human review before CP-CA3 submits to myEDD.

Generate a draft California Form DE 1 (covers SUI + Withholding + SDI/PFL on a single CA EDD form) filled with the employer's intake data and stamped with the captured officer signature. Creates a state_filings parent row + 3 employer_state_registrations rows (SUI/Withholding/PFML) all linked via filing_id. NOTIFY risk: this is a draft for human review before CP-CA3 submits to myEDD.

Bearer scopesstate_registration.write

Request body StateRegistrationGenerateDe1FilingInputrequired

FieldTypeRequiredDescription
employerEntityIdstringrequired—
recommendationIdstringoptional—
signatureConsentIdstringrequired—
intakeobjectrequired—

Responses

  • 200Successful invocationStateRegistrationGenerateDe1FilingOutput
    FieldTypeRequiredDescription
    filingIdstringrequired—
    filingPdfUrlstringrequired—
    employerStateRegistrationIdsarray<string>required—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/state-registration.generateStateFilingState-agnostic filing generator. Resolves the per-state rule for the given stateCode, validates intake against rule.stateIntakeSchema, embeds the captured signature, generates a draft PDF (from rule.pdfTemplatePath if set, else programmatic), and creates a state_filings parent row + one employer_state_registrations row per covered requirement_type. NOTIFY risk: draft only — submission to the state portal is CP-CA3 (APPROVAL_REQUIRED). Replaces the CA-specific generateDe1Filing for all new states.

State-agnostic filing generator. Resolves the per-state rule for the given stateCode, validates intake against rule.stateIntakeSchema, embeds the captured signature, generates a draft PDF (from rule.pdfTemplatePath if set, else programmatic), and creates a state_filings parent row + one employer_state_registrations row per covered requirement_type. NOTIFY risk: draft only — submission to the state portal is CP-CA3 (APPROVAL_REQUIRED). Replaces the CA-specific generateDe1Filing for all new states.

Bearer scopesstate_registration.write

Request body StateRegistrationGenerateStateFilingInputrequired

FieldTypeRequiredDescription
stateCodestringrequired—
employerEntityIdstringrequired—
recommendationIdstringoptional—
signatureConsentIdstringrequired—
intakeobjectrequired—

Responses

  • 200Successful invocationStateRegistrationGenerateStateFilingOutput
    FieldTypeRequiredDescription
    filingIdstringrequired—
    filingPdfUrlstringrequired—
    employerStateRegistrationIdsarray<string>required—
    formobjectrequired—
    recommendationobjectoptional—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/state-registration.saveIntakeDraftUpsert the in-progress State Registration Wizard intake for a given (employer, state) so wizard state survives a refresh / crash / cross-device resume. Draft-only (NOTIFY): captures answers + resume metadata, generates no filing and submits nothing. One row per (tenant, employer, state) via the table's UNIQUE constraint — re-saving updates, never duplicates.

Upsert the in-progress State Registration Wizard intake for a given (employer, state) so wizard state survives a refresh / crash / cross-device resume. Draft-only (NOTIFY): captures answers + resume metadata, generates no filing and submits nothing. One row per (tenant, employer, state) via the table's UNIQUE constraint — re-saving updates, never duplicates.

Bearer scopesstate_registration.write

Request body StateRegistrationSaveIntakeDraftInputrequired

FieldTypeRequiredDescription
employerEntityIdstringrequired—
recommendationIdstringoptional—
stateCodestringrequired—
reasonForRegisteringstringoptional—
qualifiersobjectoptional—
stateSpecificDataobjectrequired—

Responses

  • 200Successful invocationStateRegistrationSaveIntakeDraftOutput
    FieldTypeRequiredDescription
    draftIdstringrequired—
    updatedAtstringrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/state-registration.provisionPortalAccountAllocate a Kreto-owned portal-account credential for a (tenant, entity, state, agency) tuple. Generates a unique alias e-mail of the form <slug>.portal-<state>-<agency>@kreto.ai, generates a strong base64url password, and writes the credential to vault under agency_id='portal_<stateCode>'. Idempotent: re-call with the same tuple returns the existing credential. APPROVAL_REQUIRED (Phase 0 metadata-only; KRT-1554 step-up gates later).

Allocate a Kreto-owned portal-account credential for a (tenant, entity, state, agency) tuple. Generates a unique alias e-mail of the form <slug>.portal-<state>-<agency>@kreto.ai, generates a strong base64url password, and writes the credential to vault under agency_id='portal_<stateCode>'. Idempotent: re-call with the same tuple returns the existing credential. APPROVAL_REQUIRED (Phase 0 metadata-only; KRT-1554 step-up gates later).

Bearer scopesstate_registration.write

Request body StateRegistrationProvisionPortalAccountInputrequired

FieldTypeRequiredDescription
entityIdstringrequired—
stateCodestringrequired—
agencyCodestringrequired—

Responses

  • 200Successful invocationStateRegistrationProvisionPortalAccountOutput
    FieldTypeRequiredDescription
    credentialIdstringrequired—
    aliasEmailstring · emailrequired—
    agencyIdstringrequired—
    statusstringrequired—
    passwordstringrequired—
    newlyProvisionedbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/state-registration.listCatalogRulesBrowse the state-registration catalog (state_filing_rules, global config — NO tenant filter). Optional filters: stateCode, requirementType, verificationStatus, needsResearch, monopolistic, jurisdictionType. Returns the matching rows plus HONEST per-status counts ({unverified, human_reviewed, verified}) for the filtered set — never a 'done/complete' claim (AC-23). AUTONOMOUS, read-only.

Browse the state-registration catalog (state_filing_rules, global config — NO tenant filter). Optional filters: stateCode, requirementType, verificationStatus, needsResearch, monopolistic, jurisdictionType. Returns the matching rows plus HONEST per-status counts ({unverified, human_reviewed, verified}) for the filtered set — never a 'done/complete' claim (AC-23). AUTONOMOUS, read-only.

Bearer scopesstate_registration.read

Request body StateRegistrationListCatalogRulesInputrequired

FieldTypeRequiredDescription
stateCodestringoptional—
requirementTypeenum: "sui_registration" | "withholding_registration" | "workers_comp" | "pfml" | …optional—
verificationStatusenum: "unverified" | "human_reviewed" | "verified"optional—
needsResearchbooleanoptional—
monopolisticbooleanoptional—
jurisdictionTypeenum: "federal" | "state" | "locality"optional—

Responses

  • 200Successful invocationStateRegistrationListCatalogRulesOutput
    FieldTypeRequiredDescription
    rowsarray<object>required—
    countsobjectrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/state-registration.promoteCatalogRuleHuman gate: promote a catalog cell along the verification ladder (unverified → human_reviewed → verified). APPROVAL_REQUIRED, UI-only — excluded from MCP/CLI, and the handler REJECTS any non-human principal before any write (AC-20). Verifying requires provenance (source_url + retrieved_at, AC-18). Stamps reviewed_by / last_reviewed_at, and on verify also verified_at / verified_by / needs_research=false.

Human gate: promote a catalog cell along the verification ladder (unverified → human_reviewed → verified). APPROVAL_REQUIRED, UI-only — excluded from MCP/CLI, and the handler REJECTS any non-human principal before any write (AC-20). Verifying requires provenance (source_url + retrieved_at, AC-18). Stamps reviewed_by / last_reviewed_at, and on verify also verified_at / verified_by / needs_research=false.

Bearer scopesstate_registration.write

Request body StateRegistrationPromoteCatalogRuleInputrequired

FieldTypeRequiredDescription
stateCodestringrequired—
requirementTypeenum: "sui_registration" | "withholding_registration" | "workers_comp" | "pfml" | …required—
toStatusenum: "human_reviewed" | "verified"required—
reasonstringoptional—

Responses

  • 200Successful invocationStateRegistrationPromoteCatalogRuleOutput
    FieldTypeRequiredDescription
    stateCodestringrequired—
    requirementTypeenum: "sui_registration" | "withholding_registration" | "workers_comp" | "pfml" | …required—
    fromStatusenum: "unverified" | "human_reviewed" | "verified"required—
    toStatusenum: "unverified" | "human_reviewed" | "verified"required—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden. `SYSTEM_ADMIN_REQUIRED`: the caller is not a Kreto system administrator on an interactive session — the global catalog every firm pins (handbook modules, state filing / foreign-qualification / annual-report / e-delivery rules) is staff-only, exactly as on the REST admin door. Checked before any catalog read, so the answer is the same whether or not the row exists. `STEP_UP_REQUIRED`: the dispatcher's APPROVAL_REQUIRED gate (an API key, or no fresh re-authentication). Nothing is written.one of (L2SystemAdminRequiredRefusal · L2ErrorEnvelope)
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/state-registration.getCatalogRuleRead one catalog cell (state_filing_rules, global config — NO tenant filter): the rule row plus its typed ordered steps (state_filing_rule_steps), its prerequisite edges (state_filing_rule_prerequisites), and the resolved jurisdiction (via jurisdiction_code, else null). Powers the Cell Detail screen (jobs a+b, AC-5). AUTONOMOUS, read-only.

Read one catalog cell (state_filing_rules, global config — NO tenant filter): the rule row plus its typed ordered steps (state_filing_rule_steps), its prerequisite edges (state_filing_rule_prerequisites), and the resolved jurisdiction (via jurisdiction_code, else null). Powers the Cell Detail screen (jobs a+b, AC-5). AUTONOMOUS, read-only.

Bearer scopesstate_registration.read

Request body StateRegistrationGetCatalogRuleInputrequired

FieldTypeRequiredDescription
stateCodestringrequired—
requirementTypeenum: "sui_registration" | "withholding_registration" | "workers_comp" | "pfml" | …required—

Responses

  • 200Successful invocationStateRegistrationGetCatalogRuleOutput
    FieldTypeRequiredDescription
    ruleobjectrequired—
    stepsarray<object>required—
    prerequisitesarray<object>required—
    jurisdictionobjectrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/state-registration.listFilingRuleStepsList the typed, ordered registration steps for a (state, requirement) cell (state_filing_rule_steps, global config — NO tenant filter), ordered by step_order. The canonical step sequence is typed rows, never a prose blob (AC-7). AUTONOMOUS, read-only.

List the typed, ordered registration steps for a (state, requirement) cell (state_filing_rule_steps, global config — NO tenant filter), ordered by step_order. The canonical step sequence is typed rows, never a prose blob (AC-7). AUTONOMOUS, read-only.

Bearer scopesstate_registration.read

Request body StateRegistrationListFilingRuleStepsInputrequired

FieldTypeRequiredDescription
stateCodestringrequired—
requirementTypeenum: "sui_registration" | "withholding_registration" | "workers_comp" | "pfml" | …required—

Responses

  • 200Successful invocationStateRegistrationListFilingRuleStepsOutput
    FieldTypeRequiredDescription
    stepsarray<object>required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/state-registration.listFilingRulePrerequisitesList the prerequisite dependency edges for a (state, requirement) cell (state_filing_rule_prerequisites, global config — NO tenant filter). Edges model account-type dependencies and SOS foreign qualification (prerequisite_kind='sos_foreign_qual', an edge — NOT a 6th requirement_type), queryable as a dependency chain (AC-10/13). AUTONOMOUS, read-only.

List the prerequisite dependency edges for a (state, requirement) cell (state_filing_rule_prerequisites, global config — NO tenant filter). Edges model account-type dependencies and SOS foreign qualification (prerequisite_kind='sos_foreign_qual', an edge — NOT a 6th requirement_type), queryable as a dependency chain (AC-10/13). AUTONOMOUS, read-only.

Bearer scopesstate_registration.read

Request body StateRegistrationListFilingRulePrerequisitesInputrequired

FieldTypeRequiredDescription
stateCodestringrequired—
requirementTypeenum: "sui_registration" | "withholding_registration" | "workers_comp" | "pfml" | …required—

Responses

  • 200Successful invocationStateRegistrationListFilingRulePrerequisitesOutput
    FieldTypeRequiredDescription
    prerequisitesarray<object>required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/state-registration.listJurisdictionsList jurisdictions (federal | state | locality) from the jurisdictions table (global config — NO tenant filter). Optional filters: jurisdictionType, parentCode. Locality-ready model — v1 seeds state-grain only, but a locality is just a future row (AC-15/16). AUTONOMOUS, read-only.

List jurisdictions (federal | state | locality) from the jurisdictions table (global config — NO tenant filter). Optional filters: jurisdictionType, parentCode. Locality-ready model — v1 seeds state-grain only, but a locality is just a future row (AC-15/16). AUTONOMOUS, read-only.

Bearer scopesstate_registration.read

Request body StateRegistrationListJurisdictionsInputrequired

FieldTypeRequiredDescription
jurisdictionTypeenum: "federal" | "state" | "locality"optional—
parentCodestringoptional—

Responses

  • 200Successful invocationStateRegistrationListJurisdictionsOutput
    FieldTypeRequiredDescription
    jurisdictionsarray<object>required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/state-registration.proposeFilingRuleResearch one (state, requirement) catalog cell via the Claude + web_search agent and upsert an UNVERIFIED proposal onto the cell BY PK (state_filing_rules, global config — NO tenant filter). The write ALWAYS leaves verification_status='unverified' and NEVER sets verified/human_reviewed (AC-19/20 — agents may never verify); writes provenance (source_url + retrieved_at, AC-18); idempotent-by-PK (AC-22). MUST NOT downgrade a human-promoted cell: on a human_reviewed OR verified cell it leaves the authoritative fields untouched and returns the existing row + the fresh proposal so a human can compare (no-downgrade). PII-free. NOTIFY: agents may call; a human reviews the proposal before promotion.

Research one (state, requirement) catalog cell via the Claude + web_search agent and upsert an UNVERIFIED proposal onto the cell BY PK (state_filing_rules, global config — NO tenant filter). The write ALWAYS leaves verification_status='unverified' and NEVER sets verified/human_reviewed (AC-19/20 — agents may never verify); writes provenance (source_url + retrieved_at, AC-18); idempotent-by-PK (AC-22). MUST NOT downgrade a human-promoted cell: on a human_reviewed OR verified cell it leaves the authoritative fields untouched and returns the existing row + the fresh proposal so a human can compare (no-downgrade). PII-free. NOTIFY: agents may call; a human reviews the proposal before promotion.

Bearer scopesstate_registration.write

Request body StateRegistrationProposeFilingRuleInputrequired

FieldTypeRequiredDescription
stateCodestringrequired—
requirementTypeenum: "sui_registration" | "withholding_registration" | "workers_comp" | "pfml" | …required—

Responses

  • 200Successful invocationStateRegistrationProposeFilingRuleOutput
    FieldTypeRequiredDescription
    rowobjectrequired—
    proposalobjectrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/state-registration.approveRegistrationFilingDORMANT (fold-83): Kreto does not file state registrations yet, so every approval refuses STATE_FILING_NOT_AVAILABLE before any write or charge. When enabled: record a CPA's approval to file a prepared registration and, for fee-bearing filings, authorize payment up to a bound amount (AC-15). The consequential human gate — APPROVAL_REQUIRED, UI-only, never exposed to MCP/CLI. Moves the registration to 'awaiting_approval' and enqueues pre-flight. Handler: Phase B.

DORMANT (fold-83): Kreto does not file state registrations yet, so every approval refuses STATE_FILING_NOT_AVAILABLE before any write or charge. When enabled: record a CPA's approval to file a prepared registration and, for fee-bearing filings, authorize payment up to a bound amount (AC-15). The consequential human gate — APPROVAL_REQUIRED, UI-only, never exposed to MCP/CLI. Moves the registration to 'awaiting_approval' and enqueues pre-flight. Handler: Phase B.

Bearer scopesstate_registration.write

Request body StateRegistrationApproveRegistrationFilingInputrequired

FieldTypeRequiredDescription
registrationIdstringrequired—
authorizedAmountCentsintegerrequired—
billingDisclosurestringoptional—

Responses

  • 200Successful invocationStateRegistrationApproveRegistrationFilingOutput
    FieldTypeRequiredDescription
    registrationIdstringrequired—
    statusstringrequired—
    approvalIdstringrequired—
    preflightobjectrequired—
    enqueuedbooleanrequired—
    feeobjectrequired—
    usageobjectrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.write`.L2ErrorEnvelope
  • 409Not available (code `STATE_FILING_NOT_AVAILABLE`): Kreto does not file state registrations yet. Nothing is written, nothing is charged, nothing is enqueued and the row's status does not change.L2StateFilingNotAvailableRefusal
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/state-registration.runFilingPreflightRun the pre-flight filing validator (KRT-1544 rules: registration status, duplicate-account, idempotency, payload completeness) before any browser session launches (AC-2). Read-only, AUTONOMOUS. A failure blocks the filing and names the failing rule. Handler: Phase B.

Run the pre-flight filing validator (KRT-1544 rules: registration status, duplicate-account, idempotency, payload completeness) before any browser session launches (AC-2). Read-only, AUTONOMOUS. A failure blocks the filing and names the failing rule. Handler: Phase B.

Bearer scopesstate_registration.read

Request body StateRegistrationRunFilingPreflightInputrequired

FieldTypeRequiredDescription
registrationIdstringrequired—

Responses

  • 200Successful invocationStateRegistrationRunFilingPreflightOutput
    FieldTypeRequiredDescription
    okbooleanrequired—
    failuresarray<object>required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/state-registration.verifyFilingOutcomeVerify an executor's claimed filing outcome against captured evidence (confirmation number + screenshot, optional re-check) before the registration is allowed to flip to 'submitted' (AC-5/AC-6). An unverifiable claim yields 'rejected' → needs_human, never 'submitted'. NOTIFY. Handler: Phase E.

Verify an executor's claimed filing outcome against captured evidence (confirmation number + screenshot, optional re-check) before the registration is allowed to flip to 'submitted' (AC-5/AC-6). An unverifiable claim yields 'rejected' → needs_human, never 'submitted'. NOTIFY. Handler: Phase E.

Bearer scopesstate_registration.write

Request body StateRegistrationVerifyFilingOutcomeInputrequired

FieldTypeRequiredDescription
attemptIdstringrequired—

Responses

  • 200Successful invocationStateRegistrationVerifyFilingOutcomeOutput
    FieldTypeRequiredDescription
    verdictenum: "confirmed" | "rejected"required—
    confirmationNumberstringrequired—
    reasonstringrequired—
  • 400Validation error (input schema rejected). risk_level=notify.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/state-registration.storeIssuedCredentialStore a portal credential created during registration into the per-tenant credential vault (AC-11). APPROVAL_REQUIRED — creating a credential is a consequential vault write, so it stays UI-only and is NOT exposed to MCP/CLI (verifier-gate finding: a NOTIFY op would be auto-exposed, contra the approved roster). The secret is captured by the vault layer, never passed in model context. Handler: Phase F (needs a real portal-issued credential).

Store a portal credential created during registration into the per-tenant credential vault (AC-11). APPROVAL_REQUIRED — creating a credential is a consequential vault write, so it stays UI-only and is NOT exposed to MCP/CLI (verifier-gate finding: a NOTIFY op would be auto-exposed, contra the approved roster). The secret is captured by the vault layer, never passed in model context. Handler: Phase F (needs a real portal-issued credential).

Bearer scopesstate_registration.write

Request body StateRegistrationStoreIssuedCredentialInputrequired

FieldTypeRequiredDescription
registrationIdstringrequired—
portalUsernamestringrequired—
agencyIdstringrequired—

Responses

  • 200Successful invocationStateRegistrationStoreIssuedCredentialOutput
    FieldTypeRequiredDescription
    credentialIdstringrequired—
    storedbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/state-registration.recordManualFilingOutcomeRecord the outcome of a Kreto operator completing a filing from the ops-fallback queue (AC-10). Writes the SAME evidence shape as the agentic path (actor=user, via='manual'). APPROVAL_REQUIRED — a human is doing the consequential act; UI-only, never MCP/CLI. Handler: Phase E.

Record the outcome of a Kreto operator completing a filing from the ops-fallback queue (AC-10). Writes the SAME evidence shape as the agentic path (actor=user, via='manual'). APPROVAL_REQUIRED — a human is doing the consequential act; UI-only, never MCP/CLI. Handler: Phase E.

Bearer scopesstate_registration.write

Request body StateRegistrationRecordManualFilingOutcomeInputrequired

FieldTypeRequiredDescription
registrationIdstringrequired—
attemptIdstringoptional—
outcomeenum: "submitted" | "rejected" | "needs_more_info"required—
confirmationNumberstringoptional—
feeAmountCentsintegeroptional—
notesstringoptional—

Responses

  • 200Successful invocationStateRegistrationRecordManualFilingOutcomeOutput
    FieldTypeRequiredDescription
    registrationIdstringrequired—
    statusstringrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `state_registration.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

l2:team-invitations6

POST/v1/team-invitations.createInvitationCreate a team-invitation row, generate a single-use accept token, and email it to the invitee. The accept link is never returned to the caller (ruling 97(a)); the response reports whether the email was sent.

Create a team-invitation row, generate a single-use accept token, and email it to the invitee. The accept link is never returned to the caller (ruling 97(a)); the response reports whether the email was sent.

Bearer scopesemployers.write

Request body TeamInvitationsCreateInvitationInputrequired

FieldTypeRequiredDescription
entity_idstringoptional—
emailstring · emailrequired—
roleenum: "controller" | "viewer"required—

Responses

  • 200Successful invocationTeamInvitationsCreateInvitationOutput
    FieldTypeRequiredDescription
    invitationobjectrequired—
    email_sentbooleanrequired—
    email_failure_reasonenum: "suppressed_non_prod" | "rate_limited" | "rejected" | "unavailable" | …optional—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `employers.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/team-invitations.listInvitationsList team invitations for an entity (defaults to caller's owned entity). Never returns plaintext tokens or token hashes.

List team invitations for an entity (defaults to caller's owned entity). Never returns plaintext tokens or token hashes.

Bearer scopesemployers.read

Request body TeamInvitationsListInvitationsInputrequired

FieldTypeRequiredDescription
entity_idstringoptional—
statusenum: "pending" | "accepted" | "revoked" | "expired"optional—

Responses

  • 200Successful invocationTeamInvitationsListInvitationsOutput
    FieldTypeRequiredDescription
    invitationsarray<object>required—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `employers.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/team-invitations.revokeInvitationRevoke a pending invitation. Idempotent for already-revoked/accepted rows (returns current status). Only entity owners may revoke.

Revoke a pending invitation. Idempotent for already-revoked/accepted rows (returns current status). Only entity owners may revoke.

Bearer scopesemployers.write

Request body TeamInvitationsRevokeInvitationInputrequired

FieldTypeRequiredDescription
invitation_idstringrequired—

Responses

  • 200Successful invocationTeamInvitationsRevokeInvitationOutput
    FieldTypeRequiredDescription
    idstringrequired—
    statusstringrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `employers.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/team-invitations.resendInvitationRe-send a pending team invitation's e-mail. Mints a NEW single-use accept token and invalidates the previous one, so the earlier link stops working. The accept link is never returned to the caller (ruling 97(a)); the response reports whether the e-mail was sent. Expired, accepted and revoked invitations cannot be resent. Only entity owners may call.

Re-send a pending team invitation's e-mail. Mints a NEW single-use accept token and invalidates the previous one, so the earlier link stops working. The accept link is never returned to the caller (ruling 97(a)); the response reports whether the e-mail was sent. Expired, accepted and revoked invitations cannot be resent. Only entity owners may call.

Bearer scopesemployers.write

Request body TeamInvitationsResendInvitationInputrequired

FieldTypeRequiredDescription
invitation_idstringrequired—

Responses

  • 200Successful invocationTeamInvitationsResendInvitationOutput
    FieldTypeRequiredDescription
    idstringrequired—
    statusstringrequired—
    expires_atstringrequired—
    email_sentbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `employers.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/team-invitations.acceptInvitationAccept a team invitation. Verifies token, expiry, self-acceptance, and email-mismatch guards. Inserts entity_members row on success. The caller MUST be signed in as the invited email.

Accept a team invitation. Verifies token, expiry, self-acceptance, and email-mismatch guards. Inserts entity_members row on success. The caller MUST be signed in as the invited email.

Bearer scopesemployers.write

Request body TeamInvitationsAcceptInvitationInputrequired

FieldTypeRequiredDescription
tokenstringrequired—

Responses

  • 200Successful invocationTeamInvitationsAcceptInvitationOutput
    FieldTypeRequiredDescription
    invitation_idstringrequired—
    entity_idstringrequired—
    rolestringrequired—
    role_grantedbooleanrequired—
    existing_role_preservedbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `employers.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/team-invitations.getInvitationByTokenPublic lookup by plaintext token (hashes on read; never returns hash or plaintext). Used by /accept-team-invite/[token] for render.

Public lookup by plaintext token (hashes on read; never returns hash or plaintext). Used by /accept-team-invite/[token] for render.

Bearer scopesemployers.read

Request body TeamInvitationsGetInvitationByTokenInputrequired

FieldTypeRequiredDescription
tokenstringrequired—

Responses

  • 200Successful invocationTeamInvitationsGetInvitationByTokenOutput
    FieldTypeRequiredDescription
    invitationobjectrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `employers.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

l2:tenants1

POST/v1/tenants.requestDeletionFile an APPROVAL_REQUIRED request to delete the caller's own tenant. Admin-only, human-only, and NON-DESTRUCTIVE: it creates a pending approval_requests row and an immutable tenant_deletion_requested audit event, and never deletes anything. The hard delete is performed by the retention executor after the grace window, and every delete path — this one or an out-of-band SQL session — writes a tenant_deleted audit event by database trigger (20260918000000, ruling 141(a)).

File an APPROVAL_REQUIRED request to delete the caller's own tenant. Admin-only, human-only, and NON-DESTRUCTIVE: it creates a pending approval_requests row and an immutable tenant_deletion_requested audit event, and never deletes anything. The hard delete is performed by the retention executor after the grace window, and every delete path — this one or an out-of-band SQL session — writes a tenant_deleted audit event by database trigger (20260918000000, ruling 141(a)).

Bearer scopesadmin.write

Request body TenantsRequestDeletionInputrequired

FieldTypeRequiredDescription
tenantIdstringrequired—
reasonstringrequired—

Responses

  • 200Successful invocationTenantsRequestDeletionOutput
    FieldTypeRequiredDescription
    approvalRequestIdstringrequired—
    tenantIdstringrequired—
    statusenum: "pending"required—
    retentionDaysintegerrequired—
    earliestPurgeAtstringrequired—
    auditEventIdstringrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

l2:vault4

POST/v1/vault.getPolicyRead the account's vault policy (who may reveal plaintext, per-portal MFA overrides, notification routing), the immutable hard floors, and the break-glass lock state. Use to check whether vault operations are currently halted for the tenant.

Read the account's vault policy (who may reveal plaintext, per-portal MFA overrides, notification routing), the immutable hard floors, and the break-glass lock state. Use to check whether vault operations are currently halted for the tenant.

Bearer scopesadmin.read

Request body VaultGetPolicyInputrequired

application/json

Responses

  • 200Successful invocationVaultGetPolicyOutput
    FieldTypeRequiredDescription
    policyobjectrequired—
    lockobjectrequired—
    vaultV1Enforcementbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=autonomous.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.read`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/vault.setPolicyUpdate the account's configurable vault policy: reveal visibility, per-portal MFA strategy overrides, notification routing. The hard floors (money approval, freeze-on-first-failure) are not accepted by this method by construction.

Update the account's configurable vault policy: reveal visibility, per-portal MFA strategy overrides, notification routing. The hard floors (money approval, freeze-on-first-failure) are not accepted by this method by construction.

Bearer scopesadmin.write

Request body VaultSetPolicyInputrequired

FieldTypeRequiredDescription
revealVisibilityenum: "members" | "admins_only"required—
mfaMatrixOverrideobjectoptional—
notificationRoutingobjectoptional—

Responses

  • 200Successful invocationVaultSetPolicyOutput
    FieldTypeRequiredDescription
    policyobjectrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/vault.breakGlassEmergency brake: immediately halt all credential decryption, use, rotation, and reveal for the tenant WITHOUT destroying key material. Reversible — restoring requires dual confirmation. Use on suspected compromise.

Emergency brake: immediately halt all credential decryption, use, rotation, and reveal for the tenant WITHOUT destroying key material. Reversible — restoring requires dual confirmation. Use on suspected compromise.

Bearer scopesadmin.write

Request body VaultBreakGlassInputrequired

FieldTypeRequiredDescription
reasonstringrequired—
confirmenum: truerequired—

Responses

  • 200Successful invocationVaultBreakGlassOutput
    FieldTypeRequiredDescription
    lockedenum: truerequired—
    lockedAtstringrequired—
    alreadyLockedbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)
POST/v1/vault.restoreSecond half of break-glass: lift the tenant-wide vault halt. Requires two confirmations from DIFFERENT humans (interim out-of-band dual-confirm until KRT-1554 step-up auth ships). The first call marks the restore pending; a second call by a different user completes it.

Second half of break-glass: lift the tenant-wide vault halt. Requires two confirmations from DIFFERENT humans (interim out-of-band dual-confirm until KRT-1554 step-up auth ships). The first call marks the restore pending; a second call by a different user completes it.

Bearer scopesadmin.write

Request body VaultRestoreInputrequired

FieldTypeRequiredDescription
confirmenum: truerequired—
reasonstringoptional—

Responses

  • 200Successful invocationVaultRestoreOutput
    FieldTypeRequiredDescription
    lockedbooleanrequired—
    restorePendingbooleanrequired—
    restoredbooleanrequired—
    secondConfirmerRequiredbooleanrequired—
  • 400Validation error (input schema rejected). risk_level=approval_required.L2ErrorEnvelope
  • 401Unauthorized (missing or invalid bearer token)L2ErrorEnvelope
  • 402Payment required — the tenant has no active subscription for this write-class operation (code `SUBSCRIPTION_REQUIRED`). The REST twin refuses the same way; see /api/notices/upload. Read-class operations are not gated.L2ErrorEnvelope
  • 403Forbidden (missing scope or RLS denied). APPROVAL_REQUIRED ops additionally need a human approval token; see KRT-1554 step-up flow. Requires scope `admin.write`.L2ErrorEnvelope
  • 429Rate limit exceeded — the same refusal the REST doors emit (`rateLimitRefusalBody`), with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` and `Retry-After`. Bearer keys are metered per key; a response carrying `X-RateLimit-Enforced: false` was not counted (limiter store unavailable, fail-open).MiscRateLimitRefusalResponse
  • 500Internal server error (`L2ServerError`), or — on the cookie-session leg only — a signed-in user with no profile gets the auth gate's `AccountSetupIncompleteResponse` (the dispatcher authenticates through `requireBearerOrSession`, whose session leg is `requireAuthAPI`).one of (L2ServerError · AccountSetupIncompleteResponse)

My Compliance16

POST/api/my-compliance/signature-requests/{id}/fresh-linkGet a fresh signing link

Replace a PENDING, unexpired signing link on one of the employer's own authorizations with a new one and return it once (no email). The previous link stops working; the expiry is unchanged. Shares the 5-send cap with the firm's Resend. Cookie session only, employer role only. Unknown, foreign and other-employer ids share one 404 body.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredSigning request (signature_requests) UUID

Responses

  • 200Link rotated and returned onceFreshSigningLinkResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    urlstringrequiredThe new signing link, returned this once. The previous link stops working. Never stored, logged or shown again.
    expires_atstringrequiredUnchanged — the new link keeps the request's expiry.
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Forbidden — employer access requiredMiscUnauthorizedResponse
  • 404Signing request not found (or not the caller's)FreshSigningLinkNotFound
  • 409Not renewable — signed, expired, cancelled, or changedFreshSigningLinkRefusal
  • 429This request has reached its send limitFreshSigningLinkLimit
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
  • 503App origin not configured, or unavailable on this deploymentFreshSigningLinkUnavailable
GET/api/my-compliance/letterheadGet the employer's letterhead

The letterhead every letter from a standalone employer's workspace prints, and which required fields are still empty. Cookie session only, employer role only. A CPA firm's client gets the same 404 as an absent workspace — its letters carry the firm's letterhead.

Cookie session or Bearer token

Responses

  • 200The letterheadEmployerLetterheadResponse
    FieldTypeRequiredDescription
    namestringrequiredThe workspace's name (from sign-up); read-only here.
    letterhead_address_line1stringrequired—
    letterhead_address_line2stringrequired—
    letterhead_citystringrequired—
    letterhead_statestringrequired—
    letterhead_postal_codestringrequired—
    letterhead_phonestringrequired—
    letterhead_emailstringrequired—
    letterhead_signer_namestringrequired—
    letterhead_signer_titlestringrequired—
    missingarray<string>requiredRequired letterhead fields still empty. Non-empty means every new draft is marked letterhead-incomplete and cannot be approved.
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Forbidden — employer access requiredMiscUnauthorizedResponse
  • 404No letterhead door for this workspaceEmployerLetterheadNotFound
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
PUT/api/my-compliance/letterheadUpdate the employer's letterhead

Edit a standalone employer's letterhead. Absent key = untouched; an empty string or null clears the field. Validated by the same rules as the firm's letterhead. Audited (field names, never values). Cookie session only, employer role only; a CPA firm's client gets the 404.

Cookie session or Bearer token

Request body EmployerLetterheadUpdateBodyrequired

FieldTypeRequiredDescription
letterhead_address_line1stringoptional—
letterhead_address_line2stringoptional—
letterhead_citystringoptional—
letterhead_statestringoptional—
letterhead_postal_codestringoptional—
letterhead_phonestringoptional—
letterhead_emailstringoptional—
letterhead_signer_namestringoptional—
letterhead_signer_titlestringoptional—

Responses

  • 200Letterhead savedEmployerLetterheadResponse
    FieldTypeRequiredDescription
    namestringrequiredThe workspace's name (from sign-up); read-only here.
    letterhead_address_line1stringrequired—
    letterhead_address_line2stringrequired—
    letterhead_citystringrequired—
    letterhead_statestringrequired—
    letterhead_postal_codestringrequired—
    letterhead_phonestringrequired—
    letterhead_emailstringrequired—
    letterhead_signer_namestringrequired—
    letterhead_signer_titlestringrequired—
    missingarray<string>requiredRequired letterhead fields still empty. Non-empty means every new draft is marked letterhead-incomplete and cannot be approved.
  • 400Not a valid letterhead editEmployerLetterheadInvalid
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Forbidden — employer access requiredMiscUnauthorizedResponse
  • 404No letterhead door for this workspaceEmployerLetterheadNotFound
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/employer-cpa-attachmentsGet the employer's accountant attachment

The 'Your accountant' card state for a standalone employer (or the attached accountant). Cookie session only. Flag off, wrong plane or a firm member with no attachment answer the one 404 marker.

Cookie session or Bearer token

Responses

  • 200Card stateEmployerCpaStatus
    FieldTypeRequiredDescription
    kindenum: "self_tenant" | "cpa_managed_client" | "firm_member"required—
    can_invitebooleanrequired—
    attachmentobjectrequired—
  • 401UnauthorizedMiscUnauthorizedResponse
  • 404Not foundEmployerCpaNotFound
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/employer-cpa-attachmentsInvite an accountant

A client_user of a standalone ('self') workspace invites ONE accountant by email. Refused when the workspace is firm-managed (403 NOT_SELF_TENANT), already has a live accountant (409 CPA_ALREADY_ATTACHED) or a pending invite (409 CPA_INVITE_PENDING).

Cookie session or Bearer token

Request body EmployerCpaInviteBodyrequired

FieldTypeRequiredDescription
emailstringrequired—
firm_namestringrequired—

Responses

  • 201Invite createdEmployerCpaInviteResponse
    FieldTypeRequiredDescription
    inviteobjectrequired—
  • 400Invalid bodyEmployerCpaRefusal
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Not a standalone employerEmployerCpaRefusal
  • 404Not foundEmployerCpaNotFound
  • 409Already attached / invite pendingEmployerCpaRefusal
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
DELETE/api/employer-cpa-attachmentsRemove the accountant (or cancel the pending invite)

Detach: soft-removes the accountant's profile in this workspace (their earlier sessions refuse) and the workspace reads standalone again; a pending invite is cancelled instead. Audited.

Cookie session or Bearer token

Responses

  • 200Detached or cancelledEmployerCpaDetachResponse
    FieldTypeRequiredDescription
    outcomeenum: "detached" | "cancelled"required—
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Not a standalone employerEmployerCpaRefusal
  • 404Nothing attachedEmployerCpaNotFound
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/employer-cpa-attachments/token/{token}Look up an accountant invite by token (public)

Public token plane: the URL-path token is the credential. Every non-live state (never issued, revoked, expired, accepted, detached, malformed) answers the one closed-invite response.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
tokenpathstringrequired—

Responses

  • 200Live inviteEmployerCpaInvitePeek
    FieldTypeRequiredDescription
    invitationobjectrequired—
  • 404Closed inviteEmployerCpaClosedInvite
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/employer-cpa-attachments/token/{token}/acceptAccept an accountant invite

The signed-in invitee (no Kreto profile yet) becomes the workspace's accountant (cpa_member). Refused with 403 CPA_HAS_WORKSPACE when the login already has a profile, 403 email_mismatch for another address, 409 already_accepted on a re-click; closed tokens answer the one closed-invite response.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
tokenpathstringrequired—

Responses

  • 200AttachedEmployerCpaAcceptResponse
    FieldTypeRequiredDescription
    successenum: truerequired—
    redirectstringrequired—
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403RefusedEmployerCpaRefusal
  • 404Closed inviteEmployerCpaClosedInvite
  • 409Already accepted / already attachedEmployerCpaRefusal
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/my-compliance/profileRead the caller's business profile (employer-tier)

Returns the entity row scoped to the authenticated user via entity_members. Works for both cpa_member (returns first entity) and client_user (returns their single entity). Bearer auth requires employers.read scope.

Cookie session or Bearer token

Responses

  • 200Business profileMyComplianceProfile
    FieldTypeRequiredDescription
    entity_idstring · uuidrequired—
    legal_namestringrequired—
    ein_last4stringrequired—
    primary_statestringrequired—
    compliance_scorenumberrequired—
    compliance_score_statusenum: "scored" | "not_scored" | "unavailable"requiredThe compliance monitor's verdict on `compliance_score`: `scored` (a number was measured), `not_scored` (nothing on file to measure), or `unavailable` (the monitor read refused). Never a manufactured 0.
    created_atstring · date-timerequired—
  • 401UnauthorizedMyComplianceError
  • 404No business profile linked to this userMyComplianceError
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth gate's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
PATCH/api/my-compliance/profileUpdate business profile fields

Partial update of the caller's entity. Sends only the fields that changed. EIN is stored as last-4 only (PII rule). Bearer auth requires employers.write scope, which only CPA-tier keys carry; an employer-tier key cannot call this.

Cookie session or Bearer token

Request body MyComplianceProfilePatchrequired

FieldTypeRequiredDescription
legal_namestringoptional—
ein_last4stringoptional—
primary_statestringoptional—
payroll_providerenum: "adp" | "gusto" | "paychex" | "quickbooks" | …optionalThe employer's payroll provider (owner walk S6). `manual` = payroll run in-house; `none` = no payroll yet; `other` requires `payroll_provider_other`. Any other value clears that name.
payroll_provider_otherstringoptionalThe provider's name when `payroll_provider` is `other`; only sent together with `payroll_provider`.

Responses

  • 200Profile updatedMyComplianceProfilePatchResponse
    FieldTypeRequiredDescription
    okbooleanrequired—
    entityobjectrequired—
    draftsRequeuedintegeroptionalDrafts re-queued because this save put a legal name on a record that had none. Absent when the save was not a blank-to-name save.
    draftsRequeueErrorenum: "requeue_failed"optionalThe name was saved but some or all of the waiting drafts could not be re-queued (draftsRequeued counts the ones that were). Generate the rest again by hand.
  • 400Validation errorMyComplianceError
  • 404No business profile linked to this userMyComplianceError
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth gate's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/my-compliance/auditAudit log for the caller's entity

Read-only timeline of audit_events filtered to the entity the caller belongs to. Composite cursor pagination — pass next_cursor back as `before` to fetch the next page. Bearer auth requires audit.read scope.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
limitqueryintegeroptional—
beforequerystringoptionalComposite cursor in the form '<iso8601>|<uuid>'. Legacy ISO-only cursors also accepted for backwards compatibility.

Responses

  • 200Audit log pageMyComplianceAuditList
    FieldTypeRequiredDescription
    eventsarray<MyComplianceAuditEvent>required—
    next_cursorstringrequired—
    entity_idsarray<string · uuid>required—
  • 400Invalid cursorMyComplianceError
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth gate's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/my-compliance/api-tokensList the caller's non-revoked Bearer tokens

Returns API keys created by the authenticated user, filtered to their tenant. Plaintext is NEVER included — only key_prefix + metadata. Bearer auth NOT supported on this route (cookie-session only) to prevent a token from listing or mass-revoking peers.

Cookie session or Bearer token

Responses

  • 200Token list (non-revoked only)MyComplianceApiTokenList
    FieldTypeRequiredDescription
    tokensarray<MyComplianceApiToken>required—
  • 401UnauthorizedMyComplianceError
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/my-compliance/api-tokensMint a new employer-scoped Bearer token

Creates a new api_keys row via the same hash-on-write pipeline as /api/api-keys, but with the scopes constrained to the employer allow-list (notices.read, employers.read, payroll.read, audit.read). Plaintext is returned ONCE in `plaintext` — store it now; we keep only the bcrypt hash. RLS migration 20260615000000 enforces the scope subset at the DB layer too (defense in depth).

Cookie session or Bearer token

Request body MyComplianceApiTokenCreaterequired

FieldTypeRequiredDescription
namestringoptional—
scopesarray<string>required—
expires_atstring · date-timeoptionalOptional future expiry. Omitted or null = 90 days from mint (DEFAULT_TOKEN_LIFETIME_DAYS); a token never mints without one.

Responses

  • 200Token created. Plaintext returned ONCE.MyComplianceApiTokenCreated
    FieldTypeRequiredDescription
    okbooleanrequired—
    plaintextstringrequired—
    tokenobjectrequired—
  • 400Refused before any key material exists: no scopes (code SCOPES_REQUIRED), a malformed or all-unmintable scopes array, or an expires_at that is not a future ISO timestamp (invalid_expires_at).MyComplianceError
  • 500Failed to create tokenone of (SanitizedServerError · AccountSetupIncompleteResponse)
DELETE/api/my-compliance/api-tokensRevoke one of your own Bearer tokens

Soft-delete via revoked_at timestamp; audit traces survive. Scoped to the caller's own tokens by created_by_user_id — you cannot revoke peers' tokens even within the same tenant.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
idquerystring · uuidrequiredUUID of the api_keys row to revoke

Responses

  • 200Token revoked (idempotent — re-revoking is a no-op 200)MyComplianceOkResponse
    FieldTypeRequiredDescription
    okbooleanrequired—
  • 400Missing token idMyComplianceError
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/my-compliance/poa/{id}/signed-pdfDownload the signed IRS 8821 for one of the caller's employers

Streams the signed Form 8821 PDF inline. The authorization is resolved through the caller's OWN entity_members set — never from the path alone — so a POA belonging to a sibling employer in the same CPA tenant is refused. Not-found and not-yours deliberately return the SAME 404 so the route is not an existence oracle for other employers' POA ids. Response is tax-authorization PII: Cache-Control is private, no-store.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredpoa_authorizations UUID

Responses

  • 200Signed 8821 PDFstring · binary
  • 400Invalid authorization id (not a UUID)MyComplianceError
  • 401UnauthorizedMyComplianceError
  • 404Authorization not found, not the caller's, or not yet signedMyComplianceError
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth gate's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/my-compliance/state-registrationsStart a state registration for the caller's own business

Employer (client_user) session only. Creates one open state-registration record for the caller's own business through the same store as the CPA manual start. One open record per business and state: a second start for the same state answers 409 naming the existing record. Only (state, requirement) pairs Kreto supports are accepted.

Cookie session or Bearer token

Request body EmployerRegisterStateBodyrequired

FieldTypeRequiredDescription
statestringrequired—
requirementenum: "sui_registration" | "withholding_registration" | "workers_comp" | "pfml" | …required—
entity_idstring · uuidoptional—

Responses

  • 201Record createdEmployerRegisterStateCreated
    FieldTypeRequiredDescription
    recommendationobjectrequired—
    feeSentencestringrequired—
  • 400Invalid body, or an unsupported state/requirementEmployerRegisterStateError
  • 401UnauthorizedEmployerRegisterStateError
  • 403Not an employer sessionEmployerRegisterStateError
  • 404Business not found (the one marker for anything out of scope)EmployerRegisterStateError
  • 409An open registration already exists for this stateEmployerRegisterStateAlreadyOpen
  • 500Server error — sanitized, or the auth gate's AccountSetupIncompleteResponse for a session with no profileone of (SanitizedServerError · AccountSetupIncompleteResponse)

Notices68

GET/api/notices/{id}/auditGet notice audit trail

Returns the append-only audit chain for a single notice, including every event's hash links and a `chain_valid` flag attesting that the SHA-256 chain verifies cryptographically. `chain_valid` is `null` when the chain is empty — an empty chain is not a verified chain — so it is a nullable boolean, never `true` beside `total: 0`.

Bearer scopesaudit.read

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Responses

  • 200Audit chain + integrity attestationNoticeAuditResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    notice_idstring · uuidrequired—
    entriesarray<AuditEntry>required—
    chain_validbooleanrequirednull when the chain is empty — an empty chain is not a verified chain
    chain_break_atintegerrequired—
    totalintegerrequired—
  • 400Validation errorNoticeValidationErrorResponse
  • 401UnauthorizedNoticeUnauthorizedResponse
  • 403Forbidden — missing required scopeNoticeUnauthorizedResponse
  • 404Notice not found in caller's tenantNoticeNotFoundResponse
  • 429Rate limit exceededNoticeRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/notices/{id}/document-urlGet signed URL for notice document

Returns a 1-hour signed URL for the original notice document. Tenant-scoped: only notices in the caller's tenant resolve to a URL.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Responses

  • 200Signed URL (or null if no document is attached)NoticeDocumentUrlResponse
    FieldTypeRequiredDescription
    urlstringrequired—
  • 400Validation errorNoticeValidationErrorResponse
  • 401UnauthorizedNoticeUnauthorizedResponse
  • 403Forbidden — missing required scopeNoticeUnauthorizedResponse
  • 404Notice not found in caller's tenant — the same landing for an unknown id and for a foreign-tenant idNoticeNotFoundResponse
  • 429Rate limit exceededNoticeRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (NoticeDocumentUrlServerError · AccountSetupIncompleteResponse)
GET/api/notices/{id}/draft-dataGet latest notice draft

Returns the latest resolution draft for a notice — content, summary, structured action_items, response_type, and status. Reads the canonical `resolution_drafts` table.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Responses

  • 200Latest draft (or null fields if none exists)NoticeDraftDataResponse
    FieldTypeRequiredDescription
    contentstringrequired—
    summarystringrequired—
    action_itemsarray<string>required—
    response_typestringrequired—
    statusstringoptional—
    created_atstring · date-timeoptional—
  • 400Validation errorNoticeValidationErrorResponse
  • 401UnauthorizedNoticeUnauthorizedResponse
  • 403Forbidden — missing required scopeNoticeUnauthorizedResponse
  • 404Notice not found in caller's tenantNoticeNotFoundResponse
  • 429Rate limit exceededNoticeRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
PATCH/api/notices/{id}/entityReassign notice to another entity

Re-attribute a notice to a different entity. Both notice and target entity must be in the caller's tenant; the entity is verified before the update so cross-tenant re-attribution is impossible.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Request body NoticeEntityPatchBodyrequired

FieldTypeRequiredDescription
entity_idstring · uuidrequired—

Responses

  • 200UpdatedNoticeEntityPatchResponse
    FieldTypeRequiredDescription
    successenum: truerequired—
  • 400Validation errorNoticeValidationErrorResponse
  • 401UnauthorizedNoticeUnauthorizedResponse
  • 403Forbidden — missing required scopeNoticeUnauthorizedResponse
  • 404Notice or target entity not found in caller's tenantNoticeNotFoundResponse
  • 422The chosen entity is the firm's own record (`code: employer_is_firm_self`) — it cannot be a notice's employer; nothing was writtenNoticeEmployerIsFirmSelfResponse
  • 429Rate limit exceededNoticeRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/notices/{id}/letterGet signed URL for the stored response letter

Returns a short-lived signed URL for the STORED response letter — bytes the browser did not compose — together with the content hash they were filed under. 404 when no letter has been stored for the notice, and a voided or deleted artifact reads as absent rather than as a stale URL.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Responses

  • 200Signed URL + the artifact's identityNoticeLetterUrlResponse
    FieldTypeRequiredDescription
    urlstringrequired—
    document_idstring · uuidrequired—
    content_hashstringrequired—
    versionintegerrequired—
    rendered_atstring · date-timerequirednull when the running database predates migration 20260977000000 and the binding columns were unreadable (§SCHEMA-FORWARD READS).
  • 400Validation errorNoticeValidationErrorResponse
  • 401UnauthorizedNoticeUnauthorizedResponse
  • 403Forbidden — missing required scope or non-CPA sessionNoticeUnauthorizedResponse
  • 404Notice not found in caller's tenant, or no stored letter for itNoticeNotFoundResponse
  • 429Rate limit exceededNoticeRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/notices/{id}/letterRender and store the notice's response letter

Renders the notice's current resolution draft into the agency-bound letter SERVER-SIDE, hashes the bytes that are stored, files them as a `documents` row (kind `response`, `checksum` set) and binds that row to the draft version. Idempotent by content hash: identical bytes reuse the existing row rather than inserting a second one. The render is behind the SAME refusal predicate as approval — a draft carrying a redaction placeholder, or blocked on payroll, is refused 409 with no override field.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Responses

  • 200The stored artifact's identityNoticeLetterArtifactResponse
    FieldTypeRequiredDescription
    document_idstring · uuidrequired—
    content_hashstringrequiredsha256 of the bytes that were STORED, hex
    versionintegerrequiredresolution_drafts.version these bytes were rendered from
    substitutionsarray<string>requiredDistinct U+XXXX code points the WinAnsi seam had to replace before drawing. A substituted character on a filed letter is a fact a human signs off on, not a log line — hence it is in the response.
    substitution_countintegerrequired—
    reused_existing_documentbooleanrequiredTrue when identical bytes were already stored and the existing documents row was reused (idempotent by content hash).
  • 400Validation errorNoticeValidationErrorResponse
  • 401UnauthorizedNoticeUnauthorizedResponse
  • 403Forbidden — missing required scope or non-CPA sessionNoticeUnauthorizedResponse
  • 404Notice not found in caller's tenant, or the notice has no draft — the same landing for an unknown id and for a foreign-tenant idNoticeNotFoundResponse
  • 409A gate held: the draft is blocked (`DRAFT_BLOCKED`) or the notice's employer is not confirmed yet (`ENTITY_UNCONFIRMED`)NoticeLetterBlockedResponse
  • 429Rate limit exceededNoticeRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/notices/{id}/confirm-stateConfirm notice state

Confirm or override the state on a notice. Used when AI extraction misses or misclassifies the state and a human corrects it. The override is logged to audit_events for compliance traceability.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Request body NoticeConfirmStateBodyrequired

FieldTypeRequiredDescription
confirmedStatestringrequired—

Responses

  • 200ConfirmedNoticeSuccessResponse
    FieldTypeRequiredDescription
    successenum: truerequired—
  • 400Validation errorNoticeValidationErrorResponse
  • 401UnauthorizedNoticeUnauthorizedResponse
  • 403Forbidden — missing required scopeNoticeUnauthorizedResponse
  • 404Notice not found in caller's tenantNoticeNotFoundResponse
  • 429Rate limit exceededNoticeRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/notices/{id}/documentDownload notice document

Streams the original notice document (PDF/PNG/JPEG/TIFF) inline. Browser sees a Kreto URL, not a Supabase storage URL.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Responses

  • 200Binary document content (PDF/PNG/JPEG/TIFF) streamed inline. OpenAPI generator requires schemas for each content type — using z.string() (binary) as a generic placeholder; clients should consume the response stream directly per Content-Type header.string
  • 400Validation errorNoticeValidationErrorResponse
  • 401Unauthorized
  • 403Forbidden — missing required scope
  • 404Document not found / notice not in caller's tenant
  • 429Rate limit exceeded
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/notices/{id}/supplementalGet notice supplemental data

Returns the active work_item, document_requests, and messages associated with a notice. All queries tenant-scoped.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Responses

  • 200Supplemental bundleNoticeSupplementalResponse
    FieldTypeRequiredDescription
    work_itemNoticeWorkItemrequired—
    document_requestsarray<NoticeDocumentRequest>required—
    messagesarray<NoticeMessage>required—
    sender_namesobjectrequired—
  • 400Validation errorNoticeValidationErrorResponse
  • 401UnauthorizedNoticeUnauthorizedResponse
  • 403Forbidden — missing required scopeNoticeUnauthorizedResponse
  • 404Notice not found in caller's tenantNoticeNotFoundResponse
  • 429Rate limit exceededNoticeRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/notices/{id}/statusGet notice pipeline status

Returns pipeline status + classification result for a notice (notice_type_classified, classification_confidence, protest_deadline, financial_impact, protest_recommended, assigned_rate, pipeline_status).

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Responses

  • 200Pipeline statusNoticeStatusResponse
    FieldTypeRequiredDescription
    idstring · uuidrequired—
    notice_typestringrequired—
    statestringrequired—
    pipeline_statusstringrequired—
    notice_type_classifiedstringrequired—
    classification_confidencenumberrequired—
    protest_deadlinestring · daterequired—
    financial_impactnumberrequired—
    protest_recommendedbooleanrequired—
    assigned_ratenumberrequired—
  • 400Validation errorNoticeValidationErrorResponse
  • 401UnauthorizedNoticeUnauthorizedResponse
  • 403Forbidden — missing required scopeNoticeUnauthorizedResponse
  • 404Notice not found in caller's tenantNoticeNotFoundResponse
  • 429Rate limit exceededNoticeRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/notices/{id}/statusUpdate notice triage fields

Update notice triage fields (assigned_rate, is_new_employer). is_new_employer is stored inside extracted_json (no dedicated column).

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Request body NoticeStatusPostBodyrequired

FieldTypeRequiredDescription
assigned_ratenumberoptional—
is_new_employerbooleanoptional—

Responses

  • 200UpdatedNoticeStatusPostResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    updatedNoticeStatusPostUpdatedrequired—
  • 400Validation errorNoticeValidationErrorResponse
  • 401UnauthorizedNoticeUnauthorizedResponse
  • 403Forbidden — missing required scopeNoticeUnauthorizedResponse
  • 404Notice not found in caller's tenantNoticeNotFoundResponse
  • 429Rate limit exceededNoticeRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/notices/{id}/entity-match-resolveResolve fuzzy entity match

Resolve a fuzzy entity-match candidate set for a notice. `action=confirmed` assigns the notice to an existing entity (verified to belong to caller's tenant). `action=create_new` creates a new entity in the notice's tenant and assigns the notice to it.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Request body NoticeEntityMatchResolveBodyrequired

application/json

Responses

  • 200ResolvedNoticeEntityMatchResolveResponse
    FieldTypeRequiredDescription
    successenum: truerequired—
    entity_idstring · uuidrequired—
    actionenum: "confirmed" | "create_new"required—
    entity_namestringoptional—
  • 400Validation errorNoticeValidationErrorResponse
  • 401UnauthorizedNoticeUnauthorizedResponse
  • 403Forbidden — missing required scopeNoticeUnauthorizedResponse
  • 404Notice or resolved entity not found in caller's tenantNoticeNotFoundResponse
  • 422The chosen entity is the firm's own record (`code: employer_is_firm_self`) — it cannot be a notice's employer; nothing was writtenNoticeEmployerIsFirmSelfResponse
  • 429Rate limit exceededNoticeRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/notices/{id}/similarFind similar resolved notices

Find anonymized resolution patterns + similar resolved notices for a given notice. The source notice lookup is tenant-scoped; the pattern + outcomes lookups are intentionally cross-tenant (anonymized aggregate, no PII).

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Responses

  • 200Resolution pattern + anonymized similar noticesNoticeSimilarResponse
    FieldTypeRequiredDescription
    patternResolutionPatternrequired—
    similar_countintegerrequired—
    similar_noticesarray<SimilarNotice>required—
    recommendationstringrequired—
  • 400Validation errorNoticeValidationErrorResponse
  • 401UnauthorizedNoticeUnauthorizedResponse
  • 403Forbidden — missing required scopeNoticeUnauthorizedResponse
  • 404Notice not found in caller's tenantNoticeNotFoundResponse
  • 429Rate limit exceededNoticeRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/notices/{id}/outcomeRecord notice outcome

Record the resolution outcome when a notice is resolved. Inserts a notice_outcomes row, optionally captures classification feedback, closes the notice, and aggregates into the cross-tenant resolution_patterns inventory for similar-notice recommendations.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Request body NoticeOutcomePostBodyrequired

FieldTypeRequiredDescription
resolution_typestringrequired—
resolution_notesstringoptional—
resolution_methodstringoptional—
corrected_typestringoptional—
original_amountnumberoptional—
final_amountnumberoptional—
documents_usedarray<DocumentRef>optional—
documents_missingarray<DocumentRef>optional—
deadline_was_correctbooleanoptional—
actual_deadlinestring · dateoptional—
agency_response_datestring · dateoptional—

Responses

  • 200Outcome recordedNoticeOutcomePostResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    outcome_idstring · uuidrequired—
    savingsnumberrequired—
  • 400Validation errorNoticeValidationErrorResponse
  • 401UnauthorizedNoticeUnauthorizedResponse
  • 403Forbidden — not a CPA-class session nor the self-tenant founderNoticeUnauthorizedResponse
  • 404Notice not found in caller's tenant (or, for the self-tenant founder, outside its entity scope)NoticeNotFoundResponse
  • 429Rate limit exceededNoticeRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/notices/{id}/interpretInterpret notice PDF (Claude)

Synchronously interpret the notice PDF using Claude. Downloads the raw PDF, runs PII-redacted extraction + classification, validates against quality gates (returns 422 if they fail), creates artifact + lineage records, and updates the notice with the result.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Responses

  • 200InterpretedNoticeInterpretResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    noticeIdstring · uuidrequired—
    artifactIdstring · uuidrequired—
    interpretationobjectrequired—
    qualityCheckobjectrequired—
    metadataobjectrequired—
    notice_updatedbooleanrequired—
    notice_update_refusedenum: "terminal" | "write_failed" | "guard_state_unreadable"optional—
    approval_supersededenum: trueoptional—
    audit_event_pendingenum: trueoptional—
  • 400Validation error / no storage path / PDF interpretation failedNoticeValidationErrorResponse
  • 401UnauthorizedNoticeUnauthorizedResponse
  • 403Forbidden — missing required scopeNoticeUnauthorizedResponse
  • 404Notice not found in caller's tenantNoticeNotFoundResponse
  • 422Quality gates failed (interpretation incomplete)NoticeInterpretQualityFailResponse
  • 429Rate limit exceededNoticeRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/notices/classifyClassify notice text/file

Classify a notice from raw text (JSON body) or file upload (multipart/form-data). Runs PII redaction first, then LLM classification with a 15-second timeout. Cookie-session callers go through the Stripe subscription gate; Bearer-key callers do not.

Bearer scopesnotices.write

Request body NoticeClassifyJsonBodyrequired

FieldTypeRequiredDescription
textstringoptional—
entity_idstring · uuidoptional—

Responses

  • 200Classification resultNoticeClassifyResponse
    FieldTypeRequiredDescription
    notice_typestringrequired—
    jurisdictionstringoptional—
    deadlinestring · dateoptional—
    severitystringoptional—
    confidencenumberrequired—
    categorystringoptional—
    subcategorystringoptional—
    extracted_factsobjectrequired—
    enrichmentobjectrequired—
  • 400Validation errorNoticeValidationErrorResponse
  • 401UnauthorizedNoticeUnauthorizedResponse
  • 402Subscription required (cookie-session callers only)NoticeUnauthorizedResponse
  • 403Forbidden — missing required scopeNoticeUnauthorizedResponse
  • 429Rate limit exceededNoticeRateLimitResponse
  • 500Server error / classification timeoutone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/notices/{id}/reopenRe-open a closed notice

CPA-only. Moves a closed (actioned) notice back to filed with a reason code; the filing is kept, the current outcome goes back to pending, and one hash-chained audit row records the move.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Request body objectrequired

FieldTypeRequiredDescription
reasonenum: "outcome_recorded_in_error" | "agency_reversed_decision" | "agency_requested_more" | "other"required—

Responses

  • 200Moved; the notice's new lifecycle stageNoticeEndingMoveResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    lifecycle_statestringrequired—
  • 400Invalid JSON, or the reason is missing / not a listed codeNoticeValidationErrorResponse
  • 401UnauthorizedNoticeUnauthorizedResponse
  • 403Forbidden — a CPA-firm role is requiredNoticeUnauthorizedResponse
  • 404Notice not found (unknown, foreign or malformed id — one body)NoticeNotFoundResponse
  • 409Refused at the notice's current stage (NOTICE_NOT_CLOSED)NoticeEndingMoveRefusal
  • 500Server error — sanitized, or the auth wrapper's AccountSetupIncompleteResponseone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/notices/{id}/withdrawWithdraw a filed response

CPA-only. Moves a filed notice back to approved with a reason code; the filing is cleared, the approval kept, and one hash-chained audit row records the move.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Request body objectrequired

FieldTypeRequiredDescription
reasonenum: "filed_in_error" | "wrong_letter_filed" | "agency_asked_to_resubmit" | "other"required—

Responses

  • 200Moved; the notice's new lifecycle stageNoticeEndingMoveResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    lifecycle_statestringrequired—
  • 400Invalid JSON, or the reason is missing / not a listed codeNoticeValidationErrorResponse
  • 401UnauthorizedNoticeUnauthorizedResponse
  • 403Forbidden — a CPA-firm role is requiredNoticeUnauthorizedResponse
  • 404Notice not found (unknown, foreign or malformed id — one body)NoticeNotFoundResponse
  • 409Refused at the notice's current stage (NOTICE_CLOSED, FILING_REQUIRED, OUTSIDE_RESPONSE)NoticeEndingMoveRefusal
  • 500Server error — sanitized, or the auth wrapper's AccountSetupIncompleteResponseone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/notices/{id}/withdraw-approvalWithdraw the approval of an unsent letter

CPA-only. Takes back the approval of a letter that has not been sent or filed: the notice returns to drafting, the approved letter of record is kept as its version marked 'withdrawn' (the next draft is the next version), and one hash-chained audit row records who, when and the required reason — in one transaction.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Request body objectrequired

FieldTypeRequiredDescription
reasonstringrequired—

Responses

  • 200Withdrawn; the notice's new lifecycle stage and the withdrawn versionNoticeWithdrawApprovalResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    lifecycle_statestringrequired—
    withdrawn_draft_versionintegerrequired—
  • 400Invalid JSON, or the reason is missing or shorter than 10 charactersNoticeValidationErrorResponse
  • 401UnauthorizedNoticeUnauthorizedResponse
  • 403Forbidden — a CPA-firm role is requiredNoticeUnauthorizedResponse
  • 404Notice not found (unknown, foreign or malformed id — one body)NoticeNotFoundResponse
  • 409Refused (LETTER_FILED, LETTER_SENT_FOR_SIGNATURE, NOTICE_CLOSED, NOT_APPROVED)NoticeWithdrawApprovalRefusal
  • 500Server error — sanitized, or the auth wrapper's AccountSetupIncompleteResponseone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/notices/{id}/restoreRestore a document marked not a notice

CPA-only. Clears the not-a-notice flag (confirmed or proposed), puts the notice back at the stage it held before the flag, writes one audit row, and runs reading again. The body is optional; a given reason is the audit rationale (10–500 characters).

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Request body object

FieldTypeRequiredDescription
reasonstringoptional—

Responses

  • 200Restored; the notice's pipeline stage and whether reading was queued againNoticeRestoreResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    pipeline_statusstringrequired—
    reading_requeuedbooleanrequired—
  • 400Invalid JSON, or a given reason is not 10 to 500 charactersNoticeValidationErrorResponse
  • 401UnauthorizedNoticeUnauthorizedResponse
  • 403Forbidden — a CPA-firm role is requiredNoticeUnauthorizedResponse
  • 404Notice not found (unknown, foreign or malformed id — one body)NoticeNotFoundResponse
  • 409Nothing to restore — the notice is not marked not a notice (NOTICE_NOT_JUNK)NoticeRestoreRefusal
  • 500Server error — sanitized, or the auth wrapper's AccountSetupIncompleteResponseone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/notices/{id}/merge-duplicateMerge a possible duplicate into the earlier notice

Ruling Q21. Re-checks that both notices share agency, account number, notice date and amount; keeps the earlier notice, moves the later one's arrivals onto it and dismisses the later one.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Request body objectrequired

FieldTypeRequiredDescription
other_notice_idstring · uuidrequired—

Responses

  • 200Merged; the kept and the merged notice idsNoticeMergeDuplicateResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    kept_notice_idstring · uuidrequired—
    merged_notice_idstring · uuidrequired—
    arrivals_movedintegerrequired—
    documents_movedintegerrequired—
  • 400Invalid JSON, or other_notice_id missingNoticeValidationErrorResponse
  • 401UnauthorizedNoticeUnauthorizedResponse
  • 403Forbidden — a CPA-firm role is requiredNoticeUnauthorizedResponse
  • 404Notice not found (unknown, foreign or malformed id)NoticeNotFoundResponse
  • 409Refused — not a possible duplicate, or the later notice was already worked onNoticeMergeDuplicateRefusal
  • 500Server error — sanitized, or the auth wrapper's AccountSetupIncompleteResponseone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/notices/{id}/draftGet notice draft

Returns the latest resolution draft for a notice (the canonical resolution_drafts table, falling back to an inline draft on notices.extracted_json). Tenant-scoped.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Responses

  • 200Latest draft + evidence packageNoticeDraftGetResponse
    FieldTypeRequiredDescription
    draftNoticeDraftRecordrequired—
    evidence_packageNoticeEvidencePackagerequired—
    notice_deadlinestring · daterequired—
    sourceenum: "resolution_drafts" | "extracted_json"required—
    signature_requestsarray<NoticeSignatureRequestSummary>optional—
  • 400Validation errorNoticeValidationErrorResponse
  • 401UnauthorizedNoticeUnauthorizedResponse
  • 403Forbidden — missing required scopeNoticeUnauthorizedResponse
  • 404Notice or draft not found in caller's tenantNoticeNotFoundResponse
  • 429Rate limit exceededNoticeRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/notices/{id}/draftGenerate notice draft

Generate a new draft via the pipeline draft generator. Cookie-session callers go through the Stripe subscription gate; Bearer-key callers do not.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Responses

  • 200Draft generatedNoticeDraftPostResponse
    FieldTypeRequiredDescription
    draftNoticeDraftRecordrequired—
    evidence_packageNoticeEvidencePackagerequired—
  • 400Validation errorNoticeValidationErrorResponse
  • 401UnauthorizedNoticeUnauthorizedResponse
  • 402Subscription required (cookie-session callers only)NoticeUnauthorizedResponse
  • 403Forbidden — missing required scopeNoticeUnauthorizedResponse
  • 404Notice not found in caller's tenantNoticeNotFoundResponse
  • 409Generation produced a BLOCKED draft (missing payroll, a redaction placeholder, or the manual-review placeholder rather than a letter). The row exists and is named by `draft_id`, but it is not a letter and cannot be approved or filed until it is regenerated. Or (`TRACKED_NOT_CHECKED`): the notice is tracked, not checked (X1), so no one-click draft is written. Write the response by hand instead (PATCH). Nothing is written. Or (`FACTS_UNCONFIRMED`): the CPA has not confirmed what Kreto read (employer, notice type, deadline) — POST /api/notices/{id}/confirm-facts first. Asked after the not-found wall, X1 and the employer refusal. Nothing is written. Or (`DRAFT_JOB_LIVE`, in `error`): a draft-producing job is already queued or running for this notice (`draft_job`); poll the detail path's `draft_job` until `none`. Nothing is written.one of (NoticeDraftBlockedResponse · NoticeDraftTrackedNotCheckedResponse · NoticeDraftFactsUnconfirmedResponse · NoticeDraftJobLiveResponse)
  • 422Generation refused as a human gate, not a fault: the draft is waiting on a fact only a person can supply (`needs_input.kind`; today the employer's legal name, added on the record `entity_id` names), or the output guard refused a letter citing evidence this notice does not have (`unbacked_evidence_claim`) or claiming timeliness on a passed or unknown deadline (`untimely_protest_claim`).NoticeDraftGenerationRefusal
  • 429Rate limit exceededNoticeRateLimitResponse
  • 500Server error / draft generation failed / the draft was generated but its evidence package could not be created (never a 200 with `evidence_package: null`)one of (SanitizedServerError · AccountSetupIncompleteResponse)
PATCH/api/notices/{id}/draftUpdate notice draft

Update the most recent draft's content. Tenant-scoped both on read and update — root-cause fix vs pre-Phase-4 which only filtered by id.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Request body NoticeDraftPatchBodyrequired

FieldTypeRequiredDescription
contentstringrequired—

Responses

  • 200Draft updatedNoticeDraftPatchResponse
    FieldTypeRequiredDescription
    draftNoticeDraftRecordrequired—
  • 400Validation errorNoticeValidationErrorResponse
  • 401UnauthorizedNoticeUnauthorizedResponse
  • 403Forbidden — missing required scope, or the caller may read this draft but is not permitted to change it (code DRAFT_UPDATE_FORBIDDEN: the update matched zero rows and nothing was saved), or — on the FIRST save, before any draft exists — the caller's role may not write drafts (same code DRAFT_UPDATE_FORBIDDEN; refused before any write)NoticeUnauthorizedResponse
  • 404No draft found in caller's tenantNoticeNotFoundResponse
  • 429Rate limit exceededNoticeRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/notices/{id}/resolution-packageGet notice resolution package

Returns the most recent resolution package for a notice, or a `not_generated` stub when the caller's own notice has none yet. An unknown, soft-deleted, out-of-scope or cross-tenant notice id answers 404 with one body (the same one POST answers). Tenant-scoped.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Responses

  • 200Existing package, or the 'not_generated' stub for the caller's own notice that has no package yetResolutionPackageGetResponse
  • 400Validation errorNoticeValidationErrorResponse
  • 401UnauthorizedNoticeUnauthorizedResponse
  • 403Forbidden — missing required scopeNoticeUnauthorizedResponse
  • 404Notice not found in caller's tenant — one body for an unknown, soft-deleted, out-of-entity-scope or cross-tenant id (no existence oracle)ResolutionPackageNotFoundResponse
  • 429Rate limit exceededNoticeRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/notices/{id}/resolution-packageGenerate notice resolution package

Generate a resolution package after confirming all required documents are present. Returns 400 if any are missing. If `protest_recommended` on the notice, generates a protest letter via Claude (PII-redacted) and bundles evidence; otherwise generates a `verification_complete` summary. Creates a submissions row and updates notice status.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Responses

  • 200Package generatedResolutionPackagePostResponse
    FieldTypeRequiredDescription
    package_idstring · uuidrequired—
    statusenum: "generated"required—
    package_typeenum: "protest_letter" | "verification_complete"required—
    documentsarray<ResolutionPackageDocument>required—
  • 400Validation error / required documents missingNoticeValidationErrorResponse
  • 401UnauthorizedNoticeUnauthorizedResponse
  • 403A valid Bearer key without `notices.write`, a session that must complete an MFA step-up (`mfa_required`), or `role_forbidden` — a non-CPA cookie role, refused before any read, write or letter generation (the same body for every id)NoticesWriteForbiddenResponse
  • 404Notice or entity not found in caller's tenantNoticeNotFoundResponse
  • 429Rate limit exceededNoticeRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/notices/{id}/detailGet notice detail

Returns the full notice + associated entity + audit_events + pipeline events + signed document URL + payroll context. Auto-applies any existing entity-level payroll verification when the tax year matches (re-uses prior payroll uploads across notices for the same entity).

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Responses

  • 200Notice detail bundleNoticeDetailResponse
    FieldTypeRequiredDescription
    noticeobjectrequiredThe notice row projected onto the route's response allowlist. Carries `has_intake_raw` (boolean — a source document exists; open it via GET /api/notices/{id}/document) and never a storage path (`intake_raw_storage_path` is server-side only since fold-82). Owner ruling M3: on the firm plane it also carries `owner` — `{ owner_user_id: string | null, owner_label: string }`, the one owner label ("Unassigned", the member's name or email, or "Former member"); absent on the employer plane or when the owner read degraded. Walk-gap 4: on the employer plane it carries `cpa_people` instead — `{ contact: { display_name: string | null, email: string | null } | null, letter_signer: { name: string | null, title: string | null } | null }`: the notice owner (the employer's contact for this notice; never a user id) and the complete letterhead's signer; absent on the firm plane. Q7 / S-12 (fold-86): on the employer plane, until `approval_status = 'approved'`, the computed figure keys (potential_savings_usd, actual_savings_usd, auto_/payroll_/smart_verification) are absent and `figure_held: true` is set. I-7 (ruling Q23): it also carries `multi_notice` — on the firm plane `{ flagged: boolean, reasons: string[], resolution: 'split' | 'single' | null, split_from_notice_id: string | null, split_from_display_id: string | null }` (flagged = may hold more than one notice and not yet resolved; its deadline is not trusted); `null` on the employer plane or when the read degraded.
    entityobjectrequired—
    cpa_membersarray<object>required—
    eventsarray<object>required—
    pipeline_eventsarray<object>required—
    document_urlstringrequired—
    payrollobjectrequired—
    entity_payrollobjectrequired—
    payroll_connectedbooleanrequired—
    payroll_live_connectionbooleanrequired—
    holiday_rollobjectoptional—
    deadline_entriesobjectrequired—
    employerNoticeEmployerAttributionrequired—
    has_cpabooleanrequired—
    employer_fills_letterbooleanrequired—
    may_override_payrollbooleanrequired—
    employer_filing_reportone of (object · object · null)required—
    source_documentNoticeSourceDocumentrequired—
    parent_noticeNoticeLinkedNoticerequired—
    child_noticesarray<NoticeLinkedNotice>required—
    claim_sides_linkNoticeClaimSidesLinkrequired—
    as_ofstring · date-timerequiredThe server's clock when this body was built (ISO-8601, one read per response). Deadline countdowns and urgency bands are computed against this, never the client clock.
    arrivalsarray<object>required—
    junk_duplicate_ofobjectoptional—
    possible_duplicateobjectrequired—
    draft_jobobjectrequired—
    cross_reference_jobobjectrequired—
    signatureobjectrequired—
  • 400Validation errorNoticeValidationErrorResponse
  • 401UnauthorizedNoticeUnauthorizedResponse
  • 403Forbidden — missing required scopeNoticeUnauthorizedResponse
  • 404Notice not found in caller's tenantNoticeNotFoundResponse
  • 429Rate limit exceededNoticeRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/notices/{id}/detailAdd note to notice timeline

Append a note to the notice's audit timeline. Tenant_id always sourced from the auth context — never from the body — to prevent cross-tenant audit writes (Gemini PR #471 review fix).

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Request body NoticeDetailPostBodyrequired

FieldTypeRequiredDescription
notestringrequired—

Responses

  • 200Note appendedNoticeOkResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
  • 400Validation errorNoticeValidationErrorResponse
  • 401UnauthorizedNoticeUnauthorizedResponse
  • 403Forbidden — missing required scopeNoticeUnauthorizedResponse
  • 404Notice not found in caller's tenantNoticeNotFoundResponse
  • 429Rate limit exceededNoticeRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
PATCH/api/notices/{id}/detailRefuses notice-status transitions (use /file or the close gate)

REFUSES every notice-status transition, by design. This endpoint owns none of the seven `notice_status` values: `filed` belongs to POST /api/notices/{id}/file (which records the confirmation number, `filed_at` and a hash-chained audit entry, and verifies the notice was approved), `closed` belongs to the close gate on the notice, and the remaining five are pipeline/derived states no control hand-edits. Every well-formed request therefore answers 409 with a `code` naming the door that owns the transition; a value outside the enum answers 400. The operation is retained because removing a published operation is a breaking SDK change, and GET on this path is load-bearing. (Batch-12 B12-1: it previously wrote `status` through with none of the filing controls.)

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Request body NoticeDetailPatchBodyrequired

FieldTypeRequiredDescription
statusenum: "open" | "in_progress" | "needs_review" | "waiting_client" | …required—

Responses

  • 400Invalid path parameter, malformed JSON, or a `status` outside the `notice_status` enum.NoticeValidationErrorResponse
  • 401UnauthorizedNoticeUnauthorizedResponse
  • 403Forbidden — missing required scopeNoticeUnauthorizedResponse
  • 409Always, for any valid `status`. `code` names the door that owns the transition: `FILING_ROUTE_REQUIRED` (use POST /api/notices/{id}/file) or `STATUS_NOT_EDITABLE_HERE`.NoticeStatusRefusal
  • 429Rate limit exceededNoticeRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/notices/{id}/required-documentsGet required-documents checklist

Returns the document checklist required to resolve the notice. Each entry tagged status=uploaded | on_file | not_uploaded based on prior direct uploads, entity-level documents, payroll-on-file (tax-year + state matched), or existing payroll_verification.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Responses

  • 200Checklist + per-doc statusRequiredDocumentsGetResponse
    FieldTypeRequiredDescription
    notice_typestringrequired—
    rating_periodstringrequired—
    rating_period_yearintegerrequired—
    payroll_neededbooleanrequired—
    descriptionstringrequired—
    documentsarray<RequiredDocument>required—
    all_uploadedbooleanrequired—
    can_generate_resolutionbooleanrequired—
  • 400Validation errorNoticeValidationErrorResponse
  • 401UnauthorizedNoticeUnauthorizedResponse
  • 403Forbidden — missing required scopeNoticeUnauthorizedResponse
  • 404Notice not found in caller's tenantNoticeNotFoundResponse
  • 409The notice has no confirmed employer yet — confirm the entity firstRequiredDocumentsEntityUnconfirmedResponse
  • 429Rate limit exceededNoticeRateLimitResponse
  • 500The notice read failed (sanitized body)one of (RequiredDocumentsServerErrorResponse · AccountSetupIncompleteResponse)
POST/api/notices/{id}/required-documentsUpload checklist document

Upload a document for a checklist requirement. multipart/form-data with `file` + `document_key`. Payroll spreadsheets (CSV/XLSX) trigger the 4-path side-effect pipeline: PATH A rate verification, PATH B balance/payroll verification, PATH C ADP per-employee SUI, PATH D smart verification + auto-protest letter generation. All side effects are tenant-scoped (KRT-1479 P4b.7 root-cause fix).

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Request body RequiredDocumentsMultipartBodyrequired

FieldTypeRequiredDescription
filestringrequiredDocument file binary
document_keystringrequiredChecklist key, e.g., quarterly_sui_wages | quarterly_payroll_summary

Responses

  • 200Uploaded; checklist re-evaluatedRequiredDocumentsPostResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    document_idstring · uuidrequired—
    all_uploadedbooleanrequired—
    path_statusRequiredDocumentsPathStatusrequired—
    rate_verificationRequiredDocumentsRateVerificationRefusaloptional—
    payroll_periodRequiredDocumentsPayrollPeriodoptional—
    idempotent_replayenum: trueoptional—
  • 400Validation error / missing file or document_keyNoticeValidationErrorResponse
  • 401UnauthorizedNoticeUnauthorizedResponse
  • 403Forbidden — missing required scopeNoticeUnauthorizedResponse
  • 404Notice not found in caller's tenantNoticeNotFoundResponse
  • 429Rate limit exceeded (uploads use the 10/min ceiling)NoticeRateLimitResponse
  • 500Server error / storage upload / document insert failedone of (SanitizedServerErrorWithErrorCode · AccountSetupIncompleteResponse)
POST/api/notices/{id}/no-responseClose a notice as No response needed

The CPA's one click: record the verdict 'No response needed' and close an open SUI rate notice whose rate checked inside the range. A standalone (self-tenant) employer's founding member may click it too.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Responses

  • 200Verdict recorded; the notice is closedNoticeNoResponsePostResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    verdictenum: "no_response"required—
  • 401UnauthorizedNoticeUnauthorizedResponse
  • 403Forbidden — not a CPA-class session nor the self-tenant founder, or the database refused the close (code NO_RESPONSE_FORBIDDEN)NoticeUnauthorizedResponse
  • 404Notice not found in caller's tenant (malformed, unknown or foreign id)NoticeNotFoundResponse
  • 409Not offered: not an open, in-range SUI rate noticeNoticeNoResponseRefusal
  • 429Rate limit exceededNoticeRateLimitResponse
  • 500Server error — sanitized (`SanitizedServerError`), or the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/noticesList notices

List notices for the caller's tenant with filters / sort / pagination. Bearer keys gate via `notices.read`.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
statusquerystringoptional—
filterqueryenum: "all" | "closes_this_week" | "closing_soon" | "open" | …optional—
statequerystringoptional—
searchqueryarray<string>optionalFree-text search across notice fields and the employer name. Repeat the key to AND terms (at most 4); more answers 400.
date_fromquerystringoptional—
date_toquerystringoptional—
sort_byqueryenum: "deadline" | "created_at" | "penalty_amount" | "notice_type" | …optional—
sort_dirqueryenum: "asc" | "desc"optional—
pagequeryintegeroptional—
page_sizequeryintegeroptional—
limitqueryintegeroptional—
idsqueryarray<string · uuid>optionalNarrow the list to these notice ids (comma-separated, at most 100 distinct UUIDs). Over the cap or a malformed id answers 400.
notice_typequerystringoptional—
entity_idquerystring · uuidoptional—
include_countsqueryenum: "1"optional—
all_businessesqueryenum: "1"optional—

Responses

  • 200NoticesNoticesListResponse
    FieldTypeRequiredDescription
    noticesarray<NoticeListItem>required—
    totalintegerrequired—
    pageintegerrequired—
    page_sizeintegerrequired—
    countsNoticeQueueCountsoptional—
    entity_scopeobjectoptional—
  • 400Validation error — a query parameter outside its vocabulary (status, filter, ids, search count, date_from/date_to). The body names the parameter.MiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Forbidden — `entity_id` names a business outside the caller's own wall (FORBIDDEN_ENTITY; same answer for a sibling, a foreign tenant's id and an id that exists nowhere).MiscUnauthorizedResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/notices/historical-finesHistorical fines analysis

Aggregated historical fines analysis with statute-of-limitations for the caller's tenant. Returns total fines, recoverable amounts, expiring counts, and per-state breakdown.

Bearer scopesnotices.read

Responses

  • 200Fines analysisHistoricalFinesResponse
    FieldTypeRequiredDescription
    totalFinesnumberrequired—
    noticeCountintegerrequired—
    recoverableAmountnumberrequired—
    recoverableCountintegerrequired—
    expiringCountintegerrequired—
    expiredAmountnumberrequired—
    topAmountsarray<object>required—
    dateRangeobjectrequired—
    byStateobjectrequired—
    estimatedRecoveryobjectrequired—
    historicalReviewPurchasedbooleanrequired—
  • 401UnauthorizedMiscUnauthorizedResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/notices/review-queueCPA review queue

CPA review queue ordered by deadline urgency. Notices in pipeline states NEEDS_REVIEW / READY_FOR_REVIEW / AWAITING_PAYROLL / VERIFIED. CPA-class roles only on cookie sessions; Bearer keys gate via `notices.read`. Each row carries `draft_approve_refusal`: the approve door's letter verdict on the row's live draft — `no_draft`, `internal_memo`, an unfilable-letter reason (e.g. `unfilled_placeholders`, `untracked_template`), or null when the draft is an approvable letter.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
limitqueryintegeroptional—
offsetqueryintegeroptional—
financial_impactquerystringoptional—

Responses

  • 200Review queueReviewQueueResponse
    FieldTypeRequiredDescription
    noticesarray<object>required—
    countintegerrequired—
    totalintegerrequired—
    cappedbooleanrequired—
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Forbidden — CPA access requiredMiscUnauthorizedResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/notices/{id}/actionCPA action on notice

CPA action on a reviewed notice (approve, dismiss, request revision). Approve records the approval and moves no pipeline_status; the notice becomes ACTIONED only when it is filed. dismissed→DISMISSED, revision_requested→NEEDS_REVIEW. CPA-class roles only on cookie sessions; Bearer keys gate via `notices.write`.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Request body NoticeActionBodyrequired

FieldTypeRequiredDescription
actionenum: "approved" | "dismissed" | "revision_requested"required—
notesstringoptional—

Responses

  • 200Action recordedNoticeActionResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    pipeline_statusstringrequired—
    audit_event_pendingenum: trueoptional—
    warningstringoptional—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Forbidden — approval access required; or the writer refused a payroll override this caller may not make (`PAYROLL_OVERRIDE_FORBIDDEN`), or an approval with no interactive session behind it (`HUMAN_APPROVAL_REQUIRED`), or an approve from a login that cannot read the letter (`LETTER_UNREADABLE`).NoticeActionForbiddenResponse
  • 404Notice not found — existence-opaque: a foreign tenant, a sibling entity and an unknown id are one answer. `code: NOTICE_NOT_FOUND` only when the lifecycle writer itself lost the row.NoticeActionNotFoundResponse
  • 409Refused — nothing was written. A gate held (payroll, reconciliation, the head letter, the entity), a concurrent decision won the optimistic lock (no `code`), the notice is not actionable (no `code`), or the lifecycle writer refused the move (its own code, rendered verbatim).NoticeActionConflictResponse
  • 422The lifecycle writer refused: the head letter still has unfilled placeholders (`DRAFT_HAS_PLACEHOLDERS`, rendered verbatim).NoticeActionUnprocessableResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
  • 503The draft-state read did not come back — a transient, not a gate holding. Nothing was changed; retry.NoticeActionUnavailableResponse
GET/api/notices/{id}/eventsList notice pipeline events

Pipeline event audit trail for a notice.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Responses

  • 200EventsNoticeEventsListResponse
    FieldTypeRequiredDescription
    eventsarray<NoticeEvent>required—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 404Notice not foundMiscNotFoundResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/notices/{id}/eventsCreate notice pipeline event

Record a manual pipeline event on a notice (e.g. evidence_verified).

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Request body NoticeEventCreateBodyrequired

FieldTypeRequiredDescription
event_typeenum: "evidence_verified"required—
descriptionstringoptional—

Responses

  • 201Event recordedNoticeEventCreateResponse
    FieldTypeRequiredDescription
    eventNoticeEventrequired—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 404Notice not foundMiscNotFoundResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/notices/{id}/generate-draftGenerate draft response

Manually trigger draft response generation for a notice. Cookie sessions go through the Stripe gate; Bearer keys bypass. Bearer scope: `notices.write`.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Request body GenerateDraftRequest

FieldTypeRequiredDescription
response_typeenum: "protest" | "optimize" | "pay_now" | "urgent" | …optional—
cpa_decision_reasonstringoptionalRequired (10+ characters) when `response_type` is `protest` on a paper unemployment claim: the CPA's own reason for protesting, recorded in the audit trail.

Responses

  • 200Draft generatedGenerateDraftResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    draft_idstring · uuidrequired—
    draft_lengthintegerrequired—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 402Subscription required (cookie-session callers only)MiscUnauthorizedResponse
  • 409Generation produced a BLOCKED draft (missing payroll, a redaction placeholder, or the manual-review placeholder rather than a letter). The row exists and is named by `draft_id`, but it is not a letter and cannot be approved or filed until it is regenerated. Or (`TRACKED_NOT_CHECKED`): the notice is tracked, not checked (X1), so no one-click draft is written. Write the response by hand instead (PATCH). Nothing is written. Or (`FACTS_UNCONFIRMED`): the CPA has not confirmed what Kreto read (employer, notice type, deadline) — POST /api/notices/{id}/confirm-facts first. Asked after the not-found wall, X1 and the employer refusal. Nothing is written. `CLAIM_ANSWERED_THROUGH_SIDES` (Q19): the paper claim is linked to a SIDES request, which is the answer of record — no draft is generated.one of (NoticeDraftClaimAnsweredThroughSidesResponse · GenerateDraftBlockedResponse · NoticeDraftTrackedNotCheckedResponse · NoticeDraftFactsUnconfirmedResponse)
  • 422Generation refused as a human gate, not a fault: the draft is waiting on a fact only a person can supply (`needs_input.kind`; today the employer's legal name, added on the record `entity_id` names), or the output guard refused a letter citing evidence this notice does not have (`unbacked_evidence_claim`) or claiming timeliness on a passed or unknown deadline (`untimely_protest_claim`). Also `CLAIM_PROTEST_REASON_REQUIRED`: a `protest` asked for on a paper unemployment claim without the CPA's `cpa_decision_reason` (owner rule N1 — Kreto never recommends a protest on a claim).NoticeDraftGenerationRefusal
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/notices/{id}/processTrigger notice pipeline

Manually trigger the notice pipeline (re-classify, re-cross-reference, regenerate draft). CPA-class roles only on cookie sessions; Bearer keys gate via `notices.write`. Cookie sessions go through the Stripe gate.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Responses

  • 200Pipeline completeProcessNoticeResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    pipelineobjectrequired—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 402Subscription required (cookie-session callers only)MiscUnauthorizedResponse
  • 403Forbidden — CPA access requiredMiscUnauthorizedResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (NoticeProcessServerError · AccountSetupIncompleteResponse)
POST/api/notices/{id}/process-with-payrollProcess notice with payroll

Queues the validate+draft pipeline (a PROCESS_PAYROLL_VERIFICATION job) after payroll has been uploaded (only valid from AWAITING_PAYROLL). `deduplicated` is true when a verification job already open for the notice absorbed the request. Cookie sessions go through the Stripe gate. Bearer scope: `notices.write`.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Responses

  • 202Verification job queuedProcessWithPayrollResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    jobIdstringrequired—
    deduplicatedbooleanrequired—
  • 400Validation error / wrong pipeline state / no payrollMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 402Subscription required (cookie-session callers only)MiscUnauthorizedResponse
  • 404Notice not foundMiscNotFoundResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/notices/{id}/confirm-factsConfirm what Kreto read

Record the CPA's confirmation of what Kreto read on the notice — the employer, the notice type and the deadline, each with its source — and write one hash-chained audit row. The draft doors answer 409 FACTS_UNCONFIRMED until this is recorded; a later change to any of the three re-opens it. CPA-only.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Request body NoticeConfirmFactsBody

FieldTypeRequiredDescription
entity_idstring · uuidrequired—
notice_typestringrequired—
deadlinestringrequired—
ein_tail_pickenum: "notice" | "profile" | "neither"optionalRequired while the notice's FEIN and the employer's EIN on file disagree unanswered: `notice` (the notice is right; the letter prints it), `profile` (the profile is right; the notice's reading is corrected) or `neither` (hold — the facts are not confirmed).

Responses

  • 200Confirmation recorded — or, for `ein_tail_pick: neither`, the hold recorded (`held: true`; the facts stay unconfirmed)NoticeConfirmFactsResponse
  • 400Validation errorNoticeValidationErrorResponse
  • 401UnauthorizedNoticeUnauthorizedResponse
  • 403Forbidden — not a CPA caller, or missing scopeNoticeUnauthorizedResponse
  • 404Notice not found in caller's tenantNoticeNotFoundResponse
  • 409A fact cannot be confirmed yet (employer unconfirmed, no type, no deadline), the notice's FEIN and the employer's EIN on file disagree with no pick (EIN_TAIL_PICK_REQUIRED), or a fact changed since the panel was opened (FACTS_CHANGED).NoticeConfirmFactsRefusal
  • 429Rate limit exceededNoticeRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/notices/{id}/payroll-comparisonPayroll vs notice rate comparison

The SUI-rate notice's payroll comparison, read from its one stored cross-reference result (the same figure, state and recommendation as GET /cross-reference; never recomputed). Bearer keys gate via `notices.read`.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Responses

  • 200ComparisonPayrollComparisonResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 404Notice not foundMiscNotFoundResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/notices/{id}/draft/advanceAdvance draft through approval chain

Advance the draft response through approval steps (reviewed → counter_signed → submitted). CPA-class roles only on cookie sessions; Bearer keys gate via `notices.write`.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Request body DraftAdvanceBodyrequired

FieldTypeRequiredDescription
stepenum: "reviewed" | "counter_signed" | "submitted"required—
notesstringoptional—

Responses

  • 200Draft advancedDraftAdvanceResponse
    FieldTypeRequiredDescription
    successenum: truerequired—
    currentStepenum: "reviewed"required—
    unchangedenum: trueoptional—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Forbidden — CPA access requiredMiscUnauthorizedResponse
  • 404Notice not foundMiscNotFoundResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/notices/{id}/draft-feedbackSubmit draft feedback

Submit thumbs-up/thumbs-down feedback on a generated draft response. Used to train future draft generation. Bearer keys gate via `notices.write`.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Request body DraftFeedbackBodyrequired

FieldTypeRequiredDescription
draft_idstring · uuidrequired—
ratingenum: "thumbs_up" | "thumbs_down"required—
commentstringoptional—

Responses

  • 200Feedback recordedDraftFeedbackResponse
    FieldTypeRequiredDescription
    successenum: truerequired—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 404Notice or draft not foundMiscNotFoundResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/notices/{id}/confirm-sui-rateConfirm/override SUI rate

CPA confirms or overrides the ADP-sourced SUI rate. `confirm` marks the ADP rate as verified; `override` stores a CPA-corrected rate alongside the ADP original; `assign` sets the notice's own assigned rate (`assigned_rate` in the declared `rate_unit`) without a rate record; `previous` stores the CPA-entered previous (last year's) rate (`previous_rate` in the declared `rate_unit`), labelled CPA-entered, and re-runs the figure. `confirm`/`override`/`assign` re-queue a draft parked on the rate and clear that wait. Bearer keys gate via `notices.write`.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Request body ConfirmSuiRateBodyrequired

FieldTypeRequiredDescription
actionenum: "confirm" | "override" | "assign" | "previous"required—
override_ratenumberoptional—
previous_ratenumberoptional—
assigned_ratenumberoptional—
rate_unitenum: "percent_points" | "fraction"optional—

Responses

  • 200Rate confirmed/overriddenConfirmSuiRateResponse
  • 400Validation error / cannot determine stateMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 404Notice or SUI rate record not foundMiscNotFoundResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/notices/{id}/confirm-tax-yearConfirm tax year

CPA states the tax year a notice does not print. Stored on the notice as a stated fact with source `cpa_confirmed`, which the payroll request, the document checklist and the AWAITING_PAYROLL gate then read. Refused (409) when the notice already states its year. Cookie sessions require a CPA role; Bearer keys gate via `notices.write`.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Request body ConfirmTaxYearBodyrequired

FieldTypeRequiredDescription
tax_yearintegerrequired—

Responses

  • 200Tax year confirmedConfirmTaxYearResponse
    FieldTypeRequiredDescription
    successenum: truerequired—
    tax_yearintegerrequired—
    sourceenum: "cpa_confirmed"required—
    confirmed_bystringrequired—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Cookie session without a CPA roleMiscUnauthorizedResponse
  • 404Notice not foundMiscNotFoundResponse
  • 409The notice already states its tax year, or the write matched no rowMiscValidationErrorResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/notices/{id}/fileMark notice filed

Mark a notice as filed with the agency (KRT-1476 Feature 1 AC5). Captures confirmation_number + filed_at. Auto two-step transition open|needs_review → in_progress → filed. Optimistic lock prevents double-file races. SESSION-ONLY, CPA-class roles: recording a filing is a human attestation, so a Bearer key is refused with 403 SESSION_REQUIRED even when it carries `notices.write` (parity with the APPROVAL_REQUIRED v1 `resolutions.execute`). Requires the notice to be APPROVED (`cpa_action = 'approved'`) and not already filed; otherwise 409 APPROVAL_REQUIRED with no writes.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Request body NoticeFileBodyrequired

FieldTypeRequiredDescription
confirmation_numberstringrequired—
notesstringoptional—

Responses

  • 200Notice filed, or the confirmation number recorded on a notice already filed without one (`confirmation_backfilled: true`)NoticeFileResponse
  • 202Owner rule E2: a CPA firm's client recorded an employer STATEMENT (`filing.employer_reported`); nothing is filed and the notice stays open until the CPA confirmsNoticeFileEmployerReportResponse
    FieldTypeRequiredDescription
    statusenum: "employer_reported"required—
    reported_atstringrequired—
    messagestringrequired—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Forbidden — CPA access required, or SESSION_REQUIRED for a Bearer callerMiscUnauthorizedResponse
  • 404Notice not foundMiscNotFoundResponse
  • 409Already filed / closed / not yet approved (APPROVAL_REQUIRED) / race lost / E2: EMPLOYER_FILING_REPORT_UNAVAILABLE (the employer's report cannot be recorded yet; nothing was saved)one of (NoticeFileConflictResponse · NoticeFileEmployerReportUnavailableResponse)
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/notices/{id}/verify-rateVerify SUI rate

Verify a notice's assigned SUI rate against the employer's payroll experience. If payroll data is incomplete, returns needsData=true + the list of required fields. No input is ever defaulted: each figure is taken from the body, else the latest payroll upload, else the figures printed on the notice. When years in business, the stated tax year, or an experience figure the state's method reads (benefit charges, contributions, average annual payroll, employee count) is not stated, the answer is the named refusal (`resultState: "pending_data"`, `missing_inputs`). Cookie sessions go through the Stripe gate. Bearer scope: `notices.write`.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Request body VerifyRateBody

FieldTypeRequiredDescription
totalBenefitChargesnumberoptional—
totalContributionsnumberoptional—
totalTaxableWagesnumberoptional—
averageAnnualPayrollnumberoptional—
yearsInBusinessintegeroptional—
employeeCountintegeroptional—
voluntaryContributionsnumberoptional—

Responses

  • 200Rate verification result, OR the named refusal (`resultState: "pending_data"` + `missing_inputs`) when an input the verdict reads is not stated, OR needsData promptone of (VerifyRateResultResponse · VerifyRatePendingDataResponse · VerifyRateNeedsDataResponse)
  • 400Validation error / state not supportedMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 402Subscription required (cookie-session callers only)MiscUnauthorizedResponse
  • 404Notice not foundMiscNotFoundResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerErrorWithDetails · AccountSetupIncompleteResponse)
POST/api/notices/uploadUpload notice

Single-notice intake. Auto-matches/creates entity from extracted text, uploads to storage, creates notice + document records, fires the pipeline asynchronously. Cookie sessions go through Stripe gate + rate-limit + idempotency middleware. SESSION-ONLY: an API key is refused (401 naming the session plane; 403 SESSION_REQUIRED from the handler's second wall) even when it carries `notices.write`. There is no API, MCP or CLI upload today: it is unpublished until it takes a file through the job queue with an approval step (ruling G4-Q6).

Cookie session or Bearer token

Request body objectrequired

FieldTypeRequiredDescription
filestring · binaryrequired—
entity_idstring · uuidoptional—
parent_notice_idstring · uuidoptional—
entity_attribution_sourcestringoptionalCaller's attribution claim. Only the exact value "user_selected" is honoured (it relaxes the classifier's authority over the employer match); any other value is ignored, never rejected.

Responses

  • 200Uploaded, or the duplicate receipt when the same bytes are already on file (`duplicate: true`, with the placement disclosure)NoticeUploadResponse
  • 400Validation error (`error_code: invalid_body`) — missing/oversized form, bad `entity_id` / `parent_notice_id` format, absent fileMiscUploadClassifiedErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 402Subscription required (cookie-session callers only)MiscUnauthorizedResponse
  • 403Forbidden — cookie caller's role is not permitted, or SESSION_REQUIRED (`reason: session_required`) for any non-session caller (owner-inbox item 170)MiscUnauthorizedResponse
  • 404`entity_id` or `parent_notice_id` is not visible to this tenant (absent, soft-deleted and cross-tenant share one body)MiscNotFoundResponse
  • 409Duplicate notice (`error_code: duplicate`) — same tenant, same SHA-256 content hashMiscUploadClassifiedErrorResponse
  • 413File too large (`error_code: file_too_large`) — over MAX_UPLOAD_BYTESMiscUploadClassifiedErrorResponse
  • 415Unsupported media type (`error_code: unsupported_mime`) — the declared mime is outside ALLOWED_MIMEMiscUploadClassifiedErrorResponse
  • 422Received but unreadable (`error_code: parse_failed`) — empty, corrupt or password-protected bytes; no notice row was createdMiscUploadClassifiedErrorResponse
  • 429Rate limit exceededMiscRateLimitRefusalResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
  • 502Storage upload failed (`error_code: storage_upload_failed`) — the notice row was rolled backMiscUploadClassifiedErrorResponse
GET/api/notices/{id}/cross-referenceCross-reference notice with payroll

Cross-reference notice against payroll data; runs SUI-specific analysis with state-by-state taxable wage bases + max rates when applicable. Returns needs_payroll_data:true when payroll data is missing for the relevant period (auto-falls-back to adjacent years). Persists cross_reference_analysis on the notice. Bearer keys gate via `notices.read` AND `payroll.read` — it reads payroll rows, and `hasScope()` is exact membership, so a key holding only `notices.read` is refused. SWEEP C16 corrected the declaration, which named notices.read alone.

Bearer scopesnotices.readpayroll.read

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Responses

  • 200Result OR needs_payroll_data promptone of (CrossReferenceResultResponse · CrossReferenceNeedsDataResponse · CrossReferenceNoFormulaResponse · object)
  • 401UnauthorizedMiscUnauthorizedResponse
  • 404Notice not foundMiscNotFoundResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/notices/{id}/send-for-signatureSend notice response for signature

E-signature flow (KRT-1476 Feature 1 Phase 6): renders/uploads a response PDF if no document_id is provided, creates a signature_requests row with secure token + 7-day expiry, sends the signing-link email to the client. Returns signing_link + request_id even if email delivery fails (best-effort). CPA-class roles only on cookie sessions; Bearer keys gate via `notices.write`.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Request body SendForSignatureBodyrequired

FieldTypeRequiredDescription
signer_namestringrequired—
signer_emailstring · emailrequired—
signer_titlestringoptional—
expires_daysintegeroptional—
document_idstring · uuidoptional—
document_titlestringoptional—

Responses

  • 201Signature request createdSendForSignatureResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    signing_linkstringrequired—
    request_idstring · uuidrequired—
    secure_tokenstringrequired—
    expires_atstring · date-timerequired—
    document_idstring · uuidrequired—
    email_sentbooleanrequired—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Forbidden — CPA access requiredMiscUnauthorizedResponse
  • 404Notice or document not foundMiscNotFoundResponse
  • 409Notice already filed/closed, document linked elsewhere, a signing request already live, a reused document that is a government form with no mapped signature line (owner ruling S2), or — owner rule E1 — the notice's letter has not been approved by a CPA (`reason: letter_not_approved`, `code: LETTER_NOT_APPROVED_FOR_SIGNING`).MiscValidationErrorResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/signature-requests/{id}/cancelCancel a signing request

Withdraw one PENDING, unexpired e-signature request. The signing link stops working at once, and the notice's signature-requested fact is re-derived in the same call. Firm plane only: CPA-class cookie roles, or a Bearer key with `notices.write` and an unrestricted entity scope. Unknown, foreign and out-of-scope ids share one 404 body.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredSigning request (signature_requests) UUID

Responses

  • 200Request cancelledCancelSigningRequestResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    request_idstringrequired—
    statusenum: "cancelled"required—
    notice_idstringrequired—
    signature_requested_atstringrequiredThe notice's re-derived signature-requested fact. null = no request holds it any more, so the draft can be regenerated or revised.
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Forbidden — CPA-class access requiredMiscUnauthorizedResponse
  • 404Signing request not found (or not the caller's)CancelSigningRequestNotFound
  • 409Not cancellable — signed, expired or already cancelledCancelSigningRequestRefusal
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server error — including: cancelled, but the notice's signature fact was not re-derivedone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/signature-requests/{id}/resendResend a signing link

Replace a signing link with a new one and email it to the signer. The previous link stops working at once. A live request keeps its expiry; a lapsed one (pending past expiry, or expired) is renewed with a new window and returns to pending (E4). Each request may be emailed at most 5 times in all. Cookie session only, CPA-class roles. Unknown and foreign ids share one 404 body.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredSigning request (signature_requests) UUID

Responses

  • 200Link rotated; email attemptedResendSigningRequestResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    request_idstringrequired—
    email_sentbooleanrequired—
    expires_atstringrequiredA LIVE request keeps its expiry. A LAPSED one (E4) gets a new window equal to its original lifetime, clamped to 1–30 days.
    signing_linkstringoptionalPresent ONLY when the email could not be sent: the new link, shown this once so the firm can deliver it another way. Never stored or shown again.
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Forbidden — CPA-class access requiredMiscUnauthorizedResponse
  • 404Signing request not found (or not the caller's)ResendSigningRequestNotFound
  • 409Not resendable — signed, expired, cancelled, document withdrawn, or changed concurrentlyResendSigningRequestRefusal
  • 429This request has been emailed the maximum number of timesResendSigningRequestLimit
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
  • 503App origin not configured, or resend is unavailable on this deploymentResendSigningRequestUnavailable
GET/api/signature/request/{token}View a signing link

The signing page's view of a LIVE signing link: title, the employer it is for, signer, expiry and an app link to the document. Every closed link answers one 404 body.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
tokenpathstringrequiredThe signing link's secret token

Responses

  • 200The live requestSigningRequestView
    FieldTypeRequiredDescription
    document_titlestringrequired—
    employer_namestringrequiredE3: the employer the document is for (its display name), or null when the request is tied to no live employer.
    document_urlstringrequired—
    signer_namestringrequired—
    signer_titlestringrequired—
    expires_atstringrequired—
    statusstringrequired—
    signature_placementSignaturePlacementrequiredS2: where the signature goes — the same plan the stamp follows. `form_line` boxes are PDF points (origin bottom-left) on the form's own signature line; `appended_page` (letters and uploads) means no overlay on the document; `not_signable` is an unmapped form. Null when it could not be read (the sign door still decides).
    sign_actionenum: "response" | "authorization" | "document"requiredWhat signing does for this document — a protest response letter (`response`), a government authorization form (`authorization`), or any other document. The page's button words come from it.
  • 404Closed — the one body for every closed or unknown linkSigningLinkClosed
  • 500Server errorSanitizedServerError
POST/api/signature/request/{token}Request a new signing link, or send / verify the signer's code

Request a new link (E4). When the token is a real request whose link has lapsed, the person who sent it is emailed (at most once a day); nothing is ever re-issued to the holder. One 200 body for every token. With `?code=send|verify` (owner ruling S1) the same door emails or checks the signer's one-time code; a closed or unknown link answers the GET's one 404 body, and a wrong, expired, absent or locked code answers one 422 body.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
tokenpathstringrequiredThe signing link's secret token
codequeryenum: "send" | "verify"optionalS1 one-time code: `send` emails a 6-digit code to the request's signer address (only while customer sends are on); `verify` checks the typed code. Absent → the renewal ask.

Request body SignerCodeVerifyBody

FieldTypeRequiredDescription
codestringrequiredThe 6-digit code

Responses

  • 200Renewal ask: the one answer for every token. `?code=send`: the code was emailed, or `sent: false` when customer sends are off (nothing stored). `?code=verify`: verified, with the time signing must happen by.one of (SigningRenewalAsked · SignerCodeSent · SignerCodeNotSent · SignerCodeVerified)
  • 400`?code=verify` with an unparseable bodySigningLinkClosed
  • 404`?code=` on a closed or unknown link — GET's one 404 bodySigningLinkClosed
  • 422`?code=verify`: the code is wrong, expired, absent or lockedSigningLinkClosed
  • 429Too many requests for this link or from this address, or too many codes sent for this linkSigningLinkClosed
  • 500Server errorSanitizedServerError
  • 503`?code=`: signing is not available on this deployment yet (the code store's migration is not applied) — nothing sent or verified.object
POST/api/notifications/alerts/{id}/acknowledgeAcknowledge an isolation alert

Release a tenant-scope isolation failure bell on an open notice. The body is optional; when sent it must be `{ raisers_rev }`. Bells with an automatic release are refused (409 `ALERT_HAS_AUTOMATIC_RELEASE`); a bell that changed since it was read is refused (409 `ALERT_CHANGED`). Cookie session only, CPA-class roles. Malformed, unknown and foreign ids share one 404 body.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredOperational alert (notice_operational_alerts) UUID. A malformed id answers the same 404 as an unknown one.

Request body AcknowledgeAlertBody

FieldTypeRequiredDescription
raisers_revintegerrequiredThe `raisers_rev` the operator saw on the bell (the feed's hint). If the bell moved since, the answer is 409 `ALERT_CHANGED`.

Responses

  • 200ReleasedAcknowledgeAlertResponse
    FieldTypeRequiredDescription
    acknowledgedenum: truerequired—
    remainingintegerrequiredOther raisers still holding the bell up after this release (0 = the bell is resolved).
  • 400A body was sent and it is not JSON or not `{ raisers_rev }`AcknowledgeAlertBadRequest
  • 401Unauthorized — no session, or a Bearer key was presentedMiscUnauthorizedResponse
  • 403Forbidden — CPA-class access requiredMiscUnauthorizedResponse
  • 404Alert not found (or not the caller's)AcknowledgeAlertNotFound
  • 409The bell changed since it was read, or it has an automatic releaseAcknowledgeAlertConflict
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/notices/{id}/confirm-deadlineSet a missing deadline

CPA sets a deadline the notice does not state (N2). Only an EMPTY clock is set; a clock that already holds a date is refused (409) and never overwritten here. A date in the past or over a year ahead needs `confirm_unusual: true` (422 otherwise). The entry is audited as `deadline_confirmed` with who entered it. Cookie sessions require a CPA role, or a `client_user` session of a self-serve employer workspace (no CPA on the roster); a CPA firm's client is refused 403 `CPA_SETS_DEADLINE` after the 404. Bearer keys gate via `notices.write`.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Request body ConfirmDeadlineBodyrequired

FieldTypeRequiredDescription
clockenum: "filing" | "protest"requiredWhich clock to set: the filing due date or the protest date
datestringrequired—
confirm_unusualenum: trueoptionalRequired to save a date in the past or more than a year ahead (422 otherwise)

Responses

  • 200Deadline setConfirmDeadlineResponse
    FieldTypeRequiredDescription
    successenum: truerequired—
    clockenum: "filing" | "protest"required—
    datestringrequired—
    provenanceenum: "entered_by_hand"required—
    entered_bystringrequired—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Cookie session without a CPA role, or a CPA firm's client (CPA_SETS_DEADLINE — the CPA sets the deadline)MiscUnauthorizedResponse
  • 404Notice not foundMiscNotFoundResponse
  • 409That clock already holds a date, or the write matched no row (CLOCK_ALREADY_SET)MiscValidationErrorResponse
  • 422The date is in the past or over a year ahead and was not confirmed (DEADLINE_NEEDS_CONFIRM)MiscValidationErrorResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/notices/{id}/employer-filing-report/rejectReject an employer's filing report

Owner rule E2 — decline the employer's standing report that they filed the response, with a required reason the employer reads; they may report again. Writes one `filing.employer_report_rejected` audit event; the notice is unchanged. Cookie session only, CPA-class roles. Malformed, unknown, foreign and out-of-scope ids share one 404 body; the 400 and 409 refusals come only after it.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
idpathstringrequiredNotice UUID. A malformed id answers the same 404 as an unknown one.

Request body RejectEmployerFilingReportBodyrequired

FieldTypeRequiredDescription
reasonstringrequiredWhy the CPA could not confirm — the employer reads it.

Responses

  • 200Rejected — the employer sees the reasonRejectEmployerFilingReportResponse
    FieldTypeRequiredDescription
    stateenum: "rejected"required—
    reasonstringrequired—
    event_idstringrequired—
  • 400The reason is missing or too longRejectEmployerFilingReportError
  • 401Unauthorized — no session, or a Bearer key was presentedMiscUnauthorizedResponse
  • 403Forbidden — CPA-class access requiredMiscUnauthorizedResponse
  • 404Notice not found (or not the caller's)RejectEmployerFilingReportNotFound
  • 409No employer filing report is waiting for confirmationRejectEmployerFilingReportConflict
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/notices/{id}/agency-replyRecord agency reply

CPA records the agency's reply to a FILED notice (Q6): type + date (+ an optional document of this notice). Writes `agency_response_type` + `agency_response_date` and one chained `notice_agency_reply_recorded` audit row; never the pipeline status. The same reply again replays (200, nothing written). Session only — API keys are refused (403 SESSION_REQUIRED); cookie sessions require a CPA role, or the standalone (self-tenant) employer's founding member.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Request body AgencyReplyBodyrequired

FieldTypeRequiredDescription
replyTypeenum: "acceptance" | "rejection" | "counter" | "additional_info_request"required—
receivedOnstringrequired—
documentIdstring · uuidoptionalOne of THIS notice's live documents holding the reply

Responses

  • 200Reply recorded, or the same reply replayedAgencyReplyResponse
    FieldTypeRequiredDescription
    outcomeenum: "recorded" | "replayed"required—
    replyTypeenum: "acceptance" | "rejection" | "counter" | "additional_info_request"required—
    receivedOnstringrequired—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403API key (SESSION_REQUIRED), a cookie session that is neither a CPA role nor the self-tenant founder, or the database refused the write (code AGENCY_REPLY_FORBIDDEN)MiscUnauthorizedResponse
  • 404Notice not found (absent, foreign, out of scope), or DOCUMENT_NOT_FOUNDMiscNotFoundResponse
  • 409NOT_FILED, REPLY_ALREADY_RECORDED (an acceptance/rejection is final) or WRITE_REFUSED (the notice changed mid-write)MiscValidationErrorResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/notices/{id}/splitSplit a document into notices by pages

CPA cuts a document flagged as possibly holding more than one notice into one child notice per page range (I-7, ruling Q23). Each child is linked to the parent and re-read through the pipeline; the parent is closed as split. Cookie sessions require a CPA role; Bearer keys gate via `notices.write`.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Request body SplitNoticeBodyrequired

FieldTypeRequiredDescription
rangesstringrequiredOne notice per comma-separated page range (1-based, no overlap)

Responses

  • 200Split into separate noticesSplitNoticeResponse
    FieldTypeRequiredDescription
    successenum: truerequired—
    resolutionenum: "split"required—
    child_notice_idsarray<string>required—
    messagestringrequired—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Cookie session without a CPA roleMiscUnauthorizedResponse
  • 404Notice not found — unknown, malformed, foreign, soft-deleted or out-of-scope id (one body)MiscNotFoundResponse
  • 409Not flagged (MULTI_NOTICE_NOT_FLAGGED), already split or confirmed (MULTI_NOTICE_ALREADY_RESOLVED), or no stored document (SPLIT_NO_DOCUMENT)MiscValidationErrorResponse
  • 422The page ranges do not fit the document (SPLIT_RANGES_INVALID), ask for more notices than one split may make (SPLIT_TOO_MANY_RANGES), or the stored PDF cannot be opened (SPLIT_PDF_UNREADABLE)MiscValidationErrorResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/notices/{id}/confirm-singleConfirm a flagged document is one notice

CPA confirms a document flagged as possibly holding more than one notice is ONE notice (I-7, ruling Q23); its deadline is trusted again. Cookie sessions require a CPA role; Bearer keys gate via `notices.write`.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Responses

  • 200Confirmed as one noticeConfirmSingleNoticeResponse
    FieldTypeRequiredDescription
    successenum: truerequired—
    resolutionenum: "single"required—
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Cookie session without a CPA roleMiscUnauthorizedResponse
  • 404Notice not found — unknown, malformed, foreign, soft-deleted or out-of-scope id (one body)MiscNotFoundResponse
  • 409Not flagged (MULTI_NOTICE_NOT_FLAGGED) or already split or confirmed (MULTI_NOTICE_ALREADY_RESOLVED)MiscValidationErrorResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/notices/{id}/decision-cardGet the notice decision card

The notice decision card: four rows (Read · Checked · Proposed · Decision), each with a state, labelled figures and a link to the page that holds the evidence. Read-only. An employer caller (client_user session or an employer-minted key) gets the employer projection. An unknown, foreign-tenant, sibling-employer or deleted notice answers the same 404.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Responses

  • 200The decision cardNoticeDecisionCard
    FieldTypeRequiredDescription
    noticeIdstring · uuidrequired—
    planeenum: "cpa" | "employer"required—
    currentenum: "read" | "checked" | "proposed" | "decision"required—
    figureHeldbooleanrequiredEmployer plane: Kreto's computed figures are held until the CPA approves.
    rowsobjectrequired—
  • 400Validation errorNoticeValidationErrorResponse
  • 401UnauthorizedNoticeUnauthorizedResponse
  • 403Forbidden — missing required scopeNoticeUnauthorizedResponse
  • 404Notice not found in caller's tenant or employer scopeNoticeNotFoundResponse
  • 429Rate limit exceededNoticeRateLimitResponse
  • 500Server error — sanitized (`SanitizedServerError`), or the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/notices/{id}/experience-figuresRead the experience-data request

The notice's open experience-data request (the figures asked, the tax year and the state) and whether the caller may answer it (the employer's owner). `request: null` when none is open. Bearer scope: `notices.read`.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Responses

  • 200The open request (or null) and the caller's answer rightExperienceFiguresGetResponse
    FieldTypeRequiredDescription
    requestExperienceRequestrequired—
    can_answerbooleanrequired—
  • 401UnauthorizedMiscUnauthorizedResponse
  • 404Notice not foundMiscNotFoundResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerErrorWithDetails · AccountSetupIncompleteResponse)
POST/api/notices/{id}/experience-figuresAnswer the experience-data request

The employer owner states the experience figures the open request asks for. They are stored as employer-stated notice facts (never as a Kreto-verified figure) and the rate decision re-runs through the job queue. Only the asked keys are accepted; `average_annual_payroll` must be above 0. A replay of the same answer returns 200 `deduplicated: true`; a different answer to an answered request is 409 `request_not_open`. Bearer scope: `notices.write` on an entity-scoped employer key.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Request body ExperienceFiguresBodyrequired

FieldTypeRequiredDescription
request_idstringrequired—
figuresobjectrequired—

Responses

  • 200Answered; the re-run is queuedExperienceFiguresAccepted
    FieldTypeRequiredDescription
    answeredenum: truerequired—
    request_idstringrequired—
    deduplicatedbooleanoptional—
    rerunobjectoptional—
  • 400`figure_invalid` — a figure is missing, unknown or out of rangeone of (ExperienceFiguresError · MiscValidationErrorResponse)
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403`ENTITY_OWNER_REQUIRED` — only the employer's owner answersExperienceFiguresError
  • 404Notice not foundMiscNotFoundResponse
  • 409`request_not_open` — no open request with that idExperienceFiguresError
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerErrorWithDetails · AccountSetupIncompleteResponse)
POST/api/notices/{id}/experience-figures/requestAsk the employer for experience figures

The CPA asks the employer's owner for the experience-statement figures that check an in-range SUI rate increase Kreto could only check against the state's range. Opens the same experience-data request Kreto fires on its own. Idempotent: an open request answers `deduplicated: true` and nothing is re-sent. 409 `figures_on_file` / `not_owed` / `nothing_to_ask` / `no_employer_owner` (the honest reason). Session only, CPA roles.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredNotice UUID

Responses

  • 200The open request (new, or deduplicated)ExperienceAskResponse
    FieldTypeRequiredDescription
    requestedenum: truerequired—
    deduplicatedbooleanrequired—
    requestExperienceRequestrequired—
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Not a CPA roleExperienceFiguresError
  • 404Notice not foundMiscNotFoundResponse
  • 409The ask cannot be opened (the code names why)ExperienceFiguresError
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerErrorWithDetails · AccountSetupIncompleteResponse)
POST/api/notices/bulk/actionBulk approve, dismiss or request revision on notices

Apply one CPA action to up to 200 notices. Per-id: tenant assert, no-op guard, one hash-chained audit row. If the tenant-scoped existence probe fails, or a cross-tenant row is detected post-fetch, the batch answers 500 and NOTHING is written.

Bearer scopesnotices.write

Request body objectrequired

FieldTypeRequiredDescription
actionenum: "approved" | "dismissed" | "revision_requested"required—
idsarray<string · uuid>requiredTarget ids, 1..200. Duplicates are collapsed before any write.
notesstringoptional—

Responses

  • 200Per-id ledger. Partial success is a 200.NoticesBulkActionResult
    FieldTypeRequiredDescription
    appliedintegerrequiredIds that produced a real state change (and therefore exactly one audit row each).
    skippedarray<object>requiredIds deliberately not mutated. Present on a 200 — partial success is the contract, not an error.
    failedarray<object>requiredIds whose mutation raised. `probe_failed` means the tenant-scoped existence probe itself errored, so nothing was attempted.
    unauditedarray<object>optionalA SUBSET of `applied`: ids that really moved but are missing a hash-chained audit entry. Absent on the normal path.
    untimelinedarray<object>optionalA SUBSET of `applied`: ids whose decision and audit entry committed but whose timeline (notice_events) row failed to write. Absent on the normal path.
  • 400Body failed validation (ids outside 1..200, or a bad uuid).
  • 403Authenticated but not CPA-class (`FORBIDDEN_ROLE`), or the Bearer key lacks the declared scope.
  • 422At least one id does not resolve in this tenant (`unresolved_ids`). The whole batch is refused and nothing is written.
  • 500A tenant-scope leak was detected post-fetch (defense-in-depth third wall). No writes occurred.one of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/notices/bulk/exportExport notices as CSV

Export notices either by explicit id list OR by the same narrowing the /notices queue applies — exactly one of `ids` / `query`. A `query` that resolves to more than 1000 rows is REFUSED 413 with the true size, never truncated into a file that looks complete. `amount` is emitted human-formatted (the column is dollars, not the stored cents).

Bearer scopesnotices.read

Request body objectrequired

FieldTypeRequiredDescription
idsarray<string · uuid>optionalTarget ids, 1..1000. Over the cap the request is REFUSED, never silently truncated.
queryobjectoptional—

Responses

  • 200CSV attachment: `text/csv; charset=utf-8` with `Content-Disposition: attachment; filename="notices-export-<YYYY-MM-DD>.csv"`. Header row, in order: `notice_id`, `employer`, `agency`, `state`, `type`, `amount`, `deadline`, `status`, `status_code`, `confirmation_number`, `filed_at`, `outcome`, `received`.string
  • 400Body failed validation — `ids` outside 1..1000, or a value that is not a UUID.
  • 403Authenticated but out of scope — a `client_user` asking outside its entity memberships, or a Bearer key that does not carry the declared scope.
  • 413`EXPORT_TOO_LARGE` — the `query` matches more than 1000 notices. The body names the true `total` and the `limit`; nothing is written.
  • 500The read failed, or a post-fetch tenant-scope leak was detected (defense-in-depth third wall). No partial file is written.one of (SanitizedServerError · AccountSetupIncompleteResponse)

Ops1

GET/api/admin/notice-intake/unmatchedUnmatched inbound email queue (ops only)

OPS ONLY: a staff door for Kreto operators, not a customer API. Lists the 50 most recent inbound emails that matched no firm. It has no page by design. Requires a system administrator session.

Cookie session or Bearer token

Responses

  • 200The most recent unmatched inbound emails (max 50).UnmatchedIntakeResponse
    FieldTypeRequiredDescription
    unmatchedarray<UnmatchedIntakeRecord>required—
  • 401No admin session (`requireAdminAPI`).
  • 403The session is not a system administrator, or must complete an MFA step-up (`requireAdminAPI`).
  • 500Unexpected failure: the sanitized body `captureApiError` writes. An admin session with no `user_profiles` row answers `requireAdminAPI`'s `AccountSetupIncompleteResponse` instead.one of (SanitizedServerError · AccountSetupIncompleteResponse)

Packets4

GET/api/packets/{id}Get response packet

Get response packet with status + document list + linked notice metadata (KRT-224).

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredResponse packet UUID

Responses

  • 200PacketPacketDetailResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    packetPacketDetailrequired—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 404Packet not foundMiscNotFoundResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/packets/{id}/downloadDownload response packet

Generate 7-day signed download URLs for all packet documents.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredResponse packet UUID

Responses

  • 200Signed URLsPacketDownloadResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    packet_idstring · uuidrequired—
    documentsarray<PacketDownloadDocument>required—
    expires_in_secondsintegerrequired—
    messagestringrequired—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 404Packet or documents not foundMiscNotFoundResponse
  • 409Packet still assembling, or its assembly failed (code PACKET_FAILED) — only a finished pack (ready / submitted / acknowledged / rejected) is servedMiscValidationErrorResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/packets/{id}/submitSubmit response packet

Mark a response packet as submitted, record submission reference, and transition the linked notice to `filed` status. Refused unless packet is in `ready` state. SESSION-ONLY: this files a notice, which is a human attestation, so a Bearer key is refused with 403 SESSION_REQUIRED even when it carries `notices.write` (parity with POST /api/notices/{id}/file and the APPROVAL_REQUIRED v1 `resolutions.execute`). Also requires the linked notice to be APPROVED (`cpa_action = 'approved'`) unless it is already filed; otherwise 409 APPROVAL_REQUIRED with no writes of any kind — neither the notice nor the packet moves.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredResponse packet UUID

Request body PacketSubmitBodyrequired

FieldTypeRequiredDescription
submission_refstringrequired—
submission_methodenum: "email" | "fax" | "portal" | "mail"optional—
notesstringoptional—

Responses

  • 200SubmittedPacketSubmitResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    packetobjectrequired—
    messagestringrequired—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Forbidden — SESSION_REQUIRED: a Bearer key cannot submit (this files a notice); role_forbidden: a session without CPA-level access (e.g. an employer user) cannot submit — refused before any read or writeMiscUnauthorizedResponse
  • 404Packet not foundMiscNotFoundResponse
  • 409Packet not in ready status / notice not yet approved (APPROVAL_REQUIRED)MiscValidationErrorResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/packets/assembleAssemble response packet

Trigger an async packet assembly job. Returns a job_id; poll the packet endpoint for status. Cookie sessions go through rate-limit + idempotency middleware.

Bearer scopesnotices.write

Request body PacketAssembleBodyrequired

FieldTypeRequiredDescription
notice_idstring · uuidrequired—
resolution_idstring · uuidoptional—
evidence_idsarray<string · uuid>optional—

Responses

  • 200Job queuedPacketAssembleResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    job_idstring · uuidrequired—
    notice_idstring · uuidrequired—
    statusenum: "assembling"required—
    messagestringrequired—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 404Notice not foundMiscNotFoundResponse
  • 409Nothing may be sealed: no approved letter, the letter is the manual-review placeholder, or a filed notice's letter was never approved (rule-pack-placeholder).PacketAssembleRefusal
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)

Payroll9

GET/api/payroll/dataGet payroll dropdown data

Returns entities visible to the caller, optionally with notices for a specific entity. Backs the payroll-page entity dropdown.

Bearer scopespayroll.read

Parameters

NameInTypeRequiredDescription
entity_idquerystring · uuidoptionalWhen present, also returns notices for this entity (tenant-scoped)

Responses

  • 200Entities + (optional) noticesPayrollDataResponse
    FieldTypeRequiredDescription
    entitiesarray<PayrollEntity>required—
    noticesarray<PayrollNoticeSummary>required—
    payroll_uploadsPayrollUploadHistoryrequired—
  • 400Validation errorPayrollValidationErrorResponse
  • 401UnauthorizedPayrollUnauthorizedResponse
  • 403Forbidden — missing required scopePayrollUnauthorizedResponse
  • 429Rate limit exceededPayrollRateLimitResponse
  • 500Entity-scope resolution or the entities read failed (sanitized, with empty lists), or a post-fetch tenant-scope leak on the entities or notices read was refused (`isolation`). No partial data is returned.one of (PayrollServerErrorResponse · PayrollDataServerErrorResponse · AccountSetupIncompleteResponse)
GET/api/payroll/template.csvDownload payroll CSV template

Returns a CSV template for payroll data upload. Columns match the generic format payroll-validator.ts expects.

Bearer scopespayroll.read

Responses

  • 200CSV templatestring
  • 401Unauthorized
  • 403Forbidden — missing required scope
  • 429Rate limit exceeded
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/payroll/parse-previewPreview payroll file headers

Server-side payroll preview parser. Returns only the header row + row count for client-side column-mapping; the full file still ships to /api/payroll/upload for processing. Routes XLSX through the hardened safeParseXLSX wrapper (xlsx@0.18.5 has known CVEs that the wrapper neutralizes).

Bearer scopespayroll.read

Request body PayrollMultipartFileBodyrequired

FieldTypeRequiredDescription
filestringrequiredCSV or Excel binary

Responses

  • 200Header preview + row countPayrollParsePreviewResponse
    FieldTypeRequiredDescription
    headersarray<string>required—
    rowCountintegerrequired—
    formatenum: "xlsx" | "csv"required—
  • 400Validation error / invalid filePayrollValidationErrorResponse
  • 401UnauthorizedPayrollUnauthorizedResponse
  • 403Forbidden — missing required scopePayrollUnauthorizedResponse
  • 413File too large (>10 MB)PayrollValidationErrorResponse
  • 415Unsupported file typePayrollValidationErrorResponse
  • 429Rate limit exceededPayrollRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/payroll/validateValidate ADP payroll CSV

ADP payroll CSV validator (KRT-396). Accepts multipart with `file` + `notice_id`. Validates the CSV against the ADP-specific validator and persists the result to validation_results. Tenant-scoped on the notice access check + insert.

Bearer scopespayroll.write

Request body PayrollValidateMultipartBodyrequired

FieldTypeRequiredDescription
filestringrequiredADP CSV file
notice_idstring · uuidrequiredNotice UUID to validate against

Responses

  • 200Validation result + persisted record idPayrollValidateResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    validation_result_idstring · uuidrequired—
    validationobjectrequired—
  • 400Validation error / file too large / not CSVPayrollValidationErrorResponse
  • 401UnauthorizedPayrollUnauthorizedResponse
  • 403A valid Bearer key without `payroll.write`, a session that must complete an MFA step-up (`mfa_required`), or a non-CPA cookie role (`role_forbidden` — refused before any read or write)PayrollValidateForbiddenResponse
  • 404Notice not found in caller's tenantPayrollNotFoundResponse
  • 429Rate limit exceededPayrollRateLimitResponse
  • 500Entity-scope resolution, the notice read or the validation_results insert failed, or the validator threw — every arm is finalized, so sanitized to the shared server-error bodyone of (PayrollServerErrorResponse · AccountSetupIncompleteResponse)
GET/api/payroll/pullGet payroll data status

Returns the current payroll-data status for an entity — cache tier, age in days, and active provider connection (if any). All queries tenant-scoped.

Bearer scopespayroll.read

Parameters

NameInTypeRequiredDescription
entity_idquerystring · uuidrequiredEmployer entity UUID (required)

Responses

  • 200Status bundlePayrollPullStatusResponse
    FieldTypeRequiredDescription
    connectionobjectrequired—
    payroll_dataobjectrequired—
    cache_tierenum: "hot" | "warm" | "cold" | "none"required—
    age_daysintegerrequired—
  • 400`entity_id` missing or not a UUID — the validator's refusal, with its issuesPayrollPullStatusValidationErrorResponse
  • 401UnauthorizedPayrollUnauthorizedResponse
  • 403A valid Bearer key without `payroll.read` (`Forbidden — missing required scope: payroll.read`), a session that must complete an MFA step-up (`mfa_required`), or an `entity_id` outside a client_user's entities (`Not authorized for this entity`)PayrollPullStatusForbiddenResponse
  • 429Rate limit exceededPayrollRateLimitResponse
  • 500Server error — a snapshot/connection read failed or the tenant third wall refused (both finalized, so sanitized)one of (PayrollServerErrorResponse · AccountSetupIncompleteResponse)
POST/api/payroll/pullTrigger payroll pull (removed — always 410)deprecated

Deprecated. The automated payroll pull this endpoint triggered has been removed. It still authenticates, validates the body and gates the entity (so a malformed or out-of-scope call gets a deterministic 4xx), then answers 410 Gone. Upload payroll with POST /api/payroll/upload instead; GET /api/payroll/pull still serves the cached status.

Bearer scopespayroll.write

Request body PayrollPullPostBodyrequired

FieldTypeRequiredDescription
entity_idstring · uuidrequired—
trigger_typestringrequired—
trigger_refstringoptional—
force_refreshbooleanoptional—

Responses

  • 400Validation errorPayrollValidationErrorResponse
  • 401UnauthorizedPayrollUnauthorizedResponse
  • 403Forbidden — missing required scope, or the entity is outside a client_user's scopePayrollUnauthorizedResponse
  • 410Gone — the payroll pull was removed; returned for every well-formed, in-scope callPayrollPullGoneResponse
  • 429Rate limit exceededPayrollRateLimitResponse
  • 500No route-level 500 arm — past auth, body validation and the entity gate the call answers 410. Declared on the payroll family's shared server-error bodyone of (PayrollServerErrorResponse · AccountSetupIncompleteResponse)
POST/api/payroll/notice-syncADP payroll sync for a notice (removed — always 410)deprecated

Deprecated. The automated ADP payroll pull this endpoint drove has been removed. It still authenticates and validates the body (so a malformed call gets a deterministic 4xx), then answers 410 Gone. Upload payroll with POST /api/payroll/upload instead.

Bearer scopespayroll.write

Request body PayrollNoticeSyncBodyrequired

FieldTypeRequiredDescription
entity_idstring · uuidrequired—
notice_idstring · uuidrequired—
needed_yearintegerrequired—
needed_quarterintegerrequired—
usernamestringrequired—
passwordstringrequired—
company_codestringoptional—
save_connectionbooleanoptional—

Responses

  • 400Validation errorPayrollValidationErrorResponse
  • 401UnauthorizedPayrollUnauthorizedResponse
  • 403Forbidden — missing required scopePayrollUnauthorizedResponse
  • 410Gone — the ADP pull was removed; returned for every well-formed callPayrollNoticeSyncGoneResponse
  • 429Rate limit exceededPayrollRateLimitResponse
  • 500No route-level 500 arm — past auth and body validation the call answers 410. Declared on the payroll family's shared server-error bodyone of (PayrollServerErrorResponse · AccountSetupIncompleteResponse)
POST/api/payroll/upload-reportUpload payroll report

Upload a payroll-related report file (CSV/PDF/Excel) for an entity. Persists the file to Supabase storage and one `documents` row, nothing else — no parse, no job, no payroll rows. The file then appears in the document library.

Bearer scopespayroll.write

Request body PayrollUploadReportMultipartBodyrequired

FieldTypeRequiredDescription
filestringrequiredCSV/PDF/Excel binary (max 50 MB)
report_typestringrequiredNormalized report type (e.g., payroll_journal)
entity_idstring · uuidrequired—

Responses

  • 200Uploaded — the storage object and the documents row were both persisted; the body names themPayrollUploadReportResponse
    FieldTypeRequiredDescription
    successenum: truerequired—
    documentIdstring · uuidrequired—
    content_hashstringrequired—
  • 400Validation errorPayrollValidationErrorResponse
  • 401UnauthorizedPayrollUnauthorizedResponse
  • 403Forbidden — missing required scopePayrollUnauthorizedResponse
  • 404Entity not found in caller's tenantPayrollNotFoundResponse
  • 429Rate limit exceeded (uploads use the 10/min ceiling)PayrollRateLimitResponse
  • 500Entity-scope resolution or the entity read failed, the tenant third wall refused, or the handler threw (the document insert included) — finalized or captureApiError, sanitized either wayone of (PayrollServerErrorResponse · AccountSetupIncompleteResponse)
  • 502The file could not be written to storage (`storage_upload_failed`, sanitized) — nothing was persisted; retryStorageUploadFailedResponse
POST/api/payroll/uploadUpload payroll file (heavy)

Upload a payroll CSV or Excel file for an entity, optionally linked to a notice. Pipeline: storage upload → payroll_uploads + payroll_records insert → SUI detail extraction (per-employee/month/state from Excel) → mergePayrollData into entity_payroll_data → validation_results when notice-linked → auto-trigger verification pipeline for AWAITING_PAYROLL notices. Idempotency-aware: same x-idempotency-key replays the prior response.

Bearer scopespayroll.write

Request body PayrollUploadFormBodyrequired

FieldTypeRequiredDescription
filestringrequiredCSV or Excel binary (max 10 MB)
entity_idstring · uuidrequired—
notice_idstring · uuidoptional—
column_mapstringoptionalJSON-stringified ColumnMap (employee_name, gross_wages, state, etc.)

Responses

  • 200Uploaded (validation summary + row counts), or the duplicate receipt when the same file is already on file (`duplicate: true`)PayrollUploadResponse
  • 400Validation error / file too large / unsupported file type / unsafe XLSXPayrollValidationErrorResponse
  • 401UnauthorizedPayrollUnauthorizedResponse
  • 402Subscription required (cookie-session callers only)PayrollUnauthorizedResponse
  • 403Forbidden — missing required scopePayrollUnauthorizedResponse
  • 404Entity or notice not found in caller's tenantPayrollNotFoundResponse
  • 422Refused — nothing was saved. `payroll_identities_unresolved`: some employees could not be matched to the roster. `payroll_name_collision` (owner ruling 2026-10-04 item 3): two name-only lines with the same name, date and state state different SUI rates — a COUNT and a DATE, never a name. `payroll_period_implausible` (walk restyle cause 5): lines state a pay period outside the plausible range — a COUNT and one example period. Also `parse_failed` / `LEGACY_XLS_FORMAT`.PayrollUploadRefusalResponse
  • 429Rate limit exceeded (uploads use the 10/min ceiling)PayrollRateLimitResponse
  • 500Entity-scope resolution, the entity or notice read, or the snapshot tenant third wall failed, or the pipeline threw — finalized or captureApiError, sanitized either wayone of (PayrollServerErrorResponse · AccountSetupIncompleteResponse)
  • 502The file could not be written to storage (`storage_upload_failed`, sanitized) — no upload row was written; retryStorageUploadFailedResponse

POA7

GET/api/poa-authorizationsList POA authorizations

List POA authorizations for the caller's tenant (KRT-464). Filter by employer_entity_id / poa_entity_id. Revoked rows are included by default; `include_revoked=false` narrows to non-revoked rows. Each row carries `status`: revoked | rejected | expired | active | draft. Bearer keys gate via `notices.read`.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
employer_entity_idquerystring · uuidoptional—
poa_entity_idquerystring · uuidoptional—
include_revokedquerybooleanoptional—

Responses

  • 200POA authorizationsPOAAuthorizationsListResponse
    FieldTypeRequiredDescription
    authorizationsarray<POAAuthorization>required—
  • 401UnauthorizedMiscUnauthorizedResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/poa-authorizationsRequest POA authorization

Request a new POA authorization (KRT-464). 409 if an authorization already exists for the same employer + poa + scope. Bearer keys gate via `notices.write`.

Bearer scopesnotices.write

Request body POAAuthorizationCreateBodyrequired

FieldTypeRequiredDescription
employer_entity_idstring · uuidrequired—
poa_entity_idstring · uuidrequired—
scopearray<POAScope>required—

Responses

  • 201Authorization requestedPOAAuthorizationCreateResponse
    FieldTypeRequiredDescription
    authorizationPOAAuthorizationrequired—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403A valid Bearer key without `notices.write`, a session that must complete an MFA step-up (`mfa_required`), or `role_forbidden` — a non-CPA cookie role, refused before any read or writeNoticesWriteForbiddenResponse
  • 409Authorization already existsMiscValidationErrorResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/poa-authorizations/{id}/generateGenerate pre-filled POA PDF

Generate a pre-filled POA PDF for a previously-created authorization. Two modes: `download` (returns the PDF + filing instructions only) and `send-for-signature` (creates a signature_request and emails the signer + a copy to the originating CPA). Bearer keys gate via `notices.write`. Kreto does NOT submit the form to the agency — the user files manually using the agency portal or mailing address in the `filing` block of the response.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredPOA authorization UUID

Request body POAGenerateBody

FieldTypeRequiredDescription
template_idstringrequiredPre-auth template id (e.g., 'poa-tax-representation'). Browse via GET /api/pre-authorization-templates.
modeenum: "download" | "send-for-signature"optional'download' returns the PDF only. 'send-for-signature' creates a signature_request, emails the signer + originator. Default: send-for-signature.
signer_namestringoptional—
signer_emailstring · emailoptional—
overridesobjectoptional—

Responses

  • 200Mode=download: pre-filled PDF + filing instructionsobject
    FieldTypeRequiredDescription
    modeenum: "download"required—
    authorization_idstring · uuidrequired—
    template_idstringrequired—
    template_namestringrequired—
    document_hashstringrequired—
    filingobjectrequired—
    missing_required_fieldsarray<string>required—
  • 201Mode=send-for-signature: signing request created + email queuedobject
    FieldTypeRequiredDescription
    modeenum: "send-for-signature"required—
    authorization_idstring · uuidrequired—
    signature_request_idstring · uuidrequired—
    secure_tokenstringrequired—
    signing_linkstringrequired—
    expires_atstring · date-timerequired—
    document_hashstringrequired—
    template_idstringrequired—
    template_namestringrequired—
    filingobjectrequired—
    missing_required_fieldsarray<string>required—
    signer_emailstring · emailrequired—
    originator_emailedbooleanrequired—
  • 400Validation error (missing template_id, invalid body, etc.), or `code: signer_name_conflict` — the request names two different signers: `signer_name` and a signer-name field in `overrides` (employer_signatory_name / signatory_name / authorized_rep_name) disagree. One authorization has one signer of record; send one name, or the same name in both.one of (POAGenerateSignerConflictResponse · MiscValidationErrorResponse)
  • 401UnauthorizedMiscUnauthorizedResponse
  • 404Authorization or employer entity not found in your tenantMiscNotFoundResponse
  • 409Authorization is revoked — cannot generate a document; or (send mode) the form has no signature line mapped: "This form has no signature line mapped yet — Kreto cannot stamp it." (owner ruling S2 — no state POA form is mapped today, so send mode is refused before anything is generated; download mode is unchanged).MiscValidationErrorResponse
  • 422Refused: a required template field has no value. No PDF is built — a placeholder is never printed into a legal document. `reason: entity_incomplete` = fix the employer record; `reason: needs_input` = supply the field in `overrides` and retry. OR `resultState: "pending_data"` + `missing_inputs: ["firm_name"]` when the template prints the firm on a required line and the firm's record states no name (never a stand-in label).one of (POAGenerateFieldsMissingResponse · POAGenerateFirmNameMissingResponse)
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server error (PDF generation failure, DB error)one of (SanitizedServerError · AccountSetupIncompleteResponse)
PATCH/api/poa-authorizations/{id}Revoke POA authorization

Revoke a POA authorization. Bearer keys gate via `notices.write`. 409 if already revoked.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredPOA authorization UUID

Responses

  • 200Authorization revokedPOAAuthorizationRevokeResponse
    FieldTypeRequiredDescription
    authorizationPOAAuthorizationrequired—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403A valid Bearer key without `notices.write`, a session that must complete an MFA step-up (`mfa_required`), or `role_forbidden` — a non-CPA cookie role, refused before any read, or a revoking UPDATE the database refused on a row the caller can readNoticesWriteForbiddenResponse
  • 404Authorization not foundMiscNotFoundResponse
  • 409Already revoked (`already_revoked`) — including when a concurrent revoke won the race (the conditional UPDATE matched no active row and the re-read shows it revoked)POAAuthorizationRevokeConflictResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/pre-authorization-templatesList pre-auth templates

List pre-authorization templates from the global Kreto library. No tenant data is read. Bearer keys gate via `notices.read`.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
categoryquerystringoptional—
statequerystringoptional—

Responses

  • 200TemplatesPreAuthTemplatesListResponse
    FieldTypeRequiredDescription
    templatesarray<PreAuthTemplateSummary>required—
    totalintegerrequired—
  • 401UnauthorizedMiscUnauthorizedResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/pre-authorization-templates/{id}Get pre-auth template

Retrieve full template details (content template + field definitions + signature fields). Bearer keys gate via `notices.read`.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
idpathstringrequiredPre-auth template ID

Responses

  • 200TemplatePreAuthTemplateDetailResponse
    FieldTypeRequiredDescription
    templatePreAuthTemplateFullrequired—
  • 401UnauthorizedMiscUnauthorizedResponse
  • 404Template not foundMiscNotFoundResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/pre-authorization-templates/{id}/autofillAuto-fill pre-auth template

Auto-fill a pre-authorization template from entity data. Optionally renders to PDF and uploads to storage when generate_pdf=true (and employer_id is provided). CPA-class roles only on cookie sessions; Bearer keys gate via `notices.write`.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstringrequiredPre-auth template ID

Request body PreAuthAutofillBodyrequired

FieldTypeRequiredDescription
entity_dataobjectrequired—
overridesobjectoptional—
generate_pdfbooleanoptional—
employer_idstring · uuidoptional—
cpa_namestringoptional—
cpa_firmstringoptional—

Responses

  • 200Auto-fill result (with optional pdf_hash)PreAuthAutofillResponse
    FieldTypeRequiredDescription
    rendered_contentstringrequired—
    resolved_fieldsobjectrequired—
    missing_required_fieldsarray<string>required—
    missing_optional_fieldsarray<string>required—
    pdf_hashstringoptional—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Forbidden — CPA access requiredMiscUnauthorizedResponse
  • 404Template not foundMiscNotFoundResponse
  • 422Refused (generate_pdf=true only): a required template field has no value. No PDF is built or uploaded — a placeholder is never printed into a legal document. `reason: entity_incomplete` = fix the employer record / entity_data; `reason: needs_input` = supply the field in `overrides` and retry.POAGenerateFieldsMissingResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)

Reports7

GET/api/reports/notice-summaryNotice summary report

Group notices by status + (primary) type for the caller's tenant. Bearer keys gate via `notices.read`.

Bearer scopesnotices.read

Responses

  • 200SummaryReportsNoticeSummaryResponse
    FieldTypeRequiredDescription
    by_statusobjectrequired—
    by_typeobjectrequired—
    totalintegerrequired—
  • 401UnauthorizedReportsUnauthorizedResponse
  • 429Rate limit exceededReportsRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/reports/roiROI report

ROI metrics: total savings, notices resolved, average resolution days, protests won, and ROI multiple. Bearer keys gate via `notices.read`.

Bearer scopesnotices.read

Responses

  • 200ROIReportsRoiResponse
    FieldTypeRequiredDescription
    total_savingsnumberrequired—
    notices_resolvedintegerrequired—
    avg_resolution_daysnumberrequired—
    resolution_sample_sizeintegerrequired—
    protests_wonintegerrequired—
    roi_multiplenumberrequired—
  • 401UnauthorizedReportsUnauthorizedResponse
  • 429Rate limit exceededReportsRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/reports/sui-impactSUI impact report

SUI-related notice analysis with per-notice breakdown. Bearer keys gate via `notices.read`.

Bearer scopesnotices.read

Responses

  • 200SUI impactReportsSuiImpactResponse
    FieldTypeRequiredDescription
    summaryobjectrequired—
    noticesarray<ReportsSuiImpactNotice>required—
  • 401UnauthorizedReportsUnauthorizedResponse
  • 429Rate limit exceededReportsRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/reports/compliance-scorecardCompliance scorecard

Entity-level compliance scores (the compliance monitor's) with a nullable trend (no manufactured signal). Bearer keys gate via `notices.read`.

Bearer scopesnotices.read

Responses

  • 200ScorecardReportsScorecardResponse
    FieldTypeRequiredDescription
    entitiesarray<ReportsScorecardEntity>required—
  • 401UnauthorizedReportsUnauthorizedResponse
  • 429Rate limit exceededReportsRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/reports/state-authorizationState authorization report

POA status per entity per state, joining poa_authorizations + documents tables. Bearer keys gate via `notices.read`.

Bearer scopesnotices.read

Responses

  • 200State authorizationReportsStateAuthResponse
    FieldTypeRequiredDescription
    entitiesarray<ReportsStateAuthEntity>required—
  • 401UnauthorizedReportsUnauthorizedResponse
  • 429Rate limit exceededReportsRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/reports/response-timeResponse-time report

Notice processing-time metrics (classification seconds, resolution days, draft hours) with monthly breakdown. Bearer keys gate via `notices.read`.

Bearer scopesnotices.read

Responses

  • 200Response-time metricsReportsResponseTimeResponse
    FieldTypeRequiredDescription
    avg_classification_secondsintegerrequired—
    classification_sample_sizeintegerrequired—
    avg_resolution_daysnumberrequired—
    resolution_sample_sizeintegerrequired—
    avg_draft_hoursnumberrequired—
    draft_sample_sizeintegerrequired—
    data_errorsobjectrequired—
    total_processedintegerrequired—
    by_montharray<ReportsResponseTimeMonth>required—
  • 401UnauthorizedReportsUnauthorizedResponse
  • 429Rate limit exceededReportsRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/reports/generateGenerate report

Generate a compliance report for the requested entities + date range. Returns JSON or a CSV file (Content-Disposition: attachment). Up to 50 entities per call. Bearer keys gate via `audit.write` (full tenant scope) — KRT-1479 P5 review RATIFIED this as a compute-heavy mutation so read-only keys cannot burn compute (route.ts docblock); SWEEP C16 corrected the declaration, which still said `audit.read`. Cookie sessions use RLS-enforced entity access.

Bearer scopesaudit.write

Request body ReportsGenerateBodyrequired

FieldTypeRequiredDescription
typestringrequired—
entityIdsarray<string · uuid>required—
dateRangeobjectrequired—
formatenum: "json" | "csv"optional—

Responses

  • 200Report (JSON when format=json; CSV download otherwise)ReportsGenerateJsonResponse
    FieldTypeRequiredDescription
    typestringrequired—
    rowsarray<object>required—
    summaryobjectoptional—
    csvstringoptional—
  • 400Validation errorReportsValidationErrorResponse
  • 401UnauthorizedReportsUnauthorizedResponse
  • 403Access denied for one or more entityIdsReportsUnauthorizedResponse
  • 429Rate limit exceededReportsRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)

Resolutions4

GET/api/resolutions/{id}Get resolution draft

Returns the draft + tenant status counts.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredResolution draft UUID

Responses

  • 200Draft + statsResolutionGetResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    draftResolutionDraftRecordrequired—
    statsobjectrequired—
  • 400Validation errorResolutionValidationErrorResponse
  • 401UnauthorizedResolutionUnauthorizedResponse
  • 403Forbidden — missing required scopeResolutionUnauthorizedResponse
  • 404Draft not found in caller's tenantResolutionNotFoundResponse
  • 429Rate limit exceededResolutionRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
PATCH/api/resolutions/{id}Edit resolution draft (CPA)

CPA edits draft text. Only allowed in `draft` or `in_review` status.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredResolution draft UUID

Request body ResolutionPatchBodyrequired

FieldTypeRequiredDescription
cpa_editsstringrequired—
statusenum: "in_review"optional—

Responses

  • 200Draft updatedResolutionPatchResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    draft_idstring · uuidrequired—
    statusstringrequired—
  • 400Validation error / draft in non-editable statusResolutionValidationErrorResponse
  • 401UnauthorizedResolutionUnauthorizedResponse
  • 403Forbidden — CPA access requiredResolutionUnauthorizedResponse
  • 404Draft not found in caller's tenantResolutionNotFoundResponse
  • 429Rate limit exceededResolutionRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/resolutions/{id}/approveApprove resolution draft (CPA)

CPA approves a resolution draft. Status transitions in_review → approved. Only valid from in_review.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredResolution draft UUID

Request body ResolutionApproveBody

FieldTypeRequiredDescription
notesstringoptional—

Responses

  • 200ApprovedResolutionApproveResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    draft_idstring · uuidrequired—
    statusenum: "approved"required—
    audit_event_pendingenum: trueoptional—
  • 400Invalid draft id, malformed JSON (`INVALID_REQUEST_BODY`), a schema-invalid body (`details`), or a draft not in draft/in_review.ResolutionApproveBadRequestResponse
  • 401UnauthorizedResolutionUnauthorizedResponse
  • 402An active subscription is required for this action.ResolutionApprovePaymentRequiredResponse
  • 403Forbidden — not an interactive session (`SESSION_REQUIRED`), not a CPA-firm role, a payroll override this caller may not make, no human behind the approval, or a login that cannot read the letter (`LETTER_UNREADABLE`).ResolutionApproveForbiddenResponse
  • 404Draft (or its notice) not found in the caller's tenant — existence-opaque: a foreign tenant's id is the same answer.ResolutionApproveNotFoundResponse
  • 409Refused — nothing was approved. The draft is blocked, payroll is unverified or its state unreadable, the letterhead is incomplete, the notice bridge refused, or the lifecycle writer refused the move (its own code, rendered verbatim). `draft_superseded`: the draft was replaced by a newer version (one live draft per notice) — approve `live_draft_id` instead.one of (object · ResolutionApproveConflictResponse)
  • 422The letter still has unfilled placeholders (`DRAFT_HAS_PLACEHOLDERS`).ResolutionApproveUnprocessableResponse
  • 429Rate limit exceededResolutionRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/resolutions/{id}/rejectReject resolution draft (CPA)

CPA rejects a draft (draft|in_review → rejected).

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredResolution draft UUID

Request body ResolutionRejectBodyrequired

FieldTypeRequiredDescription
rejection_reasonstringrequired—

Responses

  • 200RejectedResolutionRejectResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    draft_idstring · uuidrequired—
    statusenum: "rejected"required—
  • 400Validation error / wrong statusResolutionValidationErrorResponse
  • 401UnauthorizedResolutionUnauthorizedResponse
  • 403Forbidden — CPA access requiredResolutionUnauthorizedResponse
  • 404Draft not found in caller's tenantResolutionNotFoundResponse
  • 429Rate limit exceededResolutionRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)

Settings3

GET/api/settings/firmGet firm settings

Firm/tenant settings (name, slug). Bearer keys gate via `admin.read`.

Bearer scopesadmin.read

Responses

  • 200SettingsSettingsFirmResponse
    FieldTypeRequiredDescription
    namestringrequired—
    slugstringrequired—
    created_atstring · date-timerequired—
  • 401UnauthorizedMiscUnauthorizedResponse
  • 404Tenant not foundMiscNotFoundResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
PATCH/api/settings/firmUpdate firm settings

Update firm/tenant settings (currently: name only). Bearer keys gate via `admin.write` — the route ENFORCES admin.write (route.ts), and `hasScope()` is exact membership, so a key holding only `admin.read` is refused. SWEEP C16: the declaration said `admin.read` and was the half that was wrong; a PATCH is a write.

Bearer scopesadmin.write

Request body SettingsFirmUpdateBodyrequired

FieldTypeRequiredDescription
namestringrequired—

Responses

  • 200Settings updatedSettingsFirmResponse
    FieldTypeRequiredDescription
    namestringrequired—
    slugstringrequired—
    created_atstring · date-timerequired—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/settings/email-intake/client-addressesList client forwarding addresses

Each client's notice-forwarding address, paged (25 per page) and searchable by legal name. CPA-class cookie session only. `entity_id` narrows to one client; an unknown or foreign id answers an empty list.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
pagequerystringoptionalZero-based page
qquerystringoptionalLegal-name search
entity_idquerystring · uuidoptional—

Responses

  • 200Addresses (or forwarding not switched on)ClientForwardingAddressesResponse
    FieldTypeRequiredDescription
    forwarding_onbooleanrequiredFalse when email forwarding is not switched on here; `clients` is then empty.
    clientsarray<ClientForwardingAddress>required—
    pageintegerrequired—
    page_sizeintegerrequired—
    totalintegerrequired—
  • 400Malformed entity_idMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Not a CPA-class roleClientForwardingAddressesForbidden
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Internal errorone of (SanitizedServerError · AccountSetupIncompleteResponse)

SIDES12

GET/api/sides/enrollmentsList SIDES enrollments

List all SIDES employer enrollments for the caller's tenant. CPA-class roles only on cookie sessions; Bearer keys gate via `notices.read`.

Bearer scopesnotices.read

Responses

  • 200EnrollmentsSidesEnrollmentsListResponse
    FieldTypeRequiredDescription
    enrollmentsarray<SidesEnrollment>required—
  • 401UnauthorizedSidesUnauthorizedResponse
  • 403Forbidden — CPA access requiredSidesUnauthorizedResponse
  • 429Rate limit exceededSidesRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/sides/enrollmentsEnroll employer in SIDES

Enroll an employer in SIDES for a specific state. NASWA admin-site enrollment is tracked locally; status starts as `pending`. CPA-class roles only on cookie sessions; Bearer keys gate via `notices.write`.

Bearer scopesnotices.write

Request body SidesEnrollmentCreateBodyrequired

FieldTypeRequiredDescription
entity_idstring · uuidrequired—
state_codestringrequired—
employer_account_numberstringoptional—

Responses

  • 201Enrollment createdSidesEnrollmentCreateResponse
    FieldTypeRequiredDescription
    enrollmentSidesEnrollmentrequired—
  • 400Validation errorSidesValidationErrorResponse
  • 401UnauthorizedSidesUnauthorizedResponse
  • 403Forbidden — CPA access requiredSidesUnauthorizedResponse
  • 404Entity not foundSidesNotFoundResponse
  • 409Already enrolled in this stateSidesValidationErrorResponse
  • 429Rate limit exceededSidesRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/sides/exchangesList SIDES exchanges

List inbound SIDES exchanges for the caller's tenant. Supports status / exchange_type / state_code filters, pagination, and sort. Any authenticated caller can read. Bearer keys gate via `notices.read`.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
statusquerystringoptional—
exchange_typequerystringoptional—
state_codequerystringoptional—
sortqueryenum: "due_date" | "received_date" | "status"optional—
limitqueryintegeroptional—
offsetqueryintegeroptional—

Responses

  • 200Exchanges + totalSidesExchangesListResponse
    FieldTypeRequiredDescription
    exchangesarray<SidesExchangeListItem>required—
    totalintegerrequired—
    limitintegerrequired—
    offsetintegerrequired—
    intakeEnabledbooleanrequiredCompat name for intake.connected.
    intakeSidesIntakeStatusrequired—
  • 400Validation errorSidesValidationErrorResponse
  • 401UnauthorizedSidesUnauthorizedResponse
  • 429Rate limit exceededSidesRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/sides/exchanges/{id}Get SIDES exchange

Fetch a single SIDES exchange + audit log. Any authenticated caller can read; an employer caller (a `client_user` cookie session or a client_user-minted Bearer key) gets a PII-reduced view: raw XML, request facts, routing ids and claimant first/last names are absent and `exchange.claimant_name` reads 'Name withheld'. The CPA plane keeps the names and `claimant_name` carries the joined name (or 'Unknown claimant'). Bearer keys gate via `notices.read`.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredSIDES exchange / claim UUID

Responses

  • 200Exchange + audit logSidesExchangeDetailResponse
    FieldTypeRequiredDescription
    exchangeobjectrequired—
    audit_logarray<SidesAuditEntry>required—
  • 401UnauthorizedSidesUnauthorizedResponse
  • 404Exchange not foundSidesNotFoundResponse
  • 429Rate limit exceededSidesRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
PUT/api/sides/exchanges/{id}Save SI draft response

Save a drafted SI response for an exchange. Validates via Zod and serializes to XML when valid; otherwise stashes the JSON body for round-trip editing. CPA-class roles only on cookie sessions; Bearer keys gate via `notices.write`.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredSIDES exchange / claim UUID

Request body SidesSaveDraftBodyrequired

FieldTypeRequiredDescription
responseobjectrequired—

Responses

  • 200Draft savedSidesSaveDraftResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    validbooleanrequired—
    errorsarray<object>required—
  • 400Validation error / unsupported exchange typeSidesValidationErrorResponse
  • 401UnauthorizedSidesUnauthorizedResponse
  • 403Forbidden — CPA access requiredSidesUnauthorizedResponse
  • 404Exchange not foundSidesNotFoundResponse
  • 429Rate limit exceededSidesRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/sides/exchanges/{id}/submitSubmit SI response (CPA)

Approve and submit an SI response to the SIDES Central Broker. The server injects claimantSSNLast4 from the original request XML, validates, serializes, and posts via the NASWA Model Connector CLI. Re-submit is rejected (409) once an exchange reaches responding/responded/acknowledged. CPA-class roles only on cookie sessions; Bearer keys gate via `notices.write`.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredSIDES exchange / claim UUID

Request body SidesSubmitBodyrequired

FieldTypeRequiredDescription
responseobjectrequired—

Responses

  • 200SubmittedSidesSubmitResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    statusenum: "responded" | "delivery_unverified"required—
    response_filestringrequired—
  • 400Validation error / unsupported exchange typeSidesValidationErrorResponse
  • 401UnauthorizedSidesUnauthorizedResponse
  • 403Forbidden — CPA access requiredSidesUnauthorizedResponse
  • 404Exchange not foundSidesNotFoundResponse
  • 409Exchange already submitted/acknowledged — re-submit blockedSidesValidationErrorResponse
  • 422Zod validation failed (with broker-rule remediation)SidesValidationErrorResponse
  • 429Rate limit exceededSidesRateLimitResponse
  • 500Server error (e.g., missing SSN in raw_request_xml)one of (SanitizedServerErrorWithCode · AccountSetupIncompleteResponse)
  • 502Broker rejected (with business-rule errors)SidesValidationErrorResponse
GET/api/sides/transmissionsList SIDES transmissions

SIDES transmission audit log. Filter by direction (inbound/outbound) and transmission_status. CPA-class roles only on cookie sessions; Bearer keys gate via `audit.read`.

Bearer scopesaudit.read

Parameters

NameInTypeRequiredDescription
directionqueryenum: "inbound" | "outbound"optional—
statusquerystringoptional—
limitqueryintegeroptional—
offsetqueryintegeroptional—

Responses

  • 200TransmissionsSidesTransmissionsListResponse
    FieldTypeRequiredDescription
    transmissionsarray<SidesTransmission>required—
    totalintegerrequired—
  • 401UnauthorizedSidesUnauthorizedResponse
  • 403Forbidden — CPA access requiredSidesUnauthorizedResponse
  • 429Rate limit exceededSidesRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/sides/claimsList SIDES claims (legacy)

List SIDES claims (legacy /sides_separation_requests path). Filter by pending / auto_filled / in_review / overdue / submitted. CPA-class roles only on cookie sessions; Bearer keys gate via `notices.read`.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
filterqueryenum: "all" | "pending" | "auto_filled" | "in_review" | …optional—
limitqueryintegeroptional—
offsetqueryintegeroptional—
todayquerystringoptional—

Responses

  • 200ClaimsSidesClaimsListResponse
    FieldTypeRequiredDescription
    claimsarray<SidesClaimListItem>required—
    totalintegerrequired—
  • 400Unknown `filter` value, or a malformed / impossible `today` dateSidesUnauthorizedResponse
  • 401UnauthorizedSidesUnauthorizedResponse
  • 403Forbidden — CPA access requiredSidesUnauthorizedResponse
  • 429Rate limit exceededSidesRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/sides/claims/{id}Get SIDES claim (legacy)

SIDES claim detail (legacy path) with related transmissions + linked notice. CPA-class roles only on cookie sessions; Bearer keys gate via `notices.read`.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredSIDES exchange / claim UUID

Responses

  • 200Claim + transmissions + noticeSidesClaimDetailResponse
    FieldTypeRequiredDescription
    claimobjectrequired—
    transmissionsarray<SidesTransmission>required—
    transmissions_unavailable_reasonenum: "no_linked_notice"optional—
    noticeobjectrequired—
  • 401UnauthorizedSidesUnauthorizedResponse
  • 403Forbidden — CPA access requiredSidesUnauthorizedResponse
  • 404Claim not foundSidesNotFoundResponse
  • 429Rate limit exceededSidesRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/sides/claims/{id}/respondSave SIDES claim draft (legacy)

Save a draft response on the legacy claims path. The `approve` action is permanently blocked (410) — the legacy table lacks the FEIN/state-account/SSN data required for a valid SIDES response. Use the v2 sides_exchanges path instead. CPA-class roles only on cookie sessions; Bearer keys gate via `notices.write`. Cookie sessions go through the Stripe subscription gate.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredSIDES exchange / claim UUID

Request body SidesClaimRespondBodyrequired

FieldTypeRequiredDescription
actionenum: "save" | "approve"required—
separation_reasonstringoptional—
last_day_workedstringoptional—
final_wages_centsintegeroptional—
discharge_reasonstringoptional—
voluntary_quit_reasonstringoptional—
return_to_work_offeredbooleanoptional—
notesstringoptional—

Responses

  • 200Draft savedSidesClaimRespondResponse
    FieldTypeRequiredDescription
    successenum: truerequired—
    response_statusstringrequired—
    actionstringrequired—
  • 400Validation errorSidesValidationErrorResponse
  • 401UnauthorizedSidesUnauthorizedResponse
  • 402Subscription required (cookie-session callers only)SidesUnauthorizedResponse
  • 403Forbidden — CPA access requiredSidesUnauthorizedResponse
  • 404Claim not foundSidesNotFoundResponse
  • 410Approve is permanently blocked on the legacy pathSidesValidationErrorResponse
  • 429Rate limit exceededSidesRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/sides/claims/bulk/assignBulk assign SIDES separation requests

Assign up to 200 SIDES separation requests to one in-tenant user, or clear the assignee with `null`. The assignee is validated in-tenant first.

Bearer scopesnotices.write

Request body objectrequired

FieldTypeRequiredDescription
idsarray<string · uuid>requiredTarget ids, 1..200. Duplicates are collapsed before any write.
assigneestring · uuidrequired—

Responses

  • 200Per-id ledger. Partial success is a 200.BulkActionResult
    FieldTypeRequiredDescription
    appliedintegerrequiredIds that produced a real state change (and therefore exactly one audit row each).
    skippedarray<object>requiredIds deliberately not mutated. Present on a 200 — partial success is the contract, not an error.
    failedarray<object>requiredIds whose mutation raised. `probe_failed` means the tenant-scoped existence probe itself errored, so nothing was attempted.
  • 400Body failed validation (ids outside 1..200, or a bad uuid).
  • 403Authenticated but not CPA-class (`FORBIDDEN_ROLE`), or the Bearer key lacks the declared scope.
  • 500A tenant-scope leak was detected post-fetch (defense-in-depth third wall). No writes occurred.one of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/sides/claims/bulk/exportExport SIDES claims as CSV

Export up to 1000 SIDES claims by id. Employer FEINs are emitted masked (`***-NNNN`), never in full.

Bearer scopesnotices.read

Request body objectrequired

FieldTypeRequiredDescription
idsarray<string · uuid>requiredTarget ids, 1..1000. Over the cap the request is REFUSED, never silently truncated.

Responses

  • 200CSV attachment: `text/csv; charset=utf-8` with `Content-Disposition: attachment; filename="claims-export-<YYYY-MM-DD>.csv"`. Header row, in order: `claim_id`, `type`, `state`, `employer`, `employer_fein_masked`, `status`, `due`, `confidence`, `created`.string
  • 400Body failed validation — `ids` outside 1..1000, or a value that is not a UUID.
  • 403Authenticated but out of scope — a `client_user` asking outside its entity memberships, or a Bearer key that does not carry the declared scope.
  • 500The read failed, or a post-fetch tenant-scope leak was detected (defense-in-depth third wall). No partial file is written.one of (SanitizedServerError · AccountSetupIncompleteResponse)

State registrations1

POST/api/state-registrations/bulk/exportExport state registrations as CSV

Export up to 1000 state-registration requirements by id, tenant-scoped and entity-narrowed for a `client_user`.

Bearer scopesstate_registration.read

Request body objectrequired

FieldTypeRequiredDescription
idsarray<string · uuid>requiredTarget ids, 1..1000. Over the cap the request is REFUSED, never silently truncated.

Responses

  • 200CSV attachment: `text/csv; charset=utf-8` with `Content-Disposition: attachment; filename="state-registrations-export-<YYYY-MM-DD>.csv"`. Header row, in order: `state`, `requirement`, `status`, `source`, `effective_date`, `next_renewal`, `submitted_at`, `created`, `updated`.string
  • 400Body failed validation — `ids` outside 1..1000, or a value that is not a UUID.
  • 403Authenticated but out of scope — a `client_user` asking outside its entity memberships, or a Bearer key that does not carry the declared scope.
  • 500The read failed, or a post-fetch tenant-scope leak was detected (defense-in-depth third wall). No partial file is written.one of (SanitizedServerError · AccountSetupIncompleteResponse)

State Registrations2

POST/api/state-registrations/bulk/assignBulk assign state registrations to a firm user

Assign up to 200 state registrations to one in-tenant user, or clear the assignee with `null`. The assignee is validated in-tenant before any write.

Bearer scopesstate_registration.write

Request body objectrequired

FieldTypeRequiredDescription
idsarray<string · uuid>requiredTarget ids, 1..200. Duplicates are collapsed before any write.
assigneestring · uuidrequired—

Responses

  • 200Per-id ledger. Partial success is a 200.BulkActionResult
    FieldTypeRequiredDescription
    appliedintegerrequiredIds that produced a real state change (and therefore exactly one audit row each).
    skippedarray<object>requiredIds deliberately not mutated. Present on a 200 — partial success is the contract, not an error.
    failedarray<object>requiredIds whose mutation raised. `probe_failed` means the tenant-scoped existence probe itself errored, so nothing was attempted.
  • 400Body failed validation (ids outside 1..200, or a bad uuid).
  • 403Authenticated but not CPA-class (`FORBIDDEN_ROLE`), or the Bearer key lacks the declared scope.
  • 500A tenant-scope leak was detected post-fetch (defense-in-depth third wall). No writes occurred.one of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/state-registrations/bulk/transitionBulk transition state-registration status

Move up to 200 state registrations to `newStatus`. A registration already in that status is skipped with `no_change`; `reason` is required and is recorded on each audit row.

Bearer scopesstate_registration.write

Request body objectrequired

FieldTypeRequiredDescription
idsarray<string · uuid>requiredTarget ids, 1..200. Duplicates are collapsed before any write.
newStatusenum: "pending" | "prepared" | "submitted" | "approved" | …required—
reasonstringrequired—

Responses

  • 200Per-id ledger. Partial success is a 200.BulkActionResult
    FieldTypeRequiredDescription
    appliedintegerrequiredIds that produced a real state change (and therefore exactly one audit row each).
    skippedarray<object>requiredIds deliberately not mutated. Present on a 200 — partial success is the contract, not an error.
    failedarray<object>requiredIds whose mutation raised. `probe_failed` means the tenant-scoped existence probe itself errored, so nothing was attempted.
  • 400Body failed validation (ids outside 1..200, or a bad uuid).
  • 403Authenticated but not CPA-class (`FORBIDDEN_ROLE`), or the Bearer key lacks the declared scope.
  • 500A tenant-scope leak was detected post-fetch (defense-in-depth third wall). No writes occurred.one of (SanitizedServerError · AccountSetupIncompleteResponse)

States6

GET/api/statesList state agencies

List state agencies with tax rules for a given year (default 2026). Read-only Kreto reference data — no tenant scope.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
yearqueryintegeroptional—

Responses

  • 200StatesStatesListResponse
    FieldTypeRequiredDescription
    dataarray<StateAgency>required—
    countintegerrequired—
  • 401UnauthorizedMiscUnauthorizedResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/states/{code}Get state detail

Single state detail with tax rules, surcharges, notice types, protest rules, POA rules, and statutes. Read-only reference data.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
codepathstringrequired2-letter state code
yearqueryintegeroptional—

Responses

  • 200State detailStateDetailResponse
    FieldTypeRequiredDescription
    dataStateDetailrequired—
  • 401UnauthorizedMiscUnauthorizedResponse
  • 404State not foundMiscNotFoundResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/states/{code}/noticesList state notice types

List state notice types ranked by financial impact. Read-only reference data.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
codepathstringrequired2-letter state code

Responses

  • 200Notice typesStateNoticeTypesResponse
    FieldTypeRequiredDescription
    state_codestringrequired—
    state_namestringrequired—
    dataarray<StateNoticeType>required—
    countintegerrequired—
  • 401UnauthorizedMiscUnauthorizedResponse
  • 404State not foundMiscNotFoundResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/states/{code}/poaList state POA rules

List POA rules for a state. Read-only reference data.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
codepathstringrequired2-letter state code

Responses

  • 200POA rulesStatePoaResponse
    FieldTypeRequiredDescription
    state_codestringrequired—
    state_namestringrequired—
    dataarray<StatePoaRule>required—
    countintegerrequired—
  • 401UnauthorizedMiscUnauthorizedResponse
  • 404State not foundMiscNotFoundResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/states/compareCompare states

Compare up to 5 states side-by-side (full ruleset for each). Read-only reference data.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
statesquerystringrequired—
yearqueryintegeroptional—

Responses

  • 200Comparison dataStateComparisonResponse
    FieldTypeRequiredDescription
    dataarray<StateComparison>required—
    countintegerrequired—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/states/withholdingWithholding reciprocity

Withholding reciprocity calculator. Without query params, returns the full reciprocity map. With work_state + home_state, returns the specific reciprocity decision plus each state's full reciprocity list.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
work_statequerystringoptional—
home_statequerystringoptional—

Responses

  • 200Reciprocity dataStateWithholdingResponse
    FieldTypeRequiredDescription
    reciprocityAgreementsobjectoptional—
    totalStatesWithReciprocityintegeroptional—
    resultobjectoptional—
    workStateReciprocityarray<string>optional—
    homeStateReciprocityarray<string>optional—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)

TaxDocuments10

GET/api/tax-documents/{id}/items/{itemId}/downloadDownload a delivered document

The file a client delivered against this checklist item. Without `format`, answers this door's own `?format=file` address (never a storage URL); with `format=file`, streams the bytes of the item's own published `uploaded_file` after the same walls. `portal_uploads` is joined only for filename / size / mime. Cookie session only (no Bearer) and CPA-tier.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredTax document request UUID
itemIdpathstring · uuidrequiredTax document item UUID
formatqueryenum: "file"optional—

Responses

  • 200The download address (no format), or the file bytes (format=file)TaxDocumentItemDownloadResponse
    FieldTypeRequiredDescription
    urlstringrequired—
    file_namestringrequired—
    file_size_bytesintegerrequired—
    mime_typestringrequired—
    delivered_atstringrequired—
  • 400Invalid id formatMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Forbidden — requires CPA-level accessMiscUnauthorizedResponse
  • 404Request or item not foundMiscNotFoundResponse
  • 409No file has been delivered for this item yetMiscValidationErrorResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/tax-documentsList tax document requests

List tax document requests for caller's tenant. Bearer keys gate via `notices.read`.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
entity_idquerystring · uuidoptional—
statusquerystringoptional—
tax_yearqueryintegeroptional—

Responses

  • 200RequestsTaxDocumentRequestsListResponse
    FieldTypeRequiredDescription
    successenum: truerequired—
    requestsarray<TaxDocumentRequest>required—
    countintegerrequired—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/tax-documentsCreate tax document request

Create a tax document collection request with optional AI checklist generation (KRT-322). Cookie sessions go through rate-limit + idempotency middleware. CPA-class roles only on cookie sessions; Bearer keys gate via `notices.write`.

Bearer scopesnotices.write

Request body TaxDocumentRequestCreateBodyrequired

FieldTypeRequiredDescription
entity_idstring · uuidrequired—
client_idstring · uuidoptional—
tax_yearintegerrequired—
tax_typeenum: "personal" | "business"required—
entity_typeenum: "individual" | "s_corp" | "c_corp" | "partnership" | …optional—
notice_idstring · uuidoptional—
due_datestringoptional—
generate_checklistbooleanoptional—
prior_year_itemsarray<string>optional—

Responses

  • 201Request createdTaxDocumentRequestCreateResponse
    FieldTypeRequiredDescription
    successenum: truerequired—
    requestTaxDocumentRequestrequired—
    items_countintegerrequired—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Forbidden — CPA access requiredMiscUnauthorizedResponse
  • 404Entity not found in caller's tenantMiscNotFoundResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerErrorWithDetails · AccountSetupIncompleteResponse)
GET/api/tax-documents/{id}Get tax document request

Get tax document request detail with all items.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredTax document request UUID

Responses

  • 200Request + itemsTaxDocumentRequestDetailResponse
    FieldTypeRequiredDescription
    successenum: truerequired—
    requestTaxDocumentRequestrequired—
    itemsarray<TaxDocumentRequestItem>required—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 404Request not foundMiscNotFoundResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
PATCH/api/tax-documents/{id}/items/{itemId}Update tax document item

Update a tax document request item. CPA-class can update label / description / required; client_user / client can update status / uploaded_file / notes. Auto-completes the parent request when all required items reach verified or waived. Bearer keys gate via `notices.write` (treated as CPA-class).

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredTax document request UUID
itemIdpathstring · uuidrequiredTax document item UUID

Request body TaxDocumentItemUpdateBodyrequired

FieldTypeRequiredDescription
statusenum: "pending" | "uploaded" | "verified" | "rejected" | …optional—
uploaded_filestringoptionalStorage path of a portal upload for THIS item. Must match `tenant/<your tenant_id>/portal/<request_id>/<item_id>/<filename>` exactly — the path the client-portal upload writes; any other value is rejected with 400. The ids are the lowercase UUIDs of the addressed request/item and your own tenant, and `<filename>` is the sanitized basename the portal stores: characters `A-Za-z0-9._-` only, no `..`, whole path ≤1024 chars. The portal upload route is the natural writer — set this only to re-point the item at a file already delivered for it. The field is set-only: it cannot be cleared or nulled via PATCH.
notesstringoptional—
labelstringoptional—
descriptionstringoptional—
requiredbooleanoptional—

Responses

  • 200Item updatedTaxDocumentItemUpdateResponse
    FieldTypeRequiredDescription
    successenum: truerequired—
    itemTaxDocumentRequestItemrequired—
  • 400Validation error / expired requestMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 404Request or item not foundMiscNotFoundResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerErrorWithDetails · AccountSetupIncompleteResponse)
DELETE/api/tax-documents/{id}/portal-tokenRevoke a document request's upload link

Revoke the outstanding client upload (portal) link for a tax document request. The emailed link stops resolving immediately. CPA-class roles only on cookie sessions; Bearer keys gate via `notices.write`. 409 when there is no link to revoke or it was already revoked.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredTax document request UUID

Responses

  • 200Upload link revokedTaxDocumentPortalTokenRevokeResponse
    FieldTypeRequiredDescription
    successenum: truerequired—
    revoked_atstring · date-timerequired—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 404Request not foundMiscNotFoundResponse
  • 409No outstanding link, or already revokedMiscValidationErrorResponse
  • 429Rate limitedMiscRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/tax-documents/{id}/remindersList tax document reminders

List reminders sent for a tax document request + current reminder settings (paused state, custom deadline, due date). Bearer keys gate via `notices.read`.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredTax document request UUID

Responses

  • 200Reminders + settingsTaxDocumentRemindersListResponse
    FieldTypeRequiredDescription
    successenum: truerequired—
    remindersarray<TaxDocumentReminder>required—
    settingsobjectrequired—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 404Request not foundMiscNotFoundResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
PATCH/api/tax-documents/{id}/remindersUpdate tax document reminder settings

Update reminder settings (pause/resume, set custom deadline). CPA-class roles only on cookie sessions; Bearer keys gate via `notices.write`.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredTax document request UUID

Request body TaxDocumentRemindersUpdateBodyrequired

FieldTypeRequiredDescription
reminders_pausedbooleanoptional—
custom_deadlinestringoptional—

Responses

  • 200Settings updatedTaxDocumentRemindersUpdateResponse
    FieldTypeRequiredDescription
    successenum: truerequired—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Forbidden — CPA access requiredMiscUnauthorizedResponse
  • 404Request not foundMiscNotFoundResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/tax-documents/{id}/reminders/sendSend manual reminder

Send a manual reminder for a specific tax document request (KRT-324). CPA-class roles only on cookie sessions; Bearer keys gate via `notices.write`. Refused for completed/expired requests.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredTax document request UUID

Responses

  • 200Reminder sent (or skipped with reason)TaxDocumentManualReminderResponse
    FieldTypeRequiredDescription
    successbooleanoptional—
    reminder_idstring · uuidrequired—
    pending_items_countintegerrequired—
    skippedbooleanoptional—
    skip_reasonstringoptional—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Forbidden — CPA access requiredMiscUnauthorizedResponse
  • 404Request not foundMiscNotFoundResponse
  • 422Cannot send reminder for completed/expired requestMiscValidationErrorResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerErrorWithCode · AccountSetupIncompleteResponse)
POST/api/tax-documents/reminders/bulkBulk-send tax document reminders

Send reminders for the SELECTED documents. Each document id is resolved to its request_id (tenant-scoped) and each distinct request is reminded and audited independently (one audit row per request). Terminal (complete/expired) requests are skipped. CPA-class roles only on cookie sessions; Bearer keys gate via `notices.write`.

Bearer scopesnotices.write

Request body TaxDocumentBulkRemindersRequest

FieldTypeRequiredDescription
idsarray<string · uuid>requiredSelected document ids (1..200)

Responses

  • 200Bulk send result (applied / skipped / failed)TaxDocumentBulkRemindersResponse
    FieldTypeRequiredDescription
    appliedintegerrequired—
    skippedarray<object>required—
    failedarray<object>required—
  • 401UnauthorizedMiscUnauthorizedResponse
  • 403Forbidden — CPA access requiredMiscUnauthorizedResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)

TaxLiens2

GET/api/tax-liensList tax liens

List tax liens for the caller's tenant. Filters: entity_id, status, lien_type. Bearer keys gate via `notices.read`.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
entity_idquerystring · uuidoptional—
statusquerystringoptional—
lien_typequerystringoptional—
limitqueryintegeroptional—
offsetqueryintegeroptional—

Responses

  • 200Tax liensTaxLiensListResponse
    FieldTypeRequiredDescription
    liensarray<TaxLien>required—
    totalintegerrequired—
  • 401UnauthorizedMiscUnauthorizedResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/tax-liensCreate tax lien

Create a tax lien record. Bearer keys gate via `notices.write`.

Bearer scopesnotices.write

Request body TaxLienCreateBodyrequired

FieldTypeRequiredDescription
entity_idstring · uuidrequired—
lien_amountnumberrequired—
notice_idstring · uuidoptional—
lien_typestringoptional—
lien_numberstringoptional—
filing_datestringoptional—
filing_jurisdictionstringoptional—
tax_periodsarray<string>optional—
tax_typestringoptional—
response_deadlinestringoptional—
release_conditionsstringoptional—
agencystringoptional—

Responses

  • 201Tax lien createdTaxLienCreateResponse
    FieldTypeRequiredDescription
    lienTaxLienrequired—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 404Entity not found in caller's tenantMiscNotFoundResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)

Team3

DELETE/api/team/members/{userId}Remove a member from the firm

Firm admins only (cookie session; Bearer keys are refused). ONE database transaction (`remove_team_member`) locks the firm's admins, stamps `user_profiles.deleted_at` (the database auth helpers then refuse any token the member still holds), revokes every API key the member created in this tenant and writes one `team_member_removed` audit event; THEN the member is signed out everywhere (ban + credential rotation). If that sign-out fails the member is still removed and `ban_pending` is true. Calling DELETE again on that already-removed member is the retry: it re-runs ONLY the sign-out and answers 200 (`api_keys_revoked` 0) — unless a pending re-invitation for the member is live, which reads the 404. A foreign-tenant or unknown id reads the same 404. Refusals: 409 `cannot_remove_self`, 409 `last_admin` (the firm's only active firm admin), 409 `removal_conflict` (removed concurrently), 403 `target_role_not_mutable` (a platform system admin).

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
userIdpathstring · uuidrequired—

Responses

  • 200Member removedRemoveTeamMemberResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    user_idstring · uuidrequired—
    removed_atstring · date-timerequired—
    api_keys_revokedintegerrequired—
    ban_pendingbooleanrequired—
  • 401No sessionTeamMemberError
  • 403Caller is not a firm admin, or the target is a platform system adminTeamMemberError
  • 404Member not found in your firm (existence-opaque)TeamMemberError
  • 409cannot_remove_self | last_admin | removal_conflict — `error` is the sentence to showTeamMemberError
  • 500Server error — sanitized (`SanitizedServerError`), or a signed-in session with no profile gets `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/team/invitationsInvite a teammate to the firm

Cookie session only (Bearer keys are refused). Firm admins, or the tenant's founding member, invite an address as `cpa_member` or `staff`. The accept link is NEVER returned (owner ruling 97(a)) — the e-mail is the only channel. Re-inviting a member this firm removed lifts their sign-in ban last; if that fails the row exists and the answer is the named 502 carrying its id (resend it to retry).

Cookie session or Bearer token

Request body CpaTeamInvitationCreateRequestrequired

FieldTypeRequiredDescription
emailstring · emailrequired—
invited_roleenum: "cpa_member" | "staff"required—

Responses

  • 201Invitation created (and, unless `email_sent` is false, mailed)CpaTeamInvitationCreateResponse
    FieldTypeRequiredDescription
    invitationobjectrequired—
    email_sentbooleanrequired—
    email_failure_reasonenum: "suppressed_non_prod" | "rate_limited" | "rejected" | "unavailable" | …optional—
  • 400Invalid JSON, invalid email, a role other than cpa_member | staff, or the caller's own emailTeamMemberError
  • 401No sessionTeamMemberError
  • 403Not a firm admin or the founding member, not a CPA-tier caller, or not a sessionTeamMemberError
  • 409already_member (the email already has firm access) | duplicate_pending (a live invitation exists)TeamMemberError
  • 500Server error — sanitized (`SanitizedServerError`), or a signed-in session with no profile gets `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
  • 502invite_unban_failed — the invitation exists but the removed member's sign-in could not be restored; no e-mail was sent. Resend it to retry.CpaTeamInvitationUnbanFailed
  • 503invite_schedule_failed — the re-invitation's expiry could not be queued. Nothing was written; retry.CpaTeamInvitationScheduleFailed
POST/api/team/invitations/{id}Resend a pending invitation

Firm admins only (cookie session; Bearer keys are refused). Mints a NEW accept token — the previously mailed link stops working — and re-sends the e-mail. No URL or token is returned. Capped per client IP (429). A foreign-tenant or unknown id reads the same 404; an expired, accepted or revoked invitation is a 409. For a removed member the resend re-attempts the sign-in unban first; if it fails again the link is unchanged and the answer is the create door's named 502.

Cookie session or Bearer token

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequired—

Responses

  • 200Invitation re-sent with a fresh token and windowCpaTeamInvitationResendResponse
    FieldTypeRequiredDescription
    idstringrequired—
    statusstringrequired—
    expires_atstringrequired—
    email_sentbooleanrequired—
    email_failure_reasonenum: "suppressed_non_prod" | "rate_limited" | "rejected" | "unavailable" | …optional—
  • 400The id is not a uuidTeamMemberError
  • 401No sessionTeamMemberError
  • 403Caller is not a firm admin, or not a sessionTeamMemberError
  • 404Invitation not found in your firm (existence-opaque)TeamMemberError
  • 409The invitation can no longer be resentTeamMemberError
  • 429Too many resends from this client; retry after a minuteTeamMemberError
  • 500Server error — sanitized (`SanitizedServerError`), or a signed-in session with no profile gets `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
  • 502invite_unban_failed — the removed member's sign-in still could not be restored; the link is unchanged and no e-mail was sent.CpaTeamInvitationUnbanFailed

WageHour2

GET/api/wage-hourList W&H investigations

List Wage & Hour investigations (compliance category #5 — DOL). Bearer keys gate via `notices.read`.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
entity_idquerystring · uuidoptional—
statusquerystringoptional—
investigation_typequerystringoptional—
limitqueryintegeroptional—
offsetqueryintegeroptional—

Responses

  • 200InvestigationsWageHourListResponse
    FieldTypeRequiredDescription
    investigationsarray<WageHourInvestigation>required—
    totalintegerrequired—
  • 401UnauthorizedMiscUnauthorizedResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/wage-hourCreate W&H investigation

Create a Wage & Hour investigation record. Bearer keys gate via `notices.write`.

Bearer scopesnotices.write

Request body WageHourCreateBodyrequired

FieldTypeRequiredDescription
entity_idstring · uuidrequired—
investigation_typestringrequired—
notice_idstring · uuidoptional—
investigation_numberstringoptional—
complaint_typestringoptional—
investigation_scopestringoptional—
back_wages_owednumberoptional—
penalty_amountnumberoptional—
workers_affectedintegeroptional—
classification_issuestringoptional—
misclassified_countintegeroptional—
overtime_hours_owednumberoptional—
overtime_ratenumberoptional—
notice_datestringoptional—
response_deadlinestringoptional—
hearing_datestringoptional—
investigator_namestringoptional—
agencystringoptional—

Responses

  • 201Investigation createdWageHourCreateResponse
    FieldTypeRequiredDescription
    investigationWageHourInvestigationrequired—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 404Entity not found in caller's tenantMiscNotFoundResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)

WorkersComp6

GET/api/workers-compList W/C policies

List workers' comp policies for the caller's tenant (compliance category #3). Bearer keys gate via `notices.read`.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
entity_idquerystring · uuidoptional—
statusquerystringoptional—
limitqueryintegeroptional—
offsetqueryintegeroptional—

Responses

  • 200PoliciesWorkersCompPoliciesListResponse
    FieldTypeRequiredDescription
    policiesarray<WorkersCompPolicy>required—
    totalintegerrequired—
  • 401UnauthorizedMiscUnauthorizedResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/workers-compCreate W/C policy

Create a workers' comp policy. Bearer keys gate via `notices.write`.

Bearer scopesnotices.write

Request body WorkersCompPolicyCreateBodyrequired

FieldTypeRequiredDescription
entity_idstring · uuidrequired—
policy_numberstringrequired—
carrierstringrequired—
statestringrequired—
e_modnumberoptional—
annual_premiumnumberoptional—
statusstringoptional—
effective_datestringoptional—
expiration_datestringoptional—

Responses

  • 201Policy createdWorkersCompPolicyCreateResponse
    FieldTypeRequiredDescription
    policyWorkersCompPolicyrequired—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 404Entity not found in caller's tenantMiscNotFoundResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/workers-comp/auditsList W/C audits

List workers' comp audits for the caller's tenant. Each row carries an inlined dispute recommendation when both original_premium and audited_premium are present. Bearer keys gate via `notices.read`.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
entity_idquerystring · uuidoptional—
statusquerystringoptional—
limitqueryintegeroptional—
offsetqueryintegeroptional—

Responses

  • 200AuditsWorkersCompAuditsListResponse
    FieldTypeRequiredDescription
    auditsarray<WorkersCompAudit>required—
    totalintegerrequired—
  • 401UnauthorizedMiscUnauthorizedResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/workers-comp/auditsCreate W/C audit

Create a workers' comp audit record. Bearer keys gate via `notices.write`. Cookie sessions go through the Stripe subscription gate; Bearer keys bypass.

Bearer scopesnotices.write

Request body WorkersCompAuditCreateBodyrequired

FieldTypeRequiredDescription
entity_idstring · uuidrequired—
policy_idstring · uuidoptional—
notice_idstring · uuidoptional—
audit_typestringoptional—
period_startstringoptional—
period_endstringoptional—
original_premiumnumberoptional—
audited_premiumnumberoptional—
deadlinestringoptional—

Responses

  • 201Audit createdWorkersCompAuditCreateResponse
    FieldTypeRequiredDescription
    auditWorkersCompAuditrequired—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 402Subscription required (cookie-session callers only)MiscUnauthorizedResponse
  • 404Entity not found in caller's tenantMiscNotFoundResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/workers-comp/calculate-emodCalculate W/C e-mod

Stateless e-mod (experience modifier) calculator for workers' comp. No tenant data is read or written. Bearer keys gate via `notices.read`.

Bearer scopesnotices.read

Request body WorkersCompEmodBodyrequired

FieldTypeRequiredDescription
actualLossesnumberrequired—
expectedLossesnumberrequired—
credibilityWeightnumberoptional—
ballastValuenumberoptional—

Responses

  • 200e-mod calculation, or the named refusal (`resultState: "pending_data"` + `missing_inputs`) when expected losses ≤ 0 or the credibility weight / ballast is not sentWorkersCompEmodResponse
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/workers-comp/calculate-premiumCalculate W/C premium

Stateless workers' comp premium calculator. Bearer keys gate via `notices.read`.

Bearer scopesnotices.read

Request body WorkersCompPremiumBodyrequired

FieldTypeRequiredDescription
classCodesarray<WorkersCompPremiumClassCode>required—
eModnumberoptional—

Responses

  • 200Premium calculation, or the named refusal (`missing_inputs: ["experience_mod"]`) when no e-mod is sentWorkersCompPremiumResponse
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server error — a route-level 5xx leaves sanitized (`SanitizedServerError`: the generic message + `requestId?`), or a signed-in session with no profile gets the auth wrapper's `AccountSetupIncompleteResponse`one of (SanitizedServerError · AccountSetupIncompleteResponse)

Workflows6

GET/api/workflowsList workflow rules

List workflow rules for the caller's tenant.

Bearer scopesnotices.read

Responses

  • 200RulesWorkflowRulesListResponse
    FieldTypeRequiredDescription
    rulesarray<WorkflowRule>required—
  • 401UnauthorizedMiscUnauthorizedResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
POST/api/workflowsCreate workflow rule

Create a workflow rule. Trigger must be one of `notice.created`, `notice.deadline_approaching`, `notice.status_changed`, or `document.uploaded`.

Bearer scopesnotices.write

Request body WorkflowRuleCreateBodyrequired

FieldTypeRequiredDescription
namestringrequired—
triggerenum: "notice.created" | "notice.deadline_approaching" | "notice.status_changed" | "document.uploaded"required—
conditionsarray<WorkflowCondition>optional—
actionsarray<WorkflowAction>optional—

Responses

  • 201Rule createdWorkflowRuleResponse
    FieldTypeRequiredDescription
    ruleWorkflowRulerequired—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
PATCH/api/workflows/{id}Update workflow rule

Update a workflow rule (toggle is_active, edit fields).

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredWorkflow rule UUID

Request body WorkflowRuleUpdateBodyrequired

FieldTypeRequiredDescription
namestringoptional—
triggerenum: "notice.created" | "notice.deadline_approaching" | "notice.status_changed" | "document.uploaded"optional—
conditionsarray<WorkflowCondition>optional—
actionsarray<WorkflowAction>optional—
is_activebooleanoptional—

Responses

  • 200Rule updatedWorkflowRuleResponse
    FieldTypeRequiredDescription
    ruleWorkflowRulerequired—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 404Rule not foundMiscNotFoundResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
DELETE/api/workflows/{id}Delete workflow rule

Delete a workflow rule.

Bearer scopesnotices.write

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequiredWorkflow rule UUID

Responses

  • 200Rule deletedobject
    FieldTypeRequiredDescription
    successenum: truerequired—
  • 400Validation errorMiscValidationErrorResponse
  • 401UnauthorizedMiscUnauthorizedResponse
  • 404Rule not foundMiscNotFoundResponse
  • 429Rate limit exceededMiscRateLimitResponse
  • 500Server errorone of (SanitizedServerError · AccountSetupIncompleteResponse)
GET/api/packetsList audit packs

Every response packet in the caller's tenant, newest first, each stamped with its seal state and document count. A `client_user` is narrowed to packs whose parent notice belongs to one of its entities.

Bearer scopesnotices.read

Responses

  • 200The tenant's audit packs.PacketListResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    packetsarray<PacketListItem>required—
  • 403Authenticated but the Bearer key does not carry the declared scope.
  • 500The read failed, or a post-fetch tenant-scope leak was detected (defense-in-depth third wall). The failure body repeats `packets: []` so a client cannot mistake it for an empty tenant without reading `error`.one of (PacketsListServerError · AccountSetupIncompleteResponse)
GET/api/packets/{id}/verifyVerify an audit pack's seal

Replay the pack's manifest and report the seal state. Structural by default; `?deep=1` additionally re-downloads every document and re-hashes its bytes. Every verification is recorded as one `packet_verification_run` audit event. An unknown pack, a pack in another tenant and a pack outside a `client_user`'s entities all answer the SAME 404 — the refusal is not an existence oracle.

Bearer scopesnotices.read

Parameters

NameInTypeRequiredDescription
idpathstring · uuidrequired—
deepquerystringoptional`1` runs the byte-level re-hash pass. Any other value (or absent) is structural-only, and `content_ok` comes back null.

Responses

  • 200Seal verification result.PacketVerificationResponse
    FieldTypeRequiredDescription
    okenum: truerequired—
    sealedbooleanrequiredTrue once the pack has a sealed root (`sealed_at` AND `content_hash` set).
    hash_versionintegerrequired—
    content_hashstringrequired—
    structural_okbooleanrequiredTrue when the replayed manifest root matches the sealed `content_hash`. FALSE also when the pack is unsealed — an unsealed pack has nothing to attest.
    content_okbooleanrequiredDeep byte pass: NULL when `?deep=1` was not requested, true when every non-placeholder document re-hashes to its leaf, false when at least one drifted.
    first_bad_docstringrequiredId of the FIRST `packet_documents` row that failed the deep byte check, or null when none failed / deep was not run.
    verdictenum: "verified" | "tampered" | "unsealed"requiredThe verdict in one word, derived from the fields above by the same function the product panel renders.
    verified_atstringrequiredISO-8601 instant the verdict was reached.
    buildstringrequiredThe commit the server was running when it reached the verdict — the value `GET /api/health` reports as `commit` (`unknown` when the server has no build sha).
    verdict_event_idstringrequiredId of the `audit_events` row (`packet_verification_run`) this verification was recorded as, or null when that write failed.
  • 403Authenticated but the Bearer key does not carry the declared scope.
  • 404Pack not found, in another tenant, or out of a `client_user`'s scope. One body for all three inputs.
  • 500Verification raised, or a post-fetch tenant-scope leak was detected on the parent-notice read.one of (SanitizedServerError · AccountSetupIncompleteResponse)

Build on the same spec.

Create a key scoped to your firm, or talk through an integration with us.