openapi: 3.0.3
info:
  title: CARNETIA Public API
  description: |
    Public REST API for the CARNETIA trust registry — the verification layer
    for autonomous AI agents.

    All endpoints in this spec are reachable without authentication unless
    explicitly marked otherwise. JSON in, JSON out.

    Read https://carnetia.ai/llms.txt for the narrative version of this spec.

    **Native Claude / MCP support:** CARNETIA also exposes a public Model
    Context Protocol server at https://mcp.carnetia.ai/mcp (transport:
    streamable-http, spec: https://modelcontextprotocol.io) so any
    MCP-capable client (Claude Desktop, Claude Code, Claude API) can
    register and verify agents in natural language.
  version: "2.0.0"
  contact:
    name: CARNETIA
    email: hello@carnetia.ai
    url: https://carnetia.ai
  license:
    name: Open access (verify endpoints)
    url: https://carnetia.ai/terms

servers:
  - url: https://api.carnetia.ai
    description: Canonical production API (preferred)
  - url: https://carnetia.ai/api
    description: Legacy alias (still works permanently)

tags:
  - name: registry
    description: Identity registration and verification
  - name: liveness
    description: TOTP / heartbeat liveness signals
  - name: founders
    description: Founder program (limited-cohort badges)
  - name: health
    description: Service status
  - name: operator
    description: Authenticated operator-side endpoints

paths:

  /v2/health:
    get:
      tags: [health]
      summary: Service health and global stats
      operationId: getHealth
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Health"

  /v2/register:
    post:
      tags: [registry]
      summary: Self-register a new agent (open, rate-limited)
      operationId: registerAgent
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RegisterRequest"
      responses:
        "200":
          description: Created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RegisterResponse"
        "201":
          description: |
            Agent created. May be in `pending_email` status if email verification is required;
            in that case the response includes `verification_required: true` and `verification_expires_at`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RegisterResponse"
        "403":
          description: |
            AGENT_LIMIT_REACHED — the user has exceeded the tier's agent limit.
            Response includes `tier`, `current`, `limit`, `upgrade_url`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean, enum: [false] }
                  error: { type: string, enum: ["AGENT_LIMIT_REACHED"] }
                  message: { type: string }
                  tier: { type: string }
                  current: { type: integer }
                  limit: { type: integer, nullable: true }
                  upgrade_url: { type: string }
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ValidationError"
        "429":
          description: |
            Rate limited. Buckets: 3/min, 10/hour, 20/day per IP; 2/hour, 5/day per email.
            Whitelisted admin emails bypass the limiter.

  /v2/verify-email:
    get:
      tags: [registry]
      summary: Confirm email verification token (activates the agent)
      operationId: verifyEmail
      parameters:
        - in: query
          name: token
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Email verified, agent activated.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  carnetia_id: { type: string }
                  status: { type: string }
                  is_founder: { type: boolean }
                  founder_rank: { type: integer, nullable: true }
                  verify_url: { type: string }
                  message: { type: string }
        "400":
          description: MISSING_TOKEN
        "404":
          description: TOKEN_INVALID_OR_EXPIRED

  /v2/verify/{carnetia_id}:
    get:
      tags: [registry]
      summary: Public verification of an agent
      operationId: verifyAgent
      parameters:
        - $ref: "#/components/parameters/CarnetiaId"
      responses:
        "200":
          description: Agent record
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Agent"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiError"

  /v2/agent/{carnetia_id}:
    get:
      tags: [registry]
      summary: Lightweight agent info (for liveness widgets)
      operationId: getAgentPublic
      parameters:
        - $ref: "#/components/parameters/CarnetiaId"
      responses:
        "200":
          description: Public agent snapshot
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentPublic"
        "404":
          description: Not found

  /v2/challenge/{carnetia_id}:
    get:
      tags: [liveness]
      summary: Get a TOTP challenge envelope
      operationId: getChallenge
      parameters:
        - $ref: "#/components/parameters/CarnetiaId"
      responses:
        "200":
          description: Challenge envelope
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

  /v2/verify-totp:
    post:
      tags: [liveness]
      summary: Verify a TOTP code an agent has produced
      operationId: verifyTotp
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [carnetia_id, code]
              properties:
                carnetia_id:
                  type: string
                  example: CAR-2026-MX-0049-GDQJ
                code:
                  type: string
                  example: "123456"
      responses:
        "200":
          description: Result of verification
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

  /v2/agents/{carnetia_id}/heartbeat:
    post:
      tags: [liveness]
      summary: Heartbeat signal from a running agent
      operationId: agentHbSignal
      parameters:
        - $ref: "#/components/parameters/CarnetiaId"
      responses:
        "200":
          description: Acknowledged

  /v2/founders/stats:
    get:
      tags: [founders]
      summary: Founder program statistics
      operationId: getFoundersStats
      responses:
        "200":
          description: Stats
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/FoundersStats"

  /v2/founders:
    get:
      tags: [founders]
      summary: Public roll-call of founder agents
      operationId: listFounders
      responses:
        "200":
          description: List
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/Agent"

  /v2/auth/token:
    post:
      tags: [operator]
      summary: Exchange operator credentials for a Sanctum bearer token
      operationId: authToken
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email:    { type: string, format: email }
                password: { type: string }
      responses:
        "200":
          description: Token
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

  /v2/auth/me:
    get:
      tags: [operator]
      summary: Current authenticated operator
      operationId: authMe
      security:
        - bearerAuth: []
      responses:
        "200":
          description: User
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

  /v2/my-agents:
    get:
      tags: [operator]
      summary: Agents owned by the current operator
      operationId: myAgents
      security:
        - bearerAuth: []
      responses:
        "200":
          description: List
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true

  /v2/agents/{carnetia_id}/totp/enable:
    post:
      tags: [Liveness]
      summary: Enable TOTP (live challenge) for an agent
      description: |
        Generates a TOTP secret (base32), stores it encrypted, marks the agent's
        `totp_enabled_at = now()`, and returns the secret, otpauth:// URI and a
        QR code (base64-encoded PNG) for enrollment in Google Authenticator,
        Authy, 1Password, etc. After enabling, any third party can verify the
        agent's liveness in real time via `POST /v2/verify-totp`.
        Idempotent: returns 409 TOTP_ALREADY_ENABLED if already on.
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: carnetia_id
          required: true
          schema: { type: string }
      responses:
        '200':
          description: TOTP enabled
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:               { type: boolean, example: true }
                  carnetia_id:      { type: string }
                  secret:           { type: string, example: "UKEI5J5I2YNDCKUIQ2GGMYEFKJKMFPU6", description: "Base32 TOTP secret (32 chars). Shown ONCE." }
                  uri:              { type: string, example: "otpauth://totp/CARNETIA:CAR-...?secret=...&issuer=CARNETIA&algorithm=SHA1&digits=6&period=30" }
                  qr_png_base64:    { type: string, format: byte, description: "PNG image of the QR, base64-encoded. Decode and display to operator." }
                  qr_url:           { type: string, format: uri }
                  totp_enabled_at:  { type: string, format: date-time }
                  message:          { type: string }
                  instructions:     { type: string }
        '401':
          description: Missing or invalid bearer token
        '403':
          description: Token not authorized for this agent's operator
        '404':
          description: Agent not found
        '409':
          description: TOTP already enabled
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:    { type: boolean, example: false }
                  error: { type: string, example: TOTP_ALREADY_ENABLED }
        '410':
          description: Agent revoked

  /v2/agents/{carnetia_id}/totp/qr.png:
    get:
      tags: [Liveness]
      summary: PNG QR code for an enabled agent
      description: Renders the agent's otpauth:// provisioning URI as a PNG. Requires the operator's bearer token.
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: carnetia_id
          required: true
          schema: { type: string }
      responses:
        '200':
          description: PNG image
          content:
            image/png:
              schema: { type: string, format: binary }
        '401': { description: Unauthenticated }
        '403': { description: Forbidden }
        '404': { description: Agent not found or TOTP not enabled }


components:

  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: Sanctum

  parameters:
    CarnetiaId:
      name: carnetia_id
      in: path
      required: true
      description: Public CARNETIA ID of the agent (CAR-YYYY-CC-NNNN-XXXX)
      schema:
        type: string
        pattern: '^CAR-\d{4}-[A-Z]{2}-\d{4}-[A-Z0-9]{4}$'
        example: CAR-2026-MX-0049-GDQJ

  schemas:

    Health:
      type: object
      properties:
        status:    { type: string, example: ok }
        service:   { type: string, example: "CARNETIA Identity Registry API" }
        version:   { type: string, example: "2.0.0-laravel" }
        timestamp: { type: string, format: date-time }
        stats:
          type: object
          properties:
            users:           { type: integer }
            operators:       { type: integer }
            agents:          { type: integer }
            founder_agents:  { type: integer }
            subscribers:     { type: integer }

    RegisterRequest:
      type: object
      required: [operator_type, operator_name, country, user_email, agent_name]
      properties:
        operator_type:
          type: string
          enum: [person, company]
        operator_name:
          type: string
          maxLength: 200
        country:
          type: string
          minLength: 2
          maxLength: 2
          description: ISO-3166-1 alpha-2 country code
          example: MX
        user_email:
          type: string
          format: email
        agent_name:
          type: string
          maxLength: 200
        agent_purpose:
          type: string
          maxLength: 500
        agent_vendor:
          type: string
          maxLength: 100
        agent_model:
          type: string
          maxLength: 200

    RegisterResponse:
      type: object
      properties:
        ok:           { type: boolean, example: true }
        carnetia_id:  { type: string,  example: CAR-2026-MX-1234-AB5C }
        fingerprint:  { type: string }
        verify_url:   { type: string, format: uri }
        badge:        { type: string, enum: [gold, silver, bronze, none] }
        is_founder:   { type: boolean }
        founder_tier: { type: string, nullable: true }

    Agent:
      type: object
      properties:
        carnetia_id:    { type: string }
        name:           { type: string }
        purpose:        { type: string, nullable: true }
        vendor:         { type: string, nullable: true }
        model:          { type: string, nullable: true }
        country:        { type: string }
        status:         { type: string, example: active }
        fingerprint:    { type: string }
        is_founder:     { type: boolean }
        founder_tier:   { type: string, nullable: true }
        founder_cohort: { type: string, nullable: true }
        badge:          { type: string }
        operator:
          type: object
          properties:
            operator_id: { type: string }
            type:        { type: string, enum: [person, company] }
            legal_name:  { type: string }
            country:     { type: string }
        activated_at:  { type: string, format: date-time, nullable: true }
        created_at:    { type: string, format: date-time }
        verify_url:    { type: string, format: uri }

    AgentPublic:
      type: object
      properties:
        ok:    { type: boolean }
        agent:
          type: object
          properties:
            carnetia_id:          { type: string }
            name:                 { type: string }
            status:               { type: string }
            is_founder:           { type: boolean }
            founder_rank:         { type: integer, nullable: true }
            badge:                { type: string }
            totp_enabled_at:      { type: string, format: date-time, nullable: true }
            last_seen_at:         { type: string, format: date-time, nullable: true }
            last_verified_at:     { type: string, format: date-time, nullable: true }
            verifications_count:  { type: integer }
            aliveness_score:      { type: integer }

    FoundersStats:
      type: object
      properties:
        ok: { type: boolean }
        stats:
          type: object
          properties:
            cap:             { type: integer, example: 1000000 }
            total_founders:  { type: integer }
            persons:         { type: integer }
            companies:       { type: integer }
            remaining:       { type: integer }
            percentage_used: { type: number, format: float }
            pack_open:       { type: boolean }
            by_tier:
              type: object
              additionalProperties:
                type: object
                properties:
                  limit:     { type: integer }
                  used:      { type: integer }
                  remaining: { type: integer }

    ApiError:
      type: object
      properties:
        ok:      { type: boolean, example: false }
        error:   { type: string }
        message: { type: string }

    ValidationError:
      type: object
      properties:
        message: { type: string }
        errors:
          type: object
          additionalProperties:
            type: array
            items: { type: string }
