openapi: 3.1.0
info:
  title: AgentPKI HTTP API
  description: |
    Cryptographic identity infrastructure for AI agents. This spec covers the
    public HTTP endpoints across the AgentPKI verifier, demo issuer, and
    bootstrap services. See the protocol spec at https://agentpki.dev/spec/v0.2
    for the wire format and trust model.
  version: 0.2.0
  contact:
    name: AgentPKI
    email: hello@agentpki.dev
    url: https://agentpki.dev
  license:
    name: Apache 2.0
    url: https://www.apache.org/licenses/LICENSE-2.0

servers:
  - url: https://verify.agentpki.dev
    description: Public verifier
  - url: https://demo.agentpki.dev
    description: Demo issuer (rate-limited, public)
  - url: https://agentpki.dev
    description: Marketing + bootstrap + claim API

tags:
  - name: verify
    description: Token verification (the trust contract)
  - name: mint
    description: Token minting (demo issuer)
  - name: directory
    description: Issuer directory + CRL
  - name: abuse
    description: Abuse reporting + aggregation
  - name: snapshots
    description: Shareable verification result permalinks
  - name: bootstrap
    description: Convenience flows for first-time users
  - name: heuristic
    description: URL / domain reputation checks

paths:
  /v1/verify:
    post:
      tags: [verify]
      summary: Verify a PASETO v4.public token
      description: |
        Returns the verdict (`allow` or `deny`), and on deny, the failure
        reason and detail. Sub-50 ms p99 globally.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token]
              properties:
                token:
                  type: string
                  description: PASETO v4.public token
                  example: v4.public.eyJpc3MiOiJkZW1vLmFnZW50cGtpLmRldiI...
      responses:
        '200':
          description: Verifier response (verdict + structured metadata)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerifyResult'

  /v1/abuse:
    post:
      tags: [abuse]
      summary: Report agent abuse
      description: |
        Receiving sites submit signed reports about bad-actor passports.
        Aggregated by (issuer, kid, jti) in KV; downstream verifiers see the
        rolled-up score in subsequent /v1/verify calls.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token, category]
              properties:
                token: { type: string }
                category:
                  type: string
                  enum: [spam, fraud, scrape_excess, policy_violation, scam, other]
                detail: { type: string }
                reporter_url: { type: string }
      responses:
        '200':
          description: Report accepted
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  report_id: { type: string }

  /v1/verification/store:
    post:
      tags: [snapshots]
      summary: Persist a verification result for sharing
      description: |
        Returns a short ID that powers /check/result/&lt;id&gt; permalinks.
        Default 24 h TTL.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [result]
              properties:
                result: { type: object }
                input: { type: string, description: 'Original token (echoed back for context)' }
                ttl_seconds: { type: integer, minimum: 60, maximum: 86400 }
      responses:
        '200':
          description: Stored
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string, example: '27c7f86c8563' }
                  ttl_seconds: { type: integer }
                  expires_at: { type: integer }

  /v1/verification/{id}:
    get:
      tags: [snapshots]
      summary: Fetch a stored verification snapshot
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, pattern: '^[0-9a-f]{8,16}$' }
      responses:
        '200':
          description: Snapshot found
          content:
            application/json:
              schema: { type: object }
        '404':
          description: Not found (expired or never existed)

  /v1/heuristic:
    post:
      tags: [heuristic]
      summary: Heuristic phishing/reputation check on URL or token
      description: |
        Extracts domains from the input and checks them against a 5,000-domain
        phishing list (PhishStats + OpenPhish + URLhaus, refreshed daily).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                input: { type: string, description: 'URL, token, or arbitrary text' }
      responses:
        '200':
          description: Heuristic result
          content:
            application/json:
              schema:
                type: object
                properties:
                  overall_verdict: { type: string, enum: [clear, suspicious, malicious] }
                  hits:
                    type: array
                    items:
                      type: object
                      properties:
                        domain: { type: string }
                        source: { type: string, enum: [agentpki_phishing_lists, openphish, phishstats, urlhaus] }

  /mint:
    get:
      tags: [mint]
      summary: Mint a demo passport (public, rate-limited)
      description: |
        Convenience endpoint for testing. Issues a real PASETO v4.public token
        signed by the demo issuer's Ed25519 key. Pass `revoked=1` to sign with
        a kid that's listed in the issuer's CRL — useful for testing deny modes.
      parameters:
        - in: query
          name: scope
          schema: { type: string }
          example: 'read:articles'
        - in: query
          name: sub
          schema: { type: string }
          example: 'agent:mybot/v1'
        - in: query
          name: lifetime
          schema: { type: integer, minimum: 60, maximum: 3600 }
        - in: query
          name: revoked
          schema: { type: integer, enum: [0, 1] }
          description: If 1, sign with the rotated kid (will fail verification)
      servers:
        - url: https://demo.agentpki.dev
      responses:
        '200':
          description: Demo passport issued
          content:
            application/json:
              schema:
                type: object
                properties:
                  token: { type: string }
                  passport: { type: object }
                  expires_in: { type: integer }
                  try_verify: { type: string, description: 'A ready-to-paste curl command' }

  /.well-known/agentpki-issuer.json:
    get:
      tags: [directory]
      summary: Issuer directory + CRL
      description: |
        Standard issuer directory format. Verifiers fetch this on every
        first-time-seen issuer; cached at edge for ~5 min.
      servers:
        - url: https://demo.agentpki.dev
      responses:
        '200':
          description: Directory document
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IssuerDirectory'

  /api/v1/bootstrap-claim:
    post:
      tags: [bootstrap]
      summary: Bootstrap claim API
      description: |
        Convenience flow for the bootstrap script. Derives a per-email demo
        subdomain, mints a passport, verifies it, stores the snapshot — all in
        one round-trip.
      servers:
        - url: https://agentpki.dev
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email: { type: string, format: email }
      responses:
        '200':
          description: Claim succeeded
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  subdomain: { type: string }
                  passport_token: { type: string }
                  verdict: { type: string }
                  check_url: { type: string }

components:
  schemas:
    VerifyResult:
      type: object
      required: [verified, verdict, verifier_id, elapsed_ms]
      properties:
        verified: { type: boolean }
        verdict: { type: string, enum: [allow, deny] }
        failure_reason:
          type: string
          enum: [bad_signature, revoked_key, expired, malformed, replay, unknown_kid, unknown_issuer, abuse_threshold_exceeded]
          description: Present only when verdict is `deny`.
        failure_detail: { type: string }
        passport:
          type: object
          properties:
            issuer: { type: string }
            issuer_name: { type: string }
            agent_id: { type: string }
            scopes:
              type: array
              items: { type: string }
            tier: { type: integer }
            issued_at: { type: integer }
            expires_at: { type: integer }
            jti: { type: string }
        abuse_score: { type: number, minimum: 0, maximum: 1 }
        cached_until: { type: integer }
        crl_fresh: { type: boolean }
        replay_checked: { type: boolean }
        verifier_id: { type: string }
        elapsed_ms: { type: integer }

    IssuerDirectory:
      type: object
      required: [v, issuer, current_keys]
      properties:
        v: { type: integer, example: 1 }
        issuer: { type: string }
        name: { type: string }
        tier: { type: integer, enum: [1, 2, 3] }
        current_keys:
          type: array
          items:
            type: object
            properties:
              kid: { type: string }
              alg: { type: string, enum: [Ed25519] }
              pubkey: { type: string, description: 'Base64-encoded SPKI public key' }
              valid_from: { type: integer }
              valid_to: { type: integer }
        revoked_keys:
          type: array
          items:
            type: object
            properties:
              kid: { type: string }
              revoked_at: { type: integer }
              reason: { type: string }
        crl_url: { type: string }
        abuse_report_url: { type: string }
        contact:
          type: object
          properties:
            abuse: { type: string }
            security: { type: string }
