components:
  parameters: {}
  schemas:
    DispositionItem:
      properties:
        id:
          description: >-
            The Lead identifier your system holds: the Cavis Lead id, your own reference (third-party id) or the
            original submission id. Tried in that order.
          example: kplaw_123
          minLength: 1
          type: string
        idempotencyKey:
          description: Retrying with the same key returns the original result as `duplicate` and stores nothing.
          maxLength: 255
          minLength: 1
          nullable: true
          type: string
        metadata:
          additionalProperties: true
          description: Any extra fields your system produces; stored as sent.
          nullable: true
          type: object
        occurredAt:
          description: When the outcome happened (ISO 8601 with offset). Defaults to when Cavis received it.
          example: '2026-10-16T09:30:00+01:00'
          format: date-time
          nullable: true
          type: string
        reason:
          description: Optional reason, e.g. why the Lead was rejected.
          nullable: true
          type: string
        status:
          description: Your own status wording, verbatim (e.g. "Accept", "Awaiting docs").
          example: Accept
          minLength: 1
          type: string
      required:
        - id
        - status
      type: object
    DispositionResult:
      properties:
        dispositionId:
          description: Set when stored (or, for `duplicate`, the original's id).
          type: string
        errors:
          description: Validation errors for an `invalid` item (not stored).
          items:
            properties:
              message:
                type: string
              path:
                items:
                  anyOf:
                    - type: string
                    - type: number
                type: array
            required:
              - path
              - message
            type: object
          type: array
        index:
          type: integer
        leadId:
          description: The Cavis Lead id, when matched.
          type: string
        matchedOn:
          description: >-
            Which identifier matched. `manual`: a Cavis reviewer attached it to a Lead none of whose identifiers equals
            `id`.
          enum:
            - id
            - third_party_id
            - dynamo_id
            - manual
          type: string
        result:
          enum:
            - matched
            - unmatched
            - invalid
            - duplicate
          type: string
        unmatchReason:
          description: Why no single Lead matched. The Disposition is still stored for follow-up.
          enum:
            - no-lead-found
            - ambiguous
          type: string
      required:
        - index
        - result
      type: object
    DispositionsRequest:
      properties:
        dispositions:
          items:
            $ref: '#/components/schemas/DispositionItem'
          maxItems: 500
          minItems: 1
          type: array
      required:
        - dispositions
      type: object
    DispositionsResponse:
      properties:
        results:
          items:
            $ref: '#/components/schemas/DispositionResult'
          type: array
      required:
        - results
      type: object
    InternalErrorResponse:
      properties:
        error:
          enum:
            - internal_error
          type: string
        message:
          type: string
      required:
        - error
        - message
      type: object
    UnauthorizedResponse:
      properties:
        error:
          enum:
            - unauthorized
          type: string
        message:
          type: string
      required:
        - error
        - message
      type: object
    ValidationFailedResponse:
      properties:
        error:
          enum:
            - validation_failed
          type: string
        issues:
          items:
            additionalProperties: true
            properties:
              code:
                type: string
              message:
                type: string
              path:
                items:
                  anyOf:
                    - type: string
                    - type: number
                type: array
            required:
              - code
              - path
              - message
            type: object
          type: array
        message:
          type: string
      required:
        - error
        - message
        - issues
      type: object
  securitySchemes:
    ClientApiKey:
      description: >-
        Client API Key (`cvs_live_…` in production, `cvs_test_…` in staging). Identifies the Client; a Client may hold
        several.
      in: header
      name: X-API-Key
      type: apiKey
info:
  contact:
    email: owain.williams@cavis.co.uk
    name: Owain Williams
  description: >-
    Tell Cavis what happened to the Leads we delivered to you: accepted, rejected,

    awaiting documents, and so on. Each report is a **Disposition**.


    ## Authentication


    Send your Client API Key in the `X-API-Key` header. Cavis issues keys per

    environment: `cvs_live_…` keys work only against production and

    `cvs_test_…` keys only against staging. You may hold several keys at once, so

    you can rotate one without downtime; ask Cavis to revoke a key you no longer

    use. Keep keys server-side: never put one in a browser or mobile app.


    ## Identifying the Lead


    Put whichever identifier your system holds for the Lead in `id`: the Cavis

    Lead id, your own reference for it, or the original submission id. Cavis tries

    them in that order, and only among the Leads delivered to you.


    ## Status wording


    Send `status` in your own words, exactly as your system records it (for

    example "Accept", "Awaiting docs", "Rejected"). Cavis maps each of your

    statuses to its own reading. A status Cavis has not seen before is still

    stored and mapped later, so you never need to translate. Sharing your full list

    of statuses with Cavis up front speeds this up.


    ## History, not overwrites


    Dispositions are an append-only history. When a Lead's outcome changes, or you

    need to correct one, send a new Disposition: never resend an old one expecting

    it to be edited. Cavis treats the one with the latest `occurredAt` as the

    Lead's current outcome, so set `occurredAt` to when the outcome actually

    happened in your system.


    ## Results


    A request that passes authentication and the envelope check returns `201`

    with one result per item, in request order (`index` is the item's position):


    | `result`    | Stored? | Meaning |

    | ----------- | ------- | ------- |

    | `matched`   | Yes     | Attached to the Lead in `leadId`. |

    | `unmatched` | Yes     | No single Lead matched `id` (`unmatchReason`: `no-lead-found` or `ambiguous`). Cavis keeps
    it and follows up with you; you do not need to resend it. |

    | `invalid`   | No      | The item failed validation; see `errors`. Fix and resend that item. |

    | `duplicate` | No      | An earlier item with the same `idempotencyKey` was already stored; the original's result
    is returned. |


    One invalid item never fails the rest of the batch.


    ## Retries and idempotency


    Give every Disposition an `idempotencyKey` that is unique and stable for that

    outcome in your system (for example your status-change event id). If a request

    times out or returns `500`, resend the whole batch with the same keys: items

    that were stored come back as `duplicate` and nothing is stored twice. Without

    a key, a resent item is stored again as a second, identical history entry.


    ## Limits


    1 to 500 Dispositions per request. Send larger volumes as several requests.
  title: Cavis Disposition API
  version: 1.0.0
openapi: 3.0.3
paths:
  /dispositions:
    post:
      description: >-
        Each item's `id` is matched among your own Leads only, trying the Cavis Lead id, then your third-party id, then
        the original submission id. Items that match no Lead, or more than one, are still stored as Unmatched and
        reported back. One invalid item does not fail the batch.
      operationId: recordDispositions
      requestBody:
        content:
          application/json:
            examples:
              batch:
                description: Item 2 was sent before (same `idempotencyKey`). Item 3's `occurredAt` has no ISO 8601 offset.
                summary: A batch, including a retried item and an invalid one
                value:
                  dispositions:
                    - id: kplaw_123
                      idempotencyKey: status-change-88121
                      metadata:
                        caseHandler: J. Smith
                      occurredAt: '2026-10-16T09:30:00+01:00'
                      status: Accept
                    - id: LIT-000451
                      idempotencyKey: status-change-88122
                      occurredAt: '2026-10-16T10:05:00+01:00'
                      reason: No proof of purchase
                      status: Rejected
                    - id: kplaw_087
                      idempotencyKey: status-change-88017
                      occurredAt: '2026-10-15T16:45:00+01:00'
                      status: Awaiting docs
                    - id: kplaw_124
                      idempotencyKey: status-change-88123
                      occurredAt: 16/10/2026 09:30
                      status: Accept
              minimal:
                summary: One Disposition with only the required fields
                value:
                  dispositions:
                    - id: kplaw_123
                      status: Accept
            schema:
              $ref: '#/components/schemas/DispositionsRequest'
        required: true
      responses:
        '201':
          content:
            application/json:
              examples:
                batch:
                  summary: Results for the `batch` request example
                  value:
                    results:
                      - dispositionId: dsp_q8h2k3m9x0b1c4d5e6f7g8h9
                        index: 0
                        leadId: led_v1w2x3y4z5a6b7c8d9e0f1g2
                        matchedOn: third_party_id
                        result: matched
                      - dispositionId: dsp_r9j3l4n0y1c2d5e6f7g8h9i0
                        index: 1
                        result: unmatched
                        unmatchReason: no-lead-found
                      - dispositionId: dsp_p7g1j2l8w9a0b3c4d5e6f7g8
                        index: 2
                        leadId: led_u0v1w2x3y4z5a6b7c8d9e0f1
                        matchedOn: third_party_id
                        result: duplicate
                      - errors:
                          - message: Invalid ISO datetime
                            path:
                              - occurredAt
                        index: 3
                        result: invalid
              schema:
                $ref: '#/components/schemas/DispositionsResponse'
          description: >-
            One result per item, in request order: `matched`, `unmatched`, `invalid` (not stored) or `duplicate` (an
            idempotency replay; the original's result, nothing stored).
        '400':
          content:
            application/json:
              examples:
                emptyBatch:
                  summary: An empty `dispositions` array
                  value:
                    error: validation_failed
                    issues:
                      - code: too_small
                        inclusive: true
                        message: 'Too small: expected array to have >=1 items'
                        minimum: 1
                        origin: array
                        path:
                          - dispositions
                    message: 'Body must be { dispositions: [...] } with 1 to 500 Dispositions'
              schema:
                $ref: '#/components/schemas/ValidationFailedResponse'
          description: 'The body isn''t JSON, or isn''t `{ dispositions: [...] }` with 1 to 500 items. Nothing is stored.'
        '401':
          content:
            application/json:
              examples:
                unauthorized:
                  value:
                    error: unauthorized
                    message: Missing or invalid X-API-Key
              schema:
                $ref: '#/components/schemas/UnauthorizedResponse'
          description: The `X-API-Key` header is missing, or the key is unknown, revoked, expired or for the other environment.
        '500':
          content:
            application/json:
              examples:
                internalError:
                  value:
                    error: internal_error
                    message: Failed to record Dispositions
              schema:
                $ref: '#/components/schemas/InternalErrorResponse'
          description: Unexpected server error. Resend the batch with the same idempotency keys.
      security:
        - ClientApiKey: []
      summary: Send a batch of Dispositions
      tags:
        - Dispositions
servers:
  - description: Production (`cvs_live_…` keys)
    url: https://api.cavistech.co.uk
  - description: Staging, for integration testing (`cvs_test_…` keys)
    url: https://af63gbr3aj.execute-api.eu-west-2.amazonaws.com
tags:
  - description: Report the outcomes of Leads Cavis delivered to you.
    name: Dispositions
