openapi: "3.0.3"
info:
  title: Not Human Search API
  description: Free neutral search for AI agents. Optional provider-funded actions stay separate from organic rank; only an authenticated downstream provider outcome can create the disclosed provider charge.
  version: "1.1.0"
  x-version-policy: Descriptive release version; controlled-pilot provider endpoints carry explicit contract versions and can require an owner-gated breaking cutover.
  contact:
    email: hello@nothumansearch.ai
servers:
  - url: https://nothumansearch.ai/api/v1
paths:
  /:
    get:
      summary: API index — list of endpoints and base URLs
      operationId: getIndex
      responses:
        "200":
          description: API index document
          content:
            application/json:
              schema:
                type: object
                properties:
                  base_url:           { type: string }
                  openapi_spec:       { type: string }
                  ai_plugin_manifest: { type: string }
                  mcp_endpoint:       { type: string }
                  endpoints:          { type: object, additionalProperties: { type: string } }
  /catalog:
    get:
      summary: Agent-readable commerce catalog
      operationId: getCommerceCatalog
      responses:
        "200":
          description: Catalog for score-fix service and API subscription plans
  /quote:
    post:
      summary: Create a deterministic quote
      operationId: createCommerceQuote
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                product_id: { type: string, enum: [nhs_geo_fix_my_score, nhs_api_unlimited] }
                plan:       { type: string, enum: [unlimited] }
      responses:
        "200":
          description: Quote with amount, total, currency, and required checkout metadata
  /checkout:
    post:
      summary: Create a Stripe Checkout URL for a GEO uplift order
      operationId: createCommerceCheckout
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [host, email]
              properties:
                product_id:   { type: string, default: nhs_geo_fix_my_score }
                payment_mode: { type: string, enum: [stripe_checkout, stripe] }
                host:         { type: string }
                email:        { type: string, format: email }
                repo_url:     { type: string }
                notes:        { type: string }
      responses:
        "201":
          description: Stripe Checkout URL
        "501":
          description: Requested payment mode is not supported
  /api-keys/subscribe:
    get:
      summary: List the optional priority-throughput API key plan and checkout contract
      operationId: getAPIKeySubscriptionPlans
      responses:
        "200":
          description: Priority throughput only; baseline discovery and organic results remain free
    post:
      summary: Create a Stripe Checkout session for a paid API key subscription
      operationId: createAPIKeySubscriptionCheckout
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, plan]
              properties:
                email: { type: string, format: email }
                plan:  { type: string, enum: [unlimited], default: unlimited }
      responses:
        "200":
          description: Stripe Checkout URL and activation URL
        "503":
          description: Stripe is not configured
  /categories:
    get:
      summary: Get all category buckets and their counts
      operationId: listCategories
      responses:
        "200":
          description: Category counts across the index
          content:
            application/json:
              schema:
                type: object
                properties:
                  categories:
                    type: array
                    items:
                      type: object
                      properties:
                        name:  { type: string }
                        count: { type: integer }
  /monitor/register:
    post:
      summary: Register an email to monitor a site's agentic readiness score
      operationId: registerMonitor
      description: Sends an alert via email when the indicated domain's score drops. Returns an unsubscribe URL.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, domain]
              properties:
                email:  { type: string, format: email }
                domain: { type: string, description: "Domain to monitor (no scheme)" }
      responses:
        "201":
          description: Monitor registered
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:              { type: boolean }
                  domain:          { type: string }
                  unsubscribe_url: { type: string, format: uri }
        "400":
          description: Invalid email or domain
        "429":
          description: Too many monitors for this email
  /search:
    get:
      summary: Search for agent-ready sites
      operationId: searchSites
      parameters:
        - name: q
          in: query
          schema: { type: string }
          description: Search query (matches name, description, domain)
        - name: category
          in: query
          schema: { type: string, enum: [ai-tools, developer, data, finance, ecommerce, jobs, security, health, education, communication, productivity, news, other, spam] }
          description: Public categories plus audit-only buckets other and spam. Do not treat audit-only buckets as promoted discovery inventory.
        - name: tag
          in: query
          schema: { type: string }
          description: Exact tag match (e.g. mcp, openapi, payment, search). See /sitemap.xml for indexed tags.
        - name: min_score
          in: query
          schema: { type: integer, minimum: 0, maximum: 100 }
          description: Minimum agentic readiness score
        - name: has_api
          in: query
          schema: { type: boolean }
          description: Filter to sites with structured APIs
        - name: has_mcp
          in: query
          schema: { type: boolean }
          description: Filter to sites with a Model Context Protocol server
        - name: has_openapi
          in: query
          schema: { type: boolean }
          description: Filter to sites that publish an OpenAPI spec
        - name: has_llms_txt
          in: query
          schema: { type: boolean }
          description: Filter to sites that publish llms.txt
        - name: page
          in: query
          schema: { type: integer, default: 1 }
      responses:
        "200":
          description: Search results
          content:
            application/json:
              schema:
                type: object
                properties:
                  access: { type: string, enum: [free] }
                  receipt_recorded: { type: boolean, description: True only when the query-free receipt transaction committed }
                  search_id: { type: string, description: Query-free receipt for an optional detail-selection request }
                  results: { type: array, items: { $ref: "#/components/schemas/Site" } }
                  paid_offers_available: { type: boolean, description: True only when a committed search receipt has separately disclosed provider-funded actions for exact returned organic sites }
                  paid_offers:
                    type: array
                    description: Separate optional actions; never included in results, score, total, or organic ordering
                    items: { $ref: "#/components/schemas/PublicProviderOffer" }
                  action_interest:
                    type: object
                    description: Provider-independent way to record caller-attested principal interest against an exact returned organic result
                    required: [available, search_id, eligible_domains, action_types, caller_attestation_required, confirmation_version, confirmation_url, invocation_condition, call_contract, endpoint, provider_contacted, commercial_proof, organic_rank_affected]
                    properties:
                      available: { type: boolean }
                      search_id: { type: string, description: Exact discovery receipt to pass only when the invocation condition is true }
                      eligible_domains: { type: array, items: { type: string }, description: Domains already present in this organic result set; never paid placement }
                      action_types: { type: array, items: { type: string, enum: [quote, trial, demo, booking, application, signup, purchase] } }
                      caller_attestation_required: { type: boolean, enum: [true] }
                      endpoint: { type: string, format: uri }
                      confirmation_version: { type: string, enum: [nhs-action-interest-v1] }
                      confirmation_url: { type: string, format: uri }
                      invocation_condition: { type: string, description: Requires explicit current principal intent and forbids inference from discovery or selection }
                      call_contract: { $ref: "#/components/schemas/ActionInterestCallContract" }
                      provider_contacted: { type: boolean, enum: [false] }
                      commercial_proof: { type: boolean, enum: [false] }
                      organic_rank_affected: { type: boolean, enum: [false] }
                  total: { type: integer }
                  page: { type: integer }
                  per_page: { type: integer }
                  has_next: { type: boolean }
        "429":
          description: Temporary search safety limit exceeded; free access resumes after the reset
  /provider/claims:
    get:
      summary: List provider claims and DNS ownership-check status for the signed-in account
      operationId: listProviderClaims
      description: Each claim exposes the last successful ownership check, next scheduled check, and consecutive failure count without exposing the stored token hash or raw DNS answers.
      security: [{ SessionCookie: [] }]
      responses:
        "200":
          description: Provider claims with safe ownership-freshness status
          content:
            application/json:
              schema:
                type: object
                required: [claims]
                properties:
                  claims: { type: array, items: { $ref: "#/components/schemas/ProviderClaim" } }
        "401": { description: Human account session required }
    post:
      summary: Begin an indexed-domain provider claim
      operationId: createProviderClaim
      description: The returned TXT value must remain published after verification. NHS persists only its SHA-256 token hash, then automatically rechecks domain control; raw DNS answers are not retained.
      security: [{ SessionCookie: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [domain]
              properties:
                domain: { type: string, description: Domain already present in the NHS index }
      responses:
        "201":
          description: Claim, one-time DNS TXT challenge, and ownership-freshness contract
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ProviderClaimChallengeResponse" }
        "401": { description: Human account session required }
        "409": { description: Domain is already claimed or the account already has a claim }
  /provider/claims/{claim_id}/verify:
    post:
      summary: Verify the claim's DNS TXT challenge and return the callback key once
      operationId: verifyProviderClaim
      description: Keep the verified TXT value published. NHS stores only the token hash and automatically rechecks it. Paid-action eligibility stops after 3 consecutive failures or when the last successful check reaches 7 days old.
      security: [{ SessionCookie: [] }]
      parameters:
        - { name: claim_id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        "200":
          description: Verified claim, current ownership freshness, and a newly issued provider key returned once when applicable
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ProviderClaimVerifyResponse" }
        "409": { description: DNS challenge missing, mismatched, or expired; a real failed repeat check on a verified claim advances its freshness failure count }
  /provider/claims/{claim_id}/challenge:
    post:
      summary: Rotate a pending claim's DNS challenge
      operationId: rotateProviderClaimChallenge
      description: Replaces the pending token hash and returns the new TXT value once. After verification, keep that value published for automatic ownership rechecks.
      security: [{ SessionCookie: [] }]
      parameters:
        - { name: claim_id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        "200":
          description: Rotated one-time DNS TXT challenge and ownership-freshness contract
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ProviderClaimChallengeResponse" }
  /provider/claims/{claim_id}/revoke:
    post:
      summary: Revoke a provider claim, keys, offers, and outstanding action authorization
      operationId: revokeProviderClaim
      security: [{ SessionCookie: [] }]
      parameters:
        - { name: claim_id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        "200": { description: Claim revoked }
  /provider/claims/{claim_id}/keys/rotate:
    post:
      summary: Rotate the claim-scoped provider callback key
      operationId: rotateProviderCallbackKey
      security: [{ SessionCookie: [] }]
      parameters:
        - { name: claim_id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        "200": { description: New provider key returned once; prior keys revoked }
  /provider/offers:
    get:
      summary: List offers owned by the signed-in provider account
      operationId: listProviderOffers
      security: [{ SessionCookie: [] }]
      parameters:
        - { name: claim_id, in: query, required: false, schema: { type: string, format: uuid } }
      responses:
        "200": { description: Provider offers and commercial states }
    post:
      summary: Create a draft provider-funded action offer
      operationId: createProviderOffer
      description: Terms-only launch pilot. Drafts cannot appear beside organic results until the provider authenticates the exact capped CPA terms, the owner verifies that acceptance, and NHS activates the offer.
      security: [{ SessionCookie: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - { $ref: "#/components/schemas/ProviderOfferRequest" }
                - { type: object, required: [claim_id] }
      responses:
        "201": { description: Draft offer created }
        "409": { description: Offer inventory limit reached }
  /provider/offers/{offer_id}:
    get:
      summary: Get one offer owned by the signed-in provider account
      operationId: getProviderOffer
      security: [{ SessionCookie: [] }]
      parameters:
        - { name: offer_id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        "200": { description: Provider offer }
    put:
      summary: Update an owned draft offer
      operationId: updateProviderOffer
      security: [{ SessionCookie: [] }]
      parameters:
        - { name: offer_id, in: path, required: true, schema: { type: string, format: uuid } }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProviderOfferRequest" }
      responses:
        "200": { description: Updated offer }
  /provider/offers/{offer_id}/pause:
    post:
      summary: Pause an owned offer
      operationId: pauseProviderOffer
      security: [{ SessionCookie: [] }]
      parameters:
        - { name: offer_id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        "200": { description: Offer paused }
  /provider/commercial-acceptances:
    post:
      summary: Record one provider-authenticated commercial acceptance
      operationId: recordProviderCommercialAcceptance
      description: Provider-key-authenticated append-only acceptance only. It cannot establish a deduplicated company, funding, exact terms, renewal, or pilot proof until NHS separately records the applicable owner-verified company and commercial evidence. Accepted event shapes are exact and unknown fields are rejected.
      security: [{ ProviderKey: [] }]
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          description: Claim-scoped opaque replay key. Reuse with a different exact payload is rejected.
          schema:
            type: string
            minLength: 8
            maxLength: 128
            pattern: "^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProviderCommercialAcceptanceRequest" }
      responses:
        "201":
          description: New provider-authenticated acceptance; owner verification is still required and commercial proof is false
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ProviderCommercialAcceptanceResponse" }
        "200":
          description: Exact idempotent replay of the existing provider-authenticated acceptance
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ProviderCommercialAcceptanceResponse" }
        "400": { description: Invalid header, event shape, reference, or unknown field }
        "401": { description: Valid active claim-scoped provider key required }
        "409": { description: Stale claim, conflicting replay, mismatched offer, or invalid renewal chain }
  /provider/pilot-status:
    get:
      summary: Read claim-scoped pilot continuity status
      operationId: getProviderPilotStatus
      description: Provider-key-authenticated read-only status for the key's own DNS-verified claim. It includes provider setup, exact owned offer and terms state, and only handoff or outcome events that have crossed the NHS-observed handoff boundary. It returns no credentials, attribution material, search receipts, controlled intent, queries, identities, contacts, network data, company hashes, or action URLs.
      security: [{ ProviderKey: [] }]
      parameters:
        - name: limit
          in: query
          required: false
          description: Maximum owned offers and recent observed handoff records returned in each collection.
          schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
      responses:
        "200":
          description: Current status for the authenticated provider claim
          headers:
            Cache-Control:
              schema: { type: string, enum: ["private, no-store"] }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ProviderPilotStatusResponse" }
        "400": { description: Invalid limit }
        "401": { description: Valid active claim-scoped provider key required }
        "404": { description: Authenticated claim is no longer verified and fresh }
        "429": { description: Temporary provider read safety limit exceeded }
        "500": { description: Provider pilot status query failed }
        "503": { description: Provider key authentication unavailable }
  /provider/demand:
    get:
      summary: Read privacy-thresholded demand for the authenticated claim domain
      operationId: getProviderDemand
      description: Provider-key-authenticated read-only aggregate demand for the key's own current DNS-verified claim domain. The domain is derived from the authenticated claim and cannot be selected by the caller. Counts represent retained receipts, not unique agents or principals. Result-selection and action-interest counts and rates are suppressed below their exact receipt thresholds; topic and action-type rows are omitted below their thresholds. Organic-return counts and controlled surface labels remain reportable. No raw query, identity, contact, network data, alleged agent identity, or individual receipt is returned.
      security: [{ ProviderKey: [] }]
      parameters:
        - name: days
          in: query
          required: false
          description: Inclusive lookback window for retained aggregate receipts.
          schema: { type: integer, minimum: 1, maximum: 30, default: 30 }
      responses:
        "200":
          description: Privacy-thresholded aggregate demand for the authenticated claim domain
          headers:
            Cache-Control:
              schema: { type: string, enum: ["private, no-store"] }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ProviderDemandResponse" }
        "400": { description: Invalid days }
        "401": { description: Valid active claim-scoped provider key required }
        "404": { description: Authenticated claim is no longer verified and fresh }
        "429": { description: Temporary provider read safety limit exceeded }
        "500": { description: Provider demand query failed }
        "503": { description: Provider key authentication unavailable }
  /provider/action-tickets/resolve:
    post:
      summary: Resolve separately consented controlled intent after an observed handoff
      operationId: resolveProviderControlledIntent
      description: Read-only claim-scoped provider resolution. The body accepts only the exact signed attribution bearer. Resolution is available only after an NHS-observed handoff that included the separate nhs-provider-controlled-intent-disclosure-consent-v1 attestation. It returns only the controlled topic, optional region code, USD budget band, urgency, allowlisted requirement flags, and opaque binding metadata. It returns no query, search receipt, identity, contact, network, action URL, price, accounting data, charge, outcome, or commercial proof. Resolver access is free and does not change organic rank or readiness.
      security: [{ ProviderKey: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ProviderControlledIntentResolveRequest" }
      responses:
        "200":
          description: Exact separately consented controlled-intent bundle; no charge or proof created
          headers:
            Cache-Control:
              schema: { type: string, enum: ["private, no-store"] }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ProviderControlledIntentResolution" }
        "400": { description: Malformed, unknown-field, empty, or invalid-signature attribution bearer }
        "401": { description: Valid active claim-scoped provider key required }
        "404": { description: Consented controlled intent unavailable; wrong claim, absent consent, and ineligible state are intentionally indistinguishable }
        "410": { description: Correctly signed attribution bearer expired }
        "429": { description: Temporary provider resolver safety limit exceeded }
        "503": { description: Provider exchange, signer, or resolver dependency unavailable }
  /action-interests:
    post:
      summary: Record caller-attested principal interest in one controlled next step
      operationId: recordActionInterest
      description: Creates a private, query-free Stage 1 demand receipt bound to an exact returned organic domain. It expires with the source search, no later than 30 days after that search. It does not contact the provider, create an action ticket or charge, affect organic rank or readiness score, or count as commercial proof. The caller attests current principal interest under the exact wording at https://nothumansearch.ai/privacy#action-interest-v1; NHS does not verify identity, agency, or legal authority.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ActionInterestRequest" }
      responses:
        "201":
          description: New provider-independent action-interest receipt
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ActionInterestResponse" }
        "200":
          description: Exact idempotent replay of the existing receipt
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ActionInterestResponse" }
        "403": { description: Cross-origin browser mutation rejected; native agents without browser-origin headers remain supported }
        "404": { description: Missing, stale, synthetic, or non-returned organic source; intentionally indistinguishable }
        "409": { description: This search result already recorded a different controlled action }
        "429": { description: Temporary free abuse limit exceeded }
  /action-tickets:
    post:
      summary: Prepare an authorization-attested action for a disclosed paid offer
      operationId: createActionTicket
      description: Requires a committed organic search receipt and exact principal-consent v1 attestation. Accepts controlled constraints only; no name, email, contact detail, raw prompt, agent identity, or principal identity. Returns the raw ticket bearer and POST handoff endpoint, not the provider action URL. Creating a ticket charges neither party. See https://nothumansearch.ai/privacy#consent-v1.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ActionTicketRequest" }
      responses:
        "201":
          description: New ticket, raw attribution bearer, and POST handoff endpoint; no provider action URL and no charge
          headers:
            Cache-Control:
              schema: { type: string, enum: ["private, no-store"] }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ActionTicketPreparationResponse" }
        "200":
          description: Exact replay with the attribution bearer reconstructed from the persisted ticket snapshot
          headers:
            Cache-Control:
              schema: { type: string, enum: ["private, no-store"] }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ActionTicketPreparationResponse" }
        "400": { description: Invalid JSON, unknown field, ticket input, or consent attestation }
        "404": { description: Exact public offer or returned-offer evidence unavailable; intentionally indistinguishable }
        "409": { description: Provider claim, authorization, commercial evidence, or provider-funded capacity unavailable; or request conflicts with a prior ticket. The principal is not charged. }
        "410": { description: Exact replay refers to an expired ticket authorization }
        "429": { description: Temporary action-ticket safety limit exceeded }
        "503": { description: Signed provider actions are not configured }
  /action-tickets/handoff:
    post:
      summary: Record an NHS-observed handoff and reveal the provider action URL
      operationId: handoffActionTicket
      description: Presents the raw ticket bearer and the separate exact nhs-provider-handoff-consent-v1 principal attestation only in a bounded JSON body, never in the NHS URL or query string; every response is private, no-store. NHS atomically records one append-only privacy-safe nhs-action-handoff-v1 receipt bound to the exact ticket, offer version, commercial-terms hash, and handoff-consent version before returning the attributed provider action URL. The principal may separately and optionally authorize the exact DNS-verified provider to resolve only the bounded controlled-intent bundle under nhs-provider-controlled-intent-disclosure-consent-v1; declining that disclosure does not block this handoff or free direct provider access. The receipt contains no query, agent or principal identity, contact data, network address, referrer, or user agent. This handoff and the optional resolver charge neither party; only the configured authenticated provider-reported downstream outcome can create the disclosed provider charge. Exact wording is at https://nothumansearch.ai/privacy#handoff-consent-v1 and https://nothumansearch.ai/privacy#controlled-intent-disclosure-consent-v1.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ActionTicketHandoffRequest" }
      responses:
        "201":
          description: New durable observed-handoff receipt and attributed provider URL; neither party charged
          headers:
            Cache-Control:
              schema: { type: string, enum: ["private, no-store"] }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ActionTicketHandoffResponse" }
        "200":
          description: Exact replay of the existing durable handoff receipt and provider URL; neither party charged
          headers:
            Cache-Control:
              schema: { type: string, enum: ["private, no-store"] }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ActionTicketHandoffResponse" }
        "400": { description: Invalid JSON, unknown field, ticket ID, or empty bearer }
        "404": { description: Ticket or exact bearer not found }
        "409": { description: Verified commercial evidence unavailable, authorization revoked, ticket already terminal, or otherwise ineligible for handoff; neither party charged }
        "410": { description: Ticket attribution expired; neither party charged }
        "429": { description: Temporary handoff safety limit exceeded }
  /provider/outcomes:
    post:
      summary: Record an idempotent provider-reported action outcome
      operationId: recordProviderOutcome
      description: Provider-authenticated assertion, not an independent NHS audit. A charged ticket may later receive an invalid or duplicate credit after expiry or revocation; no positive outcome or new charge may cross those boundaries.
      security: [{ ProviderKey: [] }]
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          schema: { type: string, minLength: 8, maxLength: 200 }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [attribution_token, outcome]
              properties:
                ticket_id: { type: string, format: uuid, deprecated: true, description: "Optional compatibility assertion; NHS derives the authoritative ticket from the verified attribution token" }
                attribution_token: { type: string, description: "The exact signed bearer received in the attributed provider action URL; NHS derives its ticket and offer binding server-side" }
                outcome: { type: string, enum: [accepted, activated, converted, rejected, duplicate, invalid] }
      responses:
        "201": { description: New signed provider-outcome receipt }
        "200": { description: Exact idempotent replay }
        "401": { description: Valid claim-scoped provider key required }
        "409": { description: Invalid transition, revoked authorization, or conflicting idempotency payload }
  /provider/receipts/{receipt_id}:
    get:
      summary: Retrieve one receipt owned by the authenticated provider
      operationId: getProviderReceipt
      security: [{ ProviderKey: [] }]
      parameters:
        - { name: receipt_id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        "200": { description: Signed provider receipt }
        "401": { description: Valid claim-scoped provider key required }
        "404": { description: Receipt not found for this provider }
  /action-receipts/verify:
    post:
      summary: Verify immutable NHS signature separately from freshness and current accounting state
      operationId: verifyActionReceipt
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [signed_receipt, signature]
              properties:
                signed_receipt: { type: string, description: Exact canonical JSON returned by NHS }
                signature: { type: string, description: Unpadded base64url HMAC }
      responses:
        "200":
          description: Signature validity, time-window status, and current online commercial state when available
          content:
            application/json:
              schema:
                type: object
                properties:
                  signature_valid: { type: boolean }
                  within_validity_window: { type: boolean }
                  time_status: { type: string, enum: [current, expired, not_yet_valid, invalid_time] }
                  receipt: { $ref: "#/components/schemas/SignedOutcomeReceipt" }
                  current_state_available: { type: boolean }
                  current_state_status: { type: string, enum: [current, not_found, unavailable] }
                  current_state: { $ref: "#/components/schemas/PublicOutcomeReceiptState" }
  /site/{domain}:
    get:
      summary: Get detailed agentic readiness report for a site
      operationId: getSite
      parameters:
        - name: domain
          in: path
          required: true
          schema: { type: string }
        - name: search_id
          in: query
          required: false
          description: Optional receipt returned by search; records a detail selection only when this domain was returned. A newly recorded, non-synthetic selection also returns an optional action_interest sidecar for this domain; selection alone is never treated as interest.
          schema: { type: string }
      responses:
        "200":
          description: Site details, plus a noncommercial action-interest opportunity only after an exact new receipt-bound selection
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SelectedSiteDetail" }
  /submit:
    post:
      summary: Submit a site for crawling
      operationId: submitSite
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, format: uri }
      responses:
        "201":
          description: Submitted for crawling
  /check:
    post:
      summary: On-demand agentic readiness check (live crawl)
      operationId: checkSite
      description: |
        Crawls the target URL on demand and returns its 7-signal agentic
        readiness score. Ideal for CI pipelines that should fail when an
        agent-facing site regresses. Free tier: 10 checks/hour per IP.
        An optional priority key raises this to 100/hour while its monthly
        allocation remains; exhaustion falls back to the free tier.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, format: uri }
      responses:
        "200":
          description: Score + 7 signals
        "429":
          description: Rate limit exceeded
  /verify-mcp:
    get:
      summary: Live-probe a URL for MCP compliance
      operationId: verifyMCP
      description: |
        Sends a JSON-RPC tools/list request to the target URL and reports
        whether it responded with a spec-compliant reply. REST peer of
        the MCP verify_mcp tool. No caching — caller is asking "is it
        live right now?"
      parameters:
        - name: url
          in: query
          required: true
          description: Target URL (with or without scheme)
          schema: { type: string }
      responses:
        "200":
          description: Probe result
          content:
            application/json:
              schema:
                type: object
                properties:
                  verified: { type: boolean }
                  endpoint: { type: string }
                  note:     { type: string }
        "400":
          description: url query param missing
        "429":
          description: Temporary live-probe safety limit exceeded; free access resumes after the reset
  /stats:
    get:
      summary: Get index statistics
      operationId: getStats
      responses:
        "200":
          description: Index stats
  /top:
    get:
      summary: Top-scored agent-ready sites
      description: >
        Returns the highest-scored sites in the index (sorted by agentic_score DESC).
        Designed as a stable JSON other sites can mirror / embed. Cached 5 min.
      operationId: getTop
      parameters:
        - in: query
          name: category
          schema: { type: string }
          description: Public category filter. Audit-only buckets other and spam may be queried directly but are not promoted as discovery inventory.
        - in: query
          name: has_mcp
          schema: { type: boolean }
          description: Return only sites with a verified MCP server
        - in: query
          name: has_openapi
          schema: { type: boolean }
        - in: query
          name: has_llms_txt
          schema: { type: boolean }
        - in: query
          name: has_api
          schema: { type: boolean }
        - in: query
          name: limit
          schema: { type: integer, default: 50, maximum: 100 }
      responses:
        "200":
          description: Top sites
          content:
            application/json:
              schema:
                type: object
                properties:
                  results: { type: array, items: { $ref: "#/components/schemas/Site" } }
                  total:   { type: integer }
                  limit:   { type: integer }
components:
  securitySchemes:
    SessionCookie:
      type: apiKey
      in: cookie
      name: nhs_session
      description: Human account session created by the fail-closed email sign-in flow
    ProviderKey:
      type: apiKey
      in: header
      name: X-NHS-Provider-Key
      description: Claim-scoped provider key returned once after DNS verification or explicit rotation; used for provider reads, acceptances, controlled-intent resolution, and outcome callbacks
  schemas:
    ProviderClaim:
      type: object
      description: Provider ownership state. NHS never returns the persisted challenge-token hash or raw DNS answers.
      required: [id, site_id, domain, verification_method, verification_record_name, status, challenge_expires_at, verification_consecutive_failures, created_at, updated_at]
      properties:
        id: { type: string, format: uuid }
        site_id: { type: string, format: uuid }
        domain: { type: string }
        verification_method: { type: string, enum: [dns_txt] }
        verification_record_name: { type: string, description: TXT record name that must remain published while the claim is verified }
        status: { type: string, enum: [pending, verified, revoked] }
        challenge_expires_at: { type: string, format: date-time, description: Expiry for a pending one-time challenge; it does not authorize removal of a verified TXT record }
        verified_at: { type: string, format: date-time, nullable: true }
        verification_last_succeeded_at: { type: string, format: date-time, nullable: true, description: Last successful DNS ownership check; paid actions require this to remain within the freshness window }
        verification_last_attempted_at: { type: string, format: date-time, nullable: true }
        verification_consecutive_failures: { type: integer, minimum: 0, description: Consecutive automatic or owner-triggered DNS failures since the last success }
        verification_next_check_at: { type: string, format: date-time, nullable: true, description: Next scheduled automatic DNS ownership check }
        revoked_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    OwnershipFreshness:
      type: object
      description: Machine-readable persistent DNS ownership contract and claim-specific safe status; no token, token hash, or raw DNS answer is exposed.
      required: [proof_method, record_must_remain_published, stored_challenge_material, raw_dns_answers_retained, automatic_reverification, recheck_interval_seconds, paid_actions_stop_after_consecutive_failures, paid_actions_stop_when_last_success_age_reaches_seconds, last_succeeded_at, next_check_at, consecutive_failures]
      properties:
        proof_method: { type: string, enum: [dns_txt] }
        record_must_remain_published: { type: boolean, enum: [true] }
        stored_challenge_material: { type: string, enum: [sha256_hash_only], description: NHS persists only the SHA-256 hash of the challenge token }
        raw_dns_answers_retained: { type: boolean, enum: [false], description: TXT answers are compared in memory and are not persisted }
        automatic_reverification: { type: boolean, enum: [true] }
        recheck_interval_seconds: { type: integer, enum: [86400], description: Interval scheduled after a successful check; next_check_at reflects any earlier failure retry }
        paid_actions_stop_after_consecutive_failures: { type: integer, enum: [3], description: This consecutive failed check revokes the claim and stops paid-action eligibility }
        paid_actions_stop_when_last_success_age_reaches_seconds: { type: integer, enum: [604800], description: Paid-action eligibility stops at this age even before a revocation update is recorded }
        last_succeeded_at: { type: string, format: date-time, nullable: true }
        next_check_at: { type: string, format: date-time, nullable: true }
        consecutive_failures: { type: integer, minimum: 0 }
    ProviderClaimChallengeResponse:
      type: object
      required: [claim, dns_challenge, ownership_freshness, verify_endpoint]
      properties:
        claim: { $ref: "#/components/schemas/ProviderClaim" }
        dns_challenge:
          type: object
          required: [record_type, record_name, record_value, expires_at, returned_once]
          properties:
            record_type: { type: string, enum: [TXT] }
            record_name: { type: string }
            record_value: { type: string, description: Returned once. Keep the full TXT value published after verification; NHS stores only the token hash. }
            expires_at: { type: string, format: date-time }
            returned_once: { type: boolean, enum: [true] }
        ownership_freshness: { $ref: "#/components/schemas/OwnershipFreshness" }
        verify_endpoint: { type: string }
    ProviderClaimVerifyResponse:
      type: object
      required: [claim, verified, provider_key_returned, ownership_freshness]
      properties:
        claim: { $ref: "#/components/schemas/ProviderClaim" }
        verified: { type: boolean, enum: [true] }
        provider_key: { type: string, description: Newly issued callback key returned once when this verification created it }
        provider_key_metadata: { type: object }
        provider_key_returned: { type: boolean }
        save_this_key_now: { type: boolean }
        key_endpoint: { type: string, description: Explicit rotation endpoint when a concurrent verification already issued the active key }
        ownership_freshness: { $ref: "#/components/schemas/OwnershipFreshness" }
    ProviderOfferRequest:
      type: object
      additionalProperties: false
      required: [name, summary, action_type, action_url, charge_event, bounty_cents, currency, principal_price_mode, principal_currency, billing_mode]
      properties:
        claim_id: { type: string, format: uuid }
        name: { type: string, minLength: 1, maxLength: 80 }
        summary: { type: string, minLength: 1, maxLength: 280 }
        action_type: { type: string, enum: [lead, demo, trial, signup, purchase, quote, application, booking] }
        action_url: { type: string, format: uri, description: HTTPS URL on the verified provider domain, with no query or fragment }
        charge_event: { type: string, enum: [accepted, activated, converted] }
        bounty_cents: { type: integer, minimum: 1, maximum: 1000000 }
        currency: { type: string, enum: [usd] }
        principal_price_mode: { type: string, enum: [free, fixed, quote, provider_pricing] }
        principal_price_cents: { type: integer, minimum: 0, maximum: 100000000 }
        principal_currency: { type: string, enum: [usd] }
        billing_mode: { type: string, enum: [terms], description: "The bounded launch pilot supports provider-authenticated exact capped CPA terms only; prepaid collection is not launched" }
        terms_credit_limit_cents: { type: integer, minimum: 1, maximum: 10000000 }
        terms_period_days: { type: integer, minimum: 1, maximum: 90 }
    ProviderCommercialAcceptanceRequest:
      description: Exact provider-authenticated acceptance shapes. Unknown fields and shape-inappropriate fields are rejected.
      oneOf:
        - type: object
          additionalProperties: false
          required: [event_type, provider_acceptance_reference]
          properties:
            event_type: { type: string, enum: [pilot_company] }
            provider_acceptance_reference: { type: string, minLength: 8, maxLength: 200, pattern: "^[A-Za-z0-9][A-Za-z0-9._:/-]{7,199}$", description: Non-secret provider evidence reference }
        - type: object
          additionalProperties: false
          required: [event_type, offer_id, offer_version, exact_terms_sha256, provider_acceptance_reference]
          properties:
            event_type: { type: string, enum: [terms_acceptance] }
            offer_id: { type: string, format: uuid }
            offer_version: { type: integer, minimum: 1, description: Exact version reviewed by the provider; rejected if the current draft differs }
            exact_terms_sha256: { type: string, pattern: "^[0-9a-f]{64}$", description: Exact commercial terms hash reviewed by the provider; rejected if the current draft differs }
            provider_acceptance_reference: { type: string, minLength: 8, maxLength: 200, pattern: "^[A-Za-z0-9][A-Za-z0-9._:/-]{7,199}$", description: Non-secret provider evidence reference }
        - type: object
          additionalProperties: false
          required: [event_type, offer_id, related_acceptance_event_id, offer_version, exact_terms_sha256, provider_acceptance_reference]
          properties:
            event_type: { type: string, enum: [terms_renewal] }
            offer_id: { type: string, format: uuid }
            related_acceptance_event_id: { type: string, format: uuid, description: Prior terms_acceptance or terms_renewal in the same exact-terms chain }
            offer_version: { type: integer, minimum: 1, description: Exact version reviewed by the provider; rejected if the current draft differs }
            exact_terms_sha256: { type: string, pattern: "^[0-9a-f]{64}$", description: Exact commercial terms hash reviewed by the provider; rejected if the current draft differs }
            provider_acceptance_reference: { type: string, minLength: 8, maxLength: 200, pattern: "^[A-Za-z0-9][A-Za-z0-9._:/-]{7,199}$", description: Non-secret provider evidence reference }
    ProviderCommercialAcceptanceEvent:
      type: object
      description: Append-only provider-key-authenticated event; not owner verification or commercial proof by itself
      required: [id, provider_claim_id, provider_api_key_id, event_type, provider_acceptance_reference, provider_accepted_at, created_at]
      properties:
        id: { type: string, format: uuid }
        provider_claim_id: { type: string, format: uuid }
        provider_offer_id: { type: string, format: uuid }
        provider_api_key_id: { type: integer, format: int64 }
        event_type: { type: string, enum: [pilot_company, terms_acceptance, terms_renewal] }
        related_acceptance_event_id: { type: string, format: uuid }
        offer_version: { type: integer, minimum: 1 }
        terms_contract_version: { type: string, enum: [nhs-provider-commercial-terms-v1] }
        exact_terms_sha256: { type: string, pattern: "^[0-9a-f]{64}$" }
        provider_acceptance_reference: { type: string, minLength: 8, maxLength: 200 }
        provider_accepted_at: { type: string, format: date-time }
        valid_until: { type: string, format: date-time }
        created_at: { type: string, format: date-time }
    ProviderCommercialAcceptanceResponse:
      type: object
      additionalProperties: false
      required: [acceptance, created, idempotent_replay, provider_authenticated, owner_verification_required, commercial_proof_created, evidence_scope]
      properties:
        acceptance: { $ref: "#/components/schemas/ProviderCommercialAcceptanceEvent" }
        created: { type: boolean }
        idempotent_replay: { type: boolean }
        provider_authenticated: { type: boolean, enum: [true] }
        owner_verification_required: { type: boolean, enum: [true] }
        commercial_proof_created: { type: boolean, enum: [false] }
        evidence_scope: { type: string }
    ProviderPilotOfferStatus:
      type: object
      additionalProperties: false
      description: Exact owned offer contract and provider/owner acceptance state. Draft status does not imply activation or Merchant-of-Record acknowledgement.
      required: [offer_id, status, version, name, action_type, charge_event, bounty_cents, currency, billing_mode, commercial_terms_contract_version, commercial_terms_sha256, provider_mor_acknowledgement_required, provider_acknowledges_merchant_of_record, latest_acceptance_owner_verified, current_terms_owner_verified, renewal_eligible, activation_ready]
      properties:
        offer_id: { type: string, format: uuid }
        status: { type: string, enum: [draft, active, paused] }
        version: { type: integer, minimum: 1 }
        name: { type: string }
        action_type: { type: string, enum: [lead, demo, trial, signup, purchase, quote, application, booking] }
        charge_event: { type: string, enum: [accepted, activated, converted] }
        bounty_cents: { type: integer, format: int64, minimum: 1 }
        currency: { type: string, enum: [usd] }
        billing_mode: { type: string, enum: [prepaid, terms], description: The bounded launch pilot activates terms offers only; prepaid is retained solely for historical status compatibility. }
        terms_credit_limit_cents: { type: integer, format: int64, minimum: 1 }
        terms_period_days: { type: integer, minimum: 1, maximum: 90 }
        commercial_terms_contract_version: { type: string, enum: [nhs-provider-commercial-terms-v1] }
        commercial_terms_sha256: { type: string, pattern: "^[0-9a-f]{64}$" }
        provider_mor_acknowledgement_required: { type: boolean, enum: [true] }
        provider_acknowledges_merchant_of_record: { type: boolean, description: True only for the active provider-accepted contract; this is not an independent NHS verification. }
        latest_acceptance_id: { type: string, format: uuid }
        latest_acceptance_type: { type: string, enum: [terms_acceptance, terms_renewal] }
        latest_acceptance_at: { type: string, format: date-time }
        latest_acceptance_valid_until: { type: string, format: date-time }
        latest_acceptance_owner_verified: { type: boolean }
        latest_acceptance_owner_verified_at: { type: string, format: date-time }
        current_terms_owner_verified: { type: boolean }
        current_terms_valid_until: { type: string, format: date-time }
        renewal_eligible: { type: boolean }
        activation_ready: { type: boolean, description: Read-only readiness evidence; it does not activate the offer. }
    ProviderPilotRecentEvent:
      type: object
      additionalProperties: false
      description: Provider-owned ticket state exposed only after an NHS-observed handoff. Attribution material and controlled intent are excluded.
      required: [ticket_id, offer_id, offer_version, ticket_status, handoff_receipt_id, handoff_observed_at]
      properties:
        ticket_id: { type: string, format: uuid }
        offer_id: { type: string, format: uuid }
        offer_version: { type: integer, minimum: 1 }
        ticket_status: { type: string, enum: [created, redirected, accepted, activated, converted, rejected, duplicate, invalid, expired, revoked] }
        handoff_receipt_id: { type: string, format: uuid }
        handoff_observed_at: { type: string, format: date-time }
        outcome_receipt_id: { type: string, format: uuid }
        outcome: { type: string, enum: [accepted, activated, converted, rejected, duplicate, invalid] }
        charge_status: { type: string, enum: [charged, credited, none] }
        billed_cents: { type: integer, format: int64, minimum: 0 }
        outcome_recorded_at: { type: string, format: date-time }
    ProviderPilotStatus:
      type: object
      additionalProperties: false
      required: [as_of, provider_claim_id, domain, claim_status, verification_last_succeeded_at, verification_consecutive_failures, company_owner_verified, offers, recent_observed_handoffs]
      properties:
        as_of: { type: string, format: date-time, description: Database wall-clock boundary for this repeatable-read report. }
        provider_claim_id: { type: string, format: uuid }
        domain: { type: string, description: Domain derived from the authenticated provider claim. }
        claim_status: { type: string, enum: [verified] }
        verification_last_succeeded_at: { type: string, format: date-time }
        verification_next_check_at: { type: string, format: date-time }
        verification_consecutive_failures: { type: integer, minimum: 0 }
        company_acceptance_id: { type: string, format: uuid }
        company_accepted_at: { type: string, format: date-time }
        company_owner_verified: { type: boolean }
        company_owner_verified_at: { type: string, format: date-time }
        offers:
          type: array
          maxItems: 100
          items: { $ref: "#/components/schemas/ProviderPilotOfferStatus" }
        recent_observed_handoffs:
          type: array
          maxItems: 100
          items: { $ref: "#/components/schemas/ProviderPilotRecentEvent" }
    ProviderPilotStatusResponse:
      type: object
      additionalProperties: false
      required: [pilot_status, evidence_scope]
      properties:
        pilot_status: { $ref: "#/components/schemas/ProviderPilotStatus" }
        evidence_scope: { type: string, description: Claim-key-scoped continuity and explicit redaction boundary. }
    ProviderDemandSummary:
      type: object
      additionalProperties: false
      required: [organic_results_returned, search_receipts, average_organic_position, result_selections, result_selection_rate, result_selection_suppressed, action_interest_receipts, action_interest_rate, action_interest_suppressed]
      properties:
        organic_results_returned: { type: integer, minimum: 0 }
        search_receipts: { type: integer, minimum: 0, description: Retained receipts, not unique agents or principals. }
        average_organic_position: { type: number, format: double, minimum: 0 }
        result_selections: { type: integer, minimum: 0, nullable: true }
        result_selection_rate: { type: number, format: double, minimum: 0, maximum: 1, nullable: true }
        result_selection_suppressed: { type: boolean }
        action_interest_receipts: { type: integer, minimum: 0, nullable: true }
        action_interest_rate: { type: number, format: double, minimum: 0, maximum: 1, nullable: true }
        action_interest_suppressed: { type: boolean }
    ProviderDemandSurface:
      type: object
      additionalProperties: false
      required: [surface, organic_results_returned, result_selections, result_selection_suppressed, action_interest_receipts, action_interest_suppressed]
      properties:
        surface: { type: string, enum: [web, rest, mcp, unknown] }
        organic_results_returned: { type: integer, minimum: 0 }
        result_selections: { type: integer, minimum: 0, nullable: true }
        result_selection_suppressed: { type: boolean }
        action_interest_receipts: { type: integer, minimum: 0, nullable: true }
        action_interest_suppressed: { type: boolean }
    ProviderDemandTopic:
      type: object
      additionalProperties: false
      required: [topic, search_receipts, average_organic_position, result_selections, result_selection_suppressed, action_interest_receipts, action_interest_suppressed]
      properties:
        topic: { type: string, enum: [payments, commerce, jobs, data, search, weather, maps, email, messaging, image, video, audio, documents, security, finance, health, education, news, analytics, automation, productivity, identity, storage, ai-tools, developer-tools, other] }
        search_receipts: { type: integer, minimum: 20, description: Topic rows are omitted below the published privacy threshold. }
        average_organic_position: { type: number, format: double, minimum: 0 }
        result_selections: { type: integer, minimum: 0, nullable: true }
        result_selection_suppressed: { type: boolean }
        action_interest_receipts: { type: integer, minimum: 0, nullable: true }
        action_interest_suppressed: { type: boolean }
    ProviderDemandActionType:
      type: object
      additionalProperties: false
      required: [action_type, receipt_count]
      properties:
        action_type: { type: string, enum: [quote, trial, demo, booking, application, signup, purchase] }
        receipt_count: { type: integer, minimum: 20, description: Action-type rows are omitted below the published privacy threshold. }
    ProviderDemandAnalytics:
      type: object
      additionalProperties: false
      required: [domain, days, retention_days, action_interest_cohort, topic_receipt_threshold, result_selection_receipt_threshold, action_interest_receipt_threshold, synthetic_excluded, summary, surfaces, demand_topics, action_types]
      properties:
        domain: { type: string, description: Domain derived from the authenticated provider claim; never caller-selected. }
        days: { type: integer, minimum: 1, maximum: 30 }
        retention_days: { type: integer, enum: [30] }
        action_interest_cohort: { type: string, enum: [organic_result_returned_at] }
        topic_receipt_threshold: { type: integer, enum: [20] }
        result_selection_receipt_threshold: { type: integer, enum: [20] }
        action_interest_receipt_threshold: { type: integer, enum: [20] }
        synthetic_excluded: { type: boolean, enum: [true] }
        summary: { $ref: "#/components/schemas/ProviderDemandSummary" }
        surfaces:
          type: array
          maxItems: 4
          items: { $ref: "#/components/schemas/ProviderDemandSurface" }
        demand_topics:
          type: array
          maxItems: 20
          items: { $ref: "#/components/schemas/ProviderDemandTopic" }
        action_types:
          type: array
          maxItems: 7
          items: { $ref: "#/components/schemas/ProviderDemandActionType" }
    ProviderDemandResponse:
      type: object
      additionalProperties: false
      required: [demand, evidence_scope]
      properties:
        demand: { $ref: "#/components/schemas/ProviderDemandAnalytics" }
        evidence_scope: { type: string, description: Claim-scoped privacy-thresholded aggregates and explicit redaction boundary. }
    ProviderControlledIntentResolveRequest:
      type: object
      additionalProperties: false
      description: Exact attribution bearer only. Ticket IDs, queries, contact fields, notes, and arbitrary context are not accepted.
      required: [attribution_token]
      properties:
        attribution_token: { type: string, minLength: 1, description: Exact signed bearer returned during ticket preparation and presented during the observed handoff }
    ProviderControlledIntent:
      type: object
      additionalProperties: false
      required: [demand_topic, budget_band, urgency, requirement_flags]
      properties:
        demand_topic: { type: string, enum: [payments, commerce, jobs, data, search, weather, maps, email, messaging, image, video, audio, documents, security, finance, health, education, news, analytics, automation, productivity, identity, storage, ai-tools, developer-tools, other] }
        region_code: { type: string, pattern: "^[A-Z]{2}(-[A-Z0-9]{1,3})?$" }
        budget_band: { type: string, enum: [unspecified, under_100, 100_499, 500_1999, 2000_plus] }
        urgency: { type: string, enum: [unspecified, now, 7_days, 30_days, researching] }
        requirement_flags:
          type: array
          uniqueItems: true
          maxItems: 8
          items: { type: string, enum: [api_access, mcp, sandbox, self_serve, enterprise, compliance, multilingual, human_support] }
    ProviderControlledIntentResolution:
      type: object
      additionalProperties: false
      description: Free read-only provider view authorized separately after an observed handoff. No query, search receipt, identity, contact, network, action URL, pricing, accounting, outcome, or proof fields are returned.
      required: [resolver_contract_version, ticket_id, offer_id, offer_version, action_type, controlled_intent, observed_at, intent_available_until, consent_version]
      properties:
        resolver_contract_version: { type: string, enum: [nhs-provider-controlled-intent-resolver-v1] }
        ticket_id: { type: string, format: uuid }
        offer_id: { type: string, format: uuid }
        offer_version: { type: integer, minimum: 1 }
        action_type: { type: string, enum: [lead, demo, trial, signup, purchase, quote, application, booking] }
        controlled_intent: { $ref: "#/components/schemas/ProviderControlledIntent" }
        observed_at: { type: string, format: date-time }
        intent_available_until: { type: string, format: date-time }
        consent_version: { type: string, enum: [nhs-provider-controlled-intent-disclosure-consent-v1] }
    PublicProviderOffer:
      type: object
      description: Separate disclosed action attached to an exact returned organic site; the provider action URL is withheld until the exact ticket bearer creates an NHS-observed handoff receipt
      required: [id, offer_version, provider_domain, organic_position, name, summary, action_type, disclosure, organic_rank_paid, principal_price, nhs_compensation, commercial_terms_contract_version, commercial_terms_sha256, credit_rule, response_expectation, terms_period_anchor_rule, provider_acknowledges_merchant_of_record, prepare_action_endpoint]
      properties:
        id: { type: string, format: uuid }
        offer_version: { type: integer, minimum: 1 }
        provider_domain: { type: string }
        organic_position: { type: integer, minimum: 1 }
        name: { type: string }
        summary: { type: string }
        action_type: { type: string }
        disclosure: { type: string, enum: [Provider-funded action] }
        organic_rank_paid: { type: boolean, enum: [false] }
        principal_price:
          type: object
          properties:
            mode: { type: string, enum: [free, fixed, quote, provider_pricing] }
            amount_minor: { type: integer, minimum: 0 }
            currency: { type: string, enum: [usd] }
        nhs_compensation:
          type: object
          required: [event, amount_minor, currency]
          properties:
            event: { type: string, enum: [accepted, activated, converted] }
            amount_minor: { type: integer, minimum: 1 }
            currency: { type: string, enum: [usd] }
        commercial_terms_contract_version: { type: string, enum: [nhs-provider-commercial-terms-v1] }
        commercial_terms_sha256: { type: string, pattern: "^[0-9a-f]{64}$" }
        credit_rule: { type: string, enum: [full_credit_on_provider_reported_invalid_or_duplicate] }
        response_expectation: { type: string, enum: [provider_callback_before_attribution_expiry] }
        terms_period_anchor_rule: { type: string, enum: [billing_period_begins_at_first_activation] }
        provider_acknowledges_merchant_of_record: { type: boolean, enum: [true], description: Provider contractual acknowledgement; NHS does not independently verify Merchant-of-Record status }
        prepare_action_endpoint: { type: string, format: uri }
    ActionInterestRequest:
      type: object
      additionalProperties: false
      required: [search_id, domain, action_type, caller_attests_principal_interest, confirmation_version]
      properties:
        search_id: { type: string, pattern: "^nhs_sr_[A-Za-z0-9_-]{16}$", description: Committed query-free organic search receipt }
        domain: { type: string, description: Bare domain present in the referenced organic results; schemes, paths, queries, and fragments are rejected }
        action_type: { type: string, enum: [quote, trial, demo, booking, application, signup, purchase] }
        caller_attests_principal_interest: { type: boolean, enum: [true], description: Caller attests current human/company principal interest; this is not authority to contact the provider }
        confirmation_version: { type: string, enum: [nhs-action-interest-v1] }
    ActionInterestReceipt:
      type: object
      description: Provider-independent Stage 1 demand receipt; not a provider request, action ticket, charge, outcome, or commercial proof
      properties:
        id: { type: string, pattern: "^nhs_air_[A-Za-z0-9_-]{16}$" }
        search_id: { type: string }
        domain: { type: string }
        action_type: { type: string, enum: [quote, trial, demo, booking, application, signup, purchase] }
        surface: { type: string, enum: [rest, mcp, web, unknown] }
        caller_attests_principal_interest: { type: boolean, enum: [true] }
        confirmation_version: { type: string, enum: [nhs-action-interest-v1] }
        created_at: { type: string, format: date-time }
        expires_at: { type: string, format: date-time }
        idempotent_replay: { type: boolean }
    ActionInterestResponse:
      type: object
      required: [receipt, created, idempotent_replay, provider_contacted, row_level_shared_with_provider, action_ticket_created, charge_created, provider_or_principal_charged, commercial_proof, organic_rank_affected, rank_or_score_input, retention_days, evidence_scope]
      properties:
        receipt: { $ref: "#/components/schemas/ActionInterestReceipt" }
        created: { type: boolean }
        idempotent_replay: { type: boolean }
        provider_contacted: { type: boolean, enum: [false] }
        row_level_shared_with_provider: { type: boolean, enum: [false] }
        action_ticket_created: { type: boolean, enum: [false] }
        charge_created: { type: boolean, enum: [false] }
        provider_or_principal_charged: { type: boolean, enum: [false] }
        commercial_proof: { type: boolean, enum: [false] }
        organic_rank_affected: { type: boolean, enum: [false] }
        rank_or_score_input: { type: boolean, enum: [false] }
        retention_days: { type: integer, enum: [30] }
        evidence_scope: { type: string }
    ActionTicketRequest:
      type: object
      additionalProperties: false
      required: [offer_id, search_id, demand_topic, principal_consent, consent_version]
      properties:
        offer_id: { type: string, format: uuid }
        search_id: { type: string, description: Committed query-free organic search receipt }
        demand_topic: { type: string, enum: [payments, commerce, jobs, data, search, weather, maps, email, messaging, image, video, audio, documents, security, finance, health, education, news, analytics, automation, productivity, identity, storage, ai-tools, developer-tools, other] }
        region_code: { type: string, pattern: "^[A-Z]{2}(-[A-Z0-9]{1,3})?$" }
        budget_band: { type: string, enum: [unspecified, under_100, 100_499, 500_1999, 2000_plus], default: unspecified }
        urgency: { type: string, enum: [unspecified, now, 7_days, 30_days, researching], default: unspecified }
        requirement_flags:
          type: array
          uniqueItems: true
          items: { type: string, enum: [api_access, mcp, sandbox, self_serve, enterprise, compliance, multilingual, human_support] }
        principal_consent: { type: boolean, enum: [true], description: Caller attests it is authorized by the principal under the exact published v1 wording }
        consent_version: { type: string, enum: [nhs-principal-consent-v1] }
    ActionTicketPreparationResponse:
      type: object
      additionalProperties: false
      description: Ticket preparation returns a bearer plus the NHS handoff surface, never the provider action URL
      required: [ticket, offer, preparation_contract_version, attribution_token, handoff_endpoint, handoff_method, handoff_event_contract_version, handoff_consent_contract_url, controlled_intent_disclosure_optional, controlled_intent_disclosure_consent_version, controlled_intent_disclosure_consent_url, created, idempotent_replay, attribution_token_stored_by_nhs, token_reconstructed_for_exact_replay, principal_consent_attested, consent_contract_url, principal_charged, provider_mor_contract_required, principal_charged_by_nhs, organic_rank_affected, direct_provider_access_remains_free, disclosure]
      properties:
        ticket: { $ref: "#/components/schemas/PublicActionTicket" }
        offer: { $ref: "#/components/schemas/PublicProviderOffer" }
        preparation_contract_version: { type: string, enum: [nhs-action-ticket-preparation-v2], description: Explicit breaking contract revision that withholds the provider URL until separately consented handoff }
        attribution_token: { type: string, minLength: 1, description: Raw bearer returned in the no-store response. The raw string is not stored in ticket or handoff rows; NHS stores its SHA-256 hash and retains nonce/key metadata plus signing material that can reconstruct an exact replay. }
        handoff_endpoint: { type: string, format: uri }
        handoff_method: { type: string, enum: [POST] }
        handoff_event_contract_version: { type: string, enum: [nhs-action-handoff-v1] }
        handoff_consent_contract_url: { type: string, format: uri }
        controlled_intent_disclosure_optional: { type: boolean, enum: [true], description: The optional separate disclosure may be declined without blocking handoff or free direct provider access }
        controlled_intent_disclosure_consent_version: { type: string, enum: [nhs-provider-controlled-intent-disclosure-consent-v1] }
        controlled_intent_disclosure_consent_url: { type: string, format: uri }
        created: { type: boolean }
        idempotent_replay: { type: boolean }
        attribution_token_stored_by_nhs: { type: boolean, enum: [false] }
        token_reconstructed_for_exact_replay: { type: boolean }
        principal_consent_attested: { type: boolean, enum: [true] }
        consent_contract_url: { type: string, format: uri }
        principal_charged: { type: boolean, enum: [false] }
        provider_mor_contract_required: { type: boolean, enum: [true] }
        principal_charged_by_nhs: { type: boolean, enum: [false] }
        organic_rank_affected: { type: boolean, enum: [false] }
        direct_provider_access_remains_free: { type: boolean, enum: [true] }
        disclosure: { type: string }
    PublicActionTicket:
      type: object
      additionalProperties: false
      description: Controlled consent-attested ticket snapshot. Provider action URL, token hash, token nonce, signing-key metadata, and internal evidence references are excluded.
      required: [id, provider_claim_id, provider_offer_id, offer_version, offer_name, offer_summary, action_type, disclosure, charge_event, bounty_cents, currency, billing_mode, commercial_terms_contract_version, commercial_terms_sha256, principal_price_mode, principal_currency, demand_topic, budget_band, urgency, requirement_flags, principal_consent, consent_version, status, expires_at, created_at, updated_at]
      properties:
        id: { type: string, format: uuid }
        provider_claim_id: { type: string, format: uuid, description: Opaque provider claim identifier; not a provider identity or contact field }
        provider_offer_id: { type: string, format: uuid }
        search_receipt_id: { type: string, format: uuid, description: Removed when controlled intent is redacted }
        offer_version: { type: integer, minimum: 1 }
        offer_name: { type: string }
        offer_summary: { type: string }
        action_type: { type: string, enum: [lead, demo, trial, signup, purchase, quote, application, booking] }
        disclosure: { type: string, enum: [Provider-funded action] }
        charge_event: { type: string, enum: [accepted, activated, converted] }
        bounty_cents: { type: integer, format: int64, minimum: 1 }
        currency: { type: string, enum: [usd] }
        billing_mode: { type: string, enum: [terms] }
        commercial_terms_contract_version: { type: string, enum: [nhs-provider-commercial-terms-v1] }
        commercial_terms_sha256: { type: string, pattern: "^[0-9a-f]{64}$" }
        principal_price_mode: { type: string, enum: [free, fixed, quote, provider_pricing] }
        principal_price_cents: { type: integer, format: int64, minimum: 0 }
        principal_currency: { type: string, enum: [usd] }
        demand_topic: { type: string, enum: [payments, commerce, jobs, data, search, weather, maps, email, messaging, image, video, audio, documents, security, finance, health, education, news, analytics, automation, productivity, identity, storage, ai-tools, developer-tools, other, redacted] }
        region_code: { type: string }
        budget_band: { type: string, enum: [unspecified, under_100, 100_499, 500_1999, 2000_plus] }
        urgency: { type: string, enum: [unspecified, now, 7_days, 30_days, researching] }
        requirement_flags:
          type: array
          uniqueItems: true
          items: { type: string, enum: [api_access, mcp, sandbox, self_serve, enterprise, compliance, multilingual, human_support] }
        principal_consent: { type: boolean, enum: [true] }
        consent_version: { type: string, enum: [nhs-principal-consent-v1] }
        status: { type: string, enum: [created, redirected, accepted, activated, converted, rejected, duplicate, invalid] }
        expires_at: { type: string, format: date-time }
        intent_redacted_at: { type: string, format: date-time }
        authorization_revoked_at: { type: string, format: date-time }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    ActionTicketHandoffRequest:
      type: object
      additionalProperties: false
      description: Exact ticket bearer and separate handoff-time principal attestation are accepted only in JSON, not in the NHS URL, query string, referrer, or cookie. The controlled-intent disclosure pair is optional; false or omission requires no version, while true requires the exact v1 version. Declining it does not block handoff.
      required: [ticket_id, attribution_token, principal_handoff_consent, handoff_consent_version]
      properties:
        ticket_id: { type: string, format: uuid }
        attribution_token: { type: string, minLength: 1 }
        principal_handoff_consent: { type: boolean, enum: [true], description: Caller attests the exact published handoff-time principal authorization }
        handoff_consent_version: { type: string, enum: [nhs-provider-handoff-consent-v1] }
        principal_controlled_intent_disclosure_consent: { type: boolean, default: false, description: Optional separate authorization for the exact DNS-verified provider to resolve the bounded controlled-intent bundle after this observed handoff }
        controlled_intent_disclosure_consent_version: { type: string, enum: [nhs-provider-controlled-intent-disclosure-consent-v1], description: Required only when principal_controlled_intent_disclosure_consent is true; otherwise omit }
    ProviderActionHandoffReceipt:
      type: object
      additionalProperties: false
      description: Durable append-only privacy-safe NHS observation. Internal claim ID and presented-token hash are not returned.
      required: [id, action_ticket_id, provider_offer_id, offer_version, commercial_terms_contract_version, commercial_terms_sha256, principal_handoff_consent, handoff_consent_version, principal_controlled_intent_disclosure_consent, event_contract_version, observed_at, created_at]
      properties:
        id: { type: string, format: uuid }
        action_ticket_id: { type: string, format: uuid }
        provider_offer_id: { type: string, format: uuid }
        offer_version: { type: integer, minimum: 1 }
        commercial_terms_contract_version: { type: string, enum: [nhs-provider-commercial-terms-v1] }
        commercial_terms_sha256: { type: string, pattern: "^[0-9a-f]{64}$" }
        principal_handoff_consent: { type: boolean, enum: [true] }
        handoff_consent_version: { type: string, enum: [nhs-provider-handoff-consent-v1] }
        principal_controlled_intent_disclosure_consent: { type: boolean, description: False means no provider resolution authorization; the handoff remains valid }
        controlled_intent_disclosure_consent_version: { type: string, enum: [nhs-provider-controlled-intent-disclosure-consent-v1] }
        event_contract_version: { type: string, enum: [nhs-action-handoff-v1] }
        observed_at: { type: string, format: date-time }
        created_at: { type: string, format: date-time }
    ActionTicketHandoffResponse:
      type: object
      additionalProperties: false
      required: [ticket, handoff_receipt, action_url, observed_handoff, idempotent_replay, principal_charged, provider_charged, organic_rank_affected, direct_provider_access_is_free]
      properties:
        ticket: { $ref: "#/components/schemas/PublicActionTicket" }
        handoff_receipt: { $ref: "#/components/schemas/ProviderActionHandoffReceipt" }
        action_url: { type: string, format: uri, description: Attributed HTTPS provider URL returned only after the durable receipt commits }
        observed_handoff: { type: boolean, enum: [true] }
        idempotent_replay: { type: boolean }
        principal_charged: { type: boolean, enum: [false] }
        provider_charged: { type: boolean, enum: [false] }
        organic_rank_affected: { type: boolean, enum: [false] }
        direct_provider_access_is_free: { type: boolean, enum: [true] }
    SignedOutcomeReceipt:
      type: object
      properties:
        v: { type: integer, enum: [1] }
        kid: { type: string }
        receipt_id: { type: string, format: uuid }
        ticket_id: { type: string, format: uuid }
        offer_id: { type: string, format: uuid }
        nhs_event_id: { type: string, format: uuid }
        outcome: { type: string, enum: [accepted, activated, converted, rejected, duplicate, invalid] }
        provider_reported_at: { type: integer, format: int64 }
        recorded_at: { type: integer, format: int64 }
        expires_at: { type: integer, format: int64 }
        charged_minor: { type: integer, minimum: 0 }
        currency: { type: string, enum: [usd] }
        charge_status: { type: string, enum: [charged, credited, none] }
    PublicOutcomeReceiptState:
      type: object
      description: Mutable online state, separate from immutable signature validity
      properties:
        receipt_id: { type: string, format: uuid }
        action_ticket_id: { type: string, format: uuid }
        receipt_outcome: { type: string }
        current_ticket_status: { type: string }
        original_charge_credited: { type: boolean }
        superseded_by_later_state: { type: boolean }
        authorization_revoked: { type: boolean }
        net_commercial_effect_cents: { type: integer, format: int64, minimum: 0 }
        net_commercial_effect_currency: { type: string, enum: [usd] }
    Site:
      type: object
      properties:
        id: { type: string, format: uuid }
        domain: { type: string }
        url: { type: string, format: uri }
        name: { type: string }
        description: { type: string }
        has_llms_txt: { type: boolean }
        has_ai_plugin: { type: boolean }
        has_openapi: { type: boolean }
        has_robots_ai: { type: boolean }
        has_structured_api: { type: boolean }
        has_mcp_server: { type: boolean }
        has_schema_org: { type: boolean }
        agentic_score: { type: integer, minimum: 0, maximum: 100 }
        category: { type: string }
        tags: { type: array, items: { type: string } }
        is_verified: { type: boolean }
        is_featured:
          type: boolean
          deprecated: true
          description: Legacy display metadata only; never affects organic score or ordering
    ActionInterestCallContract:
      type: object
      description: Non-executable call contract. Fixed receipt and confirmation fields are supplied only for use after the explicit invocation condition is met; the principal-requested domain and action type remain required choices.
      required: [available, tool, fixed_arguments_if_invocation_condition_met, domain_must_be_one_of, action_type_must_be_one_of, arguments_must_contain_only, invoke_only_if, executable_without_explicit_principal_intent, query_prompt_contact_identity_fields_are_accepted]
      properties:
        available: { type: boolean }
        tool: { type: string, enum: [record_action_interest] }
        fixed_arguments_if_invocation_condition_met:
          type: object
          additionalProperties: false
          properties:
            search_id: { type: string }
            caller_attests_principal_interest: { type: boolean, enum: [true] }
            confirmation_version: { type: string, enum: [nhs-action-interest-v1] }
        domain_must_be_one_of: { type: array, items: { type: string } }
        action_type_must_be_one_of: { type: array, items: { type: string, enum: [quote, trial, demo, booking, application, signup, purchase] } }
        arguments_must_contain_only: { type: array, items: { type: string, enum: [search_id, domain, action_type, caller_attests_principal_interest, confirmation_version] } }
        invoke_only_if: { type: string, description: Explicit current-principal intent is required; discovery, ranking, selection, and provider availability are insufficient }
        executable_without_explicit_principal_intent: { type: boolean, enum: [false] }
        query_prompt_contact_identity_fields_are_accepted: { type: boolean, enum: [false] }
    SelectedSiteDetail:
      allOf:
        - { $ref: "#/components/schemas/Site" }
        - type: object
          properties:
            selection_recorded: { type: boolean, enum: [true], description: Present only when this request newly recorded an exact returned-result selection; it does not assert action interest }
            action_interest:
              type: object
              description: Optional provider-independent next step for the one selected organic domain; invoking it still requires explicit current-principal attestation
              required: [available, search_id, eligible_domains, action_types, caller_attestation_required, confirmation_version, confirmation_url, invocation_condition, call_contract, endpoint, provider_contacted, commercial_proof, organic_rank_affected]
              properties:
                available: { type: boolean, enum: [true] }
                search_id: { type: string }
                eligible_domains: { type: array, minItems: 1, maxItems: 1, items: { type: string } }
                action_types: { type: array, items: { type: string, enum: [quote, trial, demo, booking, application, signup, purchase] } }
                caller_attestation_required: { type: boolean, enum: [true] }
                endpoint: { type: string, format: uri }
                confirmation_version: { type: string, enum: [nhs-action-interest-v1] }
                confirmation_url: { type: string, format: uri }
                invocation_condition: { type: string, description: Requires explicit current principal intent and forbids inference from discovery or selection }
                call_contract: { $ref: "#/components/schemas/ActionInterestCallContract" }
                provider_contacted: { type: boolean, enum: [false] }
                commercial_proof: { type: boolean, enum: [false] }
                organic_rank_affected: { type: boolean, enum: [false] }
