Download OpenAPI specification
openapi: 3.1.0
x-computer-health-contract: /api/v1/computers/openapi.json
info:
title: CoverCore Platform API
version: 1.0.0
description: |
Public partner API for the CoverCore (Pulse) insurance operating system.
This spec covers the StudioGuard launch lifecycle plus the supporting
foundation (auth, jobs, webhooks). Lifecycle order:
**Score → Evaluate → Rate → Quote → Bind → BRDX → Commission**
For non-lifecycle endpoints (contacts, opportunities, campaigns, etc.)
see the human-readable reference at `/api/v1/docs`.
Authentication is by partner API key. Sandbox keys (`pk_test_*`) read
from a deterministic synthetic dataset; live keys (`pk_live_*`) read
production data. See the sandbox quickstart in the reference doc for
a curl-only walkthrough that takes a partner from key issuance to a
bound policy in 8 commands.
API credentials are disclosed exactly once, in the successful creation
response. Existing credentials cannot be recovered from CoverCore or any
metadata endpoint; create a replacement if the one-time value was not
stored.
**Agent credential contract.** Keys issued from an agent's CoverCore
session are immutable, agent-scoped credentials. Their closed scope set is
`policies:read`, `policies:write`, `quotes:read`, `quotes:write`, `contacts:read`,
`documents:read`, `documents:write`, `search:read`, `commission:read`,
`commission:write`, `service_requests:read`, `service_requests:write`, and
`webhooks:manage`. The server also enforces a closed route/method matrix, so
an old key containing a broader scope still cannot reach unsupported
endpoints.
Agent access is limited to records carrying direct evidence for the
credential's immutable CoverCore user/contact/FileMaker identity. Agency
membership never expands this authority to a sibling agent. Missing,
ambiguous, or mismatched linkage fails closed. Webhook subscriptions and
delivery logs belong to one API key, and tenant-bearing events are
delivered only when ownership can be proven for that key.
**Scopes** are grouped into thirteen categories — the same categories,
order, and scope names shown in the API Keys editor
(Settings → Developer → API Keys → Edit), the developer landing page,
and the reference doc. `/health` is public. `/me` requires authentication.
`/jobs/:id` is unavailable to agent-scoped keys unless it is added to the
closed agent capability matrix in a future API version.
1. **Core Insurance**: `policies:read`, `policies:write`, `quotes:read`, `quotes:write`, `claims:read`, `claims:write`, `carriers:read`, `agencies:read`, `agencies:read_pii`, `agencies:write`, `licenses:read`, `documents:read`, `documents:write`, `prevention:read`, `trim:read`, `trim:write`
2. **Core Warranty**: `warranty:rate`, `warranty:lookup`, `warranty:quote`, `warranty:test`, `warranty:audit`
3. **Sales & Growth**: `contacts:read`, `contacts:write`, `opportunities:read`, `opportunities:write`, `campaigns:read`, `campaigns:write`, `thumbtack:read`, `thumbtack:write`, `search:read`, `hubspot:read`, `hubspot:sync`
4. **Intelligence**: `analytics:read`, `analytics:write`, `social_intel:read`, `social_intel:write`, `social_queue:read`, `social_queue:write`, `publisher:read`, `publisher:write`, `enrichment:read`, `intel_feed:read`, `intel_feed:write`
5. **Operations**: `emails:read`, `emails:write`, `email_inbox:read`, `email_inbox:write`, `interactions:read`, `interactions:write`, `utm:read`, `utm:write`, `tasks:read`, `tasks:write`, `service_requests:read`, `service_requests:write`, `agents:read`, `partnerships:read`, `chat_escalations:read`, `chat_escalations:write`, `email_stats:read`, `staff:read`
6. **Compliance & Finance**: `filings:read`, `filings:write`, `billing:read`, `billing:write`, `webhooks:manage`, `zoe:read`
7. **AI Learning**: `corrections:read`, `corrections:write`
8. **AI Goals**: `ai_goals:read`, `ai_goals:write`, `ai_goals:checkins:read`, `ai_goals:checkins:write`, `ai_goals:actions:read`, `ai_goals:actions:write`, `ai_goals:evidence:read`, `ai_goals:evidence:write`, `ai_goals:results:read`, `ai_goals:results:write`, `ai_goals:admin`
9. **Internal Staff (HR)**: `internal_staff:read`, `internal_staff:write`, `internal_staff:milestones:read`, `internal_staff:milestones:write`, `internal_staff:import`, `internal_staff:audit:read`, `internal_staff:reviews:read`, `internal_staff:reviews:write`, `internal_staff:reviews:admin`
10. **StudioGuard Lifecycle**: `score:creator`, `score:weather`, `score:crime`, `score:auto-fatality`, `uw:read`, `uw:evaluate`, `rate:mpl`, `rate:property`, `rate:gl`, `rate:cyber`, `rate:trimpay`, `quote:write`, `quote:read`, `bind:write`, `brdx:read`, `commission:read`, `commission:write`
11. **Service Intelligence**: `service_enablement:read`, `service_enablement:report`, `gamification:read`, `achievements:read`
12. **Policy Forms**: `policy_forms:read`, `policy_forms:draft_write`, `policy_forms:validate`, `policy_forms:preview`, `policy_forms:authoritative_pdf_read`, `policy_forms:authoritative_pdf_write`, `policy_issuance:read`, `policy_issuance:proof_read` — _All restricted to first-party principals (partnerType admin/internal/internal_system/ai/integration). External book-of-business keys receive 403 regardless of scope grants. Authoritative PDF upload/replace requires fresh four-step review and grants no approval._
13. **Service Requests**: `plays:read`, `plays:start`, `play_runs:write`
contact:
name: Pulse Engineering
email: dwight@stuckey.com
license:
name: Proprietary
servers:
- url: https://covercore.services/api/v1
description: Production
- url: http://localhost:5000/api/v1
description: Local dev
security:
- BearerAuth: []
- ApiKeyAuth: []
tags:
- name: Thumbtack
description: Owner-scoped provider search and staff lead pipeline. No booking or contact submissions.
- name: Foundation
description: Auth, key info, async jobs, health
- name: Score
description: Risk scoring (creator, weather, crime, auto-fatality)
- name: Underwriting
description: Underwriting evaluation (gates rating)
- name: Rate
description: Per-line rating (MPL, Property, GL, Cyber, TRiMPay)
- name: Quote
description: Quote assembly (combines rated lines into one quote)
- name: External Quote Imports
description: Internal carrier-pipeline synchronization; never bind or issuance authority
- name: Bind
description: Bind a quote into a policy + collect payment
- name: BRDX
description: Bordereau reporting (written / paid + Accelerant export)
- name: Commission
description: Public Pay Commission API (ledger, wallet, withdrawals)
- name: Webhooks
description: Webhook subscriptions and delivery logs
- name: Service Requests
description: Ticket-attached service plays and play runs
- name: Chat Escalations
description: Owner-bound staff handling of live chat escalations
- name: Policyholder
description: Governed agent account intake plus policyholder self-service
- name: Credential Self-Service
description: Browser-session routes for managing the signed-in user's own API keys
paths:
/quotes/{applicationId}/head-start:
post:
operationId: startAgentHeadStart
tags: [Quotes]
summary: Start or read Head Start for an owned application
description: New agent API access only. Requires quotes:write, an explicit per-agent staff grant, and verified direct agent ownership of a saved StudioGuard v18 application. Reuses existing research and provider reservations. Send no body; inputs come only from saved confirmed business facts. Repeat POST reads the existing run without starting duplicate paid work. Existing in-app Head Start is unchanged.
parameters:
- name: applicationId
in: path
required: true
schema: { type: string, format: uuid }
responses:
'200': { description: Existing completed research in the standard applicant Head Start projection }
'202': { description: Research started or still running in the standard applicant Head Start projection }
'400': { description: Invalid identifier or caller-supplied inputs }
'401': { description: Missing or invalid API key }
'403': { description: Scope, agent identity or explicit grant required }
'404': { description: Owned saved application not found or Head Start disabled }
'422': { description: Confirm saved business details before research }
'429': { description: Key rate limit exceeded }
'503': { description: Head Start unavailable }
/quotes/prefill-draft:
post:
operationId: createAgentPrefillDraft
tags: [Quotes]
summary: Save an unsent agent prefill draft
description: Requires an agent-scoped key with quotes:write and a verified direct book identity. Saves only an unsent prefill record; no opportunity, email, rating or payment. Agent attribution is server-owned. Normal non-idempotent POST semantics apply.
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
required: [clientEmail, config]
properties:
clientEmail: { type: string, format: email, maxLength: 254 }
clientName: { type: string, maxLength: 255 }
clientPhone: { type: string, maxLength: 255 }
clientHandle: { type: string, maxLength: 255 }
agentNote: { type: string, maxLength: 2000 }
config: { type: object, description: "Draft configuration only, with no agent or agency attribution keys. Maximum serialized size 32000 characters." }
responses:
'201':
description: Unsent record saved; token and agent details are not returned.
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
id: { type: string }
status: { type: string, enum: [unsent] }
expiresAt: { type: string, format: date-time }
'400': { description: Invalid input or caller-supplied attribution }
'401': { description: Missing or invalid key }
'403': { description: Agent identity or quotes:write required }
'429': { description: Existing key rate limit exceeded }
'500': { description: Draft could not be saved }
/my/billing:
get:
tags: [Policyholder]
summary: List your billing records
description: Only explicitly owned billing links are used; unclear, unlinked and conflicting rows are excluded. No internal notes or raw provider fields are returned. New scope must be granted explicitly.
x-required-scopes: [my_billing:read]
parameters:
- $ref: "#/components/parameters/PolicyholderPage"
- $ref: "#/components/parameters/PolicyholderLimit"
responses:
"200": { description: "data contains id, activityType, activityDate, dueDate, minimumDue, accountBalance, amountPaid and carrier; pagination contains page, limit, total and totalPages." }
"400": { description: Invalid pagination or unsupported query parameters }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
/my/tickets:
get:
tags: [Policyholder]
summary: List your support tickets
description: Explicit policyholder linkage is required. Returns only id, subject, status and createdAt, never staff notes, assignments or internal fields.
x-required-scopes: [my_tickets:read]
parameters:
- $ref: "#/components/parameters/PolicyholderPage"
- $ref: "#/components/parameters/PolicyholderLimit"
responses:
"200": { description: "data is the ticket list; pagination contains page, limit, total and totalPages." }
"400": { description: Invalid pagination or unsupported query parameters }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
post:
tags: [Policyholder]
summary: Open a support ticket for yourself
description: Ownership is derived from the authenticated key, never the body. The new write scope must be granted explicitly. Creates an open support request without sending a message, assigning a staff member or changing a policy.
x-required-scopes: [my_tickets:write]
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
required: [subject, description]
properties:
subject: { type: string, minLength: 1, maxLength: 200 }
description: { type: string, minLength: 1, maxLength: 10000 }
responses:
"201": { description: "data contains the created ticket's id, subject, status and createdAt." }
"400": { description: Invalid body or unsupported parameters }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
/contacts/{id}/notes:
post:
operationId: createContactNote
tags: [Contacts]
x-required-scope: contacts:write
summary: Add an entry to a contact's Notes feed
description: >
Requires contacts:write and existing restricted-key/book authorization.
Resolves a Pulse contact id or FileMaker conId and links the note to conId,
not contacts.notes. Date is server-owned (America/Chicago). API-key
authorship stays in CoverCore, not FileMaker. The side panel continues
to display description and date only. Every accepted POST creates a new
note; retries are not idempotent. pending_push does not confirm delivery.
parameters:
- in: path
name: id
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: false
required: [description]
properties:
description:
type: string
minLength: 1
maxLength: 20000
description: Trimmed before length validation; whitespace-only is invalid.
status:
type: string
enum: [active, follow-up, closed, sms_consent]
default: active
responses:
'201':
description: Note persisted locally and queued for existing CGC push
content:
application/json:
schema:
type: object
required: [id, contactId, conId, date, description, status, createdByApiKeyId, createdByApiKeyLabel, covercoreCreatedByUserId, syncStatus]
properties:
id: { type: string }
contactId: { type: string, description: Resolved Pulse contacts.id }
conId: { type: string }
date: { type: string, format: date }
description: { type: string }
status: { type: string, enum: [active, follow-up, closed, sms_consent] }
createdByApiKeyId: { type: string }
createdByApiKeyLabel: { type: string }
covercoreCreatedByUserId: { type: [string, "null"] }
syncStatus: { type: string, const: pending_push }
'400': { description: Invalid body or extra fields; existing error envelope }
'401': { description: API key required or invalid; existing error envelope }
'403': { description: Missing scope, restricted route or contact outside book; existing error envelope }
'404': { description: Contact not found; existing error envelope }
'422': { description: Contact has no FileMaker conId; existing error envelope }
/staff/directory:
get:
operationId: staffDirectory
tags: [Operations]
x-required-scope: staff:read
summary: Read-only internal staff directory
description: >
Lists internal staff (id, name, email, role, service-team flags,
rotation order) and the mailboxes linked to them. Requires staff:read
and a first-party internal key. Does not return passwords, tokens, or
OAuth data. email_inbox:read does not grant this route.
responses:
'200': {description: Staff directory}
'401': {description: API key required}
'403': {description: Missing staff:read or key is not first-party}
/scorecard/email-stats:
get:
operationId: scorecardEmailStats
tags: [Operations]
x-required-scope: email_stats:read
summary: Per-person email stats by America/Chicago day
description: >
from and to are required YYYY-MM-DD dates, inclusive, capped at 92 days.
Optional staffId filters to one user. groupBy must be day (the default).
Counts come from stored email_classifications, outlook_messages, and
ticket_messages, not the today-only mailbox counters. Requires
email_stats:read and a first-party internal key.
parameters:
- name: from
in: query
required: true
schema: {type: string, format: date}
- name: to
in: query
required: true
schema: {type: string, format: date}
- name: staffId
in: query
required: false
schema: {type: string}
- name: groupBy
in: query
required: false
schema: {type: string, enum: [day], default: day}
responses:
'200': {description: Daily stats}
'400': {description: Invalid or oversized date range}
'401': {description: API key required}
'403': {description: Missing email_stats:read or key is not first-party}
/thumbtack/categories:
get:
operationId: thumbtackCategories
tags: [Thumbtack]
x-required-scope: thumbtack:read
summary: List installation categories
responses: &thumbtackReadResponses
'200': {description: Successful read}
'401': {description: API key required}
'403': {description: Owner role or scope denied}
'422': {description: Invalid input; invalid_zip for unknown US ZIP}
'429': {description: Rate limited; Retry-After is seconds, headers: {Retry-After: {schema: {type: integer}}}}
'502': {description: Provider temporarily unavailable}
/thumbtack/environment:
get:
operationId: thumbtackEnvironment
tags: [Thumbtack]
x-required-scope: thumbtack:read
summary: Read environment and application name only
responses:
'200':
description: No credentials or tokens
content:
application/json:
schema:
type: object
additionalProperties: false
required: [environment, app]
properties:
environment: {type: string, enum: [staging, production]}
app: {type: string, enum: [Trimriskdev, trimriskprod]}
'401': {description: API key required}
'403': {description: Owner role or scope denied}
'429': {description: Rate limited; Retry-After is seconds}
/thumbtack/search:
post:
operationId: thumbtackSearch
tags: [Thumbtack]
x-required-scope: thumbtack:read
x-rate-limit: 30/min agent, 60/min staff
summary: Search installation providers within approximately 25 miles
description: Read-only discovery; does not create leads. Category takes precedence, with sanitized trade query refinement via filtered search. Contact-like query text is redacted; brands are mapped to approved trade terms. Distances use city centroids, not verified street addresses. Unknown locations are excluded.
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/ThumbtackSearchInput'}
responses: *thumbtackReadResponses
/thumbtack/quick-search/{categoryId}:
post:
operationId: thumbtackQuickSearch
tags: [Thumbtack]
x-required-scope: thumbtack:read
x-rate-limit: 30/min agent, 60/min staff
summary: Search using a category from the categories endpoint
description: Only the path category is used; body query/category fields are ignored.
parameters:
- {name: categoryId, in: path, required: true, schema: {type: string}}
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/ThumbtackQuickSearchInput'}
responses: *thumbtackReadResponses
/thumbtack/search/{searchId}:
get:
operationId: thumbtackSearchResult
tags: [Thumbtack]
x-required-scope: thumbtack:read
summary: Read an owned search result
description: Other owners and unmapped legacy searches return 404. Admin/internal_staff have an audited read-only override. Ownership is assigned only for upstream business search IDs.
parameters:
- {name: searchId, in: path, required: true, schema: {type: string}}
responses:
<<: *thumbtackReadResponses
'404': {description: Not found or not owner}
/thumbtack/lead-settings:
get:
operationId: thumbtackLeadSettings
tags: [Thumbtack]
x-required-scope: thumbtack:read
summary: Read lead-pipeline settings (staff/admin owners only)
responses: *thumbtackReadResponses
put:
operationId: thumbtackUpdateLeadSettings
tags: [Thumbtack]
x-required-scope: thumbtack:write
summary: Update lead-pipeline settings (admin owners only)
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/ThumbtackSettingsInput'}
responses: *thumbtackReadResponses
/leads/thumbtack:
post:
operationId: thumbtackInboundLeads
tags: [Thumbtack]
x-required-scope: thumbtack:write
x-rate-limit: 120/min per key, also subject to generic key limit
summary: Post inbound leads owned by the key creator (staff/admin only)
description: Supplied user_id is ignored. Dedup uses owner, normalized business name and ZIP; a different owner's match never merges.
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/ThumbtackLeadInput'}
responses: *thumbtackReadResponses
/health:
get:
tags: [Foundation]
summary: Health probe (no auth)
security: []
responses:
"200":
description: Service healthy
content:
application/json:
schema:
type: object
properties:
status: { type: string, example: ok }
version: { type: string, example: "1.0.0" }
timestamp: { type: string, format: date-time }
/orders:
get:
tags: [Orders]
summary: Read the internal ORD book
x-required-scopes: [orders:read]
description: |
First-party keys only; agency, agent and policyholder keys are forbidden.
Omitted effective-date bounds default to the last 7 America/Chicago
calendar days inclusive of today, with appliedDateWindow.defaulted=true.
Temporary guard: explicit or defaulted ranges must be at most 31 inclusive
calendar days. Open-ended ranges are also rejected. Supply both
effectiveDateFrom/effectiveDateTo for explicit ranges. Wider ranges return
400 DATE_RANGE_TOO_WIDE before orders SQL, with appliedDateWindow retained.
Line/agency filters do not suppress the default. Page size defaults to 100.
The existing 15-second per-statement timeout and 504 remain unchanged.
parameters: &ordersDateParameters
- name: effectiveDateFrom
in: query
description: Inclusive Chicago effective day; requires effectiveDateTo, at most 31 days inclusive.
schema: { type: string, format: date }
- name: effectiveDateTo
in: query
description: Inclusive Chicago effective day; requires effectiveDateFrom, at most 31 days inclusive.
schema: { type: string, format: date }
responses: &ordersDateResponses
"200":
description: Orders result with appliedDateWindow (field, from, to, defaulted, timeZone).
"400":
description: Invalid parameters or DATE_RANGE_TOO_WIDE for over-31-day or open-ended ranges.
content:
application/json:
schema:
type: object
required: [error]
properties:
error:
type: object
properties:
code: { type: string }
message: { type: string }
retryable: { type: boolean }
appliedDateWindow:
type: object
description: Included on DATE_RANGE_TOO_WIDE responses.
properties:
field: { type: string, enum: [effective_date] }
from: { type: string, format: date, nullable: true }
to: { type: string, format: date, nullable: true }
defaulted: { type: boolean }
timeZone: { type: string, enum: [America/Chicago] }
example:
error:
code: DATE_RANGE_TOO_WIDE
message: Use effectiveDateFrom/effectiveDateTo with a range of 31 days or less.
retryable: false
appliedDateWindow:
field: effective_date
from: "2026-03-01"
to: "2026-04-01"
defaulted: false
timeZone: America/Chicago
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
"504":
description: STATEMENT_TIMEOUT; the unchanged 15-second per-statement limit was reached.
/orders/summary:
get:
tags: [Orders]
summary: Summarize the internal ORD book
x-required-scopes: [orders:read]
description: |
First-party keys only. The same temporary guard as /orders applies:
at most 31 inclusive calendar days, rejecting open-ended ranges with
400 DATE_RANGE_TOO_WIDE and appliedDateWindow before orders SQL.
Omitted bounds default to the last 7 America/Chicago calendar days
inclusive of today with appliedDateWindow.defaulted=true.
Use effectiveDateFrom/effectiveDateTo with a range of 31 days or less.
Grouping and exact totals are unchanged. The existing 15-second
per-statement timeout and 504 remain unchanged.
parameters: *ordersDateParameters
responses: *ordersDateResponses
/me:
get:
tags: [Foundation]
summary: Inspect the authenticated API key
responses:
"200":
description: API key metadata
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/ApiKeyInfo" }
"401": { $ref: "#/components/responses/Unauthorized" }
/jobs/{id}:
get:
tags: [Foundation]
summary: Poll an async job
description: |
Long-running operations (creator-intelligence scoring, BRDX
exports, etc.) return `202 Accepted` immediately with a job
stub. Poll this endpoint until `status` is `succeeded` or
`failed`. Optionally, register a webhook to be notified
instead of polling.
parameters:
- name: id
in: path
required: true
schema: { type: string }
responses:
"200":
description: Job state
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/Job" }
"404": { $ref: "#/components/responses/NotFound" }
# ─── Score ──────────────────────────────────────────────────────────
/score/creator-intelligence:
post:
tags: [Score]
summary: Score a creator/business risk profile (async)
description: |
Returns 202 + a job stub. Poll `/jobs/{id}` (or
`GET /score/creator-intelligence/{jobId}`) for the result, or
provide `webhookUrl` in the body and the result is delivered
as `score.creator-intelligence.completed`.
Required scope: `score:creator`.
parameters:
- $ref: "#/components/parameters/IdempotencyKey"
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/CreatorScoreRequest" }
responses:
"202":
description: Job queued
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/Job" }
"400": { $ref: "#/components/responses/ValidationError" }
"403": { $ref: "#/components/responses/Forbidden" }
/score/creator-intelligence/{jobId}:
get:
tags: [Score]
summary: Read a creator-intelligence job
parameters:
- name: jobId
in: path
required: true
schema: { type: string }
responses:
"200":
description: Job state
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/Job" }
"404": { $ref: "#/components/responses/NotFound" }
/score/weather:
post:
tags: [Score]
summary: Weather / CAT exposure score
description: |
Required scope `score:weather`. Synchronous. `state` and `zip`
are required; `address`/`city`/`latitude`/`longitude` are
optional and refine the result.
parameters:
- $ref: "#/components/parameters/IdempotencyKey"
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/WeatherScoreRequest" }
responses:
"200":
description: Score result
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/ScoreResultGeneric" }
"400": { $ref: "#/components/responses/ValidationError" }
/score/crime:
post:
tags: [Score]
summary: Crime exposure score
description: |
Required scope `score:crime`. Synchronous. All four address
fields (`address`, `city`, `state`, `zip`) are required.
parameters:
- $ref: "#/components/parameters/IdempotencyKey"
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/CrimeScoreRequest" }
responses:
"200":
description: Score result
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/ScoreResultGeneric" }
/score/auto-fatality:
post:
tags: [Score]
summary: Auto fatality exposure score
description: |
Required scope `score:auto-fatality`. Provide either a full
address (`address`+`city`+`state`+`zip`) **or** coordinates
(`lat`+`lng`+`radius_meters`) — not both.
parameters:
- $ref: "#/components/parameters/IdempotencyKey"
requestBody:
required: true
content:
application/json:
schema:
{ $ref: "#/components/schemas/AutoFatalityScoreRequest" }
responses:
"200":
description: Score result
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/ScoreResultGeneric" }
# ─── Underwriting ───────────────────────────────────────────────────
/underwriting/evaluate:
post:
tags: [Underwriting]
summary: Trigger an underwriting evaluation
description: |
Returns an evaluation row whose `id` MUST be passed to every
`/rate/*` call. The evaluation embeds the chosen IRPM
modifiers and the decision (`accept` / `refer` / `decline`).
Sandbox keys may submit a `fixture_key` (one of the 15 golden
fixtures, e.g. `clean_accept`); live keys must submit an
`application` object.
Required scope: `uw:evaluate`.
Jurisdiction gate: StudioGuard is offered online in approved U.S.
states only. An **Alaska (AK)**, **Hawaii (HI)**, or
**outside-the-US** address is terminally unavailable — every
requested line is `declined`, the decision is `decline`, and a
blocking `jurisdiction_unavailable` concern is returned (with a
stable `reason` of `unsupported_state_ak`, `unsupported_state_hi`,
or `unsupported_country`). To express a non-US address in the
`application` payload, set `address.country` to a non-US value
(e.g. `"Canada"`); `address.state` may then be any 2-letter region.
For a domestic application (country omitted or a US token),
`address.state` MUST be a valid 2-letter U.S. state code or the
request 400s.
High-MPL referral: a requested `coverage_limits.mpl` at/above
**$5,000,000** cannot be auto-accepted — the MPL line is
`referred`, the decision is `refer`, and a `high_mpl_limit`
concern is returned.
Correlation: every response (incl. `400`) carries
`X-CoverCore-Request-Id` and `X-CoverCore-Trace-Id`. Send
`x-external-builder-run-id` and/or `x-client-request-id` on the
request to tie your run together in the API request ledger.
The five external-builder quickstart scenarios map to these
fixtures: `studioguard_creator_mo_approved` (accept),
`studioguard_creator_ak_unavailable`,
`studioguard_creator_hi_unavailable`,
`studioguard_creator_outside_us_unavailable` (all decline), and
`studioguard_creator_high_mpl_referral` (refer).
parameters:
- $ref: "#/components/parameters/IdempotencyKey"
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/UnderwritingRequest" }
responses:
"201":
description: Evaluation created
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/UnderwritingEvaluation" }
"400": { $ref: "#/components/responses/ValidationError" }
/underwriting/evaluations/{id}:
get:
tags: [Underwriting]
summary: Read an underwriting evaluation
parameters:
- name: id
in: path
required: true
schema: { type: string, format: uuid }
responses:
"200":
description: Evaluation
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/UnderwritingEvaluation" }
"404": { $ref: "#/components/responses/NotFound" }
/underwriting/evaluations/{id}/additional-info:
post:
tags: [Underwriting]
summary: Submit follow-up info on a referred evaluation
parameters:
- name: id
in: path
required: true
schema: { type: string, format: uuid }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [answers]
properties:
answers:
type: object
additionalProperties:
oneOf:
- { type: string }
- { type: number }
- { type: boolean }
notes: { type: string, maxLength: 2000 }
responses:
"200":
description: Updated evaluation
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/UnderwritingEvaluation" }
# ─── Rate ───────────────────────────────────────────────────────────
# Each /rate/* endpoint requires a valid evaluation_id from
# /underwriting/evaluate. If absent or stale, the gate returns
# 409 EVALUATION_REQUIRED.
/rate/mpl:
post:
tags: [Rate]
summary: Rate Media Professional Liability
description: |
Required scope: `rate:mpl`. `evaluation_id` MUST point at a
valid, accepted underwriting evaluation; otherwise 409
`EVALUATION_REQUIRED`. Provide either `revenue_allocations`
OR `services` + `business_description` (used for AI
classification).
parameters:
- $ref: "#/components/parameters/IdempotencyKey"
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/MplRateRequest" }
responses:
"201":
description: Rated line
content:
application/json:
schema: { $ref: "#/components/schemas/RateLineResponse" }
"409": { $ref: "#/components/responses/EvaluationRequired" }
/rate/property:
post:
tags: [Rate]
summary: Rate Property
description: Required scope `rate:property`.
parameters:
- $ref: "#/components/parameters/IdempotencyKey"
requestBody:
required: true
content:
application/json:
schema:
{ $ref: "#/components/schemas/PropertyRateRequest" }
responses:
"201":
description: Rated line
content:
application/json:
schema: { $ref: "#/components/schemas/RateLineResponse" }
"409": { $ref: "#/components/responses/EvaluationRequired" }
/rate/gl:
post:
tags: [Rate]
summary: Rate General Liability
description: Required scope `rate:gl`.
parameters:
- $ref: "#/components/parameters/IdempotencyKey"
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/GlRateRequest" }
responses:
"201":
description: Rated line
content:
application/json:
schema: { $ref: "#/components/schemas/RateLineResponse" }
"409": { $ref: "#/components/responses/EvaluationRequired" }
/rate/cyber:
post:
tags: [Rate]
summary: Rate Cyber
description: Required scope `rate:cyber`.
parameters:
- $ref: "#/components/parameters/IdempotencyKey"
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/CyberRateRequest" }
responses:
"201":
description: Rated line
content:
application/json:
schema: { $ref: "#/components/schemas/RateLineResponse" }
"409": { $ref: "#/components/responses/EvaluationRequired" }
/rate/trimpay:
post:
tags: [Rate]
summary: Rate TRiMPay credit / debit
description: Required scope `rate:trimpay`.
parameters:
- $ref: "#/components/parameters/IdempotencyKey"
requestBody:
required: true
content:
application/json:
schema:
{ $ref: "#/components/schemas/TrimpayRateRequest" }
responses:
"201":
description: Rated line
content:
application/json:
schema: { $ref: "#/components/schemas/RateLineResponse" }
"409": { $ref: "#/components/responses/EvaluationRequired" }
# ─── External quote pipeline imports ───────────────────────────────
/quotes:
get:
tags: [External Quote Imports]
summary: List authorized quote-pipeline records
security: [{ BearerAuth: [] }, { ApiKeyAuth: [] }]
x-required-scopes: [quotes:read]
description: |
Caller-visible change: omitted submittedAt/submittedAtFrom/submittedAtTo
(including snake_case aliases), effectiveDateFrom/effectiveDateTo and
quoteDateFrom/quoteDateTo now
default to the last 90 America/Chicago calendar days inclusive of today
on submitted_at, not all-time. Submitted-at and effective-date parameters
preserve their existing open-ended behavior. Quote-date parameters
(including quote_date_from/quote_date_to aliases) suppress that default,
but require BOTH bounds and at most 31 inclusive Chicago calendar days.
Wider or open-ended quote-date ranges return 400 DATE_RANGE_TOO_WIDE,
retryable=false, with appliedDateWindow.field=quote_date before book
resolution or quotes SQL, including sandbox requests.
Other filters do not suppress the default. Fetch longer quote-date
history in separate non-overlapping windows of 31 days or less.
Page size remains 20 by default, capped at 100. Additive appliedDateWindow
returns from, to, field, defaulted, timeZone and any explicit effective-date
intersection, including count-only and empty-book responses.
Quotes reads have a 15-second request deadline and a transaction-local
statement_timeout covering the shared count/page query. Deadline or
database cancellation returns 504 STATEMENT_TIMEOUT, retryable=true;
no partial rows or totals are returned. The underlying SQL is cancelled
by PostgreSQL even when the HTTP deadline expires first.
Required scope: `quotes:read`. No extra scope. Returns legacy quote records plus
external carrier imports visible to the authenticated key. Agency and
agent keys remain restricted to proven book-of-business ownership;
sandbox and live imports are isolated. Approval-like, Rated, and
external Issued statuses never mean CoverCore bind or issuance.
`productLob` is an exact, case-insensitive match on `quotes.product_lob`
(comma-separated list allowed). It does not change `coverageType`, which
stays a case-insensitive substring match.
`quoteDateFrom` and `quoteDateTo` are inclusive America/Chicago calendar
days (`YYYY-MM-DD`) on `quotes.date` — the quote's own date. FileMaker
sync stores the FileMaker `date` field there; external import stores
`quote_date` there. This is not `submitted_at`, `effective_date`, or
`create_timestamp`. Date-only text is that Chicago day. Zone-less
timestamps are Chicago wall time. Offset-bearing timestamps use their
instant. The window is half-open and DST-safe (23 hours on spring-forward,
25 hours on fall-back). The predicate is applied in SQL.
`sort` is `quoteDate`, `submittedAt`, `effectiveDate`, or `premium`,
with optional `:asc` or `:desc`. Omitted sort is `quoteDate:desc`
(newest quote date first). `id` ascending is always the tiebreak.
Null and unparseable values sort last. `pagination.total` is the
filtered count. `countOnly=true` returns that count and an empty `data`
array without loading the page.
parameters:
- name: carrier_id
in: query
schema: { type: string }
- name: external_quote_id
in: query
schema: { type: string }
- name: source
in: query
schema: { type: string }
- name: status
in: query
description: Normalized external business status (or legacy sync status).
schema: { type: string }
- name: coverageType
in: query
description: Case-insensitive substring match on coverage_type. Unchanged.
schema: { type: string }
- name: productLob
in: query
description: Exact case-insensitive product_lob match. Comma-separated list allowed.
schema: { type: string }
- name: quoteDateFrom
in: query
description: Inclusive Chicago day on quotes.date; requires quoteDateTo and at most 31 inclusive days. Alias quote_date_from.
schema: { type: string, format: date }
- name: submittedAt
in: query
description: Single inclusive Chicago calendar day on submitted_at; suppresses the default window. Alias submitted_at.
schema: { type: string, format: date }
- name: submittedAtFrom
in: query
description: Inclusive Chicago start day on submitted_at; omitted end stays open. Alias submitted_at_from.
schema: { type: string, format: date }
- name: submittedAtTo
in: query
description: Inclusive Chicago end day on submitted_at; omitted start stays open. Alias submitted_at_to.
schema: { type: string, format: date }
- name: effectiveDateFrom
in: query
description: Inclusive existing effective_date lower-bound comparison; suppresses the default submitted_at window.
schema: { type: string, format: date }
- name: effectiveDateTo
in: query
description: Inclusive existing effective_date upper-bound comparison; suppresses the default submitted_at window.
schema: { type: string, format: date }
- name: quoteDateTo
in: query
description: Inclusive Chicago day on quotes.date; requires quoteDateFrom and at most 31 inclusive days. Alias quote_date_to.
schema: { type: string, format: date }
- name: sort
in: query
description: quoteDate, submittedAt, effectiveDate, or premium, with optional :asc or :desc. Default quoteDate:desc, then id ascending.
schema: { type: string, example: "quoteDate:desc" }
- name: countOnly
in: query
description: When true, pagination.total is the filtered count and data is empty.
schema: { type: boolean }
- name: contact_id
in: query
schema: { type: string }
- name: page
in: query
schema: { type: integer, minimum: 1, default: 1 }
- name: limit
in: query
schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
responses:
"200":
description: Authorized quote-pipeline records
content:
application/json:
schema: { $ref: "#/components/schemas/ExternalQuoteListResponse" }
"400":
description: Invalid filters, or DATE_RANGE_TOO_WIDE for a quote-date range over 31 days or missing a bound.
content:
application/json:
example:
error:
code: DATE_RANGE_TOO_WIDE
message: Use quoteDateFrom/quoteDateTo with a range of 31 days or less.
retryable: false
appliedDateWindow:
field: quote_date
from: "2026-11-01"
to: null
defaulted: false
timeZone: America/Chicago
"504":
description: STATEMENT_TIMEOUT; the 15-second quotes-read deadline or database statement timeout expired.
content:
application/json:
example:
error:
code: STATEMENT_TIMEOUT
message: Quotes read exceeded the 15-second deadline. Narrow the filters and retry.
retryable: true
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
/quotes/import:
post:
tags: [External Quote Imports]
summary: Import or update outside-carrier pipeline rows
security: [{ BearerAuth: [] }, { ApiKeyAuth: [] }]
x-required-scopes: [quotes:write]
description: |
Required scope: `quotes:write`. Accepts one record or a bounded batch
of up to 200 rows (large enough for the initial Hartford ICON sync).
Ownership may be supplied as canonical `ownership`, resolved using the
existing `ownershipFrom` portal shape, or inferred from a valid
agent-bound key. `contact_id` is optional; when present alongside
`ownership.contactId`, the canonical identities must agree. An optional
`policyholder_id` must identify an existing in-book policyholder.
This is pipeline synchronization only. It does not call underwriting
evaluation or rating and never invents premium, coverage, limits,
forms, or dates. `transaction_type` remains optional for existing import
clients: Hartford defaults an omitted value to `new_business`, while
Travelers (`source = travelers_policy_activity`) requires an explicit
discriminator at runtime. Omitted insurance fields remain absent/null. Imports are
always visible only to staff and the assigned agent, never to a
policyholder. The natural idempotency key is
`(environment, carrier_id, external_quote_id)`; database uniqueness is
authoritative under retries and concurrency. A retry cannot transfer
ownership or clear a proven policyholder link. `Idempotency-Key` adds
request replay protection.
Ownership namespaces are explicit: `ownership.agencyId` and
`fallback_agency_id` accept the exact `agencies.id` or its persisted
`agencies.ptnUuid`, while the response returns canonical `agencies.id`.
`contact_id`, `ownership.contactId`, and `fallback_contact_id` are
`contacts.id`, not carrier producer codes. A producer code is evidence
only and cannot create or infer an owner.
`submitted_at` is optional date evidence. When omitted, the server
uses parsed portal `last_activity`, then the first import timestamp.
Offset-bearing ISO-8601 values retain their instant; date-only and
offset-less values are `America/Chicago`. Collection day filters use
the DST-safe half-open range `[start_of_day, start_of_next_day)` in
that timezone (23 hours on spring-forward days and 25 hours on
fall-back days). Replays without new evidence preserve their bucket;
changed portal activity can move the row. Carrier status is pipeline
evidence only: `Approved`, `Rated`, and `Issued` never grant
evaluation, bind, or issuance authority.
parameters:
- $ref: "#/components/parameters/IdempotencyKey"
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/ExternalQuoteImportInput" }
examples:
hartfordIcon:
summary: One Hartford ICON pipeline row with omitted insurance terms
value:
source: hartford_icon
carrier_id: 72dbfa05-5b49-4790-82de-125be1f3b5d0
external_quote_id: QA-HARTFORD-ICON-0001
status: Rated
ownership:
agencyId: qa-agency-id
contactId: qa-contact-id
named_insured: QA Studio LLC
product_lob: Business Owner Policy
premium: null
effective_date: null
quote_date: null
submitted_at: "2026-09-11T14:30:00Z"
metadata:
portal: ICON
travelersPolicyActivity:
summary: One Travelers New Business row; use only isolated QA ownership and policyholder fixtures
value:
source: travelers_policy_activity
carrier_id: 0fb728c5-ca68-451b-b33b-3d3d8871678d
external_quote_id: QA-TRAVELERS-NB-0001
status: New Business
transaction_type: new_business
ownership:
agencyId: qa-agency-id
contactId: qa-contact-id
policyholder_id: qa-existing-policyholder-id
named_insured: QA Plaza LLC
last_activity: "2026-09-11 09:30"
mixedCarrierBulk:
summary: Bounded normalized JSON batch; every item has independent field paths on validation failure
value:
quotes:
- source: hartford_icon
carrier_id: 72dbfa05-5b49-4790-82de-125be1f3b5d0
external_quote_id: QA-HARTFORD-ICON-0002
status: Declined
ownership:
agencyId: qa-agency-id
contactId: qa-contact-id
- source: travelers_policy_activity
carrier_id: 0fb728c5-ca68-451b-b33b-3d3d8871678d
external_quote_id: QA-TRAVELERS-NB-0002
status: New Business
transaction_type: new_business
ownership:
agencyId: qa-agency-id
contactId: qa-contact-id
responses:
"200":
description: Existing record updated or bulk results returned
content:
application/json:
schema: { $ref: "#/components/schemas/ExternalQuoteImportResponse" }
"201":
description: Single record created
content:
application/json:
schema: { $ref: "#/components/schemas/ExternalQuoteImportCreatedResponse" }
"400": { $ref: "#/components/responses/ExternalQuoteImportValidationError" }
"401": { $ref: "#/components/responses/Unauthorized" }
"403":
description: Contact or policyholder is outside the authenticated book of business
content:
application/json:
schema: { $ref: "#/components/schemas/ExternalQuoteImportErrorResponse" }
"404":
description: Carrier, contact, or policyholder was not found
content:
application/json:
schema: { $ref: "#/components/schemas/ExternalQuoteImportErrorResponse" }
"409":
description: Ownership or idempotency conflict
content:
application/json:
schema: { $ref: "#/components/schemas/ExternalQuoteImportErrorResponse" }
"500":
description: Import failed after validation; no lifecycle action is performed
content:
application/json:
schema: { $ref: "#/components/schemas/ExternalQuoteImportErrorResponse" }
"503":
description: Idempotency protection is temporarily unavailable
content:
application/json:
schema: { $ref: "#/components/schemas/ExternalQuoteImportErrorResponse" }
/quotes/submissions:
get:
tags: [External Quote Imports]
summary: List authorized external carrier submissions
security: [{ BearerAuth: [] }, { ApiKeyAuth: [] }]
x-required-scopes: [quotes:read]
description: |
Static collection route evaluated before `/quotes/{id}`. It exposes
authorized external-carrier pipeline rows, not a new native StudioGuard
submission model.
This collection is a reporting projection, not lifecycle authority.
For staff, the Home total is the effective, post-overlay count of
distinct rows in the three workable StudioGuard lanes:
`actionable_submission + studioguard_lead + needs_review`. An eligible
imported row may join one of those non-excluded lanes with explicit
carrier-import attribution and is counted at most once. The session
dashboard exposes `metrics.submissions.external = { count, preview }`
as a labeled subset of that total, not an additional counter; it must
never be added to `metrics.submissions.total`. The effective channel
projection uses a reporting-only `carrierImport` bucket. Excluded
activity remains inspectable but is not counted.
External rows are eligible when they are in the caller's environment
and authorized book, visible to staff or the assigned agent, and their
`submittedAt` is in the requested America/Chicago calendar day.
Declined and expired rows are included by default. Travelers defaults to
`new_business`; pass a transaction filter to review retained renewal or
endorsement evidence. Policyholders cannot read unfinished external
imports, including a carrier row whose raw status says `Issued`.
x-kpi-definition:
name: Home Submissions
timezone: America/Chicago
nativeProjection:
workableLanes:
- actionable_submission
- studioguard_lead
- needs_review
excludedLane: excluded (visible for audit, not counted)
channels: [portal, email, direct, aiIntake, carrierImport]
channelRule: "Each workable row is assigned once; channel sum equals workable total."
timezone: America/Chicago
externalProjection:
field: metrics.submissions.external
shape: "{ count, preview }"
subsetOfHomeTotal: true
additiveToHomeTotal: false
attribution: "Eligible imported rows are assigned once to the workable carrierImport bucket; this field labels that subset and is not additive."
collection: "This operation returns external rows and pagination; the staff dashboard uses the same effective feed and labels carrier-import attribution separately."
parameters:
- name: externalQuoteId
in: query
description: Exact carrier portal ID.
schema: { type: string, minLength: 1, maxLength: 255 }
- name: carrier_id
in: query
schema: { type: string, minLength: 1 }
- name: submitted_at
in: query
description: Chicago calendar day in `YYYY-MM-DD`, not a UTC day.
schema: { type: string, pattern: '^\d{4}-\d{2}-\d{2}$' }
- name: agencyId
in: query
description: Still constrained to the requesting key's authorized book.
schema: { type: string, minLength: 1 }
- name: contact_id
in: query
description: Exact canonical `contacts.id`, constrained to the caller's book.
schema: { type: string, minLength: 1 }
- name: status
in: query
description: Comma-separated normalized statuses; omitted includes Declined and Expired.
schema:
type: string
example: "submitted,declined"
- name: transaction_type
in: query
description: Comma-separated transaction types; omitted excludes Travelers renewal and endorsement evidence.
style: form
explode: false
schema:
type: array
items: { $ref: "#/components/schemas/ExternalQuoteTransactionType" }
- name: page
in: query
schema: { type: integer, minimum: 1, default: 1 }
- name: limit
in: query
schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
responses:
"200":
description: |
Authorized eligible external carrier rows. `pagination.total` is
the external-carrier count for this collection only; it is not the
staff Home `metrics.submissions.total`. The dashboard summary uses
a separate `{ count, preview }` external projection.
content:
application/json:
schema: { $ref: "#/components/schemas/ExternalQuoteListResponse" }
"400": { $ref: "#/components/responses/ExternalQuoteImportValidationError" }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
/quotes/{id}:
get:
tags: [External Quote Imports]
summary: Read an authorized quote-pipeline record
security: [{ BearerAuth: [] }, { ApiKeyAuth: [] }]
x-required-scopes: [quotes:read]
description: Required scope `quotes:read`; book and environment isolation match the list route.
parameters:
- name: id
in: path
required: true
schema: { type: string }
responses:
"200":
description: Quote-pipeline record
content:
application/json:
schema:
type: object
required: [data]
properties:
data: { $ref: "#/components/schemas/ExternalQuoteRecord" }
"404": { $ref: "#/components/responses/NotFound" }
# ─── Evaluated and rated StudioGuard quote assembly ─────────────────
/quote:
post:
tags: [Quote]
summary: Assemble a quote from rated lines
description: |
Combines previously-rated `line_ids` into a single quote with
surplus-lines tax + monthly schedule + hosted URL.
Required scope: `quote:write`.
parameters:
- $ref: "#/components/parameters/IdempotencyKey"
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/QuoteAssembleRequest" }
responses:
"201":
description: Quote created
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/Quote" }
/quote/{id}:
get:
tags: [Quote]
summary: Read an assembled quote
description: Required scope `quote:read`.
parameters:
- name: id
in: path
required: true
schema: { type: string, format: uuid }
responses:
"200":
description: Quote
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/Quote" }
"404": { $ref: "#/components/responses/NotFound" }
# ─── Bind ───────────────────────────────────────────────────────────
/bind:
post:
tags: [Bind]
summary: Bind a quote into a policy
description: |
Required scope: `bind:write`. Returns `policy_id` and a
snapshot of the commission split. Emits `policy.bound`.
Returns `404 QUOTE_NOT_FOUND` if `quote_id` is unknown or
does not belong to the calling API key.
parameters:
- $ref: "#/components/parameters/IdempotencyKey"
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/BindRequest" }
responses:
"201":
description: Policy bound
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/BoundPolicy" }
"404": { $ref: "#/components/responses/NotFound" }
/bind/{policy_id}/payment-intent:
post:
tags: [Bind]
summary: Create a Stripe PaymentIntent for the bound policy
description: |
Required scope: `bind:write`. Returns a Stripe `client_secret`
the partner uses to collect first payment. Stripe webhooks
translate to `policy.payment.succeeded` /
`policy.payment.failed` / `policy.payment.cancelled`.
parameters:
- name: policy_id
in: path
required: true
schema: { type: string, format: uuid }
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
installment_number: { type: integer, minimum: 1, maximum: 12 }
responses:
"201":
description: PaymentIntent
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/PaymentIntent" }
# ─── BRDX ───────────────────────────────────────────────────────────
/brdx/written:
get:
tags: [BRDX]
summary: Bordereau — policies written
description: Required scope `brdx:read`. Partner-tenanted.
parameters:
- name: period
in: query
schema: { type: string, example: "2026-04" }
- name: page
in: query
schema: { type: integer, default: 1 }
- name: limit
in: query
schema: { type: integer, default: 50, maximum: 200 }
responses:
"200":
description: Written bordereau
content:
application/json:
schema:
type: object
properties:
data:
type: array
items: { $ref: "#/components/schemas/BrdxWrittenRow" }
pagination: { $ref: "#/components/schemas/Pagination" }
/brdx/paid:
get:
tags: [BRDX]
summary: Bordereau — payments collected
description: Required scope `brdx:read`. Partner-tenanted.
parameters:
- name: period
in: query
schema: { type: string }
- name: page
in: query
schema: { type: integer, default: 1 }
- name: limit
in: query
schema: { type: integer, default: 50, maximum: 200 }
responses:
"200":
description: Paid bordereau
content:
application/json:
schema:
type: object
properties:
data:
type: array
items: { $ref: "#/components/schemas/BrdxPaidRow" }
pagination: { $ref: "#/components/schemas/Pagination" }
/brdx/{report_id}/export:
get:
tags: [BRDX]
summary: Download a partner-scoped Accelerant `.xlsx` export
parameters:
- name: report_id
in: path
required: true
schema: { type: string }
responses:
"200":
description: xlsx file
content:
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
schema: { type: string, format: binary }
# ─── Commission ─────────────────────────────────────────────────────
/commissions/ledger:
get:
tags: [Commission]
summary: Filterable commission ledger
description: |
Required scope: `commission:read`. External (non-admin) keys
MUST scope by `agent_id`; admin / internal keys may omit it. For an
agent-scoped key, `agent_id` must be one of the direct FileMaker agent
identifiers resolved from the key's immutable user/contact linkage.
parameters:
- name: agent_id
in: query
schema: { type: string }
- name: status
in: query
description: Filter by ledger status, or pass `all` to disable filtering.
schema:
type: string
enum:
[
pending,
verified,
payable,
paid,
held,
reversed,
clawed_back,
all,
]
- name: carrier
in: query
schema: { type: string }
- name: policy_number
in: query
schema: { type: string }
- name: period
in: query
schema: { type: string, example: "2026-04" }
- name: effective_date_from
in: query
schema: { type: string, format: date }
- name: effective_date_to
in: query
schema: { type: string, format: date }
- name: page
in: query
schema: { type: integer, default: 1 }
- name: limit
in: query
schema: { type: integer, default: 50, maximum: 200 }
responses:
"200":
description: Ledger page
content:
application/json:
schema:
type: object
properties:
data:
type: array
items: { $ref: "#/components/schemas/CommissionLedgerRow" }
pagination: { $ref: "#/components/schemas/Pagination" }
"403": { $ref: "#/components/responses/Forbidden" }
/commissions/wallet/{agent_id}:
get:
tags: [Commission]
summary: Agent wallet (pending / payable / paid + compliance)
description: |
Required scope: `commission:read`. Returns balances, the next
aging date, the Lexi compliance gate snapshot, and licensed
states. Agent-scoped keys may only read a directly resolved
FileMaker agent identity; an agency or user id is not a substitute.
parameters:
- name: agent_id
in: path
required: true
schema: { type: string }
responses:
"200":
description: Wallet snapshot
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/CommissionWallet" }
"403": { $ref: "#/components/responses/Forbidden" }
"404": { $ref: "#/components/responses/NotFound" }
/commissions/withdrawals:
post:
tags: [Commission]
summary: Request a commission withdrawal
description: |
Required scope: `commission:write`. Idempotent. `amount` may
be either a positive number (partial) or the sentinel string
`"max payable"` (sweep entire payable balance). Atomic; uses
FIFO whole-row selection.
parameters:
- $ref: "#/components/parameters/IdempotencyKey"
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/WithdrawalRequest" }
responses:
"201":
description: Withdrawal accepted
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/Withdrawal" }
"403": { $ref: "#/components/responses/Forbidden" }
"409": { $ref: "#/components/responses/Conflict" }
"422": { $ref: "#/components/responses/UnprocessableEntity" }
# ─── Webhooks ───────────────────────────────────────────────────────
/webhooks:
get:
tags: [Webhooks]
summary: List webhook subscriptions
description: Required scope `webhooks:manage`.
responses:
"200":
description: Subscriptions
content:
application/json:
schema:
type: object
properties:
data:
type: array
items: { $ref: "#/components/schemas/WebhookSubscription" }
post:
tags: [Webhooks]
summary: Create a webhook subscription
description: |
Required scope `webhooks:manage`. `chat_escalation.created` is the
supported wake-up event for chat bots. It additionally requires a live,
owner-bound, first-party key with `chat_escalations:read`; sandbox keys
do not receive live chat events.
Optional `headers` are static delivery headers. `Authorization` is
allowed, but unsafe names are rejected: transport/hop-by-hop headers,
`Content-Type`, `X-Pulse-*`, proxy/`Sec-*`/`X-Forwarded-*` headers, and
cookie/origin/referrer-style headers. Header values are returned in
this create response only and are omitted from GET/list responses.
The returned webhook HMAC `secret` is separate from the `pk_live_*` or
`pk_test_*` API credential. It is disclosed only in this creation
response (or a later explicit rotation response), never by reads.
`url` must be a public HTTPS endpoint. Localhost, private, link-local,
reserved, and mixed public/private DNS results are rejected. Delivery
revalidates and pins the resolved public address, and redirects are not
followed.
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [url, events]
properties:
url:
type: string
format: uri
description: Public HTTPS endpoint; internal/reserved targets and redirects are not allowed.
events:
type: array
items:
type: string
description: Supported event name, including `chat_escalation.created`.
example: chat_escalation.created
headers:
$ref: "#/components/schemas/WebhookDeliveryHeaders"
responses:
"201":
description: Subscription created
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/WebhookSubscription" }
/webhooks/{id}:
get:
tags: [Webhooks]
summary: Read a webhook subscription
description: Required scope `webhooks:manage`.
parameters:
- name: id
in: path
required: true
schema: { type: string }
responses:
"200":
description: Subscription
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/WebhookSubscription" }
patch:
tags: [Webhooks]
summary: Update a webhook subscription
description: |
Toggle a subscription on/off, update its event list, and/or replace its
static delivery headers. Supplying `headers` replaces the complete
header map; `{}` clears it. Header values are returned only when this
request includes `headers`, and remain omitted from GET/list responses.
Reactivating an auto-paused subscription (setting
`isActive: true` when it was previously `false`) atomically
resets `failureCount` to `0` so the next single failed
delivery does not immediately re-trip the 10-failure
auto-pause threshold. The signing `secret` is preserved, so
partners can bring a paused subscription back online without
re-recording credentials. At least one of `isActive`, `events`, or
`headers` must be provided. Required scope: `webhooks:manage`.
parameters:
- name: id
in: path
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
type: object
minProperties: 1
properties:
isActive: { type: boolean }
events:
type: array
minItems: 1
items:
type: string
description: Supported event name, including `chat_escalation.created`.
example: chat_escalation.created
headers:
$ref: "#/components/schemas/WebhookDeliveryHeaders"
responses:
"200":
description: Subscription updated
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/WebhookSubscription" }
"400":
description: Validation error (empty body, wrong types, empty `events`, or a rejected header name/value).
"404":
description: Subscription not found for this API key.
/webhooks/{id}/rotate-secret:
post:
tags: [Webhooks]
summary: Rotate the signing secret for a subscription
description: |
Generates a fresh 32-byte hex HMAC signing secret, persists it
on the subscription, and returns the new plaintext value in
the response. The subscription's `id`, `url`, `events`,
`isActive` flag, and delivery history are preserved — only
the `secret` changes — so partners can rotate after a
suspected leak without recreating the subscription.
The previous secret stops being valid the instant this
response is returned. Any in-flight delivery signed with the
old secret will continue to verify on the partner side using
the old secret it was signed with, but every delivery
attempted **after** this call is signed with the new secret.
The new secret is only ever returned by this endpoint and by
`POST /webhooks` on creation; subsequent reads (including
`GET /webhooks` and `GET /webhooks/{id}`) never include it.
Required scope: `webhooks:manage`.
parameters:
- name: id
in: path
required: true
schema: { type: string }
responses:
"200":
description: Secret rotated. The response body contains the new plaintext secret in the `data.secret` field — store it now, it will never be returned again.
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/WebhookSubscription" }
"404":
description: Subscription not found for this API key.
/webhooks/{id}/test:
post:
tags: [Webhooks]
summary: Send a test event to a subscription
description: |
Performs a synchronous `test.ping` POST to the subscription
URL using the subscription's HMAC secret and returns the
delivery result. The body is `{event:"test.ping", timestamp,
data:{message}}`. Required scope: `webhooks:manage`.
parameters:
- name: id
in: path
required: true
schema: { type: string }
responses:
"200":
description: Test delivery completed (synchronous).
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
eventId: { type: string }
status:
type: string
enum: [delivered, failed]
responseStatus: { type: integer, nullable: true }
duration: { type: integer, description: Milliseconds end-to-end. }
error: { type: string, nullable: true }
/webhooks/{id}/deliveries:
get:
tags: [Webhooks]
summary: List recent deliveries for a subscription
description: |
Paginated by `page` and `limit` (default 1/20, max limit 100).
Required scope: `webhooks:manage`.
parameters:
- name: id
in: path
required: true
schema: { type: string }
- name: page
in: query
schema: { type: integer, minimum: 1, default: 1 }
- name: limit
in: query
schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
responses:
"200":
description: Recent deliveries
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
type: object
properties:
id: { type: string }
eventType: { type: string }
eventId: { type: string }
payload: { type: object, description: The exact JSON body Pulse POSTed. }
responseStatus: { type: integer, nullable: true }
responseBody: { type: string, nullable: true, description: First 1000 bytes of the partner response. }
attemptNumber: { type: integer }
status:
type: string
enum: [pending, processing, delivered, failed, exhausted]
duration: { type: integer, nullable: true, description: Milliseconds end-to-end. }
error: { type: string, nullable: true }
createdAt: { type: string, format: date-time }
pagination:
type: object
properties:
page: { type: integer }
limit: { type: integer }
total: { type: integer }
totalPages: { type: integer }
/service-enablement/daily-report:
get:
tags: [Service Intelligence]
summary: Service Enablement daily report (compact, expandable)
description: |
Stable compact daily/trailing-7 Service Enablement report (score,
actionableTickets, accessEnabledTicketCount, recognition counts,
examples per period). Expanded sections are opt-in via
`include=classifications,gamification,achievements,recognition` or the
equivalent `includeClassifications=true` style flags; include flags
additionally require `service_enablement:report`. Score is null (never
a fabricated 0%) below the minimum denominator; manual recognition can
never move the score. Required scope: `service_enablement:read`
(legacy `service_requests:read` keys still accepted). First-party
internal keys only (staff recognition data).
parameters:
- name: date
in: query
schema: { type: string, format: date }
- name: include
in: query
schema: { type: string, example: "classifications,gamification,achievements,recognition" }
responses:
"200": { description: Compact report, with expanded sections when requested. }
"403": { description: Missing scope or not a first-party key. }
/service-enablement/report:
get:
tags: [Service Intelligence]
summary: Full expanded Service Intelligence report
description: |
All expanded sections (classificationBreakdown, staffBreakdown,
gamification, achievements, denominator) for yesterday and the
trailing 7 days. `period=daily|trailing7` returns a single block.
Required scope: `service_enablement:report`. First-party keys only.
parameters:
- name: date
in: query
schema: { type: string, format: date }
- name: period
in: query
schema: { type: string, enum: [daily, trailing7] }
responses:
"200": { description: Expanded report. }
"403": { description: Missing scope or not a first-party key. }
/service-enablement/classification-summary:
get:
tags: [Service Intelligence]
summary: Classification taxonomy summary
description: |
Actionable-ticket counts across the 5 classification keys
(agent_access_enabled_work, stuckey_required_service,
training_or_access_enablement, new_agent_signup_or_access_grant,
unknown) plus the denominator block, for both periods. Required scope:
`service_enablement:read`. First-party keys only.
parameters:
- name: date
in: query
schema: { type: string, format: date }
responses:
"200": { description: Classification summary. }
"403": { description: Missing scope or not a first-party key. }
/service-enablement/staff-summary:
get:
tags: [Service Intelligence]
summary: Per-staff enablement contribution
description: |
staffBreakdown per period (actionable tickets, per-staff
classification breakdown, recognition events, XP, badges, missions).
Required scope: `service_enablement:report`. First-party keys only.
parameters:
- name: date
in: query
schema: { type: string, format: date }
responses:
"200": { description: Staff summary. }
"403": { description: Missing scope or not a first-party key. }
/gamification/daily-report:
get:
tags: [Service Intelligence]
summary: Gamification daily report
description: |
Service-enablement XP totals and event counts, top recognition,
missions completed and badges earned per period. Required scope:
`gamification:read`. First-party keys only.
parameters:
- name: date
in: query
schema: { type: string, format: date }
responses:
"200": { description: Gamification report. }
"403": { description: Missing scope or not a first-party key. }
/gamification/achievements:
get:
tags: [Service Intelligence]
summary: Achievement catalog
description: |
Service-enablement badge and mission definitions (no per-user data).
Required scope: `gamification:read`. First-party keys only.
responses:
"200": { description: Badge + mission catalog. }
"403": { description: Missing scope or not a first-party key. }
/achievements/daily-report:
get:
tags: [Service Intelligence]
summary: Achievements daily report
description: |
Badge/mission definitions plus awards earned in the window for both
periods. Required scope: `achievements:read`. First-party keys only.
parameters:
- name: date
in: query
schema: { type: string, format: date }
responses:
"200": { description: Achievements report. }
"403": { description: Missing scope or not a first-party key. }
# ─── Chat Escalations ───────────────────────────────────────────────
/chat-escalations:
get:
tags: [Chat Escalations]
summary: List chat escalations
description: |
Required scope: `chat_escalations:read`. Live, owner-bound first-party
staff keys only. Subscribe to `chat_escalation.created` with
`POST /webhooks`, verify the HMAC over the raw delivery body, then use
its `data.escalationId` to claim, fetch messages, respond, and resolve.
The signed JSON body is `{ event, id, createdAt, data }`; escalation
fields are nested inside `data`.
`status` accepts comma-separated values. Listing `status=pending`
remains a catch-up path after downtime, not the primary wake-up path.
x-required-scopes: [chat_escalations:read]
parameters:
- name: status
in: query
description: Comma-separated escalation statuses.
schema:
type: string
example: pending,assigned
- name: priority
in: query
schema: { type: string, enum: [low, normal, high, urgent] }
- name: assignment
in: query
schema: { type: string, enum: [all, me, assigned, unassigned] }
- name: page
in: query
schema: { type: integer, minimum: 1, default: 1 }
- name: limit
in: query
schema: { type: integer, minimum: 1 }
responses:
"200": { description: Paginated escalations visible to the staff owner. }
"403": { $ref: "#/components/responses/ChatEscalationForbidden" }
/chat-escalations/{id}:
get:
tags: [Chat Escalations]
summary: Read a chat escalation
description: |
Required scope: `chat_escalations:read`. Live, owner-bound first-party
staff keys only.
x-required-scopes: [chat_escalations:read]
parameters:
- $ref: "#/components/parameters/ChatEscalationId"
responses:
"200": { description: Chat escalation detail. }
"403": { $ref: "#/components/responses/ChatEscalationForbidden" }
"404": { $ref: "#/components/responses/NotFound" }
/chat-escalations/{id}/messages:
get:
tags: [Chat Escalations]
summary: Poll chat escalation messages
description: |
Required scope: `chat_escalations:read`. For an initial lower bound,
`after` accepts an exclusive ISO-8601 timestamp. For collision-safe
pagination, pass the prior response's opaque `pagination.nextAfter`
value unchanged. Live, owner-bound first-party staff keys only.
x-required-scopes: [chat_escalations:read]
parameters:
- $ref: "#/components/parameters/ChatEscalationId"
- name: after
in: query
description: An initial exclusive ISO-8601 timestamp or an opaque nextAfter cursor returned by this endpoint.
schema: { type: string }
- name: limit
in: query
schema: { type: integer, minimum: 1 }
responses:
"200": { description: Ordered escalation messages. }
"403": { $ref: "#/components/responses/ChatEscalationForbidden" }
"404": { $ref: "#/components/responses/NotFound" }
post:
tags: [Chat Escalations]
summary: Send a chat escalation message
description: |
Required scope: `chat_escalations:write`. Owner-only write requiring
Idempotency-Key. A non-owner or state conflict returns 409.
x-required-scopes: [chat_escalations:write]
parameters:
- $ref: "#/components/parameters/ChatEscalationId"
- $ref: "#/components/parameters/IdempotencyKeyRequired"
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [message]
properties:
message: { type: string, minLength: 1 }
additionalProperties: false
responses:
"201": { description: Message sent. }
"403": { $ref: "#/components/responses/ChatEscalationForbidden" }
"409": { $ref: "#/components/responses/Conflict" }
/chat-escalations/{id}/claim:
post:
tags: [Chat Escalations]
summary: Atomically claim a chat escalation
description: |
Required scope: `chat_escalations:write`. Accepts no caller identity;
ownership is atomically bound to the eligible staff user who owns the
key. Naturally retry-safe without Idempotency-Key.
x-required-scopes: [chat_escalations:write]
parameters:
- $ref: "#/components/parameters/ChatEscalationId"
responses:
"200": { description: Claimed escalation (or the same owner's retry). }
"403": { $ref: "#/components/responses/ChatEscalationForbidden" }
"409": { $ref: "#/components/responses/Conflict" }
/chat-escalations/{id}/resolve:
post:
tags: [Chat Escalations]
summary: Resolve a chat escalation
description: |
Required scope: `chat_escalations:write`. Owner-only write requiring
Idempotency-Key. `resolutionNotes` is optional. A non-owner or state
conflict returns 409.
x-required-scopes: [chat_escalations:write]
parameters:
- $ref: "#/components/parameters/ChatEscalationId"
- $ref: "#/components/parameters/IdempotencyKeyRequired"
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
resolutionNotes: { type: string }
additionalProperties: false
responses:
"200": { description: Resolved escalation. }
"403": { $ref: "#/components/responses/ChatEscalationForbidden" }
"409": { $ref: "#/components/responses/Conflict" }
# ─── Service Requests / Ticket Plays ────────────────────────────────
/plays:
get:
tags: [Service Requests]
summary: List available service plays
description: |
Returns the play catalog visible to the caller. Required scope:
`plays:read`. External callers see only partner-visible plays.
x-required-scopes: [plays:read]
parameters:
- name: page
in: query
schema: { type: integer, minimum: 1, default: 1 }
- name: limit
in: query
schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
responses:
"200":
description: Paginated play catalog
content:
application/json:
schema: { $ref: "#/components/schemas/PlayListResponse" }
"403": { $ref: "#/components/responses/Forbidden" }
/plays/{playKey}:
get:
tags: [Service Requests]
summary: Read a service play
description: |
Returns the caller-visible play definition. Required scope:
`plays:read`. The detail includes the role owner, ordered human steps,
approved CoverCore operation IDs, and definition of done. It contains
no staff PII: no staff names, email addresses, user IDs, or assignment
metadata.
x-required-scopes: [plays:read]
parameters:
- $ref: "#/components/parameters/PlayKey"
responses:
"200":
description: Play detail without staff PII
content:
application/json:
schema:
type: object
required: [data]
properties:
data: { $ref: "#/components/schemas/PlayDetail" }
"403": { $ref: "#/components/responses/Forbidden" }
"404": { $ref: "#/components/responses/NotFound" }
/service-tickets/{id}/play:
get:
tags: [Service Requests]
summary: Read the play attached to a service ticket
description: |
Returns the attached play and its runs for a caller-visible ticket.
Required scope: `plays:read`. Play detail and run output contain no
staff PII. A play run never closes or otherwise resolves the ticket;
ticket closure remains an explicit human-owned action.
x-required-scopes: [plays:read]
parameters:
- $ref: "#/components/parameters/ServiceTicketId"
responses:
"200":
description: Attached play and run history
content:
application/json:
schema:
type: object
required: [data]
properties:
data: { $ref: "#/components/schemas/TicketPlay" }
"403": { $ref: "#/components/responses/Forbidden" }
"404": { $ref: "#/components/responses/NotFound" }
/service-tickets/{id}/play-runs:
post:
tags: [Service Requests]
summary: Start a play run for a service ticket (first-party only)
description: |
Starts or attaches a run on the ticket. Required scope:
`play_runs:write`. This operation is restricted to first-party
principals (partnerType admin/internal/internal_system/ai/integration);
external keys receive 403 regardless of grants. Starting or completing
a run never closes the service ticket.
x-required-scopes: [play_runs:write]
x-first-party-only: true
parameters:
- $ref: "#/components/parameters/ServiceTicketId"
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/InternalPlayRunRequest" }
responses:
"201":
description: Play run created; ticket status is unchanged
content:
application/json:
schema:
type: object
required: [data]
properties:
data: { $ref: "#/components/schemas/PlayRun" }
"400": { $ref: "#/components/responses/ValidationError" }
"403": { $ref: "#/components/responses/Forbidden" }
"404": { $ref: "#/components/responses/NotFound" }
/plays/{playKey}/start:
post:
tags: [Service Requests]
summary: Start a partner-visible play (external only)
description: |
External partner entry point for the partner-visible `coi-proof` and
`loss-runs` plays. Required scope: `plays:start`. First-party callers
must use `POST /service-tickets/{id}/play-runs` instead. The required
Idempotency-Key makes retries safe. It creates a granular real service
ticket or safely attaches the play to an in-book deduplicated ticket,
returning a safe ticket result and never a PlayRun.
x-required-scopes: [plays:start]
x-external-only: true
parameters:
- $ref: "#/components/parameters/PlayKey"
- $ref: "#/components/parameters/IdempotencyKeyRequired"
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/PartnerPlayStartRequest" }
responses:
"201":
description: A new real service ticket was created and the play attached
content:
application/json:
schema:
type: object
required: [data]
properties:
data: { $ref: "#/components/schemas/PartnerStartedTicket" }
created: { type: boolean, enum: [true] }
deduplicated: { type: boolean, enum: [false] }
"200":
description: The play was safely attached to an in-book deduplicated real ticket
content:
application/json:
schema: { $ref: "#/components/schemas/PartnerPlayStartResponse" }
"400": { $ref: "#/components/responses/ValidationError" }
"403": { $ref: "#/components/responses/Forbidden" }
"404": { $ref: "#/components/responses/NotFound" }
"409": { $ref: "#/components/responses/Conflict" }
# ─── Agent policyholder intake ──────────────────────────────────────
/agencies/resolve-ownership:
post:
tags: [Agencies, Policyholder]
summary: Resolve carrier portal ownership to an existing agency and contact
description: |
Requires `policies:write`. Resolves supported carrier producer codes or
exact submitting-agent names to existing CoverCore records. It never
creates or guesses agencies or contacts. Unresolved or ambiguous input
returns 422 with candidates when available.
x-required-scopes: [policies:write]
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/OwnershipFrom" }
responses:
"200":
description: Existing agency and contact resolved
content:
application/json:
schema:
type: object
required: [data]
properties:
data: { $ref: "#/components/schemas/OwnershipResolution" }
"400": { $ref: "#/components/responses/ValidationError" }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
"422":
description: Ownership unresolved or ambiguous; no write occurred
content:
application/json:
schema: { $ref: "#/components/schemas/Error" }
/policyholders:
get:
tags: [Policyholder]
summary: List policyholders in the authorized book
description: |
Requires `policies:read`. Results are filtered server-side to the
authenticated agent's proven book of business (agency membership does
not expose sibling-agent records). Each result includes `ownership`
and `externalRefs`; clients must not infer ownership from a search
match or external reference.
x-required-scopes: [policies:read]
parameters:
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PolicyholderLimit"
- name: search
in: query
required: false
schema: { type: string, maxLength: 255 }
description: Case-insensitive company-name, email, or policy-number search.
responses:
"200":
description: Authorized policyholder collection
content:
application/json:
schema: { $ref: "#/components/schemas/PartnerPolicyholderListResponse" }
"400": { $ref: "#/components/responses/ValidationError" }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
post:
tags: [Policyholder]
summary: Create an agent-owned policyholder
description: |
Requires `policies:write`. Agent keys automatically inject their bound
agency and contact when ownership is omitted; supplied ownership must
match that binding. Admin/internal keys must provide either `ownership`
or `ownershipFrom`. `ownership.agencyId` accepts agencies.id or ptnUuid;
`ownership.agentId` is contacts.id. CoverCore creates the record and returns
it immediately as a CoverCore-only shell with `syncStatus: local_only`
and `actId: null`. No automatic Jarvis ACT create/writeback currently
occurs and CoverCore never invents a Jarvis identifier.
`externalRefs` are caller-supplied correlation values; `source` is
normalized to lowercase and `externalId` is preserved. If
`opportunityId` is supplied, it must identify
an existing, locked opportunity whose direct owner is proven. The link
is stored in opportunity metadata and the policyholder's
`opportunityId`; no separate opportunity API is exposed.
x-required-scopes: [policies:write]
x-error-codes:
[VALIDATION_ERROR, AUTHENTICATION_REQUIRED, INVALID_API_KEY,
INSUFFICIENT_SCOPE, FORBIDDEN, AGENT_KEY_UNBOUND,
OWNERSHIP_INVALID, OWNERSHIP_UNRESOLVED, NOT_FOUND,
OPPORTUNITY_OWNER_UNRESOLVED, OPPORTUNITY_OWNERSHIP_MISMATCH,
EXTERNAL_REF_CONFLICT, IDEMPOTENCY_KEY_REQUIRED,
OPPORTUNITY_LINK_CONFLICT, IDEMPOTENCY_CONFLICT,
IDEMPOTENCY_IN_PROGRESS, IDEMPOTENCY_UNAVAILABLE,
IDEMPOTENCY_RETRY_REQUIRED, INTERNAL_ERROR]
parameters:
- $ref: "#/components/parameters/IdempotencyKeyRequired"
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/PolicyholderCreateRequest" }
responses:
"200":
description: Existing directly owned policyholder upserted by external reference
content:
application/json:
schema: { $ref: "#/components/schemas/PolicyholderMutationResponse" }
"201":
description: CoverCore-only policyholder shell created
content:
application/json:
schema: { $ref: "#/components/schemas/PolicyholderMutationResponse" }
"400":
description: Validation failure, including `IDEMPOTENCY_KEY_REQUIRED`
content:
application/json:
schema: { $ref: "#/components/schemas/Error" }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
"404":
description: Referenced agency, agent, or opportunity does not exist (`NOT_FOUND`)
content:
application/json:
schema: { $ref: "#/components/schemas/Error" }
"409": { $ref: "#/components/responses/Conflict" }
"422":
description: Portal ownership input is unresolved or ambiguous
content:
application/json:
schema: { $ref: "#/components/schemas/Error" }
"503":
description: Idempotency service unavailable or retry required
content:
application/json:
schema: { $ref: "#/components/schemas/Error" }
/policyholders/{id}:
get:
tags: [Policyholder]
summary: Read an agent-owned policyholder
description: |
Requires `policies:read`. The record is returned only when direct
immutable ownership by the authenticated agent is proven. Agency
membership, a matching external reference, or an opportunity link
does not establish ownership. A missing or non-owned identifier
returns the same `404 NOT_FOUND` response.
x-required-scopes: [policies:read]
x-error-codes:
[AUTHENTICATION_REQUIRED, INVALID_API_KEY, INSUFFICIENT_SCOPE,
NOT_FOUND, INTERNAL_ERROR]
parameters:
- $ref: "#/components/parameters/OwnedResourceId"
responses:
"200":
description: Policyholder detail
content:
application/json:
schema: { $ref: "#/components/schemas/PartnerPolicyholderResponse" }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
"404":
description: Policyholder is missing or not owned by this agent
content:
application/json:
schema: { $ref: "#/components/schemas/Error" }
/policyholders/{id}/ownership:
patch:
tags: [Policyholder]
summary: Transfer policyholder ownership in place
description: |
Atomically changes only the existing policyholder's agency and agent
ownership. Name, external references, opportunity linkage, and partner
metadata are retained. The target agent must be an existing member of
the target agency. First-party admin/internal keys may transfer across
books; agency and agent keys remain restricted to their proven book and
immutable key binding.
Requires `policyholders:write`. Existing integrations holding the
legacy `policies:write` scope are accepted as a compatibility alias for
this operation only. Either the `Idempotency-Key` header or body
`idempotencyKey` is required; when both are sent they must match.
Identical completed retries are authorized against the policyholder's
current book and return its current full representation read-only; they
never re-apply an older ownership target.
x-required-scopes: [policyholders:write]
x-compatible-scopes: [policies:write]
x-error-codes:
[VALIDATION_ERROR, IDEMPOTENCY_KEY_REQUIRED, IDEMPOTENCY_KEY_MISMATCH,
IDEMPOTENCY_CONFLICT, IDEMPOTENCY_IN_PROGRESS,
IDEMPOTENCY_UNAVAILABLE, INSUFFICIENT_SCOPE, FORBIDDEN,
POLICYHOLDER_NOT_FOUND, AGENCY_NOT_FOUND, AGENT_NOT_FOUND,
OWNERSHIP_INVALID, OWNERSHIP_UNRESOLVED, INTERNAL_ERROR]
parameters:
- $ref: "#/components/parameters/OwnedResourceId"
- $ref: "#/components/parameters/IdempotencyKey"
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/PolicyholderOwnershipTransferRequest" }
responses:
"200":
description: Ownership transferred or already identical; current policyholder returned
content:
application/json:
schema: { $ref: "#/components/schemas/PartnerPolicyholderResponse" }
"400":
description: Invalid body or missing/mismatched idempotency key
content:
application/json:
schema: { $ref: "#/components/schemas/Error" }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
"404":
description: Policyholder, agency, or agent was not found
content:
application/json:
schema: { $ref: "#/components/schemas/Error" }
"409": { $ref: "#/components/responses/Conflict" }
"422":
description: Target agency identifier is ambiguous
content:
application/json:
schema: { $ref: "#/components/schemas/Error" }
"503":
description: Idempotency protection unavailable
content:
application/json:
schema: { $ref: "#/components/schemas/Error" }
# ─── Policyholder self-service ─────────────────────────────────────
/my:
get:
tags: [Policyholder]
summary: Inspect policyholder identity
description: Alias of `GET /my/identity`. The policyholder is derived only from the API key.
x-required-scopes: []
parameters:
- $ref: "#/components/parameters/IfNoneMatch"
responses:
"200": { $ref: "#/components/responses/PolicyholderIdentity" }
"304": { $ref: "#/components/responses/NotModified" }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
/my/identity:
get:
tags: [Policyholder]
summary: Inspect policyholder identity and effective scopes
description: Returns safe key metadata and the immutable policyholder identity selected by the credential.
x-required-scopes: []
parameters:
- $ref: "#/components/parameters/IfNoneMatch"
responses:
"200": { $ref: "#/components/responses/PolicyholderIdentity" }
"304": { $ref: "#/components/responses/NotModified" }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
/my/profile:
get:
tags: [Policyholder]
summary: Read the policyholder profile
description: "Required scope: `policies:read`."
x-required-scopes: [policies:read]
parameters:
- $ref: "#/components/parameters/IfNoneMatch"
responses:
"200":
description: Profile
headers:
ETag: { $ref: "#/components/headers/ETag" }
content:
application/json:
schema: { $ref: "#/components/schemas/PolicyholderProfileResponse" }
"304": { $ref: "#/components/responses/NotModified" }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
"404": { $ref: "#/components/responses/NotFound" }
/my/policies:
get:
tags: [Policyholder]
summary: List policies
description: >-
Required scope: `policies:read`. The policyholder is selected exclusively by
the API key. When omitted, this endpoint preserves its established legacy
coverage-row representation and legacy IDs. `selection=latest` is an additive
clean-book projection with one latest-known term per policy number and related
coverages; `selection=history` is an additive term-snapshot projection. A reported
premium is returned only when one source
amount unambiguously supports that selected term; it is never a sum of transaction
rows. Net agency premiums, commissions, and wholesale controls are not exposed.
Clean-book projections group the complete owned source authority before applying
`updated_since` filtering and pagination. A current complete base term takes
priority over a future renewal. Active status requires verified base-term
evidence with both effective and expiration dates; a single date is insufficient.
x-required-scopes: [policies:read]
parameters:
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PolicyholderLimit"
- $ref: "#/components/parameters/UpdatedSince"
- $ref: "#/components/parameters/PolicyBookSelection"
- $ref: "#/components/parameters/IfNoneMatch"
responses:
"200": { $ref: "#/components/responses/PolicyholderPolicyCollection" }
"304": { $ref: "#/components/responses/NotModified" }
"400": { $ref: "#/components/responses/ValidationError" }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
/my/policies/{id}:
get:
tags: [Policyholder]
summary: Read policy detail
description: "Required scope: `policies:read`. Only a policy owned by the key's policyholder can be returned. Omit `selection` for the established legacy detail representation; legacy row IDs and clean-book stable IDs are accepted as aliases. Pass `selection=history` only for an ID returned by the historical policy list."
x-required-scopes: [policies:read]
parameters:
- $ref: "#/components/parameters/OwnedResourceId"
- $ref: "#/components/parameters/PolicyBookSelection"
- $ref: "#/components/parameters/IfNoneMatch"
responses:
"200": { $ref: "#/components/responses/PolicyholderResource" }
"304": { $ref: "#/components/responses/NotModified" }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
"404": { $ref: "#/components/responses/NotFound" }
/my/orders:
get:
tags: [Policyholder]
summary: List policy order records
description: "Required scope: `policies:read`. Results are capped at 100 source rows."
x-required-scopes: [policies:read]
parameters:
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PolicyholderLimit"
- $ref: "#/components/parameters/UpdatedSince"
- $ref: "#/components/parameters/IfNoneMatch"
responses:
"200": { $ref: "#/components/responses/PolicyholderCollection" }
"304": { $ref: "#/components/responses/NotModified" }
"400": { $ref: "#/components/responses/ValidationError" }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
/my/quotes:
get:
tags: [Policyholder]
summary: List quotes
description: "Required scope: `quotes:read`. Results are capped at 100 source rows."
x-required-scopes: [quotes:read]
parameters:
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PolicyholderLimit"
- $ref: "#/components/parameters/UpdatedSince"
- $ref: "#/components/parameters/IfNoneMatch"
responses:
"200": { $ref: "#/components/responses/PolicyholderCollection" }
"304": { $ref: "#/components/responses/NotModified" }
"400": { $ref: "#/components/responses/ValidationError" }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
/my/documents:
get:
tags: [Policyholder]
summary: List document metadata
description: "Required scope: `documents:read`. Only explicitly owned, insured-facing documents are returned. Audience filtering precedes pagination and totals; sourceCap is null and sourceTruncated is false. Internal correspondence and ambiguous legacy files are excluded. policyNumber is null unless supported by an owned policy or authentic issued-package provenance. downloadUrl is null when no supported stored file reference is on record; a stored reference does not guarantee the bytes remain retrievable."
x-required-scopes: [documents:read]
parameters:
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PolicyholderLimit"
- $ref: "#/components/parameters/UpdatedSince"
- $ref: "#/components/parameters/IfNoneMatch"
- name: document_type
in: query
schema: { type: string, minLength: 1 }
description: Exact document-type filter.
responses:
"200": { $ref: "#/components/responses/PolicyholderCollection" }
"304": { $ref: "#/components/responses/NotModified" }
"400": { $ref: "#/components/responses/ValidationError" }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
/my/documents/{id}:
get:
tags: [Policyholder]
summary: Read document detail
description: "Required scope: `documents:read`. The same ownership and insured-facing audience gate applies to detail and download. Metadata includes the authenticated download URL, or null when no supported stored file reference is on record."
x-required-scopes: [documents:read]
parameters:
- $ref: "#/components/parameters/OwnedResourceId"
- $ref: "#/components/parameters/IfNoneMatch"
responses:
"200": { $ref: "#/components/responses/PolicyholderResource" }
"304": { $ref: "#/components/responses/NotModified" }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
"404": { $ref: "#/components/responses/NotFound" }
/my/documents/{id}/download:
get:
tags: [Policyholder]
summary: Download document bytes
description: "Required scope: `documents:read`. Returns a private, non-cacheable attachment and never redirects to an unauthenticated object URL."
x-required-scopes: [documents:read]
parameters:
- $ref: "#/components/parameters/OwnedResourceId"
responses:
"200":
description: Document attachment
headers:
Content-Disposition:
schema: { type: string }
X-Content-Type-Options:
schema: { type: string, const: nosniff }
content:
application/octet-stream:
schema: { type: string, format: binary }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
"404": { $ref: "#/components/responses/NotFound" }
"503":
description: Document temporarily unavailable; retry later
content:
application/json:
schema: { $ref: "#/components/schemas/Error" }
/my/agent:
get:
tags: [Policyholder]
summary: Read the servicing agent's public contact details
description: "Required scope: `agents:read`. `data` is null when no agent is linked."
x-required-scopes: [agents:read]
parameters:
- $ref: "#/components/parameters/IfNoneMatch"
responses:
"200": { $ref: "#/components/responses/PolicyholderResource" }
"304": { $ref: "#/components/responses/NotModified" }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
/my/claims:
get:
tags: [Policyholder]
summary: List claims
description: "Required scope: `claims:read`. Results are capped at 100 source rows."
x-required-scopes: [claims:read]
parameters:
- $ref: "#/components/parameters/Page"
- $ref: "#/components/parameters/PolicyholderLimit"
- $ref: "#/components/parameters/UpdatedSince"
- $ref: "#/components/parameters/IfNoneMatch"
responses:
"200": { $ref: "#/components/responses/PolicyholderCollection" }
"304": { $ref: "#/components/responses/NotModified" }
"400": { $ref: "#/components/responses/ValidationError" }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
/my/prevention:
get:
tags: [Policyholder]
summary: Read prevention-service destinations
description: "Required scope: `prevention:read`."
x-required-scopes: [prevention:read]
parameters:
- $ref: "#/components/parameters/IfNoneMatch"
responses:
"200": { $ref: "#/components/responses/PolicyholderResource" }
"304": { $ref: "#/components/responses/NotModified" }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
/studioguard/policy-forms/{formId}/authoritative-pdfs:
parameters:
- name: formId
in: path
required: true
schema: { type: integer, minimum: 1 }
get:
operationId: getPolicyFormAuthoritativePdfs
tags: [Policy Forms]
summary: Read the current authoritative PDF pairing and history
description: |
First-party only. Requires the exact `policy_forms:authoritative_pdf_read`
scope; `policy_forms:read`, write, or approval scopes do not imply it.
Download URLs in this response point back to this scoped Platform API.
security: [{ BearerAuth: [] }, { ApiKeyAuth: [] }]
responses:
"200":
description: Current exact-source pairing and immutable history
headers:
Cache-Control: { schema: { type: string, const: no-store } }
content:
application/json:
schema: { $ref: "#/components/schemas/AuthoritativePdfStatus" }
"400": { $ref: "#/components/responses/ValidationError" }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
"409": { $ref: "#/components/responses/Conflict" }
post:
operationId: acceptPolicyFormAuthoritativePdf
tags: [Policy Forms]
summary: Pair or replace an authoritative PDF
description: |
First-party only and requires the exact
`policy_forms:authoritative_pdf_write` scope. This operation never grants
approval authority. It binds the uploaded bytes to the exact current
source document ID and SHA-256, uses `expectedPairingId` as compare-and-swap
(`none` means no current pairing), retains replaced pairings as history,
and suspends prior approval pending the normal four-step review.
Send `multipart/form-data` with one PDF (maximum 20 MiB). The PDF must
be structurally safe, unencrypted, contain no active content, and have
printed form-number/edition identity matching the current source.
A 202 means the pairing committed but post-commit foundation preparation
failed; callers must refresh authority and must not assume rollback.
security: [{ BearerAuth: [] }, { ApiKeyAuth: [] }]
requestBody:
required: true
content:
multipart/form-data:
schema: { $ref: "#/components/schemas/AuthoritativePdfInput" }
responses:
"201":
description: Pairing committed and foundation prepared; no approval granted
content:
application/json:
schema: { $ref: "#/components/schemas/AuthoritativePdfMutationResult" }
"202":
description: Pairing committed, approval suspended, but post-commit foundation preparation failed
content:
application/json:
schema: { $ref: "#/components/schemas/AuthoritativePdfPostCommitReceipt" }
"400": { $ref: "#/components/responses/ValidationError" }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
"404": { $ref: "#/components/responses/NotFound" }
"409": { $ref: "#/components/responses/Conflict" }
"413":
description: PDF exceeds 20 MiB
"422": { $ref: "#/components/responses/ValidationError" }
/studioguard/policy-forms/{formId}/authoritative-pdfs/native-source:
parameters:
- name: formId
in: path
required: true
schema: { type: integer, minimum: 1 }
post:
operationId: acceptPolicyFormNativePdfSource
tags: [Policy Forms]
summary: Create or replace a native PDF source and authoritative pairing
description: |
PDF-first authoring path. The selected PDF becomes both source identity
and exact geometry evidence; no DOCX is required or fabricated. Existing
source and pairing evidence is retained as superseded lineage and all
four-step approval must be completed again.
security: [{ BearerAuth: [] }, { ApiKeyAuth: [] }]
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
additionalProperties: false
required: [file, expectedPairingId, confirmPairing]
properties:
file: { type: string, format: binary }
expectedPairingId: { type: string, pattern: "^(none|[1-9][0-9]*)$" }
confirmPairing: { type: string, const: "true" }
note: { type: string, maxLength: 1000 }
responses:
"201":
description: Native PDF source and pairing committed; no approval granted
content:
application/json:
schema: { $ref: "#/components/schemas/AuthoritativePdfMutationResult" }
"202":
description: PDF source and pairing committed but foundation preparation failed
content:
application/json:
schema: { $ref: "#/components/schemas/AuthoritativePdfPostCommitReceipt" }
"400": { $ref: "#/components/responses/ValidationError" }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
"404": { $ref: "#/components/responses/NotFound" }
"409": { $ref: "#/components/responses/Conflict" }
"413": { description: PDF exceeds 20 MiB }
"422": { $ref: "#/components/responses/ValidationError" }
/studioguard/policy-forms/{formId}/authoritative-pdfs/{pairingId}/download:
get:
operationId: downloadPolicyFormAuthoritativePdf
tags: [Policy Forms]
summary: Download verified authoritative PDF bytes
description: |
Downloads a current or historical pairing only after exact size and
SHA-256 readback verification. First-party only; requires the exact
`policy_forms:authoritative_pdf_read` scope.
security: [{ BearerAuth: [] }, { ApiKeyAuth: [] }]
parameters:
- name: formId
in: path
required: true
schema: { type: integer, minimum: 1 }
- name: pairingId
in: path
required: true
schema: { type: integer, minimum: 1 }
responses:
"200":
description: Verified original PDF bytes
headers:
Content-Disposition: { schema: { type: string } }
Cache-Control: { schema: { type: string, const: "private, no-store" } }
content:
application/pdf:
schema: { type: string, format: binary }
"400": { $ref: "#/components/responses/ValidationError" }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
"404": { $ref: "#/components/responses/NotFound" }
"409": { $ref: "#/components/responses/Conflict" }
# These browser-session endpoints issue and manage the signed-in user's
# credentials. They are intentionally outside the /api/v1 API-key surface.
/api/platform/my-api-keys:
servers:
- url: https://covercore.services
description: Production web application
get:
tags: [Credential Self-Service]
summary: List the signed-in user's API keys
description: For policyholder sessions, returns only policyholder-scoped keys whose immutable policyholder ID matches the account currently linked to the session.
security: [{ CookieSession: [] }]
responses:
"200":
description: Safe key metadata; secret values are never returned
content:
application/json:
schema:
type: array
items: { $ref: "#/components/schemas/SelfServiceApiKey" }
"401": { $ref: "#/components/responses/Unauthorized" }
post:
tags: [Credential Self-Service]
summary: Issue an API key
description: |
Requires an authenticated browser session. For a policyholder account,
authority is linked server-side, requested scopes must be a subset of
the six policyholder read scopes, and omitted scopes grant that complete
set. `fullKey` is disclosed exactly once in the 201 response.
security: [{ CookieSession: [] }]
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/SelfServiceApiKeyCreate" }
responses:
"201":
description: Key issued; store fullKey securely because it cannot be recovered
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/SelfServiceApiKey"
- type: object
required: [fullKey]
properties:
fullKey:
type: string
readOnly: true
description: One-time credential value. Returned only by this creation response and never recoverable later.
"400": { $ref: "#/components/responses/ValidationError" }
"401": { $ref: "#/components/responses/Unauthorized" }
"403": { $ref: "#/components/responses/Forbidden" }
/api/platform/my-api-keys/{id}:
servers:
- url: https://covercore.services
description: Production web application
get:
tags: [Credential Self-Service]
summary: Inspect an owned API key
description: Returns safe metadata only. The one-time secret and key hash are never returned. Policyholder sessions receive 404 for legacy, unscoped, or differently linked keys even when partnerId matches the user.
security: [{ CookieSession: [] }]
parameters:
- $ref: "#/components/parameters/OwnedResourceId"
responses:
"200":
description: Safe metadata for the owned key
content:
application/json:
schema: { $ref: "#/components/schemas/SelfServiceApiKey" }
"401": { $ref: "#/components/responses/Unauthorized" }
"404": { $ref: "#/components/responses/NotFound" }
patch:
tags: [Credential Self-Service]
summary: Update an owned API key
description: Policyholder sessions may update non-authority metadata only; authority fields, scopes, and rate limits are immutable and rejected when supplied.
security: [{ CookieSession: [] }]
parameters:
- $ref: "#/components/parameters/OwnedResourceId"
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/SelfServiceApiKeyUpdate" }
responses:
"200":
description: Updated safe key metadata
content:
application/json:
schema: { $ref: "#/components/schemas/SelfServiceApiKey" }
"400": { $ref: "#/components/responses/ValidationError" }
"401": { $ref: "#/components/responses/Unauthorized" }
"404": { $ref: "#/components/responses/NotFound" }
delete:
tags: [Credential Self-Service]
summary: Revoke an owned API key
security: [{ CookieSession: [] }]
parameters:
- $ref: "#/components/parameters/OwnedResourceId"
responses:
"200":
description: Key revoked
content:
application/json:
schema:
type: object
required: [success, id, isActive]
properties:
success: { type: boolean, const: true }
id: { type: string }
isActive: { type: boolean, const: false }
"401": { $ref: "#/components/responses/Unauthorized" }
"404": { $ref: "#/components/responses/NotFound" }
"409": { $ref: "#/components/responses/Conflict" }
components:
x-authoritative-pdf-schemas: &authoritativePdfSchemas
AuthoritativePdfInput: &authoritativePdfInput
type: object
additionalProperties: false
required:
[file, expectedSourceDocumentId, expectedSourceSha256, expectedPairingId, confirmPairing]
properties:
file:
type: string
format: binary
description: One `.pdf` file, at most 20 MiB.
expectedSourceDocumentId:
type: integer
minimum: 1
expectedSourceSha256:
type: string
pattern: "^[a-f0-9]{64}$"
expectedPairingId:
type: string
pattern: "^(none|[1-9][0-9]*)$"
description: Current pairing ID, or `none` when no pairing currently exists.
confirmPairing:
type: string
const: "true"
description: Explicitly confirms exact-source pairing and four-step reapproval reset.
note:
type: string
maxLength: 1000
AuthoritativePdfSource: &authoritativePdfSource
type: object
required: [id, sha256, originalFilename, mimeType, sourceKind]
properties:
id: { type: integer }
sha256: { type: string, pattern: "^[a-f0-9]{64}$" }
originalFilename: { type: string }
mimeType: { type: string }
sourceKind: { type: string, enum: [docx, native_pdf] }
AuthoritativePdfPairing: &authoritativePdfPairing
type: object
required:
[id, status, sourceDocumentId, sourceSha256, originalFilename, mimeType,
fileSizeBytes, pdfSha256, pageCount, pageSizesPt, normalizedFormNumber,
identityEvidencePages, acceptedBy, acceptedByRole, acceptedAt,
supersedesPairingId, downloadUrl]
properties:
id: { type: integer }
status: { type: string, enum: [current, superseded] }
sourceDocumentId: { type: integer }
sourceSha256: { type: string, pattern: "^[a-f0-9]{64}$" }
originalFilename: { type: string }
mimeType: { type: string, const: application/pdf }
fileSizeBytes: { type: integer }
pdfSha256: { type: string, pattern: "^[a-f0-9]{64}$" }
pageCount: { type: integer }
pageSizesPt:
type: array
items:
type: array
prefixItems: [{ type: number }, { type: number }]
minItems: 2
maxItems: 2
normalizedFormNumber: { type: string }
normalizedEdition: { type: ["string", "null"] }
identityEvidencePages:
type: array
items: { type: integer }
acceptedBy: { type: string }
acceptedByRole: { type: string }
acceptedAt: { type: ["string", "null"], format: date-time }
note: { type: ["string", "null"] }
supersedesPairingId: { type: ["integer", "null"] }
downloadUrl: { type: string }
AuthoritativePdfStatus: &authoritativePdfStatus
type: object
required:
[schema, formId, sourceDocument, current, history, authoringResetRequired]
properties:
schema: { type: string, const: governed_authoritative_pdf_pairing_v1 }
formId: { type: integer }
sourceDocument:
anyOf:
- $ref: "#/components/schemas/AuthoritativePdfSource"
- type: "null"
current:
anyOf:
- $ref: "#/components/schemas/AuthoritativePdfPairing"
- type: "null"
history:
type: array
items: { $ref: "#/components/schemas/AuthoritativePdfPairing" }
authoringResetRequired: { type: boolean }
AuthoritativePdfMutationResult: &authoritativePdfMutationResult
allOf:
- $ref: "#/components/schemas/AuthoritativePdfStatus"
- type: object
required: [pairingCommitted, foundationPreparation, approvalGranted]
properties:
pairingCommitted: { type: boolean, const: true }
foundationPreparation: { type: string, enum: [ready, failed] }
approvalGranted: { type: boolean, const: false }
preparationError:
type: object
required: [code, message]
properties:
code: { type: string }
message: { type: string }
AuthoritativePdfPostCommitReceipt: &authoritativePdfPostCommitReceipt
type: object
required:
[formId, committedPairingId, pairingCommitted, foundationPreparation,
statusRefresh, approvalGranted, preparationError]
properties:
formId: { type: integer }
committedPairingId:
type: integer
description: Exact pairing inserted by this request; never inferred from file hash.
pairingCommitted: { type: boolean, const: true }
foundationPreparation: { type: string, const: failed }
statusRefresh: { type: string, enum: [ready, failed] }
approvalGranted: { type: boolean, const: false }
status:
$ref: "#/components/schemas/AuthoritativePdfStatus"
description: Optional refreshed status; it may show a newer concurrent selection.
preparationError:
type: object
required: [code, message]
properties:
code: { type: string }
message: { type: string }
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: pk_live_/pk_test_
description: API credentials use `pk_live_*` or `pk_test_*`. Grants are additionally checked against each operation's scopes and any restricted-principal route/method matrix.
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
description: Supply the same one-time `pk_live_*` or `pk_test_*` credential. Grants are additionally checked against scopes and any restricted-principal route/method matrix.
CookieSession:
type: apiKey
in: cookie
name: connect.sid
description: CoverCore browser session. Used only by credential self-service routes; API keys cannot call them.
parameters:
Page:
name: page
in: query
schema: { type: integer, minimum: 1, default: 1 }
description: One-based page number.
PolicyholderLimit:
name: limit
in: query
schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
description: Page size; the maximum is 100.
UpdatedSince:
name: updated_since
in: query
schema: { type: string, format: date-time }
description: Include rows updated at or after this RFC 3339 timestamp. A timezone is required.
PolicyBookSelection:
name: selection
in: query
schema:
type: string
enum: [latest, history]
description: "Optional additive policy-book projection. `latest` returns one latest-known term per policy number; `history` returns term snapshots. When omitted, the established legacy coverage-row representation and IDs are returned. This never changes key-derived ownership."
IfNoneMatch:
name: If-None-Match
in: header
schema: { type: string }
description: Send a previously returned ETag. An unchanged representation returns 304 with no body.
OwnedResourceId:
name: id
in: path
required: true
schema: { type: string }
description: Opaque resource ID. Ownership is enforced from the credential or session.
IdempotencyKey:
name: Idempotency-Key
in: header
required: false
schema: { type: string, maxLength: 200 }
description: |
Optional UUID. Successful (2xx) responses are cached for 24h
keyed by `(api_key_id, idempotency_key)`. Replays return the
same body with `Idempotent-Replayed: true`.
IdempotencyKeyRequired:
name: Idempotency-Key
in: header
required: true
schema: { type: string, minLength: 1, maxLength: 200 }
description: |
Required where referenced. Reuse the same value when retrying the same
logical request; a conflicting payload returns 409.
PlayKey:
name: playKey
in: path
required: true
schema: { type: string, pattern: "^[a-z0-9][a-z0-9-]*$" }
description: Stable play key (for example, `coi-proof` or `loss-runs`).
ServiceTicketId:
name: id
in: path
required: true
schema: { type: string }
description: Service ticket ID visible to the caller.
ChatEscalationId:
name: id
in: path
required: true
schema: { type: string }
description: Chat escalation ID.
responses:
PolicyholderIdentity:
description: Immutable policyholder identity and safe credential metadata
headers:
ETag: { $ref: "#/components/headers/ETag" }
content:
application/json:
schema: { $ref: "#/components/schemas/PolicyholderIdentityResponse" }
PolicyholderCollection:
description: Policyholder-owned collection
headers:
ETag: { $ref: "#/components/headers/ETag" }
content:
application/json:
schema: { $ref: "#/components/schemas/PolicyholderCollectionResponse" }
PolicyholderPolicyCollection:
description: Policyholder-owned policy book
headers:
ETag: { $ref: "#/components/headers/ETag" }
content:
application/json:
schema: { $ref: "#/components/schemas/PolicyholderPolicyCollectionResponse" }
PolicyholderResource:
description: Policyholder-owned resource
headers:
ETag: { $ref: "#/components/headers/ETag" }
content:
application/json:
schema:
type: object
required: [data]
properties:
data:
oneOf:
- { $ref: "#/components/schemas/PolicyholderResource" }
- { type: "null" }
NotModified:
description: Representation unchanged; response has no body
headers:
ETag: { $ref: "#/components/headers/ETag" }
Unauthorized:
description: Missing/invalid API key
content:
application/json:
schema: { $ref: "#/components/schemas/Error" }
Forbidden:
description: Insufficient scope or cross-tenant access
content:
application/json:
schema: { $ref: "#/components/schemas/Error" }
ChatEscalationForbidden:
description: |
Chat escalation eligibility failure: `LIVE_KEY_REQUIRED` for sandbox,
`FIRST_PARTY_KEY_REQUIRED` for external/agent-scoped/unknown partner
types, or `STAFF_OWNER_REQUIRED` for a missing, deleted, or ineligible
key owner. Policyholder-scoped keys are rejected earlier with
`POLICYHOLDER_KEY_RESTRICTED`. Audit metadata records browser vs
platform_api, staff ID, and safe key name/prefix, never the credential.
content:
application/json:
schema: { $ref: "#/components/schemas/Error" }
NotFound:
description: Resource not found
content:
application/json:
schema: { $ref: "#/components/schemas/Error" }
Conflict:
description: Race or idempotency conflict
content:
application/json:
schema: { $ref: "#/components/schemas/Error" }
UnprocessableEntity:
description: Business-rule violation (e.g. AMOUNT_EXCEEDS_BALANCE)
content:
application/json:
schema: { $ref: "#/components/schemas/Error" }
ValidationError:
description: Request body failed Zod validation
content:
application/json:
schema: { $ref: "#/components/schemas/Error" }
ExternalQuoteImportValidationError:
description: |
Import or submission-filter validation failed. Each `fieldErrors`
entry has a full nested `path`; bulk paths preserve the zero-based item
index (for example, `quotes.3.ownership.contactId`) rather than
collapsing a single/bulk union error into an empty object.
content:
application/json:
schema: { $ref: "#/components/schemas/ExternalQuoteImportValidationErrorResponse" }
EvaluationRequired:
description: |
Returned by every `/rate/*` route when `evaluation_id` is
missing, stale, cross-tenant, or in a non-accepted state. The
canonical code is `EVALUATION_REQUIRED` and the body's
`details.reason` is one of `missing_evaluation_id`,
`not_found`, `wrong_partner`, `environment_mismatch`,
`not_approved`.
content:
application/json:
schema: { $ref: "#/components/schemas/Error" }
headers:
ETag:
schema: { type: string }
description: Strong validator for this authenticated representation. Responses also vary on Authorization and X-API-Key.
schemas:
ThumbtackSearchInput:
type: object
required: [zipCode]
anyOf: [{required: [categoryId]}, {required: [query]}]
properties:
zipCode: {type: string, pattern: '^[0-9]{5}$', description: Must exist in the US ZIP dataset}
categoryId: {type: string, pattern: '^[a-zA-Z0-9_-]{1,100}$'}
query: {type: string, minLength: 1, maxLength: 200}
limit: {type: integer, minimum: 1, maximum: 20, default: 10}
ThumbtackQuickSearchInput:
type: object
required: [zipCode]
properties:
zipCode: {type: string, pattern: '^[0-9]{5}$'}
limit: {type: integer, minimum: 1, maximum: 20, default: 10}
ThumbtackSettingsInput:
type: object
properties:
settings: {type: array, items: {type: object}}
sync_config: {type: object}
ThumbtackLeadInput:
type: object
required: [businesses, category_id, zip_code]
properties:
category_id: {type: string}
zip_code: {type: string, pattern: '^[0-9]{5}$'}
user_id: {type: string, description: Ignored; owner is the authenticated key creator}
businesses: {type: array, minItems: 1, maxItems: 100, items: {type: object, required: [businessID, businessName], properties: {businessID: {type: string}, businessName: {type: string}}}}
AuthoritativePdfInput: *authoritativePdfInput
AuthoritativePdfSource: *authoritativePdfSource
AuthoritativePdfPairing: *authoritativePdfPairing
AuthoritativePdfStatus: *authoritativePdfStatus
AuthoritativePdfMutationResult: *authoritativePdfMutationResult
AuthoritativePdfPostCommitReceipt: *authoritativePdfPostCommitReceipt
Error:
type: object
required: [error]
properties:
error:
type: object
required: [code, message, retryable]
properties:
code: { type: string, example: EVALUATION_REQUIRED }
message: { type: string }
retryable:
type: boolean
description: True when retrying later may succeed (408, 409, 429, and 5xx).
details: {}
PolicyholderIdentityResponse:
type: object
required: [data]
properties:
data:
type: object
required: [identity, key, effectiveScopes]
properties:
identity:
type: object
required: [type, id]
properties:
type: { type: string, const: policyholder }
id: { type: string }
displayName: { type: [string, "null"] }
email: { type: [string, "null"], format: email }
key:
type: object
required: [id, keyPrefix, environment, partnerType, policyholderScoped]
properties:
id: { type: string }
keyPrefix: { type: string }
environment: { type: string, enum: [live, sandbox] }
partnerType: { type: string, const: policyholder }
policyholderScoped: { type: boolean, const: true }
effectiveScopes:
type: array
items:
type: string
enum: [policies:read, quotes:read, documents:read, agents:read, claims:read, prevention:read]
PolicyholderProfileResponse:
type: object
required: [data]
properties:
data:
type: object
required: [id, address]
properties:
id: { type: string }
displayName: { type: [string, "null"] }
email: { type: [string, "null"], format: email }
phone: { type: [string, "null"] }
address:
type: object
properties:
street: { type: [string, "null"] }
city: { type: [string, "null"] }
state: { type: [string, "null"] }
postalCode: { type: [string, "null"] }
industry: { type: [string, "null"] }
businessDescription: { type: [string, "null"] }
website: { type: [string, "null"] }
effectiveDate: { type: [string, "null"] }
updatedAt: { type: [string, "null"], format: date-time }
PolicyholderCreateRequest:
type: object
required: [name]
additionalProperties: false
description: |
Agent keys may omit both ownership fields and use the key's immutable
agency/contact binding. Other callers must provide ownership or ownershipFrom.
properties:
name: { type: string, minLength: 1, maxLength: 255 }
dba: { type: [string, "null"], maxLength: 255 }
email: { type: [string, "null"], format: email, maxLength: 255 }
phone: { type: [string, "null"], maxLength: 50 }
mailingAddress:
type: [object, "null"]
additionalProperties: false
properties:
line1: { type: [string, "null"], maxLength: 255 }
city: { type: [string, "null"], maxLength: 120 }
state: { type: [string, "null"], maxLength: 120 }
postalCode: { type: [string, "null"], maxLength: 30 }
country: { type: [string, "null"], enum: [US, null] }
ownership:
type: object
additionalProperties: false
required: [agencyId, agentId]
properties:
agencyId: { type: string }
agentId:
type: string
description: Canonical contacts.id; agents.id and contacts.conId are not accepted.
ownershipFrom: { $ref: "#/components/schemas/OwnershipFrom" }
externalRefs:
type: array
maxItems: 20
description: Optional idempotent upsert references.
items:
type: object
additionalProperties: false
required: [source, externalId]
properties:
source: { type: string, minLength: 1, maxLength: 120 }
externalId: { type: string, minLength: 1, maxLength: 255 }
opportunityId: { type: [string, "null"] }
metadata:
type: object
additionalProperties: true
description: Partner metadata; serialized JSON is limited to 16,000 bytes.
PolicyholderOwnershipTransferRequest:
type: object
additionalProperties: false
required: [agencyId, agentId, reason]
properties:
agencyId:
type: string
minLength: 1
maxLength: 255
description: Existing agencies.id or its exact ptnUuid.
agentId:
type: string
minLength: 1
maxLength: 255
description: Existing canonical contacts.id belonging to agencyId.
reason:
type: string
minLength: 3
maxLength: 500
description: Durable audit reason for the ownership change.
idempotencyKey:
type: string
minLength: 1
maxLength: 200
description: May replace the Idempotency-Key header; must match it when both are sent.
OwnershipFrom:
type: object
additionalProperties: false
required: [carrier]
properties:
carrier: { type: string, example: travelers }
producer_or_agency_code: { type: [string, "null"], example: X1681 }
submitting_agent_name: { type: [string, "null"], example: Stuckey And Co }
fallback_agency_id: { type: [string, "null"] }
fallback_contact_id: { type: [string, "null"] }
OwnershipResolution:
type: object
additionalProperties: false
required: [agencyId, agencyPtnUuid, contactId, confidence, matched, warnings, candidates]
properties:
agencyId: { type: string, description: Canonical agencies.id }
agencyPtnUuid: { type: [string, "null"] }
contactId: { type: string, description: Canonical contacts.id }
confidence: { type: string, enum: [exact, fallback] }
matched:
type: object
additionalProperties: false
required: [agency, contact]
properties:
agency: { type: [string, "null"] }
contact: { type: [string, "null"] }
warnings:
type: array
items: { type: string }
candidates:
type: array
items: { type: object, additionalProperties: true }
PolicyholderMutationResponse:
type: object
required: [data]
properties:
data:
allOf:
- $ref: "#/components/schemas/PartnerPolicyholder"
- type: object
required: [syncStatus, actId]
properties:
syncStatus: { type: string, const: local_only }
actId: { type: "null" }
PartnerPolicyholderResponse:
type: object
required: [data]
properties:
data: { $ref: "#/components/schemas/PartnerPolicyholder" }
PartnerPolicyholder:
type: object
additionalProperties: false
required: [id, name, dba, email, phone, mailingAddress, ownership, externalRefs, opportunityId, metadata, syncStatus, actId]
properties:
id: { type: string }
name: { type: string }
dba: { type: [string, "null"] }
email: { type: [string, "null"], format: email }
phone: { type: [string, "null"] }
mailingAddress:
type: [object, "null"]
additionalProperties: false
required: [line1, city, state, postalCode, country]
properties:
line1: { type: [string, "null"] }
city: { type: [string, "null"] }
state: { type: [string, "null"] }
postalCode: { type: [string, "null"] }
country: { type: [string, "null"] }
ownership:
type: object
additionalProperties: false
required: [agencyId, agentId]
properties:
agencyId: { type: string }
agentId: { type: string }
externalRefs:
type: array
items:
type: object
additionalProperties: false
required: [source, externalId]
properties:
source: { type: string }
externalId: { type: string }
opportunityId: { type: [string, "null"] }
metadata: { type: [object, "null"], additionalProperties: true }
syncStatus: { type: string }
actId: { type: [string, "null"] }
PartnerPolicyholderListResponse:
type: object
required: [data, pagination]
properties:
data:
type: array
items: { $ref: "#/components/schemas/PartnerPolicyholderListItem" }
pagination:
type: object
required: [page, limit, total, totalPages]
additionalProperties: false
properties:
page: { type: integer, minimum: 1 }
limit: { type: integer, minimum: 1, maximum: 100 }
total: { type: integer, minimum: 0 }
totalPages: { type: integer, minimum: 0 }
PartnerPolicyholderListItem:
type: object
allOf:
- $ref: "#/components/schemas/PartnerPolicyholder"
PolicyholderResource:
type: object
description: Endpoint-specific read-only DTO. Unknown fields are preserved for forward compatibility.
additionalProperties: true
PolicyholderCollectionResponse:
type: object
required: [data, pagination]
properties:
data:
type: array
items: { $ref: "#/components/schemas/PolicyholderResource" }
pagination:
$ref: "#/components/schemas/PolicyholderPagination"
PolicyholderPolicyCollectionResponse:
type: object
required: [data, pagination]
properties:
data:
type: array
items: { $ref: "#/components/schemas/PolicyholderPolicy" }
pagination:
$ref: "#/components/schemas/PolicyholderPagination"
selection:
type: string
enum: [latest, history]
PolicyholderPolicy:
type: object
required: [id, source, coverageType, status]
properties:
id:
type: string
description: Clean projections use stable policy or policy-term identifiers. The unselected legacy representation retains legacy row IDs for compatibility.
source:
type: string
enum: [filemaker, quote_journey]
applicationReference:
type: [string, "null"]
description: Explicit application reference for quote-journey entries; not a policy number.
policyNumber: { type: [string, "null"] }
displayPolicyNumber: { type: [string, "null"] }
coverageType: { type: string }
displayCoverage: { type: string }
carrierName: { type: [string, "null"] }
quotedCarrierName:
type: [string, "null"]
description: Carrier named on an application/quote journey. It is not verified policy-carrier evidence.
effectiveDate: { type: [string, "null"] }
expirationDate: { type: [string, "null"] }
premium:
type: [number, "null"]
description: A single unambiguous source-reported amount for the selected term; never a calculated balance.
netPremium:
type: "null"
description: Reserved compatibility field. Net agency premium is never disclosed.
status:
type: string
enum: [active, expired, scheduled, cancelled, status_unknown]
relatedCoverages:
type: array
description: Other coverages supported by rows for the same policy number and selected term.
items:
type: object
required: [coverageType, status]
properties:
coverageType: { type: string }
effectiveDate: { type: [string, "null"] }
expirationDate: { type: [string, "null"] }
status: { type: string }
sourceStatus:
type: string
enum: [verified, incomplete, sync_mismatch]
description: Source evidence status for staff/support reconciliation. It does not assert coverage not present in synced source data.
transactionSummary:
type: [object, "null"]
properties:
count: { type: integer, minimum: 0 }
adjustmentCount: { type: integer, minimum: 0 }
grossPremiumActivity: { type: "null" }
creditsAndReversals: { type: "null" }
netPremiumActivity: { type: "null" }
transactions:
type: array
description: Present only in the unselected legacy representation. New clean-book list projections omit transaction rows; use owned history resources instead.
items:
type: object
additionalProperties: true
PolicyholderPagination:
type: object
required: [page, limit, total, totalPages, hasPreviousPage, hasNextPage, updatedSince, sourceCap, sourceTruncated]
properties:
page: { type: integer, minimum: 1 }
limit: { type: integer, minimum: 1, maximum: 100 }
total: { type: integer, minimum: 0 }
totalPages: { type: integer, minimum: 0 }
hasPreviousPage: { type: boolean }
hasNextPage: { type: boolean }
updatedSince: { type: [string, "null"], format: date-time }
sourceCap:
type: [integer, "null"]
description: Maximum rows considered from each backing source for legacy/raw resources. Null means the clean policy projection grouped the complete owned authority set.
sourceTruncated:
type: boolean
description: True when one or more backing sources had rows beyond the legacy/raw source cap; false with a null sourceCap means the clean policy projection is complete.
SelfServiceApiKey:
type: object
description: Safe API-key metadata. Hashes and recoverable secret material are never included.
required: [id, partnerId, partnerType, name, description, keyPrefix, environment, scopes, rateLimitPerMinute, rateLimitPerDay, isActive, lastUsedAt, expiresAt, contactEmails, agentScoped, agentUserId, policyholderScoped, policyholderId, createdAt, updatedAt]
properties:
id: { type: string }
partnerId: { type: string }
partnerType: { type: string }
name: { type: string }
description: { type: [string, "null"] }
keyPrefix: { type: string }
environment: { type: string, enum: [live, sandbox] }
scopes:
type: array
items: { type: string }
rateLimitPerMinute: { type: integer }
rateLimitPerDay: { type: integer }
expiresAt: { type: [string, "null"], format: date-time }
isActive: { type: boolean }
lastUsedAt: { type: [string, "null"], format: date-time }
contactEmails:
type: array
items: { type: string, format: email }
agentScoped: { type: boolean }
agentUserId: { type: [string, "null"] }
policyholderScoped: { type: boolean }
policyholderId: { type: [string, "null"] }
createdAt: { type: [string, "null"], format: date-time }
updatedAt: { type: [string, "null"], format: date-time }
SelfServiceApiKeyCreate:
type: object
additionalProperties: false
required: [name, environment]
properties:
name: { type: string, minLength: 1, maxLength: 120 }
environment: { type: string, enum: [live, sandbox] }
scopes:
type: array
uniqueItems: true
description: Mutable for eligible non-policyholder sessions; rejected as immutable for policyholder sessions.
items:
type: string
enum: [policies:read, quotes:read, documents:read, agents:read, claims:read, prevention:read]
expiresAt: { type: [string, "null"], format: date-time }
contactEmails:
type: array
items: { type: string, format: email }
rateLimitPerMinute:
type: integer
minimum: 1
description: Constrained server-side; policyholder keys always use the fixed policyholder limit.
rateLimitPerDay:
type: integer
minimum: 1
description: Constrained server-side; policyholder keys always use the fixed policyholder limit.
SelfServiceApiKeyUpdate:
type: object
additionalProperties: false
properties:
name: { type: string, minLength: 1, maxLength: 120 }
scopes:
type: array
uniqueItems: true
items:
type: string
enum: [policies:read, quotes:read, documents:read, agents:read, claims:read, prevention:read]
expiresAt: { type: [string, "null"], format: date-time }
contactEmails:
type: array
items: { type: string, format: email }
rateLimitPerMinute:
type: integer
minimum: 1
description: Immutable for policyholder keys.
rateLimitPerDay:
type: integer
minimum: 1
description: Immutable for policyholder keys.
Pagination:
type: object
properties:
page: { type: integer }
limit: { type: integer }
total: { type: integer }
totalPages: { type: integer }
PlaySummary:
type: object
required: [key, name, description, partnerVisible]
properties:
key: { type: string, example: coi-proof }
name: { type: string, example: Certificate of Insurance }
description: { type: string }
partnerVisible: { type: boolean }
PlayStep:
type: object
required: [key, title, order]
properties:
key: { type: string }
title: { type: string }
description: { type: string }
order: { type: integer, minimum: 1 }
PlayDetail:
allOf:
- $ref: "#/components/schemas/PlaySummary"
- type: object
required: [ownerRole, operationIds, definitionOfDone, steps]
properties:
ownerRole: { type: [string, "null"] }
operationIds:
type: array
items: { type: string }
definitionOfDone:
type: array
items: { type: string }
steps:
type: array
items: { $ref: "#/components/schemas/PlayStep" }
PlayListResponse:
type: object
required: [data, pagination]
properties:
data:
type: array
items: { $ref: "#/components/schemas/PlaySummary" }
pagination: { $ref: "#/components/schemas/Pagination" }
PlayRun:
type: object
required: [id, playKey, serviceTicketId, status, createdAt]
properties:
id: { type: string }
playKey: { type: string }
serviceTicketId: { type: string }
status:
type: string
enum: [pending, in_progress, completed, failed, cancelled]
actor: { type: string, enum: [human, agent] }
notes: { type: [string, "null"], maxLength: 2000 }
createdAt: { type: string, format: date-time }
updatedAt: { type: string, format: date-time }
completedAt: { type: [string, "null"], format: date-time }
TicketPlay:
type: object
required: [ticketId, play, runs]
properties:
ticketId: { type: string }
play: { $ref: "#/components/schemas/PlayDetail" }
runs:
type: array
items: { $ref: "#/components/schemas/PlayRun" }
InternalPlayRunRequest:
type: object
additionalProperties: false
required: [playKey, actor, status]
properties:
playKey: { type: string }
actor: { type: string, enum: [human, agent] }
status: { type: string, enum: [pending, in_progress, completed, failed, cancelled] }
notes: { type: string, maxLength: 2000 }
PartnerPlayStartRequest:
type: object
additionalProperties: false
required: [subject, description]
properties:
subject: { type: string, minLength: 1 }
description: { type: string, minLength: 1 }
policyholderId: { type: string }
policyNumber: { type: string }
contactName: { type: string }
contactEmail: { type: string, format: email }
PartnerStartedTicket:
type: object
required: [id, ticketType, subject, description, status, agencyId, playKey]
properties:
id: { type: string }
ticketType: { type: string, enum: [coi_request, loss_run] }
subject: { type: string }
description: { type: string }
status: { type: string }
policyholderId: { type: [string, "null"] }
policyNumber: { type: [string, "null"] }
agencyId: { type: string }
contactName: { type: [string, "null"] }
contactEmail: { type: [string, "null"], format: email }
playKey: { type: string, enum: [coi-proof, loss-runs] }
createdAt: { type: [string, "null"], format: date-time }
updatedAt: { type: [string, "null"], format: date-time }
PartnerPlayStartResponse:
type: object
required: [data, created, deduplicated]
properties:
data: { $ref: "#/components/schemas/PartnerStartedTicket" }
created: { type: boolean }
deduplicated: { type: boolean }
ApiKeyInfo:
type: object
properties:
id: { type: string }
partnerId: { type: string }
partnerType: { type: string, example: agency }
name: { type: string }
keyPrefix: { type: string }
environment: { type: string, enum: [live, sandbox] }
scopes:
type: array
items: { type: string }
rateLimitPerMinute: { type: integer }
rateLimitPerDay: { type: integer }
isActive: { type: boolean }
agencyId: { type: [string, "null"], description: Bound canonical agencies.id for agent keys }
contactId: { type: [string, "null"], description: Bound canonical contacts.id for agent keys }
agency:
type: [object, "null"]
additionalProperties: false
properties:
id: { type: string }
ptnUuid: { type: [string, "null"] }
name: { type: [string, "null"] }
contact:
type: [object, "null"]
additionalProperties: false
properties:
id: { type: string }
name: { type: [string, "null"] }
email: { type: [string, "null"], format: email }
Job:
type: object
properties:
id: { type: string }
type: { type: string, example: score.creator-intelligence }
status:
type: string
enum: [queued, running, succeeded, failed]
environment: { type: string, enum: [live, sandbox] }
request: {}
result: {}
resultUrl: { type: string, nullable: true }
error: { type: string, nullable: true }
createdAt: { type: string, format: date-time }
startedAt: { type: string, nullable: true, format: date-time }
completedAt: { type: string, nullable: true, format: date-time }
# ─── Score ────────────────────────────────────────────────────────
CreatorScoreRequest:
type: object
description: |
At least one identifier is required from the union
`email` / `youtubeHandle` / `instagramHandle` / `tiktokHandle`
/ `twitterHandle` / `linkedinUrl` / `websiteUrl`.
properties:
email: { type: string, format: email }
youtubeHandle: { type: string }
instagramHandle: { type: string }
tiktokHandle: { type: string }
twitterHandle: { type: string }
linkedinUrl: { type: string, format: uri }
websiteUrl: { type: string }
physicalAddress: { type: string }
contactName: { type: string }
businessName: { type: string }
webhookUrl: { type: string, format: uri, nullable: true }
WeatherScoreRequest:
type: object
required: [state, zip]
properties:
address: { type: string }
city: { type: string }
state: { type: string, minLength: 2, maxLength: 2, example: OR }
zip: { type: string, example: "97202" }
latitude: { type: number, minimum: -90, maximum: 90 }
longitude: { type: number, minimum: -180, maximum: 180 }
CrimeScoreRequest:
type: object
required: [address, city, state, zip]
properties:
address: { type: string }
city: { type: string }
state: { type: string, minLength: 2, maxLength: 2 }
zip: { type: string }
companyName: { type: string }
AutoFatalityScoreRequest:
type: object
description: |
Provide either address mode (`address`+`city`+`state`+`zip`)
OR coordinates mode (`lat`+`lng`+`radius_meters`), not both.
properties:
address: { type: string }
city: { type: string }
state: { type: string, minLength: 2, maxLength: 2 }
zip: { type: string }
lat: { type: number, minimum: -90, maximum: 90 }
lng: { type: number, minimum: -180, maximum: 180 }
radius_meters: { type: number, exclusiveMinimum: 0, maximum: 50000 }
companyName: { type: string }
ScoreResultGeneric:
type: object
description: Score + band + risk-specific factors. Shape varies per scorer.
additionalProperties: true
# ─── Underwriting ─────────────────────────────────────────────────
UnderwritingRequest:
type: object
description: |
Either `fixture_key` (sandbox only — references one of the 15
golden fixtures in `server/platformApi/underwriting/fixtures.ts`)
OR `application` is required.
properties:
fixture_key: { type: string, example: clean_accept }
application:
type: object
required: [company_name, address, requested_lines]
properties:
company_name: { type: string }
address:
type: object
required: [street, city, state, zip]
properties:
street: { type: string }
city: { type: string }
state:
type: string
minLength: 2
maxLength: 2
description: |
2-letter region code. For a domestic application
(country omitted or a US token) this MUST be a valid
U.S. state code. For an explicitly non-US `country`,
any 2-letter region is accepted (e.g. `ON`) and the
jurisdiction gate returns `unsupported_country`.
zip: { type: string }
country:
type: string
minLength: 2
maxLength: 56
description: |
Optional. Omitted or a US token (`US`, `USA`,
`United States`) = domestic. A non-US value (e.g.
`Canada`) routes the evaluation to the jurisdiction
gate → `unsupported_country` decline.
industry_class: { type: string }
annual_revenue: { type: number, minimum: 0 }
revenue_self_reported: { type: number, minimum: 0 }
num_losses_in_prev_five_years: { type: integer, minimum: 0, maximum: 99 }
largest_prior_loss_usd: { type: number, minimum: 0 }
requested_lines:
type: array
minItems: 1
items: { type: string, enum: [mpl, property, gl, cyber] }
coverage_limits:
type: object
additionalProperties: { type: number }
cyber_questionnaire:
type: object
additionalProperties: { type: string }
scores:
type: object
additionalProperties: true
evaluator_notes: { type: string, maxLength: 2000 }
UnderwritingEvaluation:
type: object
description: |
Public shape returned by `POST /underwriting/evaluate`,
`GET /underwriting/evaluations/{id}`, and the
additional-info endpoint. All field names are snake_case.
properties:
id: { type: string, format: uuid }
state:
type: string
enum: [draft, submitted, in_review, approved, declined]
decision:
type: string
enum: [accept, refer, decline]
environment: { type: string, enum: [live, sandbox] }
fixture_key:
type: string
nullable: true
description: Set when the evaluation was triggered via a sandbox `fixture_key`.
decision_payload:
type: object
description: |
Full evaluator output, including the per-line eligibility
grid, IRPM modifiers, missing-information items, and any
evaluator notes. Shape is determined by `EvaluationResult`.
additionalProperties: true
missing_information:
type: array
description: Items the underwriter still needs from the partner.
items:
type: object
additionalProperties: true
irpm_override: { type: number, nullable: true }
irpm_override_reason: { type: string, nullable: true }
reviewed_by: { type: string, nullable: true }
reviewed_at: { type: string, nullable: true, format: date-time }
review_notes: { type: string, nullable: true }
status_history:
type: array
items:
type: object
additionalProperties: true
created_at: { type: string, format: date-time }
updated_at: { type: string, format: date-time }
# ─── Rate ─────────────────────────────────────────────────────────
BaseRateRequest:
type: object
required: [evaluation_id]
properties:
evaluation_id: { type: string, format: uuid }
idempotency_key: { type: string, maxLength: 255 }
MplRateRequest:
allOf:
- $ref: "#/components/schemas/BaseRateRequest"
- type: object
required:
[
total_revenue,
occurrence_limit,
aggregate_limit,
deductible_amount,
experience_type,
claims_history,
business_type,
]
properties:
total_revenue: { type: number, exclusiveMinimum: 0 }
occurrence_limit: { type: integer, minimum: 1 }
aggregate_limit: { type: integer, minimum: 1 }
deductible_amount: { type: integer, minimum: 0 }
experience_type: { type: string }
claims_history: { type: string }
business_type: { type: string, enum: [new, renewal] }
optional_coverages:
type: array
items: { type: string }
retroactive_period_months: { type: integer, minimum: 0 }
renewal_number: { type: integer, minimum: 0 }
extended_reporting_months: { type: integer, minimum: 0 }
revenue_allocations:
type: array
items:
type: object
required: [serviceClassId, classNumber, revenuePercentage]
properties:
serviceClassId: { type: string }
serviceClassTitle: { type: string }
classNumber: { type: integer }
revenuePercentage: { type: number, minimum: 0, maximum: 100 }
services:
type: array
minItems: 1
items: { type: string }
business_description: { type: string }
industry: { type: string }
PropertyRateRequest:
allOf:
- $ref: "#/components/schemas/BaseRateRequest"
- type: object
properties:
equipment_value: { type: number, minimum: 0, default: 0 }
all_risk_limit: { type: number, minimum: 0, default: 0 }
business_income_monthly: { type: number, minimum: 0, default: 0 }
include_creator_continuity: { type: boolean, default: false }
state: { type: string, minLength: 2, maxLength: 2 }
GlRateRequest:
allOf:
- $ref: "#/components/schemas/BaseRateRequest"
- type: object
required: [total_revenue, tier, occurrence_limit, aggregate_limit]
properties:
total_revenue: { type: number, exclusiveMinimum: 0 }
tier: { type: string, enum: [carveout, full] }
occurrence_limit: { type: integer, minimum: 1 }
aggregate_limit: { type: integer, minimum: 1 }
deductible: { type: integer, minimum: 0, default: 0 }
hazard_class: { type: integer, minimum: 1, maximum: 5, default: 2 }
endorsements:
type: object
properties:
additional_insured: { type: boolean, default: false }
waiver_of_subrogation: { type: boolean, default: false }
affirmative_ai: { type: boolean, default: false }
CyberRateRequest:
allOf:
- $ref: "#/components/schemas/BaseRateRequest"
- type: object
required: [iso_gl_code, state, aggregate_limit]
properties:
iso_gl_code: { type: string }
state: { type: string, minLength: 2, maxLength: 2 }
aggregate_limit: { type: integer, minimum: 1 }
revenue: { type: number, minimum: 0, default: 0 }
ny_claims_made_year:
{ type: integer, minimum: 1, maximum: 10, nullable: true }
questionnaire_answers:
type: object
additionalProperties: { type: string }
nullable: true
TrimpayRateRequest:
allOf:
- $ref: "#/components/schemas/BaseRateRequest"
- type: object
required: [business_type, adr_tier, devices]
properties:
business_type: { type: string, enum: [commercial, residential] }
adr_tier:
{ type: string, enum: [adr1000, adr2000, adr3000] }
devices:
type: array
minItems: 1
items:
type: object
required: [device_id, unit_cost, quantity]
properties:
device_id: { type: string }
name: { type: string }
risk_category: { type: string }
unit_cost: { type: number, minimum: 0 }
quantity: { type: integer, minimum: 1 }
RateLineResponse:
type: object
properties:
data:
type: object
properties:
line_id: { type: string, format: uuid }
line: { type: string }
evaluation_id: { type: string, format: uuid }
premium_annual: { type: number }
premium_monthly: { type: number }
modifiers:
type: object
additionalProperties: true
# ─── External quote pipeline imports ──────────────────────────────
ExternalQuoteVisibilityInput:
type: object
additionalProperties: false
properties:
staff: { type: boolean, const: true, default: true }
agent: { type: boolean, const: true, default: true }
policyholder: { type: boolean, const: false, default: false }
ExternalQuoteOwnershipInput:
type: object
additionalProperties: false
required: [agencyId, contactId]
description: |
Canonical ownership. `agencyId` accepts `agencies.id` or its persisted
`agencies.ptnUuid` input namespace and is returned as `agencies.id`.
`contactId` is `contacts.id`, not a carrier producer code or display name.
properties:
agencyId: { type: string, minLength: 1, maxLength: 255, description: "agencies.id or agencies.ptnUuid; response canonicalizes to agencies.id." }
contactId: { type: string, minLength: 1, maxLength: 255, description: "Canonical contacts.id." }
ExternalQuoteOwnership:
type: object
additionalProperties: false
required: [agencyId, contactId]
description: Canonical persisted ownership returned with the quote record; agencyId is agencies.id and contactId is contacts.id.
properties:
agencyId: { type: [string, "null"] }
contactId: { type: [string, "null"] }
ExternalQuoteOwnershipFrom:
type: object
additionalProperties: false
required: [carrier]
description: Existing portal ownership-resolution input; it is resolved only against persisted authority and never creates an owner.
properties:
carrier: { type: string, minLength: 1, maxLength: 120 }
producer_or_agency_code: { type: [string, "null"], maxLength: 120 }
submitting_agent_name: { type: [string, "null"], maxLength: 255 }
fallback_agency_id: { type: [string, "null"], maxLength: 255 }
fallback_contact_id: { type: [string, "null"], maxLength: 255 }
ExternalQuoteTransactionType:
type: string
enum: [new_business, renewal, endorsement, other]
description: Travelers imports must provide this discriminator. Travelers `new_business` is eligible by default; retained renewal and endorsement evidence requires an explicit filter.
ExternalQuoteImportItem:
type: object
additionalProperties: false
required: [source, carrier_id, external_quote_id, status]
description: |
One carrier pipeline row. Ownership is required for an agency-scoped
key; it may be omitted only when a valid agent-bound key supplies the
same canonical ownership. `ownership` and `ownershipFrom` are mutually
exclusive. Camel-case compatibility aliases may be accepted by the
runtime only when they match the documented snake-case fields.
properties:
source:
type: string
enum: [hartford_icon, travelers_policy_activity, external_carrier]
minLength: 1
maxLength: 100
example: hartford_icon
carrier_id: { type: string, minLength: 1 }
external_quote_id: { type: string, minLength: 1, maxLength: 255 }
contact_id:
type: string
minLength: 1
description: Optional canonical contacts.id. It must agree with `ownership.contactId` when both are supplied.
policyholder_id:
type: string
minLength: 1
description: Optional existing, in-book policyholder id; no policyholder is created or inferred by name.
ownership: { $ref: "#/components/schemas/ExternalQuoteOwnershipInput" }
ownershipFrom: { $ref: "#/components/schemas/ExternalQuoteOwnershipFrom" }
named_insured: { type: [string, "null"], maxLength: 500 }
status:
type: string
minLength: 1
maxLength: 255
description: Exact carrier portal status; retained verbatim as rawStatus.
transaction_type:
$ref: "#/components/schemas/ExternalQuoteTransactionType"
description: Optional for existing clients. Hartford defaults an omitted value to `new_business`; Travelers requires an explicit value.
product_lob: { type: [string, "null"], maxLength: 120 }
effective_date: { type: [string, "null"], format: date }
premium: { type: [string, number, "null"] }
quote_date: { type: [string, "null"], format: date }
producer_or_agency_code: { type: [string, "null"], maxLength: 120 }
last_activity: { type: [string, "null"], maxLength: 64 }
submitted_at:
type: [string, "null"]
maxLength: 64
description: Optional portal date/time evidence. Date-only and offset-less values are America/Chicago; offset-bearing ISO-8601 values retain their instant. Otherwise use portal activity, then first import time.
csr: { type: [string, "null"], maxLength: 255 }
metadata:
type: [object, "null"]
additionalProperties: true
visibility:
$ref: "#/components/schemas/ExternalQuoteVisibilityInput"
allOf:
- not:
required: [ownership, ownershipFrom]
- if:
properties:
source:
pattern: '.*[Tt][Rr][Aa][Vv][Ee][Ll][Ee][Rr][Ss].*'
required: [source]
then:
required: [transaction_type]
ExternalQuoteImportBatch:
type: object
additionalProperties: false
required: [quotes]
properties:
quotes:
type: array
minItems: 1
maxItems: 200
items: { $ref: "#/components/schemas/ExternalQuoteImportItem" }
ExternalQuoteImportInput:
oneOf:
- $ref: "#/components/schemas/ExternalQuoteImportItem"
- $ref: "#/components/schemas/ExternalQuoteImportBatch"
ExternalQuoteRecord:
type: object
required:
- id
- policyholderId
- agentId
- agencyId
- ownership
- source
- externalQuoteId
- carrier
- contact
- namedInsured
- normalizedStatus
- rawStatus
- premium
- effectiveDate
- coverageType
- date
- productLob
- submittedAt
- transactionType
- metadata
- visibility
- provenance
properties:
id: { type: string }
policyholderId: { type: [string, "null"] }
agentId: { type: [string, "null"] }
agencyId: { type: [string, "null"] }
ownership:
oneOf:
- $ref: "#/components/schemas/ExternalQuoteOwnership"
- type: "null"
vendorId: { type: [string, "null"] }
source: { type: [string, "null"] }
externalQuoteId: { type: [string, "null"] }
carrier:
type: [object, "null"]
properties:
id: { type: string }
name: { type: string }
contact:
type: [object, "null"]
properties:
id: { type: string }
conId: { type: [string, "null"] }
fullName: { type: [string, "null"] }
email: { type: [string, "null"] }
namedInsured: { type: [string, "null"] }
status:
type: [string, "null"]
description: Normalized business status for external records.
normalizedStatus:
type: [string, "null"]
enum:
- submitted
- in_progress
- needs_information
- rated
- approved
- approved_with_conditions
- declined
- expired
- issued_external
- closed
- other
- null
rawStatus:
type: [string, "null"]
description: Exact carrier portal value, retained for display and audit.
syncStatus:
type: [string, "null"]
description: FileMaker synchronization state; separate from external business status.
premium: { type: [string, "null"] }
effectiveDate: { type: [string, "null"] }
coverageType: { type: [string, "null"] }
date: { type: [string, "null"] }
productLob: { type: [string, "null"] }
producerOrAgencyCode: { type: [string, "null"] }
lastActivity: { type: [string, "null"] }
submittedAt: { type: [string, "null"], format: date-time }
transactionType:
oneOf:
- $ref: "#/components/schemas/ExternalQuoteTransactionType"
- type: "null"
csr: { type: [string, "null"] }
metadata:
type: [object, "null"]
additionalProperties: true
visibility:
type: object
required: [staff, agent, policyholder]
properties:
staff: { type: boolean, const: true }
agent: { type: boolean, const: true }
policyholder: { type: boolean, const: false }
provenance:
type: [object, "null"]
properties:
environment: { type: string, enum: [sandbox, live] }
importedAt: { type: [string, "null"], format: date-time }
updatedAt: { type: [string, "null"], format: date-time }
ExternalQuoteListResponse:
type: object
required: [data, pagination]
properties:
data:
type: array
items: { $ref: "#/components/schemas/ExternalQuoteRecord" }
pagination:
type: object
required: [page, limit, total, totalPages]
description: total is the filtered count, including quote-date and productLob filters.
properties:
page: { type: integer }
limit: { type: integer }
total: { type: integer }
totalPages: { type: integer }
countOnly:
type: boolean
description: True when the caller asked for a count without a page of rows.
total:
type: integer
description: Same filtered count as pagination.total. Present when countOnly is true.
ExternalQuoteImportResult:
type: object
required: [index, ok, outcome]
properties:
index: { type: integer, minimum: 0 }
ok: { type: boolean }
outcome: { type: string, enum: [created, updated, error] }
id:
type: string
description: Persisted quote id; present for successful created or updated rows.
data: { $ref: "#/components/schemas/ExternalQuoteRecord" }
error: { $ref: "#/components/schemas/ExternalQuoteImportError" }
ExternalQuoteImportResponse:
oneOf:
- type: object
required: [outcome, created, updated, data]
properties:
outcome: { type: string, enum: [created, updated] }
created: { type: boolean }
updated: { type: boolean }
data: { $ref: "#/components/schemas/ExternalQuoteRecord" }
- type: object
required: [data]
properties:
data:
type: object
required: [results]
properties:
results:
type: array
items: { $ref: "#/components/schemas/ExternalQuoteImportResult" }
ExternalQuoteImportCreatedResponse:
type: object
required: [outcome, created, updated, data]
properties:
outcome: { type: string, const: created }
created: { type: boolean, const: true }
updated: { type: boolean, const: false }
data: { $ref: "#/components/schemas/ExternalQuoteRecord" }
ExternalQuoteImportErrorFieldErrors:
type: array
minItems: 1
description: Per-field messages retain the nested batch path, e.g. `quotes.3.ownership.contactId`.
items:
type: object
additionalProperties: false
required: [path, message]
properties:
path: { type: string, example: "quotes.3.ownership.contactId" }
message: { type: string, example: "contactId is required" }
ExternalQuoteImportError:
type: object
additionalProperties: false
description: |
Standard non-validation import error. Middleware/idempotency failures
may omit retryable and details; validation responses use
ExternalQuoteImportValidationError instead.
required: [code, message]
properties:
code:
type: string
enum: [VALIDATION_ERROR, POLICYHOLDER_REQUIRED, IDEMPOTENCY_KEY_REQUIRED, CARRIER_NOT_FOUND, CONTACT_NOT_FOUND, CONTACT_OWNERSHIP_UNRESOLVED, CONTACT_OUTSIDE_BOOK, POLICYHOLDER_NOT_FOUND, POLICYHOLDER_AMBIGUOUS, POLICYHOLDER_OUTSIDE_BOOK, OWNERSHIP_UNRESOLVED, OWNERSHIP_CONFLICT, EXTERNAL_QUOTE_OWNERSHIP_CONFLICT, IDEMPOTENCY_CONFLICT, IDEMPOTENCY_IN_PROGRESS, IDEMPOTENCY_RETRY_REQUIRED, IDEMPOTENCY_UNAVAILABLE, API_KEY_CHANGED, FORBIDDEN, INTERNAL_ERROR]
message: { type: string }
retryable:
type: boolean
description: Present when the emitting layer classifies retryability.
details:
$ref: "#/components/schemas/ExternalQuoteImportErrorDetails"
ExternalQuoteImportErrorDetails:
type: object
additionalProperties: true
properties:
fieldErrors: { $ref: "#/components/schemas/ExternalQuoteImportErrorFieldErrors" }
ExternalQuoteImportValidationErrorDetails:
type: object
additionalProperties: false
required: [fieldErrors]
properties:
fieldErrors: { $ref: "#/components/schemas/ExternalQuoteImportErrorFieldErrors" }
ExternalQuoteImportValidationError:
type: object
additionalProperties: false
required: [code, message, retryable, details]
properties:
code:
type: string
enum: [VALIDATION_ERROR, POLICYHOLDER_REQUIRED]
message: { type: string }
retryable: { type: boolean, const: false }
details: { $ref: "#/components/schemas/ExternalQuoteImportValidationErrorDetails" }
ExternalQuoteImportErrorResponse:
type: object
additionalProperties: false
required: [error]
properties:
error: { $ref: "#/components/schemas/ExternalQuoteImportError" }
ExternalQuoteImportValidationErrorResponse:
type: object
additionalProperties: false
required: [error]
properties:
error: { $ref: "#/components/schemas/ExternalQuoteImportValidationError" }
# ─── Evaluated/rated StudioGuard quote assembly ───────────────────
QuoteAssembleRequest:
type: object
required: [evaluation_id, line_ids, policyholder_state]
properties:
evaluation_id: { type: string, format: uuid }
line_ids:
type: array
minItems: 1
items: { type: string, format: uuid }
policyholder_state: { type: string, minLength: 2, maxLength: 2 }
policyholder_name: { type: string }
effective_date: { type: string, format: date }
Quote:
type: object
properties:
id: { type: string, format: uuid }
slug: { type: string }
hosted_url: { type: string, format: uri }
evaluation_id: { type: string, format: uuid }
line_ids:
type: array
items: { type: string, format: uuid }
policyholder_name: { type: string, nullable: true }
policyholder_state: { type: string }
totals:
type: object
properties:
gross_premium: { type: number }
total_taxes: { type: number }
total_with_taxes: { type: number }
monthly_amount: { type: number }
line_count: { type: integer }
eligible_line_count: { type: integer }
per_line:
type: array
items:
type: object
properties:
line: { type: string }
line_id: { type: string }
premium_annual: { type: number }
premium_monthly: { type: number }
eligible: { type: boolean }
taxes:
type: object
additionalProperties: true
monthly_schedule:
type: array
items:
type: object
properties:
due_date: { type: string, format: date }
amount: { type: number }
created_at: { type: string, format: date-time }
# ─── Bind ─────────────────────────────────────────────────────────
BindRequest:
type: object
required: [quote_id, insured]
properties:
quote_id: { type: string, format: uuid }
insured:
type: object
required: [name]
properties:
name: { type: string }
type: { type: string }
email: { type: string, format: email }
address1: { type: string }
address2: { type: string }
city: { type: string }
state: { type: string, minLength: 2, maxLength: 2 }
zip: { type: string }
website: { type: string }
trade_occupation: { type: string }
effective_date: { type: string, format: date }
expiration_date: { type: string, format: date }
retro_date: { type: string, format: date }
payment_plan:
{ type: string, enum: [monthly, annual], default: monthly }
agent_id: { type: string, nullable: true }
BoundPolicy:
type: object
properties:
policy_id: { type: string, format: uuid }
policy_number: { type: string }
quote_id: { type: string, format: uuid }
commission_split:
type: object
properties:
agent_commission_pct: { type: number }
agent_commission_amt: { type: number }
mga_net_retention_amt: { type: number }
pending_ledger_id: { type: string }
coverage_documents:
type: array
items: { type: string, format: uri }
status: { type: string, example: in_force_pending_payment }
created_at: { type: string, format: date-time }
PaymentIntent:
type: object
properties:
client_secret: { type: string }
amount: { type: number }
currency: { type: string, example: usd }
payment_intent_id: { type: string }
# ─── BRDX ─────────────────────────────────────────────────────────
BrdxWrittenRow:
type: object
properties:
report_id: { type: string }
period: { type: string }
policy_number: { type: string }
carrier: { type: string }
line_of_business: { type: string }
bound_at: { type: string, format: date-time }
gross_premium: { type: number }
BrdxPaidRow:
type: object
properties:
report_id: { type: string }
period: { type: string }
policy_number: { type: string }
amount: { type: number }
paid_at: { type: string, format: date-time }
stripe_payment_id: { type: string }
# ─── Commission ───────────────────────────────────────────────────
CommissionLedgerRow:
type: object
description: |
Public shape of a `commission_ledger` row, returned by
`GET /commissions/ledger` and embedded as `data` in the
`commission.accrued` / `commission.payable` webhooks.
properties:
id: { type: string, description: Ledger row UUID. }
source:
type: string
description: Origin system (e.g., `studioguard`, `imports`).
carrier: { type: string }
program: { type: string }
policy_number: { type: string }
insured_name: { type: string, nullable: true }
agent_id: { type: string }
effective_date: { type: string, format: date-time, nullable: true }
expiration_date: { type: string, format: date-time, nullable: true }
transaction_type: { type: string, example: "new_business" }
premium: { type: number }
status:
type: string
enum: [pending, verified, payable, paid, held, reversed, clawed_back]
hold_reason: { type: string, nullable: true }
payable_after:
type: string
format: date-time
nullable: true
description: When the row will (or did) age to `payable`. ~10-day default hold.
paid_at: { type: string, format: date-time, nullable: true }
payment_id:
type: string
nullable: true
description: FK to `commission_payments.id` once the row is included in a withdrawal.
is_reversal: { type: boolean }
reversal_of_ledger_id: { type: string, nullable: true }
installment_number: { type: integer }
created_at: { type: string, format: date-time, nullable: true }
updated_at: { type: string, format: date-time, nullable: true }
split_rationale:
type: object
description: Full audit trail of how the split was computed.
properties:
carrier_commission_pct: { type: number }
carrier_commission_amt: { type: number }
mga_expense_pct: { type: number }
mga_expense_amt: { type: number }
agent_base_commission_pct: { type: number }
agent_tier: { type: string, nullable: true }
agent_tier_split_pct: { type: number }
agent_commission_pct: { type: number }
agent_commission_amt: { type: number }
mga_net_retention_amt: { type: number }
CommissionWallet:
type: object
description: |
Returned by `GET /commissions/wallet/{agent_id}`. All dollar
fields are USD doubles. `holds.*` reflect the live Lexi
compliance gate evaluated at request time.
properties:
agent_id: { type: string }
earned: { type: number, description: Lifetime earned (pending + payable + paid). }
pending: { type: number }
pending_count: { type: integer }
payable: { type: number }
paid: { type: number }
paid_ytd: { type: number }
next_aging_date:
type: string
format: date-time
nullable: true
description: Earliest `payable_after` among rows still inside their hold window.
tier: { type: string, example: "gold" }
tier_split_pct: { type: number }
gross_program_commission_pct: { type: number, example: 18 }
direct_deposit_active: { type: boolean }
holds:
type: object
properties:
license_expired: { type: boolean }
eo_expired: { type: boolean }
no_bank_account: { type: boolean }
payment_hold: { type: boolean }
licensing:
type: object
properties:
licensed_states:
type: array
items: { type: string }
compliance:
type: object
properties:
pass: { type: boolean }
hold_reason: { type: string, nullable: true }
can_withdraw: { type: boolean }
bank_account_last4: { type: string, nullable: true }
WithdrawalRequest:
type: object
required: [agent_id]
properties:
agent_id: { type: string }
amount:
description: |
Optional partial-withdrawal amount in USD. Omit (or pass
the literal string `"max payable"`) to withdraw the
entire payable balance. Whole-row granularity — the
oldest payable rows are selected until the requested
amount is covered.
oneOf:
- { type: number, exclusiveMinimum: 0, maximum: 10000000 }
- { type: string, enum: ["max payable"] }
memo: { type: string, maxLength: 200 }
Withdrawal:
type: object
description: |
Successful withdrawal. Wrapped in `{ data, meta }` on the HTTP
response and re-emitted as `commission.paid`.
properties:
withdrawal_id: { type: string, description: FK to `commission_payments.id`. }
agent_id: { type: string }
amount: { type: number }
ach_eta:
type: string
format: date-time
description: Estimated ACH settlement timestamp.
bank_account_last4: { type: string, nullable: true }
included_ledger_ids:
type: array
items: { type: string }
status: { type: string, example: processing }
initiated_at: { type: string, format: date-time }
# ─── Webhooks ─────────────────────────────────────────────────────
WebhookDeliveryHeaders:
type: object
description: |
Optional static string-to-string headers merged into every live,
retry, test, and post-reactivation delivery. `Authorization` is
allowed. Transport/hop-by-hop names (for example `Host`, `Connection`,
`Content-Length`, `Transfer-Encoding`, `Keep-Alive`, `TE`, `Trailer`,
and `Upgrade`), `Content-Type`, `X-Pulse-*`, `Proxy-*`, `Sec-*`,
`X-Forwarded-*`, and cookie/origin/referrer-style names are rejected.
Custom headers never replace or alter Pulse's `X-Pulse-Signature`,
`X-Pulse-Event`, `X-Pulse-Delivery`, or `X-Pulse-Timestamp` headers.
If a receiver echoes a configured value, Pulse redacts that value from
stored response-body logs.
additionalProperties:
type: string
example:
Authorization: "Bearer <BOT_WAKEUP_TOKEN>"
WebhookSubscription:
type: object
description: |
Public shape of a `webhook_subscriptions` row. The plaintext
`secret` is returned **only on the create response and on
`POST /webhooks/{id}/rotate-secret`** so the partner can
record it; every other read omits it. Static delivery `headers`
are likewise write-only: values are returned only by
`POST /webhooks` and by a `PATCH /webhooks/{id}` that includes
`headers`; GET/list responses omit them. After 10 consecutive
failed deliveries the subscription is auto-paused
(`isActive=false`); reactivate it with
`PATCH /webhooks/{id}` (body `{ "isActive": true }`), which
flips it back on, resets `failureCount` to `0`, and preserves
the original signing `secret` so the partner does not need to
re-record it. To rotate the signing secret without disturbing
the rest of the subscription (e.g. after a suspected leak),
use `POST /webhooks/{id}/rotate-secret`.
properties:
id: { type: string }
url:
type: string
format: uri
description: Public HTTPS endpoint; internal/reserved targets and redirects are not allowed.
events:
type: array
items:
type: string
description: Supported event name, including `chat_escalation.created`.
example: chat_escalation.created
isActive: { type: boolean }
failureCount: { type: integer, description: Consecutive failed deliveries since the last success. }
lastDeliveredAt: { type: string, format: date-time, nullable: true }
lastFailedAt: { type: string, format: date-time, nullable: true }
createdAt: { type: string, format: date-time }
secret:
type: string
description: One-time plaintext secret. Returned ONLY by `POST /webhooks` on creation and by `POST /webhooks/{id}/rotate-secret`; never returned by reads.
headers:
allOf:
- $ref: "#/components/schemas/WebhookDeliveryHeaders"
description: Present only in the create response or a PATCH response when that request supplied `headers`; omitted from GET/list responses.