openapi: 3.1.0
info:
  title: Karvia Partner APIs
  version: '1.0.0'
  summary: Public contracts for fleets, workshops, and platforms
  description: |
    Canonical host: **https://api.karvia.mx**

    Human docs: https://karvia.mx/developers · https://karvia.mx/en/developers

    Karvia product surfaces are the **web dashboard** and **operational WhatsApp**.
    There is no native App Store / Play Store app.

    Status honesty:
    - **Live** — productized
    - **Path** — JSON/webhook contract only (no native brand SDK)
    - **Stub** — credentials savable; sync not implemented (Workday/Runa)
  contact:
    name: Karvia
    url: https://karvia.mx/agentic-hub/contact
  license:
    name: Proprietary

servers:
  - url: https://api.karvia.mx
    description: Production API

tags:
  - name: Health
  - name: TelemetryWebhooks
    description: Inbound GPS and telematics push (Agent API key with write:telemetry)
  - name: DmsWebhooks
    description: Workshop and shop OS webhooks (GetCarSignal / generic DMS via JSON — no brand-native SDK)
  - name: Mcp
    description: Model Context Protocol agent hub
  - name: Passport
    description: B2B vehicle passport (/api/v1)

paths:
  /api/health:
    get:
      tags: [Health]
      summary: Liveness / health probe
      operationId: getApiHealth
      responses:
        '200':
          description: Service is up
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

  /api/webhooks/samsara:
    post:
      tags: [TelemetryWebhooks]
      summary: Samsara inbound webhook
      operationId: postWebhookSamsara
      security:
        - McpBearer: []
      parameters:
        - $ref: '#/components/parameters/SamsaraSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        '200':
          description: Event accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookReceived'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/webhooks/geotab:
    post:
      tags: [TelemetryWebhooks]
      summary: Geotab inbound webhook
      operationId: postWebhookGeotab
      security:
        - McpBearer: []
      parameters:
        - $ref: '#/components/parameters/GeotabTokenQuery'
        - $ref: '#/components/parameters/GeotabTokenHeader'
        - $ref: '#/components/parameters/WebhookSecretHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        '200':
          description: Event accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookReceived'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/webhooks/airbag:
    post:
      tags: [TelemetryWebhooks]
      summary: Airbag inbound webhook
      operationId: postWebhookAirbag
      security:
        - McpBearer: []
      parameters:
        - name: X-Airbag-Signature
          in: header
          schema:
            type: string
        - $ref: '#/components/parameters/WebhookSecretHeader'
        - $ref: '#/components/parameters/GeotabTokenQuery'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        '200':
          description: Event accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookReceived'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/webhooks/generic:
    post:
      tags: [TelemetryWebhooks]
      summary: Generic Karvia v1 telemetry webhook
      description: |
        Accepts the Karvia v1 contract or a GPS shorthand (`latitude`/`longitude`/`speed`).
        Optional query `?provider=` labels the source (default `generic`).
      operationId: postWebhookGeneric
      security:
        - McpBearer: []
      parameters:
        - name: provider
          in: query
          schema:
            type: string
            default: generic
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GenericTelemetryEvent'
            examples:
              gpsShortcut:
                summary: GPS shorthand
                value:
                  external_vehicle_id: CAR-1234
                  latitude: 19.4326
                  longitude: -99.1332
                  speed: 65
                  odometer: 42000
      responses:
        '200':
          description: Event accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookReceived'
        '400':
          description: Invalid contract
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorMessage'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/webhooks/dms/{workshopId}:
    post:
      tags: [DmsWebhooks]
      summary: Public DMS webhook (service_completion)
      description: |
        For CDK / DealerSocket / GetCarSignal / Zapier via JSON.
        Auth required via `X-API-Key` (workshop API key from Taller → Webhooks).
        Processed event: `service_completion` (other event types are accepted and stored for review).
      operationId: postDmsWebhookPublic
      parameters:
        - $ref: '#/components/parameters/WorkshopId'
        - name: X-API-Key
          in: header
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DmsServiceCompletion'
            example:
              event_type: service_completion
              dms_reference: RO-99881
              vehicle:
                vin: 3VWXXXXXXXXXXXXXX
                plate: ABC123A
                make: Volkswagen
                model: Virtus
                year: 2022
                mileage: 48200
              service:
                type: maintenance
                description: Servicio 40k + aceite
                total_cost: 2490.0
              invoice:
                currency: MXN
                total: 2490.0
      responses:
        '200':
          description: Accepted (matched or unmatched vehicle warning)
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/DmsServiceCompletionResult'
                  - $ref: '#/components/schemas/DmsUnmatchedWarning'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Workshop API key not configured
        '404':
          description: Workshop not found

  /api/workshops/webhooks/dms/{workshopId}:
    post:
      tags: [DmsWebhooks]
      summary: Premium workshop DMS webhook (services array)
      description: |
        Requires a Karvia Workshop Premium account. Auth **required**: `X-API-Key` or
        `Authorization: Bearer` with a workshop API key from Workshop → Webhooks.
        Body must include a `services` array. Successful responses use HTTP **200**.
      operationId: postDmsWebhookPremium
      parameters:
        - $ref: '#/components/parameters/WorkshopId'
      security:
        - WorkshopApiKeyHeader: []
        - WorkshopBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DmsPremiumServicesPayload'
            example:
              dms_reference: cs-wo-12345
              vehicle:
                vin: 3VWXXXXXXXXXXXXXX
                plate: ABC123A
                make: VW
                model: Virtus
                year: 2022
              services:
                - type: oil_change
                  description: Cambio de aceite
                  total_cost: 890
      responses:
        '200':
          description: Work orders created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DmsPremiumResult'
        '400':
          description: Missing services array
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Premium subscription required
        '404':
          description: Workshop not found

  /api/mcp/sse:
    get:
      tags: [Mcp]
      summary: MCP SSE transport (legacy / Cursor)
      operationId: getMcpSse
      security:
        - McpBearer: []
      responses:
        '200':
          description: SSE stream
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/mcp/stream:
    post:
      tags: [Mcp]
      summary: MCP streamable HTTP transport
      operationId: postMcpStream
      security:
        - McpBearer: []
      responses:
        '200':
          description: Streamable MCP session
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/passport/stats:
    get:
      tags: [Passport]
      summary: Public passport platform stats
      operationId: getPassportStats
      security: []
      responses:
        '200':
          description: Stats payload
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

  /api/v1/passport/mint:
    post:
      tags: [Passport]
      summary: Queue a passport mint request
      description: Returns **201 pending** — not a synchronous on-chain mint.
      operationId: postPassportMint
      security:
        - PassportBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PassportMintRequest'
      responses:
        '201':
          description: Mint queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PassportMintQueued'
        '400':
          description: vin, make, and model are required
        '409':
          description: Vehicle already has a passport
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/passport/{tokenId}:
    get:
      tags: [Passport]
      summary: Read passport by token id (public)
      operationId: getPassport
      parameters:
        - $ref: '#/components/parameters/TokenId'
      responses:
        '200':
          description: Passport data
        '404':
          description: Not found

  /api/v1/passport/{tokenId}/attest:
    post:
      tags: [Passport]
      summary: Queue a health-score attestation
      operationId: postPassportAttest
      security:
        - PassportBearer: []
      parameters:
        - $ref: '#/components/parameters/TokenId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PassportAttestRequest'
      responses:
        '201':
          description: Attestation queued
        '400':
          description: masterScore required
        '404':
          description: Passport not found
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/telemetry:
    post:
      tags: [Passport]
      summary: Ingest a telemetry event for a VIN
      operationId: postPassportTelemetry
      security:
        - PassportBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PassportTelemetryEvent'
      responses:
        '200':
          description: Accepted
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/telemetry/batch:
    post:
      tags: [Passport]
      summary: Ingest up to 100 telemetry events
      operationId: postPassportTelemetryBatch
      security:
        - PassportBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [events]
              properties:
                events:
                  type: array
                  maxItems: 100
                  items:
                    $ref: '#/components/schemas/PassportTelemetryEvent'
      responses:
        '200':
          description: Accepted
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/passport/{tokenId}/documents:
    post:
      tags: [Passport]
      summary: Attach a document reference to a passport
      operationId: postPassportDocument
      security:
        - PassportBearer: []
      parameters:
        - $ref: '#/components/parameters/TokenId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [docType]
              properties:
                docType:
                  type: string
                ipfsHash:
                  type: string
                url:
                  type: string
      responses:
        '200':
          description: Document linked
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/v1/webhook/{apiKey}:
    post:
      tags: [Passport]
      summary: Partner callback ingress (API key in path)
      operationId: postPassportPathWebhook
      parameters:
        - name: apiKey
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [event, vin]
              properties:
                event:
                  type: string
                vin:
                  type: string
              additionalProperties: true
      responses:
        '200':
          description: Accepted
        '401':
          $ref: '#/components/responses/Unauthorized'

components:
  securitySchemes:
    McpBearer:
      type: http
      scheme: bearer
      bearerFormat: kmcp_prod_
      description: Create in Dashboard → Integrations → API / MCP. Webhooks need the write:telemetry scope.
    PassportBearer:
      type: http
      scheme: bearer
      bearerFormat: pk_live_
      description: Issued by Karvia for Passport partners (contact sales).
    WorkshopApiKeyHeader:
      type: apiKey
      in: header
      name: X-API-Key
      description: Workshop API key from Workshop → Webhooks (Premium).
    WorkshopBearer:
      type: http
      scheme: bearer
      description: Same workshop API key sent as Bearer.

  parameters:
    WorkshopId:
      name: workshopId
      in: path
      required: true
      schema:
        type: string
    TokenId:
      name: tokenId
      in: path
      required: true
      schema:
        type: string
    SamsaraSignature:
      name: X-Samsara-Signature
      in: header
      required: false
      schema:
        type: string
      description: HMAC signature using the webhook secret saved in Integrations (required in production).
    GeotabTokenQuery:
      name: token
      in: query
      required: false
      schema:
        type: string
    GeotabTokenHeader:
      name: X-Geotab-Token
      in: header
      required: false
      schema:
        type: string
    WebhookSecretHeader:
      name: X-Webhook-Secret
      in: header
      required: false
      schema:
        type: string

  responses:
    Unauthorized:
      description: Missing or invalid credentials
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorMessage'

  schemas:
    ErrorMessage:
      type: object
      properties:
        error:
          type: string
      additionalProperties: true

    WebhookReceived:
      type: object
      properties:
        received:
          type: boolean
        eventId:
          type: string
      additionalProperties: true

    GenericTelemetryEvent:
      type: object
      required: [external_vehicle_id]
      properties:
        external_vehicle_id:
          type: string
          description: Or vehicle_id / externalVehicleId
        vehicle_id:
          type: string
        type:
          type: string
          enum: [diagnostics, fault, trip, alert, behavior, gps]
        latitude:
          type: number
        longitude:
          type: number
        speed:
          type: number
        odometer:
          type: number
      additionalProperties: true

    DmsServiceCompletion:
      type: object
      required: [event_type]
      properties:
        event_type:
          type: string
          enum: [service_completion, work_order_created, status_update]
        dms_reference:
          type: string
        vehicle:
          type: object
          properties:
            vin: { type: string }
            plate: { type: string }
            make: { type: string }
            model: { type: string }
            year: { type: integer }
            mileage: { type: number }
        service:
          type: object
          properties:
            type: { type: string }
            description: { type: string }
            total_cost: { type: number }
        invoice:
          type: object
          additionalProperties: true

    DmsServiceCompletionResult:
      type: object
      properties:
        received: { type: boolean }
        work_order_id: { type: integer }
        service_record_id: { type: integer }
        vehicle_id: { type: integer }

    DmsUnmatchedWarning:
      type: object
      properties:
        received: { type: boolean }
        warning: { type: string }
        log_id: { type: integer }

    DmsPremiumServicesPayload:
      type: object
      required: [services]
      properties:
        dms_reference:
          type: string
        vehicle:
          type: object
          properties:
            vin: { type: string }
            plate: { type: string }
            make: { type: string }
            model: { type: string }
            year: { type: integer }
        services:
          type: array
          minItems: 1
          items:
            type: object
            properties:
              type: { type: string }
              description: { type: string }
              total_cost: { type: number }

    DmsPremiumResult:
      type: object
      properties:
        success: { type: boolean }
        created_orders:
          type: array
          items: { type: integer }
        count: { type: integer }

    PassportMintRequest:
      type: object
      required: [vin, make, model]
      properties:
        vin: { type: string }
        make: { type: string }
        model: { type: string }
        year: { type: integer }
        ownerWallet: { type: string }
        metadata:
          type: object
          additionalProperties: true

    PassportMintQueued:
      type: object
      properties:
        success: { type: boolean }
        requestId: { type: integer }
        status:
          type: string
          example: pending
        message: { type: string }
        estimatedTime: { type: string }

    PassportAttestRequest:
      type: object
      required: [masterScore]
      properties:
        masterScore:
          type: integer
          minimum: 0
          maximum: 10000
          description: Basis points 0–10000
        legal: { type: integer }
        mechanical: { type: integer }
        driving: { type: integer }
        maintenance: { type: integer }
        eventType: { type: string }

    PassportTelemetryEvent:
      type: object
      required: [vin, eventType, payload]
      properties:
        vin: { type: string }
        eventType: { type: string }
        payload:
          type: object
          additionalProperties: true
