openapi: 3.0.3
info:
  title: Consent Management Vendor API
  description: |
    API for vendor consent-management integrations. Vendors authenticate with a
    provisioned Bearer API key and operate across the dealers integrated with that vendor.

    **Authentication**

    Use the vendor API key issued during consent-management provisioning. Send it as
    `Authorization: Bearer <api_key>`.

    In this docs UI (Scalar), paste **only the raw API key** into the Bearer Token
    field — do not include the `Bearer ` prefix (Scalar adds it).

    **Target dealer**

    Consent endpoints require `client_key` (query string) to identify the dealer the
    request applies to. Use `GET /dealers` to list integrated dealers and their keys.

    **Contact types**

    Consent endpoints are namespaced by `contact_type` in the URL path:
    `email`, `phone_sms`, or `phone_call`.

    Contact values are normalized server-side (emails lowercased; phone numbers
    standardized) before lookup and storage.
  version: 1.0.0

servers:
  - url: https://fullpath.com/api/v2/external/consent-management
    description: Production

security:
  - bearerAuth: []

tags:
  - name: Dealers
    description: List dealers integrated with the authenticated vendor.
  - name: Consents
    description: Read and write communication consent for a contact.

paths:
  /dealers:
    get:
      tags: [Dealers]
      summary: List integrated dealers
      description: |
        Returns all dealers that have a consent-management integration with the
        authenticated vendor (both current and previous integrations).
      operationId: listDealers
      responses:
        '200':
          description: Dealer list for the authenticated vendor.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DealersResponse'
              examples:
                vendor:
                  summary: Vendor with multiple dealers
                  value:
                    dealers:
                      - client_key: 40NM-00001-1
                        name: Example Motors
                        status: connected
                        connected_at: '2026-03-15T14:22:00+00:00'
                      - client_key: 40NM-00002-1
                        name: Downtown Auto
                        status: pending
                        connected_at: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /{contact_type}/consent:
    get:
      tags: [Consents]
      summary: Get consent for a contact
      description: |
        Returns the current consent status for a single contact at the target dealer.

        When no consent record exists, the response is `200` with `opt_in` and
        `consent_timestamp` set to `null` (not `404`).

        Requires `client_key` to identify the target dealer.
      operationId: getConsent
      parameters:
        - $ref: '#/components/parameters/ContactType'
        - $ref: '#/components/parameters/ContactValue'
        - $ref: '#/components/parameters/ClientKey'
      responses:
        '200':
          description: Consent status for the contact (including when no record exists).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConsentResponse'
              examples:
                optedIn:
                  summary: Contact has opted in
                  value:
                    contact_value: user@example.com
                    opt_in: true
                    consent_timestamp: '2026-05-12T18:30:00+00:00'
                noRecord:
                  summary: No consent record on file
                  value:
                    contact_value: user@example.com
                    opt_in: null
                    consent_timestamp: null
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          $ref: '#/components/responses/TooManyRequests'

  /{contact_type}/consents:
    post:
      tags: [Consents]
      summary: Store consents in bulk
      description: |
        Stores one or more consent records for contacts at the target dealer.
        Accepts between 1 and 500 contacts per request.

        Exact duplicates in the same request (same normalized contact value and
        `consent_timestamp`) are deduplicated; the last entry wins.

        Requires `client_key` (query string) to identify the target dealer.

        On success, returns an empty JSON object and updates the vendor
        data-received timestamp for the dealer.
      operationId: storeConsents
      parameters:
        - $ref: '#/components/parameters/ContactType'
        - $ref: '#/components/parameters/ClientKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StoreConsentsRequest'
            examples:
              emailOptIn:
                summary: Single email opt-in
                value:
                  contacts:
                    - contact_value: user@example.com
                      opt_in: true
                      consent_timestamp: '2026-05-12T18:30:00Z'
              phoneOptOut:
                summary: Phone call opt-out
                value:
                  contacts:
                    - contact_value: '+1 (555) 123-4567'
                      opt_in: false
                      consent_timestamp: '2026-05-12T18:30:00Z'
      responses:
        '200':
          description: Consents stored successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmptySuccessResponse'
        '400':
          description: |
            Business-rule or normalization failure.

            Common causes:
            - One or more contacts failed validation after normalization
              (`"One or more contacts are invalid."`)
            - `client_key` missing, invalid, or not integrated with the vendor
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'
              examples:
                invalidContacts:
                  value:
                    message: One or more contacts are invalid.
                clientKeyRequired:
                  value:
                    message: client_key is required.
                dealerNotIntegrated:
                  value:
                    message: The requested dealer is not integrated with this vendor.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          $ref: '#/components/responses/TooManyRequests'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageResponse'
              examples:
                storeFailed:
                  value:
                    message: Failed to store consents.
                vendorStatusFailed:
                  value:
                    message: Consents were stored but vendor integration status could not be updated.

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        Vendor API key issued during consent-management provisioning.
        In Scalar, paste only the raw key (e.g. `cmk_live_...`) — do not type `Bearer `.
        HTTP clients should send `Authorization: Bearer <api_key>`.
        Keys may expire; expired or unknown keys receive `401 Unauthorized`.

  parameters:
    ContactType:
      in: path
      name: contact_type
      required: true
      description: Channel the consent applies to.
      schema:
        $ref: '#/components/schemas/ContactType'

    ContactValue:
      in: query
      name: contact_value
      required: true
      description: |
        Raw contact identifier to look up. Emails are lowercased; phone numbers are
        standardized before lookup. The normalized value is echoed in the response.
      schema:
        type: string
      examples:
        email:
          value: user@example.com
        phone:
          value: '+1 (555) 123-4567'

    ClientKey:
      in: query
      name: client_key
      required: true
      description: |
        Fullpath client key for the target dealer (format `40NM-{number}-1`).
        Required on all consent endpoints.
      schema:
        type: string
        pattern: '^40NM-\d+-1$'
      example: 40NM-00001-1

  responses:
    Unauthorized:
      description: Missing, invalid, or expired Bearer token.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/MessageResponse'
          example:
            message: Unauthorized

    BadRequest:
      description: Request failed business-rule validation outside Laravel form validation.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/MessageResponse'

    ValidationError:
      description: "Laravel validation failure (422 Unprocessable Entity)."
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ValidationErrorResponse'
          example:
            message: The contact value field is required.
            errors:
              contact_value:
                - The contact value field is required.

    TooManyRequests:
      description: Rate limit exceeded (100 requests per minute per API key).
      headers:
        Retry-After:
          description: Seconds until the rate limit resets.
          schema:
            type: integer
        X-RateLimit-Limit:
          schema:
            type: integer
        X-RateLimit-Remaining:
          schema:
            type: integer
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/MessageResponse'
          example:
            message: Too Many Attempts.

    ServerError:
      description: Unexpected server error.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/MessageResponse'
          example:
            message: An unexpected error occurred.

  schemas:
    ContactType:
      type: string
      enum:
        - email
        - phone_sms
        - phone_call

    IntegrationStatus:
      type: string
      description: Consent-management integration status for a dealer.
      enum:
        - connected
        - pending

    Dealer:
      type: object
      required:
        - client_key
        - name
        - status
        - connected_at
      properties:
        client_key:
          type: string
          description: "Fullpath client key (40NM-{number}-1)."
          example: 40NM-00001-1
        name:
          type: string
          description: Dealer display name.
          example: Example Motors
        status:
          $ref: '#/components/schemas/IntegrationStatus'
        connected_at:
          type: string
          format: date-time
          nullable: true
          description: |
            ISO 8601 timestamp when the integration reached `connected` status.
            `null` when status is not `connected` or no connection date is recorded.

    DealersResponse:
      type: object
      required:
        - dealers
      properties:
        dealers:
          type: array
          items:
            $ref: '#/components/schemas/Dealer'

    ConsentResponse:
      type: object
      required:
        - contact_value
        - opt_in
        - consent_timestamp
      properties:
        contact_value:
          type: string
          description: Normalized contact value used for the lookup.
          example: user@example.com
        opt_in:
          type: boolean
          nullable: true
          description: |
            `true` if opted in, `false` if opted out, `null` when no consent record
            exists for this contact.
        consent_timestamp:
          type: string
          format: date-time
          nullable: true
          description: |
            ISO 8601 timestamp of the consent event. `null` when no consent record
            exists.

    ConsentContact:
      type: object
      required:
        - contact_value
        - opt_in
        - consent_timestamp
      properties:
        contact_value:
          type: string
          description: |
            Raw contact identifier. Must be a valid email for `email`, or a valid
            phone number for `phone_sms` / `phone_call`.
        opt_in:
          type: boolean
          description: "true to record opt-in; false to record opt-out."
        consent_timestamp:
          type: string
          format: date-time
          description: When the consent event occurred (ISO 8601).

    StoreConsentsRequest:
      type: object
      required:
        - contacts
      properties:
        contacts:
          type: array
          minItems: 1
          maxItems: 500
          items:
            $ref: '#/components/schemas/ConsentContact'

    EmptySuccessResponse:
      type: object
      description: Empty JSON object returned on successful bulk store.
      additionalProperties: false
      example: {}

    MessageResponse:
      type: object
      required:
        - message
      properties:
        message:
          type: string

    ValidationErrorResponse:
      type: object
      required:
        - message
        - errors
      properties:
        message:
          type: string
          description: Summary of the validation failure.
        errors:
          type: object
          description: Field-level validation errors keyed by input name.
          additionalProperties:
            type: array
            items:
              type: string
