openapi: 3.0.3
info:
  title: PitchPoint Findings API
  description: |
    Read and manage the compliance findings produced by a workflow: the findings themselves (individually, as a flat list, or grouped into categories), each finding's audit trail, its attachments, and its review note, review status and result override.
    CONCEPTS. A workflow is the compliance review this API is scoped to — every endpoint takes a workflowId identifying which one you're working with. A finding is one compliance result produced inside that workflow: a message for a person to read, an optional severity, and (depending on configuration) a requirement that someone review and annotate it before the workflow can be considered complete.
    GETTING STARTED. A typical integration: (1) GET a finding, the findings list, or the categorized view. (2) GET .../findings/{findingId}/history and .../attachments for a finding's history and attachments, and POST .../findings/{findingId} to edit it. (3) Read the finding's authorization.allow to see which changes you are permitted to make, and offer only those. Permission is checked again on every call, so a call can still be refused with a 403.
    HTTP VERBS ON THIS API. GET reads and never changes anything — safe to call repeatedly and to poll. POST performs an action: editing a finding's review note, review status and/or result override (a single partial-update call — see POST .../findings/{findingId}), or adding an attachment. DELETE removes an attachment.
    The one exception to that GET rule is the report download published as `ReportLinks.download` (the `_latestpdf` content address): fetching it always records a read against the report and acknowledges the workflow's pending report notification, and on some configurations it also regenerates the report and advances the workflow's status. Fetch it when a person actually wants the document, never as a GET you poll — see the `ReportLinks` schema. Every other GET on this API, the attachment download included, has no side effects.
    IDENTIFIERS. `workflowId`, `findingId` and `attachmentId` are opaque strings. Use each one exactly as this API returned it: store it and send it back unchanged. Don't parse it, build one yourself, or rely on its format. An unrecognized id returns a 404.
    ERRORS. Every error response in this document that carries a body is an RFC 9457 problem document, served as `application/problem+json` with the `Problem` shape defined in problems-v1.yaml, which is published beside this document and shared with the other PitchPoint APIs; keep the files side by side, since the refs to it are relative. There is no second, envelope-style error shape on this API. Branch on the problem `type`, never on the text of `title` or `detail`. This API returns a subset of the platform's problem types; the complete set it can return:

      * `about:blank` — RFC 9457's own placeholder, meaning the status alone describes the
        problem. Carried by a 401 when no credential was presented, a 400 for a request this API
        rejected, a 403 refused before the endpoint ran, a 404 or 405 for a URL or method this API
        does not serve, and every 5xx.

      * `https://pointservices.com/problems/invalid-request` (400, POST .../findings/{findingId}
        only, the one operation with a JSON body) — the Content-Type header names a character set
        this service cannot use. Raised before the body is read, so it says nothing about the body's
        content.

      * `https://pointservices.com/problems/invalid-token` (401) — a credential was presented and
        rejected.

      * `https://pointservices.com/problems/not-authorized` (403) — you are authenticated and
        policy will not let you act on this workflow or finding.

      * `https://pointservices.com/problems/not-found` (404) — an unknown workflowId, findingId or
        attachmentId. A URL this API does not serve at all is the `about:blank` 404 above, so the
        two are told apart by `type`.

      * `https://pointservices.com/problems/concurrent-modification` (409, write operations only)
        — another writer changed the finding while your write was in flight, so nothing was
        applied. It is not retried for you: read the finding again before resubmitting.

    Not every rejection carries a JSON body. Some requests are answered with a bare status and nothing this document models, so write your client to tolerate an error response it cannot parse rather than assuming every failure is a problem document.
    Every timestamp this API publishes is an RFC 3339 instant in UTC with a trailing `Z`, for example `2026-08-07T15:01:42Z`. They are declared as `type: string` with `format: date-time`. Fractional seconds are emitted only when they are not zero, so do not assume a fixed field width, and parse with a library rather than by slicing the string.
  version: 1.0.0
servers:
  - url: /riskinsight-services-ws/resources
    description: Everything after the host. Combine it with your PPS environment's host to form absolute URLs — for example `https://<your-pps-host>/riskinsight-services-ws/resources/v1/findings/workflows`.
security:
  - BearerAuth: []
paths:
  /v1/findings/workflows/{workflowId}:
    get:
      summary: Read the workflow's summary — id, score, severity tally and reports. Read-only.
      description: |
        A summary of the workflow as a whole: its id, its computed score, its workflow-wide severity tally and its generated reports. It does not include the findings themselves — use GET .../findings for the flat list or GET .../bycategories for the categorized view.
        score is computed whenever the workflow's data allows it, whether the workflow is still in progress or complete. See score's own description for what its absence means.
        Read-only and safe to poll.
      operationId: getWorkflowFindings
      parameters:
        - name: workflowId
          in: path
          description: |
            The workflow's id, exactly as this API returned it — see this document's IDENTIFIERS note.
          required: true
          style: simple
          explode: false
          schema:
            type: string
      responses:
        '200':
          description: The workflow's findings summary — its workflowId and computed score.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FindingsSummaryEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: |
            Unknown workflowId. Body is an RFC 9457 problem document, NOT an envelope.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
  /v1/findings/workflows/{workflowId}/findings:
    get:
      summary: Read every finding in the workflow as a flat list. Read-only.
      description: |
        Every finding in the workflow, as one flat list — use this when you want all findings without walking categories. Each entry is identical, field for field, to its counterpart in the categorized view (GET .../bycategories) and to a direct GET of that finding by id.
        Read-only and safe to poll.
      operationId: getWorkflowFindingsList
      parameters:
        - name: workflowId
          in: path
          description: |
            The workflow's id, exactly as this API returned it — see this document's IDENTIFIERS note.
          required: true
          style: simple
          explode: false
          schema:
            type: string
      responses:
        '200':
          description: Every finding in the workflow, as a flat list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FindingsEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: |
            Unknown workflowId. Body is an RFC 9457 problem document, NOT an envelope.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
  /v1/findings/workflows/{workflowId}/findings/{findingId}:
    get:
      summary: Read one finding by its handle. Read-only.
      description: |
        Fetch a single finding by id. Field for field identical to the same finding as it appears in the list and categorized responses.
      operationId: getWorkflowFinding
      parameters:
        - name: workflowId
          in: path
          required: true
          style: simple
          explode: false
          schema:
            type: string
        - name: findingId
          in: path
          description: |
            The finding's id, exactly as published in Finding.findingId — see this document's IDENTIFIERS note.
          required: true
          style: simple
          explode: false
          schema:
            type: string
      responses:
        '200':
          description: The finding.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FindingEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: |
            Unknown workflowId or unknown findingId. RFC 9457 problem body. A findingId resolves only under the workflowId it was issued under; under any other workflowId it is a 404.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
    post:
      summary: Edit a finding's review note, review status, and/or result override, in one call.
      description: |
        A partial update: send any of reviewNote, reviewed and overrideResult in one body. A field left out of the body is left unchanged.
        reviewNote replaces the finding's current note. Send an empty or blank string to clear it. Earlier notes stay on the finding's audit trail (GET .../findings/{findingId}/history).
        reviewed set to true marks the finding reviewed; false returns it to unreviewed. Changing reviewed does not change the note.
        overrideResult records a reviewer's corrected outcome, published afterward as Finding.resultOrOverriddenResult. Finding.result keeps the check's original outcome.
        Each field needs its own permission, listed in Finding.authorization.allow: reviewNote needs riskinsight:JournalResolvedValueUpdateNote; reviewed and overrideResult need riskinsight:JournalResolvedValueReview. A body that sends fields needing different permissions needs all of them. If any is missing the call is refused with a 403 and nothing is changed.
      operationId: editWorkflowFinding
      parameters:
        - name: workflowId
          in: path
          description: |
            The workflow's id, exactly as this API returned it — see this document's IDENTIFIERS note.
          required: true
          style: simple
          explode: false
          schema:
            type: string
        - name: findingId
          in: path
          description: |
            The finding's id, exactly as published in Finding.findingId — see this document's IDENTIFIERS note.
          required: true
          style: simple
          explode: false
          schema:
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FindingEditRequest'
        required: true
      responses:
        '200':
          description: |
            The finding, reflecting the change(s) just recorded. Identical to a GET of the same finding except that category is omitted; GET the finding for its categories.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FindingEnvelope'
        '400':
          description: |
            The body is malformed, or overrideResult is present but blank. RFC 9457 problem body carrying `about:blank`; `detail` names the field at fault, for a person reading your logs. A Content-Type naming an unusable character set carries `https://pointservices.com/problems/invalid-request` instead.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: |
            You may not read this workflow, or you lack permission to change a field sent in this request or to update this workflow. Nothing is changed. RFC 9457 problem body.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '404':
          description: |
            Unknown workflowId or unknown findingId. RFC 9457 problem body.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '409':
          $ref: '#/components/responses/ConcurrentModification'
  /v1/findings/workflows/{workflowId}/findings/{findingId}/history:
    get:
      summary: Read a finding's audit trail, newest first, capped at 500 entries. Read-only.
      description: |
        The finding's audit trail, newest first, up to 500 entries. Not paged: entries beyond the 500 newest are not returned.
        Every recorded event on the finding appears, including ones that changed no field (the finding was created, read, edited, or exported); each of those is one entry. An event that changed fields produces one entry per field, and those entries share the event's eventType, reviewer and changedAt. One save can record several events — for example an edit that changes the note and marks the finding reviewed records a noteChanged event, a reviewedChanged event and an edited event.
      operationId: getWorkflowFindingHistory
      parameters:
        - name: workflowId
          in: path
          required: true
          style: simple
          explode: false
          schema:
            type: string
        - name: findingId
          in: path
          required: true
          style: simple
          explode: false
          schema:
            type: string
      responses:
        '200':
          description: The finding's audit trail, newest first, capped at 500 entries.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FindingHistoryEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: |
            Unknown workflowId or unknown findingId. RFC 9457 problem body. A finding with no recorded history is a 200 with an empty history array, never a 404.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
  /v1/findings/workflows/{workflowId}/findings/{findingId}/attachments:
    get:
      summary: Read one finding's live attachments. Read-only.
      description: |
        The finding's attachments, each with its download link. A finding itself carries only hasAttachments.
        A removed attachment no longer appears here. A finding with no attachments returns a 200 with an empty array.
      operationId: getWorkflowFindingAttachments
      parameters:
        - name: workflowId
          in: path
          required: true
          style: simple
          explode: false
          schema:
            type: string
        - name: findingId
          in: path
          required: true
          style: simple
          explode: false
          schema:
            type: string
      responses:
        '200':
          description: The finding's attachments, each carrying its own download link.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AttachmentsEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: |
            Unknown workflowId or unknown findingId. RFC 9457 problem body.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
  /v1/findings/workflows/{workflowId}/bycategories:
    get:
      summary: Read the workflow's configured categories, each with its own findings. Read-only.
      description: |
        The workflow's findings grouped into categories, in configured order — use this when you want findings grouped the way the workflow owner set them up, rather than the flat list from GET .../findings. Each finding here is identical, field for field, to its counterpart in the flat list and in a direct single-finding GET.
        If you only need counts per category, use GET .../categories instead; it returns tallies only, no findings.
        Read-only and safe to poll.
      operationId: getWorkflowFindingsByCategory
      parameters:
        - name: workflowId
          in: path
          description: |
            The workflow's id, exactly as this API returned it — see this document's IDENTIFIERS note.
          required: true
          style: simple
          explode: false
          schema:
            type: string
      responses:
        '200':
          description: The workflow's configured categories, each carrying its own findings.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CategoriesEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: |
            Unknown workflowId. Body is an RFC 9457 problem document, NOT an envelope.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
  /v1/findings/workflows/{workflowId}/categories:
    get:
      summary: Read the workflow's configured categories as counts, with no findings. Read-only.
      description: |
        Each configured category, with its review and severity tallies only — never the findings themselves. Use this when you only need counts (for example, a summary dashboard); use GET .../bycategories when you need the findings.
        Per-category counts only. The workflow-wide severity tally is on GET /v1/findings/workflows/{workflowId}.
        Read-only and safe to poll.
      operationId: getWorkflowCategoryCounts
      parameters:
        - name: workflowId
          in: path
          description: |
            The workflow's id, exactly as this API returned it — see this document's IDENTIFIERS note.
          required: true
          style: simple
          explode: false
          schema:
            type: string
      responses:
        '200':
          description: The workflow's configured categories, each carrying only its own counts.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CategoryCountsEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: |
            Unknown workflowId. Body is an RFC 9457 problem document, NOT an envelope.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
  /v1/findings/workflows/{workflowId}/findings/{findingId}/attachments/{attachmentId}:
    delete:
      summary: Remove an attachment from a finding.
      description: |
        Removes the attachment from the finding. Once removed, it no longer appears in GET .../findings/{findingId}/attachments and no longer counts toward hasAttachments.
        An attachment cannot be edited in place. To change one, remove it and add the replacement.
        Needs riskinsight:JournalResolvedValueUpdateAttachment in Finding.authorization.allow — the same permission adding an attachment needs.
      operationId: deleteWorkflowFindingAttachment
      parameters:
        - name: workflowId
          in: path
          required: true
          style: simple
          explode: false
          schema:
            type: string
        - name: findingId
          in: path
          required: true
          style: simple
          explode: false
          schema:
            type: string
        - name: attachmentId
          in: path
          description: |
            The attachment's id, exactly as published in Attachment.attachmentId — see this document's IDENTIFIERS note.
          required: true
          style: simple
          explode: false
          schema:
            type: string
      responses:
        '200':
          description: |
            The finding, reflecting the attachment's removal. Identical to a GET of the same finding except that category is omitted; GET the finding for its categories.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FindingEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: |
            You may not read this workflow, or you lack permission to update this finding's attachments or this workflow. RFC 9457 problem body.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '404':
          description: |
            Unknown workflowId, unknown findingId, or unknown attachmentId (including an attachmentId that belongs to a different finding). RFC 9457 problem body.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '409':
          $ref: '#/components/responses/ConcurrentModification'
components:
  schemas:
    FindingsSummaryEnvelope:
      required:
        - workflowId
      type: object
      properties:
        workflowId:
          type: string
          description: |
            The workflow's opaque id, echoed on every response so a stored or logged payload still says which workflow it belongs to.
        score:
          maximum: 99
          minimum: 0
          type: integer
          description: |
            The scale is counter-intuitive: 0 to 99, and higher is worse. 75 and above means there are unreviewed findings of HIGH severity. Don't assume the usual "higher is better" reading — it's inverted here.
            May be absent: when the workflow's data can't be scored, the key is omitted. Absence means "not computable right now", never "zero" — don't substitute 0 for it.
        severityCounts:
          type: array
          description: |
            Workflow-wide severity tally, one entry per distinct (severity, reviewed) combination actually present. Omitted under the same condition that makes score absent (never emitted as an empty array in that case). When a score IS computable but nothing matched, this is an empty array, not omitted.
          items:
            $ref: '#/components/schemas/SeverityCount'
        reports:
          type: array
          description: |
            This workflow's generated reports. Always an array — [] when nothing has been generated yet, never null. Today it holds at most one entry, the workflow's newest report; more entries may be added in future. Reading this field has no side effects, unlike following reports[].links.download — see ReportLinks.
          items:
            $ref: '#/components/schemas/Report'
      description: |
        The response shape for GET /v1/findings/workflows/{workflowId}.
    FindingsEnvelope:
      required:
        - findings
        - workflowId
      type: object
      properties:
        workflowId:
          type: string
          description: |
            The workflow's opaque id, echoed on every response so a stored or logged payload still says which workflow it belongs to.
        findings:
          type: array
          description: |
            Every finding in the workflow, in no particular order. An empty array when the workflow has none, never null.
          items:
            $ref: '#/components/schemas/Finding'
      description: |
        The response shape for GET /v1/findings/workflows/{workflowId}/findings — every finding in the workflow as a flat list.
    CategoriesEnvelope:
      required:
        - categories
        - workflowId
      type: object
      properties:
        workflowId:
          type: string
          description: |
            The workflow's opaque id, echoed on every response so a stored or logged payload still says which workflow it belongs to.
        categories:
          type: array
          description: |
            The workflow owner's configured categories, in configured order — present them in this order. Always an array, never null.
            An empty array does not mean "nothing to review": it is also what a workflow with no categories configured returns.
          items:
            $ref: '#/components/schemas/Category'
      description: |
        The response shape for GET /v1/findings/workflows/{workflowId}/bycategories — the workflow owner's configured categories, each with the findings that fell into it.
    FindingEnvelope:
      required:
        - finding
        - workflowId
      type: object
      properties:
        workflowId:
          type: string
        finding:
          $ref: '#/components/schemas/Finding'
      description: |
        The response shape for GET and POST /v1/findings/workflows/{workflowId}/findings/{findingId}, and for the attachment add and remove calls. The POST and the attachment calls omit finding.category.
    FindingHistoryEnvelope:
      required:
        - findingId
        - history
        - workflowId
      type: object
      properties:
        workflowId:
          type: string
        findingId:
          type: string
          description: |
            The finding's opaque id, echoed so a stored payload still says which finding it describes.
        history:
          type: array
          description: |
            Up to 500 entries, newest first. An empty array when the finding has no recorded history, never null.
          items:
            $ref: '#/components/schemas/FindingHistoryEntry'
      description: |
        The response shape for GET /v1/findings/workflows/{workflowId}/findings/{findingId}/history.
    AttachmentsEnvelope:
      required:
        - attachments
        - findingId
        - workflowId
      type: object
      properties:
        workflowId:
          type: string
        findingId:
          type: string
        attachments:
          type: array
          description: The finding's attachments; an empty array when it has none, never null.
          items:
            $ref: '#/components/schemas/Attachment'
      description: |
        The response shape for GET /v1/findings/workflows/{workflowId}/findings/{findingId}/attachments.
    SeverityCount:
      required:
        - count
        - reviewed
        - severity
      type: object
      properties:
        severity:
          type: string
          description: |
            The severity, as on Finding.severity — not a closed set; see that field.
          example: HIGH
        reviewed:
          type: boolean
          description: Whether the findings in this tally have been reviewed.
        count:
          type: integer
          description: How many findings share this severity/reviewed combination.
      description: |
        One tally: how many findings carry this exact severity/reviewed combination. The same (severity, reviewed) pair never appears twice within one severityCounts array.
    CategoryCounts:
      required:
        - category
        - orderIndex
        - reviewOutstandingCount
        - reviewRequiredCount
        - reviewedCount
        - severityCounts
      type: object
      properties:
        category:
          type: string
          description: |
            This category's configured display name — a label to show a person, never a stable key; see Category.category.
          example: Income
        orderIndex:
          type: integer
          description: |
            The category's configured presentation order — the same order GET .../bycategories uses. A bucket for findings with no configured category, when present, sorts last.
        reviewedCount:
          type: integer
          description: |
            How many of this category's findings have reviewed:true, whether or not they required review. Because it includes findings that did not require review, it can be larger than reviewRequiredCount; it is not "required reviews completed". See Finding.reviewed for how reviewed and reviewRequired combine.
        reviewOutstandingCount:
          type: integer
          description: |
            How many of this category's findings still need review: reviewRequired:true and reviewed:false. 0 means nothing in this category is waiting on a reviewer.
        reviewRequiredCount:
          type: integer
          description: |
            How many of this category's findings have reviewRequired:true, reviewed or not. reviewRequiredCount minus reviewOutstandingCount is the number of required reviews already done.
        severityCounts:
          type: array
          description: |
            This category's findings, tallied by (severity, reviewed). Always an array, [] when the category has no findings.
          items:
            $ref: '#/components/schemas/SeverityCount'
      description: |
        One configured category's review and severity counts, with no findings included. Shares its category name with the matching Category in GET .../bycategories, so the two views can be correlated by name.
    CategoryCountsEnvelope:
      required:
        - categories
        - workflowId
      type: object
      properties:
        workflowId:
          type: string
          description: |
            The workflow's opaque id, echoed on every response so a stored or logged payload still says which workflow it belongs to.
        categories:
          type: array
          description: |
            Every configured category's counts, in configured order. Always an array, never null.
            An empty array does not mean "nothing to review": it is also what a workflow with no categories configured returns.
          items:
            $ref: '#/components/schemas/CategoryCounts'
      description: |
        The response shape for GET /v1/findings/workflows/{workflowId}/categories — the workflow's configured categories as counts only, with no findings.
    Category:
      required:
        - category
        - findings
        - status
      type: object
      properties:
        category:
          type: string
          description: |
            The display name, exactly as configured — there is no closed set of values. Category names come from per-workflow configuration, so two workflows can legitimately use different names, and names can be reworded or added over time without changing this contract. Treat this as a label to show a human, never as a key to switch on. If you need something stable to key on, use the findings' code instead.
          example: Income
        status:
          type: string
          description: |
            Rollup review state for this category: IN_PROGRESS when any matched finding still needs review, else RESOLVED.
          example: RESOLVED
        findings:
          type: array
          description: |
            The findings in this category; an empty array when there are none, never null. Each is identical, field for field, to the same finding in the list and single-finding responses.
          items:
            $ref: '#/components/schemas/Finding'
      description: |
        One configured category among the workflow's categorized findings: its name, its own review state, and the findings that fell into it.
    Finding:
      required:
        - findingId
        - hasAttachments
        - hasHistory
        - references
        - reviewRequired
        - reviewed
        - userMessage
        - userSuggestion
      type: object
      properties:
        findingId:
          type: string
          description: |
            The finding's opaque id. Use it in the findingId path parameter to re-fetch this finding, its history or its attachments.
        code:
          type: string
          description: |
            The stable, machine-readable code for what was checked. Branch on this, not on displayName.
          example: HP.ST
        displayName:
          type: string
          description: |
            The human-facing label. Authored for a person and expected to be reworded, so never branch on it.
        category:
          type: array
          description: |
            The names of the categories this finding falls into — the same names as Category.category in GET .../bycategories. Always present on the GET and list responses, as an empty array when it falls into none. Omitted from the responses to the edit and attachment add/remove calls, which don't compute it.
          example:
            - Income
          items:
            type: string
        references:
          type: array
          description: |
            The subjects this finding's own run concerns — the persons, properties, participants and companies, and a person's employers, residences and properties, identified anywhere in the run the finding belongs to. An empty array, never omitted, when that run identifies no such subject (e.g. a loan-level check with no named subject at all).
            Read this as RUN-scoped, not finding-scoped. It is deliberately not narrowed to the one subject a given check happened to evaluate, so do not infer from it that this particular finding is about one specific entry in the array. Every finding from the same run carries the same set.
            It is NOT workflow-scoped. The findings of one workflow can come from several workflow-runs — a reconfirm, for example, adds a run with its own, newly staged subjects — so findings in one response can carry different sets. Read each finding's own array; never reuse one finding's array as the subject set of the whole workflow.
            Only subjects of the six kinds listed under Reference.type are included, so the array may name fewer subjects than the run holds.
            Entries are distinct: no two carry the same correlationId and type pair. Order is not significant and is not guaranteed stable between calls.
          items:
            $ref: '#/components/schemas/Reference'
        severity:
          type: string
          description: |
            The severity, as the product configured it. Usually one of CLEAR, LOW, MEDIUM, HIGH, ERROR, but not a closed set: findings produced under older configurations can carry other values. Treat an unrecognized severity as unknown and fall back on reviewed/reviewRequired rather than dropping the finding or failing the parse.
          example: HIGH
        result:
          type: string
          description: |
            The check's own outcome, exactly as the check produced it — never changed by a later override (see resultOrOverriddenResult) or by review. Like severity, not a closed set: values are defined per check, so handle one you don't recognize.
          example: Fail
        resultOrOverriddenResult:
          type: string
          description: |
            The effective outcome: the reviewer's override if one has been recorded (overrideResult on POST .../findings/{findingId}), otherwise the same as result. result keeps the check's original outcome for comparison. An override stays until replaced by another; it cannot be removed through this API. Not a closed set, like result and severity.
          example: Fail
        userMessage:
          type: array
          description: The message authored for the person reading this finding.
          items:
            type: string
        userSuggestion:
          type: array
          description: |
            The suggested next steps. Routinely empty and legitimately so — a finding that passed has nothing to suggest.
          items:
            type: string
        reviewed:
          type: boolean
          description: |
            Whether the finding has been reviewed, by a person or by an automated process (for example, a whitelist match). Always present, never null. While reviewed is true, reviewer names who or what reviewed it, when that resolves to a display name. This API does not distinguish a person's review from an automated one.
            HOW TO READ reviewed TOGETHER WITH reviewRequired. The two fields answer different questions. reviewRequired asks "does this finding need someone to look at it?"; it is decided when the finding is produced and does not change afterwards — editing a finding never changes it. reviewed asks "has it been looked at?"; it is the field that review actions change. All four combinations occur:

              * reviewRequired:true, reviewed:false — OUTSTANDING. The finding needs review and has
                not had it. These are the findings still to work.

              * reviewRequired:true, reviewed:true — REVIEW DONE. The required review has happened,
                by a person or automatically (for example, a whitelist match).

              * reviewRequired:false, reviewed:true — NO REVIEW NEEDED, ALREADY CLEARED. Typical of
                an automatically evaluated check that passed, and also the state of a finding someone
                reviewed even though it did not require it.

              * reviewRequired:false, reviewed:false — NO REVIEW NEEDED. Informational; nobody is
                expected to act on it.

            To decide whether a finding still needs attention, test reviewRequired:true AND reviewed:false; neither field alone answers that. To decide whether a finding has been cleared, read reviewed on its own — reviewed:true means cleared whatever reviewRequired says. Setting reviewed back to false (POST .../findings/{findingId}) returns a finding that requires review to OUTSTANDING, because reviewRequired is unchanged. A workflow has no outstanding reviews when no finding is in the OUTSTANDING state; the per-category counts in GET .../categories report the same states in aggregate.
        reviewRequired:
          type: boolean
          description: |
            Whether this finding needs someone to review it, whether or not that has happened yet. Always present, never null. Set when the finding is produced, from how the product configures the check, and never changed afterwards — reviewing, un-reviewing or otherwise editing the finding leaves it as it is. It does not say whether review has happened; read it together with reviewed (see reviewed's description for what each combination means). reviewRequired:true with reviewed:false is the "still needs review" state.
        reviewer:
          type: string
          description: |
            Who or what last changed the finding's review status, as a display summary and nothing more — never a user identifier. While reviewed is true, that is who reviewed it; after a review has been cleared back to unreviewed, it is who cleared it. Absent when no review status change has been recorded, or when the recorded identity could not be resolved to a display name.
        reviewNote:
          type: string
          description: |
            The current review note. null when the finding has no note, including after the note was cleared. Set, replace or clear it with reviewNote on POST .../findings/{findingId}. Earlier notes stay on the finding's history (GET .../findings/{findingId}/history).
          nullable: true
        updatedAt:
          type: string
          description: |
            When this finding last changed, as an RFC 3339 UTC instant (see this document's description). Omitted when none was recorded.
          format: date-time
          example: '2026-08-07T15:01:42Z'
        hasAttachments:
          type: boolean
          description: |
            Whether this finding has at least one attachment; a removed attachment does not count. Always present, never null. GET .../findings/{findingId}/attachments for the attachments themselves.
        hasHistory:
          type: boolean
          description: |
            Whether GET .../findings/{findingId}/history returns at least one entry for this finding: true once any event has been recorded on it, including its creation. Always present, never null.
        authorization:
          required:
            - allow
          type: object
          properties:
            allow:
              type: array
              example:
                - riskinsight:JournalResolvedValueReview
                - riskinsight:JournalResolvedValueUpdateNote
                - riskinsight:JournalResolvedValueUpdateAttachment
              items:
                type: string
          description: |
            The changes you are permitted to make to this finding. allow lists one token per permission you hold; it is [] when you hold none.

              * riskinsight:JournalResolvedValueReview — set reviewed and overrideResult on POST
                .../findings/{findingId}.

              * riskinsight:JournalResolvedValueUpdateNote — set, replace or clear reviewNote on POST
                .../findings/{findingId}.

              * riskinsight:JournalResolvedValueUpdateAttachment — add or remove attachments on this
                finding (.../findings/{findingId}/attachments).

            Not a closed set: ignore a token you don't recognize. Evaluated on every request. Use it to decide what to offer; each call checks permission again and can still be refused with a 403, for example when you may not update this workflow.
      description: |
        One compliance finding. New fields are added only as announced additions to this contract.
        hasAttachments, hasHistory, userMessage and userSuggestion are always present. The two booleans are never null; userMessage and userSuggestion are always arrays of strings, [] when empty.
        Attachments and history are not embedded: hasAttachments and hasHistory say whether any exist, and GET /v1/findings/workflows/{workflowId}/findings/{findingId}/attachments and .../history return them.
        To edit the finding, POST to /v1/findings/workflows/{workflowId}/findings/{findingId}. authorization.allow lists the changes you are permitted to make, so you can offer only those. Permission is checked again on every call, so a call can still be refused with a 403.
    Reference:
      required:
        - correlationId
        - type
      type: object
      properties:
        correlationId:
          type: string
          description: |
            The referenced subject's correlationId as recorded on the finding's own run: your own value when you supplied a usable one, otherwise the one generated when that stage's records were created. A generated value is the one echoed on the SAMI terms of that same stage. See TermCorrelationId in sami-terms-v1.yaml for when a value is generated — including that a new one is minted on every stage and that a generated company id is a positional label (Company0, Company1, ...).
            Because a new id is minted on every stage, a generated value on a finding from an earlier stage need not appear on the workflow's current terms, which reflect its newest stage. Supply your own correlationIds if you need values that match across stages. Opaque — match it, never parse it.
        type:
          type: string
          description: |
            The kind of subject referenced, using the same vocabulary as the SAMI terms model (sami-terms-v1.yaml): person, property, participant, company, employer, or residence, where employer and residence are entries of a person's employers and residences, and a property may be an entry of a person's properties (real estate that person owns) rather than a top-level property term. Not a closed set — new subject types can appear, so handle one you don't recognize.
          example: person
      description: |
        A pointer to one subject in scope for the finding — for example a particular borrower or a particular property. Always appears as an element of Finding.references, never on its own, and that array is run-scoped: a Reference names a subject the finding's run concerns, not necessarily the single subject this finding was raised against. See Finding.references for how to read the array as a whole.
    Report:
      required:
        - contentType
        - links
        - name
      type: object
      properties:
        name:
          type: string
          description: |
            Suggested file name for the downloaded document: the report's own recorded name when it has one, otherwise `report` plus the extension implied by contentType. Treat it as a hint; the authoritative name is the Content-Disposition header the download returns.
          example: report.pdf
        contentType:
          type: string
          description: |
            Media type of the bytes links.download returns. Not always `application/pdf`: a product configured with several report types (for example PDF and XML) can have a non-PDF report as its newest. Read this field rather than assuming PDF. Reported as `application/pdf` when the report recorded no usable type of its own.
          example: application/pdf
        links:
          allOf:
            - $ref: '#/components/schemas/ReportLinks'
      description: |
        One generated report document for this workflow. Metadata only — never the bytes. Follow links.download for the document itself.
    ReportLinks:
      required:
        - download
      type: object
      properties:
        download:
          $ref: '#/components/schemas/ContentLink'
      description: |
        One report's links: exactly one key, `download`, to the report's bytes. `download` is an ordinary authenticated address — never a signed or expiring URL — like AttachmentLinks.download.
        Unlike AttachmentLinks.download, following `download` has side effects; it is the only GET on this API that does. Every fetch records that the report was read and acknowledges the workflow's pending report notification. On some configurations a fetch also regenerates the report when it is out of date with later edits. Which of these apply depends on the workflow's configuration, not on your request, so fetch it only when a person wants the document — never poll it or use it as an existence check. To learn whether a report exists, read the workflow summary's `reports` array instead.
    Attachment:
      required:
        - attachmentId
        - modifiable
      type: object
      properties:
        attachmentId:
          type: string
          description: |
            The attachment's opaque id. Resolves only under the finding it belongs to.
        name:
          type: string
          description: |
            The document's file name — a display value, not an identifier. Two attachments on one finding may share a name. Omitted when it can't be resolved; the attachment is still listed.
        contentType:
          type: string
          description: |
            The document's media type, so you can decide how to render or store it before downloading. Omitted when none was recorded.
          example: application/pdf
        modifiable:
          type: boolean
          description: |
            Whether a user may change or remove this document: true for a user's own upload, false for a document the system captured. Always present, never null.
        updatedAt:
          type: string
          description: When this attachment last changed. Omitted when none was recorded.
          format: date-time
          example: '2026-08-07T15:01:42Z'
        links:
          description: The attachment's download link.
          allOf:
            - $ref: '#/components/schemas/AttachmentLinks'
      description: |
        One document attached to a finding. Metadata only — follow links.download for the document itself.
    FindingEditRequest:
      type: object
      properties:
        reviewNote:
          type: string
          description: |
            The finding's new review note. It replaces the current note rather than appending to it. An empty or blank string clears the note, after which Finding.reviewNote is null. Omit this key to leave the note unchanged. Needs riskinsight:JournalResolvedValueUpdateNote.
        reviewed:
          type: boolean
          description: |
            The finding's new reviewed state: true marks the finding reviewed, false returns it to unreviewed. Does not change reviewRequired or reviewNote. Omit this key to leave reviewed unchanged. Needs riskinsight:JournalResolvedValueReview.
        overrideResult:
          type: string
          description: |
            A reviewer's corrected outcome for the finding, published as Finding.resultOrOverriddenResult. Finding.result keeps the check's original outcome. Omit this key to leave any existing override unchanged. A present-but-blank string (empty, or only whitespace) is a 400. Needs riskinsight:JournalResolvedValueReview — the same permission as reviewed.
      description: |
        The request body for POST .../findings/{findingId} — a partial update. Every field is optional; a field left out of the body is left unchanged, and a JSON null is treated the same as leaving the field out. A body with none of them changes nothing and returns the finding as it is.
        reviewNote can be cleared by sending an empty or blank string. reviewed and overrideResult cannot be cleared: reviewed can be set to false, and an override can only be replaced by another.
    AttachmentCreateRequest:
      required:
        - file
        - name
      type: object
      properties:
        name:
          type: string
          description: The document's file name, as it will be published on Attachment.name.
        file:
          type: string
          description: |
            The document's raw bytes, as one multipart part. This part's own Content-Type header is published as Attachment.contentType.
          format: binary
      description: |
        The multipart/form-data body for POST .../findings/{findingId}/attachments. Set the file part's own Content-Type header to the document's actual media type — it is published as Attachment.contentType. For example:

        ```
        --boundary
        Content-Disposition: form-data; name="name"

        statement.pdf
        --boundary
        Content-Disposition: form-data; name="file"; filename="statement.pdf"
        Content-Type: application/pdf

        <binary bytes>
        --boundary--
        ```
    FindingHistoryEntry:
      type: object
      properties:
        eventType:
          type: string
          description: |
            What kind of event recorded this entry. Known values: created, read, edited (the finding was edited, or one of its attachments was added, changed or removed), resultChanged, reviewedChanged, noteChanged, empowerExportSuccess, empowerExportFailed. Treat the list as open: a client should tolerate values not listed here. Omitted when the stored event type isn't recognized.
          example: reviewedChanged
        field:
          type: string
          description: |
            The name of the field this entry changed. Omitted on an entry for an event that changed no field, and when none was recorded.
          example: reviewed
        previousValue:
          type: string
          description: |
            The value before the change, rendered as text. Omitted when the field had no value before the change, and on an entry for an event that changed no field.
        newValue:
          type: string
          description: |
            The value after the change, rendered as text. A stored value that is not a plain string arrives as its JSON form, so a client should not assume this parses as a bare scalar. Omitted when the stored change recorded none.
        reviewer:
          type: string
          description: |
            Who recorded this event, as a display name — never a user identifier.
        changedAt:
          type: string
          description: |
            When this event was recorded. Omitted when none was recorded; such entries sort last.
          format: date-time
          example: '2026-08-07T15:01:42Z'
      description: |
        One entry on a finding's audit trail: either one field change, or one event that changed no field (the finding was created, read, edited, or exported).
        An event that changed fields publishes one entry per field, all sharing the event's eventType, reviewer and changedAt. An event that changed no field publishes as one entry with no field, previousValue or newValue.
    AttachmentLinks:
      required:
        - download
      type: object
      properties:
        download:
          $ref: '#/components/schemas/ContentLink'
      description: |
        One attachment's links: exactly one key, `download`, to the document's bytes.
    ContentLink:
      required:
        - src
      type: object
      properties:
        src:
          type: string
        method:
          type: string
          description: Omitted when the verb is GET (the default).
          example: GET
      description: |
        A link whose target is a document's raw bytes rather than JSON — used by AttachmentLinks.download and ReportLinks.download. The address is in src.
    Problem:
      required:
        - status
        - title
        - type
      type: object
      properties:
        type:
          type: string
          description: |
            An absolute URI identifying the problem type. THIS is the stable value your program matches on. `about:blank` is RFC 9457's own placeholder and means the status code alone describes the problem.
          format: uri
          example: https://pointservices.com/problems/invalid-token
        title:
          type: string
          description: |
            A short summary of the problem type, written for a person. Stable for a given `type`, but written for a reader rather than for matching.
          example: The access token was not accepted
        status:
          type: integer
          description: |
            The HTTP status code, repeated in the body so a logged problem document stands on its own.
          format: int32
          example: 401
        detail:
          type: string
          description: |
            An explanation of THIS occurrence, written for a person to read and naming the offending field where one is known. Intended for your logs rather than for display to an end user, and NOT a stable value your program can match on. Match on `type`, never on this string.
          example: The access token was rejected.
        instance:
          type: string
          description: |
            A URI identifying THIS occurrence, written as `urn:pps:request:<id>`. The id inside it is the request correlation id, the same value returned in the `x-pps-request-id` and `X-PPS-CorrelationID` response headers. Quote it when you raise a support ticket; it is what lets PPS find this exact request.
          format: uri
          example: urn:pps:request:4b8c2d90-7f31-4a6e-95b2-0c3e8a7d1f45
      description: |
        THE PLATFORM'S ERROR BODY, as RFC 9457 Problem Details, served as `application/problem+json`.
        `type` is the stable identifier your program matches on. Branch on it rather than on any text in this body. It is always an absolute URI. `about:blank` is RFC 9457's own placeholder, meaning the status alone describes the problem; every other value is a `https://pointservices.com/problems/...` URI or an IANA-registered problem type. The complete set a given API can return is listed in that API's own document, because the APIs do not all return the same ones.
        A type URI identifies a problem; it is not a document you fetch. Compare it as a string.
        The per property examples below are drawn from ONE occurrence, the 401 `invalid-token` case, so that they read together as a single coherent document.
  securitySchemes:
    BearerAuth:
      type: http
      description: 'A bearer token in the standard `Authorization` header — `Authorization: Bearer <token>`.'
      scheme: bearer
      bearerFormat: JWT
  responses:
    Unauthorized:
      description: |
        The request carried no valid authentication. Authentication is enforced BEFORE the request body is read, so an unauthenticated request never reveals whether its body would have been accepted.
        Two cases produce this status and the problem `type` tells them apart. `about:blank` means no credential was presented. `https://pointservices.com/problems/invalid-token` means one was presented and rejected — and it deliberately does NOT say why, because the platform cannot distinguish an expired token from a revoked one from a badly signed one from a malformed one. Treat every `invalid-token` the same way: obtain a fresh token rather than inspecting the one you sent.
        WIRE YOUR TOKEN REFRESH TO THIS STATUS. An expiring token moves a client from 200 to 401, never to 403. A 403 means the opposite thing — your identity was established and the answer is still no — so refreshing a token will never resolve one.
      headers:
        WWW-Authenticate:
          description: |
            The RFC 6750 section 3 challenge. `Bearer realm="Helix"` when no credential was presented, and that same value with `, error="invalid_token"` appended when one was presented and rejected — which is the example shown here.
          style: simple
          explode: false
          schema:
            type: string
          example: Bearer realm="Helix", error="invalid_token"
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://pointservices.com/problems/invalid-token
            title: The access token was not accepted
            status: 401
            detail: The access token was rejected.
            instance: urn:pps:request:4b8c2d90-7f31-4a6e-95b2-0c3e8a7d1f45
    Forbidden:
      description: |
        You are authenticated and the answer is still no. This is an AUTHORIZATION decision, never a statement about your credential: a token that is expired, revoked or malformed is a 401, and no amount of refreshing turns this status into a 200.
        Two cases produce it and the problem `type` tells them apart. `about:blank` is a denial answered before the endpoint's own code ran. `https://pointservices.com/problems/not-authorized` is a policy decision made inside it, about the specific resource you named.
        A REFUSED REQUEST LEAVES NOTHING BEHIND. A request refused with this status is all or nothing, so a write made earlier in the same call does not survive. Nothing needs cleaning up on your side.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://pointservices.com/problems/not-authorized
            title: Not authorized
            status: 403
            detail: The authenticated identity is not permitted to perform this action.
            instance: urn:pps:request:2f7e4c83-b019-4d56-8a3f-6e1d0b9c5a27
    ConcurrentModification:
      description: |
        Another writer changed the resource while your write was in flight, so NOTHING was applied. The problem type is `https://pointservices.com/problems/concurrent-modification`.
        It is not retried for you. Read the resource again, decide whether your change still applies to what you now see, and resubmit if it does.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          example:
            type: https://pointservices.com/problems/concurrent-modification
            title: Concurrent modification
            status: 409
            detail: The resource was modified by another request while this one was being processed. This request was not applied; re-read the resource before resubmitting.
            instance: urn:pps:request:3a6f80d2-1e74-4b93-a5c0-8d2b7e4f16a9
