# Canonical contract. Copied to web/public/openapi/provenance.yaml by `npm run build` in web/ (sync-openapi.mjs).
#
# Parity gaps (implemented in gateway, not yet documented below):
#   GET  /api/provenance/v1/app/notifications/stream  — SSE
#   POST /api/provenance/v1/lifecycle/resend/webhook  — operator lifecycle
#   DELETE /api/provenance/v1/app/customer            — soft-delete customer
openapi: 3.0.3
info:
  title: Provenance API
  version: 0.3.0
  description: |
    Public read API + BFF /app/* authenticated JSON. Base path `/api/provenance/v1`.
servers:
  - url: https://provenance.naburis.cloud
paths:
  /api/provenance/v1/usage:
    post:
      summary: Record test usage (local / non-production only)
      x-internal: true
      responses:
        "202":
          description: Accepted
  /api/provenance/v1/webhooks/test:
    post:
      summary: Publish a test Svix message (local / non-production only)
      x-internal: true
      responses:
        "202":
          description: Accepted
  /api/provenance/v1/vendors:
    get:
      summary: List directory vendors
      parameters:
        - in: query
          name: offset
          schema: { type: integer, minimum: 0, default: 0 }
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, default: 50 }
        - in: query
          name: q
          schema: { type: string }
        - in: query
          name: verified
          schema: { type: string, enum: ["true", "false", "1", "0"] }
        - in: query
          name: ai_act
          schema: { type: string, enum: [green, amber, red, unknown] }
        - in: query
          name: readiness
          schema: { type: string, enum: [verified, fresh, sourced, incomplete, unavailable] }
        - in: query
          name: source_status
          schema: { type: string, enum: [ok, degraded, failing, unavailable] }
        - in: query
          name: sort
          schema: { type: string, enum: [quality, name], default: quality }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [vendors, total]
                properties:
                  vendors:
                    type: array
                    items: { $ref: "#/components/schemas/Vendor" }
                  total: { type: integer, example: 42 }
  /api/provenance/v1/vendors/{slug}:
    get:
      summary: Get vendor with recent change events and evidence count
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/VendorPublic" }
  /api/provenance/v1/vendors/{slug}/evidence-profile:
    get:
      summary: Evidence-backed public vendor profile
      description: Crawler-derived public-source claims, official sources, source freshness, and gaps. These are not vendor attestations unless the vendor listing is separately verified.
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/VendorEvidenceProfileV1" }
  /api/provenance/v1/vendors/{slug}/events:
    get:
      summary: Vendor change events (paginated)
  /api/provenance/v1/public/vendors/featured:
    get:
      summary: Featured vendors for marketing
  /api/provenance/v1/public/stats:
    get:
      summary: Aggregate marketing stats
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PublicStats" }
  /api/provenance/v1/public/team-price:
    get:
      summary: Team plan price metadata from Stripe (currency, unit_amount); configured false when Stripe not wired
  /api/provenance/v1/public/trust-pages:
    get:
      parameters:
        - in: query
          name: published
          schema:
            type: string
            enum: ["true"]
      summary: List published trust page slugs (sitemap/SEO)
  /api/provenance/v1/public/claims/verify:
    post:
      summary: Verify a pending profile claim (public token)
  /api/provenance/v1/trust-pages/{slug}:
    get:
      summary: Published trust page with subprocessors, disclosures, and approved evidence metadata
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PublishedTrustPageDetail" }
  /api/provenance/v1/public/signup:
    post:
      summary: Create tenant before OIDC
  /api/provenance/v1/billing/stripe/webhook:
    post:
      summary: Stripe webhook
  /api/provenance/v1/app/claims:
    get:
      summary: List my claims
    post:
      summary: Start vendor profile claim
  /api/provenance/v1/app/disclosures:
    get:
      summary: List disclosure drafts
    post:
      summary: Create disclosure draft
  /api/provenance/v1/app/disclosures/{id}:
    patch:
      summary: Update disclosure
  /api/provenance/v1/app/evidence/presign:
    post:
      summary: Presigned PUT for evidence blob
  /api/provenance/v1/app/evidence:
    get:
      summary: List evidence rows
    post:
      summary: Register evidence after blob upload
  /api/provenance/v1/app/evidence/{id}/url:
    get:
      summary: Presigned GET for hot tier, or JSON metadata if storage_tier is cold
  /api/provenance/v1/app/evidence/{id}:
    patch:
      summary: Set reviewer status on evidence
  /api/provenance/v1/app/me:
    get:
      summary: Current customer + trust pages
    patch:
      summary: Update customer profile fields
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                display_name: { type: string, nullable: true }
                support_email: { type: string, nullable: true }
                homepage_url: { type: string, nullable: true }
            example:
              display_name: Acme Trust
              support_email: trust@acme.example
              homepage_url: https://acme.example
      responses:
        "200":
          description: Updated customer row
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Customer" }
  /api/provenance/v1/app/data-quality:
    get:
      summary: Data-quality and money-readiness summary for the directory
      responses:
        "200":
          description: Data-quality summary
          content:
            application/json:
              schema: { type: object }
  /api/provenance/v1/app/orgs:
    get:
      summary: List organizations visible to current user
  /api/provenance/v1/app/me/export:
    get:
      summary: GDPR Article 20 — download all customer-owned data as JSON
      description: |
        Returns the calling customer's record, trust pages, vendor picks,
        evidence items metadata, audit events, API keys metadata, and
        memberships. Secrets and password hashes are intentionally excluded.
      responses:
        "200":
          description: Customer data archive
          content:
            application/json:
              schema:
                type: object
  /api/provenance/v1/app/overview:
    get:
      summary: Authenticated app overview metrics
  /api/provenance/v1/app/search:
    get:
      summary: Global authenticated app search
      parameters:
        - in: query
          name: q
          schema: { type: string }
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 25, default: 10 }
      responses:
        "200":
          description: Grouped search results
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AppSearchResponse" }
  /api/provenance/v1/app/trust-center/risk:
    get:
      summary: Trust center risk summary
  /api/provenance/v1/app/crawls/summary:
    get:
      summary: Crawl, extractor, and graphsync health summary
      responses:
        "200":
          description: Crawl operations summary
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CrawlSummary" }
  /api/provenance/v1/app/crawls/jobs:
    get:
      summary: Cursor-paginated crawl jobs
      parameters:
        - in: query
          name: cursor
          schema: { type: string }
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
      responses:
        "200":
          description: Crawl job page
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CrawlJobsResponse" }
  /api/provenance/v1/app/crawls/run:
    post:
      summary: Queue an operator-triggered crawl run
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                vendor_id: { type: string }
                source_kind: { type: string }
                source_url: { type: string, format: uri }
      responses:
        "202":
          description: Crawl job queued
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CrawlJob" }
  /api/provenance/v1/app/crawls/graph/requeue:
    post:
      summary: Requeue transient graphsync dead letters
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                include_auth: { type: boolean, default: false }
                limit: { type: integer, minimum: 1, maximum: 500, default: 100 }
      responses:
        "202":
          description: Graphsync dead-letter requeue summary
          content:
            application/json:
              schema: { $ref: "#/components/schemas/GraphRequeueResponse" }
  /api/provenance/v1/app/entitlements:
    get:
      summary: Current entitlement flags
  /api/provenance/v1/app/statements:
    get:
      summary: Customer statements and signature status
  /api/provenance/v1/app/requests:
    get:
      summary: List trust requests
    post:
      summary: Create trust request
  /api/provenance/v1/app/requests/{id}:
    get:
      summary: Get trust request detail
  /api/provenance/v1/app/requests/{id}/stages/{stageId}/advance:
    post:
      summary: Advance a trust request stage
  /api/provenance/v1/app/storage/metrics:
    get:
      summary: Evidence storage metrics
  /api/provenance/v1/app/storage/policies:
    get:
      summary: Retention policies
    post:
      summary: Create retention policy
  /api/provenance/v1/app/storage/policies/{id}:
    patch:
      summary: Update retention policy
  /api/provenance/v1/app/storage/cleanup-runs:
    get:
      summary: Recent retention cleanup runs
  /api/provenance/v1/app/notification-preferences:
    get:
      summary: Notification preferences
    patch:
      summary: Update notification preferences
  /api/provenance/v1/app/sessions:
    get:
      summary: Active sessions
  /api/provenance/v1/app/sessions/{id}:
    delete:
      summary: Revoke a session
  /api/provenance/v1/app/api-keys:
    get:
      summary: List API keys
    post:
      summary: Create API key
  /api/provenance/v1/app/api-keys/{id}:
    delete:
      summary: Revoke API key
  /api/provenance/v1/app/audit-log:
    get:
      summary: Audit log cursor page
  /api/provenance/v1/app/billing/usage:
    get:
      summary: Billing usage
  /api/provenance/v1/app/billing/invoices:
    get:
      summary: Billing invoices
  /api/provenance/v1/app/billing/payment-method:
    get:
      summary: Payment method
  /api/provenance/v1/app/billing/payment-method/setup-intent:
    post:
      summary: Create Stripe setup intent
  /api/provenance/v1/app/billing/cancel:
    post:
      summary: Cancel subscription
  /api/provenance/v1/app/billing/feedback:
    post:
      summary: Record billing feedback
  /api/provenance/v1/app/vendors:
    get:
      summary: List customer-pinned vendors
    post:
      summary: Pin customer vendor
  /api/provenance/v1/app/vendors/{vendor_id}:
    patch:
      summary: Update customer vendor pick
    delete:
      summary: Remove customer vendor pick
  /api/provenance/v1/app/iam/{proxy_path}:
    get:
      summary: Proxy IAM request through the Provenance BFF
    parameters:
      - in: path
        name: proxy_path
        required: true
        schema: { type: string }
  /api/provenance/v1/app/webhooks:
    get:
      summary: Webhook subscription
    put:
      summary: Upsert webhook subscription
  /api/provenance/v1/app/billing/checkout:
    post:
      summary: Start Stripe checkout session
  /api/provenance/v1/app/pages:
    get:
      summary: List customer pages
    post:
      summary: Create customer page
  /api/provenance/v1/app/pages/{slug}:
    get:
      summary: Get customer page
    patch:
      summary: Update customer page; publishing is gated by readiness and Team entitlement
  /api/provenance/v1/app/pages/{slug}/publish-readiness:
    get:
      summary: Check whether a trust page can be published
  /api/provenance/v1/app/pages/{slug}/subprocessors:
    get:
      summary: List subprocessor picks
    post:
      summary: Add subprocessor pick
  /api/provenance/v1/app/pages/{slug}/subprocessors/{vendor_slug}:
    delete:
      summary: Remove subprocessor pick
  /api/provenance/v1/app/notifications/stream:
    get:
      summary: Server-Sent Events stream of audit notifications for the authenticated customer
      description: |
        `text/event-stream` response. Emits `event: audit` with JSON payloads filtered to the
        resolved customer. Heartbeat comment lines every 15s. Requires BFF session (or demo /
        forwarded-auth app modes).
      responses:
        "200":
          description: SSE stream (Content-Type text/event-stream)
        "403":
          description: Customer not resolved
  /api/provenance/v1/app/customer:
    delete:
      summary: Soft-delete the authenticated customer workspace
      responses:
        "204":
          description: Customer deleted
        "403":
          description: Customer not resolved
        "404":
          description: Customer not found
  /api/provenance/v1/lifecycle/resend/webhook:
    post:
      summary: Resend lifecycle email webhook (Svix-signed)
      description: |
        Ingests Resend bounce/complaint/delivery events. Not part of the BFF `/app` surface;
        mounted at the API root with `RESEND_WEBHOOK_SECRET` verification.
      responses:
        "200":
          description: Event accepted (idempotent)
        "401":
          description: Invalid signature
        "503":
          description: Webhook not configured

components:
  schemas:
    PublicStats:
      type: object
      properties:
        vendor_count: { type: integer }
        vendors_with_sources: { type: integer }
        vendors_with_crawled_docs: { type: integer }
        vendors_without_crawled_docs: { type: integer }
        stale_crawled_docs: { type: integer }
        extractor_completed_docs: { type: integer }
        extractor_failed_docs: { type: integer }
        graph_delivered: { type: integer }
        graph_retry: { type: integer }
        graph_dead_letter: { type: integer }
        ai_act_green: { type: integer }
        ai_act_amber: { type: integer }
        ai_act_red: { type: integer }
        certs_soc2: { type: integer }
        certs_iso27001: { type: integer }
        events_7d: { type: integer }
        last_crawl_at: { type: string, format: date-time, nullable: true }
        source_kind_counts:
          type: object
          additionalProperties: { type: integer }
        publishable_vendor_count: { type: integer }
        vendors_with_source_issues: { type: integer }
    CrawlSummary:
      type: object
      properties:
        active_crawls: { type: integer }
        success_rate: { type: number, format: double }
        average_duration_seconds: { type: integer }
        failed_jobs: { type: integer }
        attempt_count: { type: integer }
        successful_attempts: { type: integer }
        failed_attempts: { type: integer }
        robots_blocked: { type: integer }
        fetch_failed: { type: integer }
        documents: { type: integer }
        empty_documents: { type: integer }
        extracted_documents: { type: integer }
        pending_documents: { type: integer }
        graph_delivered: { type: integer }
        graph_retry: { type: integer }
        graph_dead_letter: { type: integer }
        graph_auth_dead_letter: { type: integer }
        last_crawl_at: { type: string, format: date-time, nullable: true }
        last_graph_sync_at: { type: string, format: date-time, nullable: true }
        source_coverage: { type: number, format: double }
        document_coverage: { type: number, format: double }
        source_quality_score: { type: number, format: double }
        source_count: { type: integer }
        seed_source_count: { type: integer }
        real_source_count: { type: integer }
        vendors_with_sources: { type: integer }
        vendors_with_crawled_docs: { type: integer }
        vendors_without_crawled_docs: { type: integer }
        source_kind_counts:
          type: array
          items:
            type: object
            properties:
              kind: { type: string }
              count: { type: integer }
        missing_doc_vendors:
          type: array
          items:
            type: object
            properties:
              vendor_id: { type: string }
              vendor_slug: { type: string }
              vendor_name: { type: string }
              primary_domain: { type: string }
              source_count: { type: integer }
              last_error_class: { type: string }
              last_http_status: { type: integer, nullable: true }
              last_crawl_at: { type: string, format: date-time, nullable: true }
        duplicate_domain_groups:
          type: array
          items:
            type: object
            properties:
              primary_domain: { type: string }
              vendor_count: { type: integer }
              vendors:
                type: array
                items:
                  type: object
                  properties:
                    slug: { type: string }
                    display_name: { type: string }
        recommendations:
          type: array
          items: { type: string }
    CrawlJob:
      type: object
      properties:
        id: { type: string }
        vendor_id: { type: string }
        vendor_slug: { type: string }
        vendor_name: { type: string }
        source_url: { type: string }
        source_kind: { type: string }
        status: { type: string }
        started_at: { type: string, format: date-time, nullable: true }
        completed_at: { type: string, format: date-time, nullable: true }
        duration_seconds: { type: integer }
        extracted_items: { type: integer }
        error_detail: { type: string }
        robots_allowed: { type: boolean, nullable: true }
        http_status: { type: integer, nullable: true }
        error_class: { type: string }
        retryable: { type: boolean }
        content_bytes: { type: integer }
        created_at: { type: string, format: date-time }
    CrawlJobsResponse:
      type: object
      required: [jobs, next]
      properties:
        jobs:
          type: array
          items: { $ref: "#/components/schemas/CrawlJob" }
        next: { type: string }
    GraphRequeueResponse:
      type: object
      properties:
        requeued: { type: integer }
        skipped_auth_dead: { type: integer }
        remaining_dead_letter: { type: integer }
    AppSearchResult:
      type: object
      properties:
        id: { type: string }
        kind: { type: string }
        title: { type: string }
        subtitle: { type: string }
        url: { type: string }
        status: { type: string }
    AppSearchResponse:
      type: object
      required: [query, vendors, evidence, statements, trust_pages]
      properties:
        query: { type: string }
        vendors:
          type: array
          items: { $ref: "#/components/schemas/AppSearchResult" }
        evidence:
          type: array
          items: { $ref: "#/components/schemas/AppSearchResult" }
        statements:
          type: array
          items: { $ref: "#/components/schemas/AppSearchResult" }
        trust_pages:
          type: array
          items: { $ref: "#/components/schemas/AppSearchResult" }
    Vendor:
      type: object
      properties:
        id: { type: string }
        slug: { type: string }
        display_name: { type: string }
        primary_domain: { type: string }
        website_url: { type: string }
        logo_url: { type: string }
        ai_act_article50_ready: { type: string }
        verified: { type: boolean }
        has_published_trust_page: { type: boolean }
        has_pending_profile_claim: { type: boolean }
        evidence_count: { type: integer, minimum: 0 }
        source_count: { type: integer, minimum: 0 }
        claim_count: { type: integer, minimum: 0 }
        observation_count: { type: integer, minimum: 0 }
        profile_completeness: { type: integer, minimum: 0, maximum: 100 }
        source_freshness: { type: string, enum: [fresh, recent, stale, unavailable] }
        source_status: { type: string, enum: [ok, degraded, failing, unavailable] }
        confidence_band: { type: string, enum: [low, medium, high] }
        evidence_readiness_v1: { type: string, enum: [verified, fresh, sourced, incomplete, unavailable] }
        last_verified_at: { type: string, format: date-time, nullable: true }
        data_quality_warnings:
          type: array
          items: { type: string }
        trust_score_v1:
          type: object
          nullable: true
          properties:
            score: { type: number }
            risk_band: { type: string }
            decision_id: { type: string }
            as_of: { type: string, format: date-time, nullable: true }
    ChangeEvent:
      type: object
      properties:
        id: { type: string }
        event_type: { type: string }
        payload: { type: object, additionalProperties: true }
        source_url: { type: string, nullable: true }
        content_hash: { type: string, nullable: true }
        observed_at: { type: string, format: date-time }
    VendorPublic:
      allOf:
        - { $ref: "#/components/schemas/Vendor" }
        - type: object
          properties:
            recent_events:
              type: array
              items: { $ref: "#/components/schemas/ChangeEvent" }
            evidence_count: { type: integer, minimum: 0 }
    VendorEvidenceSource:
      type: object
      properties:
        source_kind: { type: string }
        url: { type: string }
        canonical_url: { type: string }
        title: { type: string }
        http_status: { type: integer, nullable: true }
        source_status: { type: string, enum: [ok, pending, degraded, failing, inactive] }
        status_reason: { type: string }
        last_success_at: { type: string, format: date-time, nullable: true }
        last_crawl_at: { type: string, format: date-time, nullable: true }
        content_bytes: { type: integer, minimum: 0 }
        discovery_method: { type: string }
        discovered_from_url: { type: string }
        replacement_for_source_id: { type: string }
        repair_method: { type: string }
        repair_reason: { type: string }
        soft_404: { type: boolean }
        inactive_reason: { type: string }
    VendorEvidenceClaim:
      type: object
      properties:
        category: { type: string }
        predicate: { type: string }
        value: { type: string }
        claim_text: { type: string }
        confidence: { type: number, minimum: 0, maximum: 1 }
        relation: { type: string, enum: [supports, refutes, mentions, background] }
        source_url: { type: string }
        evidence_artifact_id: { type: string }
        fetched_at: { type: string, format: date-time, nullable: true }
        snippet: { type: string }
    VendorStatusSummary:
      type: object
      properties:
        source_url: { type: string }
        last_checked_at: { type: string, format: date-time, nullable: true }
        latest_signal: { type: string }
        incident_signal: { type: boolean }
    VendorEvidenceGap:
      type: object
      properties:
        id: { type: string }
        message: { type: string }
    VendorEvidenceProfileV1:
      type: object
      required: [vendor, sources, claims, certifications, subprocessors, recent_source_changes, gaps]
      properties:
        vendor: { $ref: "#/components/schemas/VendorPublic" }
        sources:
          type: array
          items: { $ref: "#/components/schemas/VendorEvidenceSource" }
        claims:
          type: array
          items: { $ref: "#/components/schemas/VendorEvidenceClaim" }
        certifications:
          type: array
          items: { $ref: "#/components/schemas/VendorEvidenceClaim" }
        subprocessors:
          type: array
          items: { $ref: "#/components/schemas/VendorEvidenceClaim" }
        status_summary:
          nullable: true
          allOf:
            - { $ref: "#/components/schemas/VendorStatusSummary" }
        recent_source_changes:
          type: array
          items: { $ref: "#/components/schemas/ChangeEvent" }
        gaps:
          type: array
          items: { $ref: "#/components/schemas/VendorEvidenceGap" }
    SubprocessorPick:
      type: object
      properties:
        vendor_id: { type: string }
        slug: { type: string }
        display_name: { type: string }
        primary_domain: { type: string }
    PublicTrustDisclosure:
      type: object
      properties:
        kind: { type: string }
        title: { type: string }
        body: { type: string }
    PublicTrustEvidence:
      type: object
      properties:
        title: { type: string }
        content_type: { type: string }
        status: { type: string }
    Customer:
      type: object
      properties:
        id: { type: string }
        zitadel_org_id: { type: string, nullable: true }
        stripe_customer_id: { type: string, nullable: true }
        plan: { type: string }
        trial_ends_at: { type: string, format: date-time, nullable: true }
        subscription_status: { type: string }
        subscription_current_period_end: { type: string, format: date-time, nullable: true }
        display_name: { type: string, nullable: true }
        support_email: { type: string, nullable: true }
        homepage_url: { type: string, nullable: true }
    PublishedTrustPageDetail:
      type: object
      description: Published trust page row plus public tab payloads
      properties:
        id: { type: string }
        customer_id: { type: string }
        slug: { type: string }
        display_name: { type: string }
        custom_domain: { type: string, nullable: true }
        footer_branding: { type: boolean }
        published: { type: boolean }
        customer_plan: { type: string }
        subprocessors:
          type: array
          items: { $ref: "#/components/schemas/SubprocessorPick" }
        disclosures:
          type: array
          items: { $ref: "#/components/schemas/PublicTrustDisclosure" }
        evidence:
          type: array
          items: { $ref: "#/components/schemas/PublicTrustEvidence" }
