openapi: 3.1.1
info:
  title: VGO Framework Rank API
  version: 1.0.0
  description: >-
    Frozen wire design contract for Framework-owned VGO Rank capabilities.
    No operation here asserts a live production endpoint. Formal issuance is
    disabled until the Rank v1.0 publication gates pass. Protocol version is
    independent of HTTP API version.
servers:
  - url: https://api.vgoframework.org/v1
    description: Planned production endpoint; do not use until announced
  - url: https://sandbox-api.vgoframework.org/v1
    description: Planned isolated sandbox
security:
  - OAuth2: []
tags:
  - name: Protocols
  - name: Entities
  - name: Diagnostics
  - name: Ratings
  - name: Certificates
  - name: Appeals
  - name: Subscriptions
paths:
  /protocols:
    get:
      tags: [Protocols]
      operationId: listProtocols
      security: []
      parameters:
        - $ref: '#/components/parameters/Market'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200': {description: Public versioned protocols, content: {application/json: {schema: {$ref: '#/components/schemas/ProtocolList'}}}}
        default: {$ref: '#/components/responses/Error'}
  /entities:
    post:
      tags: [Entities]
      operationId: submitEntity
      security: [{OAuth2: ['entities:write']}]
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/EntityInput'}}}}
      responses:
        '202': {description: Identity review accepted, content: {application/json: {schema: {$ref: '#/components/schemas/Entity'}}}}
        default: {$ref: '#/components/responses/Error'}
  /entities/{entityId}:
    get:
      tags: [Entities]
      operationId: getEntity
      security: [{OAuth2: ['entities:write']}]
      parameters: [{$ref: '#/components/parameters/EntityId'}]
      responses:
        '200': {description: Entity and identity review state, content: {application/json: {schema: {$ref: '#/components/schemas/Entity'}}}}
        default: {$ref: '#/components/responses/Error'}
  /scopes:
    post:
      tags: [Entities]
      operationId: createScope
      security: [{OAuth2: ['entities:write']}]
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/ScopeInput'}}}}
      responses:
        '202': {description: Scope accepted, content: {application/json: {schema: {$ref: '#/components/schemas/Scope'}}}}
        default: {$ref: '#/components/responses/Error'}
  /scopes/{scopeId}:
    get:
      tags: [Entities]
      operationId: getScope
      security: [{OAuth2: ['entities:write']}]
      parameters: [{$ref: '#/components/parameters/ScopeId'}]
      responses:
        '200': {description: Scope, content: {application/json: {schema: {$ref: '#/components/schemas/Scope'}}}}
        default: {$ref: '#/components/responses/Error'}
  /diagnostic-runs:
    post:
      tags: [Diagnostics]
      operationId: startDiagnosticRun
      security: [{OAuth2: ['diagnostics:run']}]
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/RunInput'}}}}
      responses:
        '202': {description: Run queued, content: {application/json: {schema: {$ref: '#/components/schemas/Run'}}}}
        default: {$ref: '#/components/responses/Error'}
  /diagnostic-runs/{runId}:
    get:
      tags: [Diagnostics]
      operationId: getDiagnosticRun
      security: [{OAuth2: ['diagnostics:run']}]
      parameters: [{$ref: '#/components/parameters/RunId'}]
      responses:
        '200': {description: Run and authorized summary; no formal level, content: {application/json: {schema: {$ref: '#/components/schemas/Run'}}}}
        default: {$ref: '#/components/responses/Error'}
  /rating-requests:
    post:
      tags: [Ratings]
      operationId: requestRating
      security: [{OAuth2: ['ratings:request']}]
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/RatingRequestInput'}}}}
      responses:
        '202': {description: Formal assessment requested; not yet issued, content: {application/json: {schema: {$ref: '#/components/schemas/RatingRequest'}}}}
        default: {$ref: '#/components/responses/Error'}
  /rating-requests/{requestId}:
    get:
      tags: [Ratings]
      operationId: getRatingRequest
      security: [{OAuth2: ['ratings:request']}]
      parameters: [{$ref: '#/components/parameters/RequestId'}]
      responses:
        '200': {description: Progress, evidence qualification and issuance reference, content: {application/json: {schema: {$ref: '#/components/schemas/RatingRequest'}}}}
        default: {$ref: '#/components/responses/Error'}
  /scopes/{scopeId}/current-certificate:
    get:
      tags: [Certificates]
      operationId: getCurrentCertificate
      security: []
      parameters: [{$ref: '#/components/parameters/ScopeId'}]
      responses:
        '200': {description: Current public certificate, content: {application/json: {schema: {$ref: '#/components/schemas/Certificate'}}}}
        '404': {$ref: '#/components/responses/Error'}
        default: {$ref: '#/components/responses/Error'}
  /certificates/{certificateId}:
    get:
      tags: [Certificates]
      operationId: getCertificate
      security: []
      parameters: [{$ref: '#/components/parameters/CertificateId'}]
      responses:
        '200': {description: Historical signed certificate, content: {application/json: {schema: {$ref: '#/components/schemas/Certificate'}}}}
        default: {$ref: '#/components/responses/Error'}
  /certificates/{certificateId}/status:
    get:
      tags: [Certificates]
      operationId: getCertificateStatus
      security: []
      parameters: [{$ref: '#/components/parameters/CertificateId'}]
      responses:
        '200': {description: Authoritative current state; no-store or max-age 30, content: {application/json: {schema: {$ref: '#/components/schemas/CertificateStatus'}}}}
        default: {$ref: '#/components/responses/Error'}
  /.well-known/jwks.json:
    servers:
      - url: https://api.vgoframework.org
        description: Production issuer root (planned)
      - url: https://sandbox-api.vgoframework.org
        description: Sandbox issuer root (planned)
    get:
      tags: [Certificates]
      operationId: getIssuerJwks
      security: []
      responses:
        '200': {description: Public issuer keys, content: {application/json: {schema: {type: object, required: [keys], properties: {keys: {type: array, items: {type: object, additionalProperties: true}}}}}}}
  /certificates/verify:
    post:
      tags: [Certificates]
      operationId: verifyCertificate
      security: []
      description: Verify signature and current state. Only certificate ID or JWS is accepted; never dereference arbitrary URLs.
      requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/VerifyInput'}}}}
      responses:
        '200': {description: Validity report, content: {application/json: {schema: {$ref: '#/components/schemas/Verification'}}}}
        default: {$ref: '#/components/responses/Error'}
  /rankings:
    get:
      tags: [Certificates]
      operationId: listRankings
      security: []
      description: Published market/industry board only after formal issuance gate and public listing review.
      parameters:
        - {name: market, in: query, required: true, schema: {type: string}}
        - {name: locale, in: query, required: true, schema: {type: string}}
        - {name: industry, in: query, required: true, schema: {type: string}}
        - {name: question_space_version, in: query, required: true, schema: {type: string}}
        - {$ref: '#/components/parameters/Cursor'}
        - {name: limit, in: query, required: false, schema: {type: integer, minimum: 1, maximum: 100, default: 20}}
      responses:
        '200': {description: Eligible public listings for one comparable cohort, content: {application/json: {schema: {$ref: '#/components/schemas/RankingList'}}}}
        default: {$ref: '#/components/responses/Error'}
  /appeals:
    post:
      tags: [Appeals]
      operationId: submitAppeal
      security: [{OAuth2: ['appeals:write']}]
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/AppealInput'}}}}
      responses:
        '202': {description: Appeal received without fee, content: {application/json: {schema: {$ref: '#/components/schemas/Appeal'}}}}
        default: {$ref: '#/components/responses/Error'}
  /appeals/{appealId}:
    get:
      tags: [Appeals]
      operationId: getAppeal
      security: [{OAuth2: ['appeals:write']}]
      parameters: [{$ref: '#/components/parameters/AppealId'}]
      responses:
        '200': {description: Appeal with rationale and deadline, content: {application/json: {schema: {$ref: '#/components/schemas/Appeal'}}}}
        default: {$ref: '#/components/responses/Error'}
  /subscriptions:
    post:
      tags: [Subscriptions]
      operationId: createSubscription
      security: [{OAuth2: ['subscriptions:write']}]
      parameters: [{$ref: '#/components/parameters/IdempotencyKey'}]
      requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/SubscriptionInput'}}}}
      responses:
        '201': {description: Subscription; secret returned only once via secure channel, content: {application/json: {schema: {$ref: '#/components/schemas/Subscription'}}}}
        default: {$ref: '#/components/responses/Error'}
webhooks:
  rankEvent:
    post:
      operationId: receiveRankEvent
      description: >-
        At-least-once signed delivery. Deduplicate by event_id, order certificate
        states by status_version, fetch current status before showing validity.
      parameters:
        - name: X-VGO-Signature
          in: header
          required: true
          schema: {type: string}
        - name: X-VGO-Timestamp
          in: header
          required: true
          schema: {type: string}
      requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/RankEvent'}}}}
      responses:
        '200': {description: Accepted or deduplicated}
components:
  securitySchemes:
    OAuth2:
      type: oauth2
      flows:
        clientCredentials:
          tokenUrl: https://api.vgoframework.org/oauth/token
          scopes:
            entities:write: Submit entities and scopes
            diagnostics:run: Start and read authorized diagnostics
            ratings:request: Submit and read own rating requests
            appeals:write: Submit and read authorized appeals
            subscriptions:write: Manage callback subscriptions
            evidence:read: Read authorized evidence
  parameters:
    Market: {name: market, in: query, required: false, schema: {type: string}}
    Cursor: {name: cursor, in: query, required: false, schema: {type: string}}
    IdempotencyKey: {name: Idempotency-Key, in: header, required: true, schema: {type: string, minLength: 8, maxLength: 128}}
    EntityId: {name: entityId, in: path, required: true, schema: {type: string}}
    ScopeId: {name: scopeId, in: path, required: true, schema: {type: string}}
    RunId: {name: runId, in: path, required: true, schema: {type: string}}
    RequestId: {name: requestId, in: path, required: true, schema: {type: string}}
    CertificateId: {name: certificateId, in: path, required: true, schema: {type: string}}
    AppealId: {name: appealId, in: path, required: true, schema: {type: string}}
  responses:
    Error:
      description: Error, including 400/401/403/404/409/422/429/5xx
      headers:
        X-Request-Id: {schema: {type: string}}
        Retry-After: {schema: {type: string}}
      content: {application/json: {schema: {$ref: '#/components/schemas/Error'}}}
  schemas:
    Error:
      type: object
      required: [request_id, code, message, retryable]
      properties:
        request_id: {type: string}
        code: {type: string, enum: [INVALID_INPUT, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, IDEMPOTENCY_CONFLICT, INELIGIBLE_SCOPE, INSUFFICIENT_EVIDENCE, PROTOCOL_UNAVAILABLE, RATE_LIMITED, INTERNAL_ERROR]}
        message: {type: string}
        retryable: {type: boolean}
        details: {type: object, additionalProperties: true}
    EntityInput:
      type: object
      required: [name, entity_type, market, identity_evidence]
      properties:
        name: {type: string}
        entity_type: {type: string, enum: [brand, product]}
        market: {type: string}
        aliases: {type: array, items: {type: string}}
        domain: {type: [string, 'null'], format: hostname}
        identity_evidence: {type: array, minItems: 2, items: {type: string, format: uri}}
    Entity:
      allOf:
        - {$ref: '#/components/schemas/EntityInput'}
        - {type: object, required: [entity_id, review_status], properties: {entity_id: {type: string}, review_status: {type: string, enum: [pending, verified, rejected]}}}
    ScopeInput:
      type: object
      required: [entity_id, market, locale, industry, question_space_version]
      properties:
        entity_id: {type: string}
        market: {type: string}
        locale: {type: string}
        industry: {type: string}
        question_space_version: {type: string}
    Scope:
      allOf:
        - {$ref: '#/components/schemas/ScopeInput'}
        - {type: object, required: [scope_id, eligibility], properties: {scope_id: {type: string}, eligibility: {type: string, enum: [pending, eligible, ineligible]}}}
    Protocol:
      type: object
      required: [rank_protocol_version, market, locale, industry, question_space_version, surface_protocol_version, manifest_sha256, status]
      properties:
        rank_protocol_version: {type: string}
        market: {type: string}
        locale: {type: string}
        industry: {type: string}
        question_space_version: {type: string}
        surface_protocol_version: {type: string}
        manifest_sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
        status: {type: string, enum: [pilot, published, retired]}
    ProtocolList:
      type: object
      required: [items]
      properties:
        items: {type: array, items: {$ref: '#/components/schemas/Protocol'}}
        next_cursor: {type: [string, 'null']}
    RunInput:
      type: object
      required: [scope_id]
      properties:
        scope_id: {type: string}
        callback_subscription_id: {type: string}
    Run:
      type: object
      required: [run_id, scope_id, status]
      properties:
        run_id: {type: string}
        scope_id: {type: string}
        status: {type: string, enum: [queued, running, completed, partial, insufficient_evidence, failed]}
        summary: {type: object, additionalProperties: true}
        evidence_manifest_hash: {type: string}
    RatingRequestInput:
      type: object
      required: [scope_id]
      properties:
        scope_id: {type: string}
        callback_subscription_id: {type: string}
    RatingRequest:
      type: object
      required: [request_id, scope_id, status]
      properties:
        request_id: {type: string}
        scope_id: {type: string}
        status: {type: string, enum: [queued, running, insufficient_evidence, completed, failed]}
        reason_codes: {type: array, items: {type: string}}
        certificate_id: {type: [string, 'null']}
    CertificatePayload:
      type: object
      description: Signed JWS payload. Scores are integer basis points in [0,10000].
      required: [iss, aud, jti, iat, nbf, exp, certificate_id, entity_id, entity_type, scope_id, market, locale, industry, question_space_version, surface_protocol_version, rank_protocol_version, window_start, window_end, level, s_raw, s_conservative, confidence_90, coverage, accuracy_gate, evidence_manifest_hash, score_run_id, roles, affiliations]
      properties:
        iss: {type: string, const: 'https://api.vgoframework.org'}
        aud: {type: string, const: vgo-rank-verifiers}
        jti: {type: string}
        iat: {type: integer, description: Unix seconds}
        nbf: {type: integer, description: Unix seconds}
        exp: {type: integer, description: Unix seconds; at most 45 days from iat}
        certificate_id: {type: string}
        entity_id: {type: string}
        entity_type: {type: string, enum: [brand, product]}
        scope_id: {type: string}
        market: {type: string}
        locale: {type: string}
        industry: {type: string}
        question_space_version: {type: string}
        surface_protocol_version: {type: string}
        rank_protocol_version: {type: string}
        window_start: {type: string, format: date-time}
        window_end: {type: string, format: date-time}
        level: {type: integer, minimum: 0, maximum: 10}
        s_raw: {type: integer, minimum: 0, maximum: 10000}
        s_conservative: {type: integer, minimum: 0, maximum: 10000}
        confidence_90:
          type: object
          required: [lower, upper]
          properties:
            lower: {type: integer, minimum: 0, maximum: 10000}
            upper: {type: integer, minimum: 0, maximum: 10000}
        coverage:
          type: object
          required: [planned, qualified, by_surface]
          properties:
            planned: {type: integer, minimum: 0}
            qualified: {type: integer, minimum: 0}
            by_surface: {type: object, additionalProperties: {type: integer, minimum: 0}}
        accuracy_gate: {type: string}
        evidence_manifest_hash: {type: string, pattern: '^[0-9a-f]{64}$'}
        score_run_id: {type: string}
        roles:
          type: object
          required: [standard_owner, evidence_providers, issuer, appeal_reviewer]
          properties:
            standard_owner: {type: string}
            evidence_providers: {type: array, items: {type: string}}
            issuer: {type: string}
            appeal_reviewer: {type: string}
        affiliations: {type: array, items: {type: string}}
    Certificate:
      type: object
      required: [certificate_id, jws, status_url]
      properties:
        certificate_id: {type: string}
        jws: {type: string, description: JWS Compact ES256; payload per certificate contract}
        status_url: {type: string, format: uri}
    CertificateStatus:
      type: object
      required: [certificate_id, status, effective_at, status_version, checked_at, next_check_before]
      properties:
        certificate_id: {type: string}
        status: {type: string, enum: [rated, watch, restricted, revoked, expired]}
        effective_at: {type: string, format: date-time}
        reason_code: {type: [string, 'null']}
        status_version: {type: integer, minimum: 1}
        checked_at: {type: string, format: date-time}
        next_check_before: {type: string, format: date-time}
    VerifyInput:
      type: object
      oneOf:
        - {required: [certificate_id]}
        - {required: [jws]}
      properties:
        certificate_id: {type: string}
        jws: {type: string}
      additionalProperties: false
    Verification:
      type: object
      required: [certificate_id, signature_valid, current_status, currently_valid, checked_at]
      properties:
        certificate_id: {type: string}
        signature_valid: {type: boolean}
        current_status: {type: string, enum: [rated, watch, restricted, revoked, expired, unknown]}
        currently_valid: {type: boolean}
        checked_at: {type: string, format: date-time}
        reason_code: {type: [string, 'null']}
    RankingList:
      type: object
      required: [items, market, locale, industry, question_space_version]
      properties:
        items:
          type: array
          items:
            type: object
            required: [entity_id, certificate_id, level, scope_id]
            properties:
              entity_id: {type: string}
              certificate_id: {type: string}
              level: {type: integer, minimum: 0, maximum: 10}
              scope_id: {type: string}
        market: {type: string}
        locale: {type: string}
        industry: {type: string}
        question_space_version: {type: string}
        next_cursor: {type: [string, 'null']}
    AppealInput:
      type: object
      required: [certificate_id, reason, evidence_urls, contact]
      properties:
        certificate_id: {type: string}
        reason: {type: string, minLength: 10}
        evidence_urls: {type: array, items: {type: string, format: uri}}
        contact: {type: string, format: email}
    Appeal:
      type: object
      required: [appeal_id, status, received_at, due_at]
      properties:
        appeal_id: {type: string}
        status: {type: string, enum: [received, reviewing, extended, upheld, corrected, rejected]}
        received_at: {type: string, format: date-time}
        due_at: {type: string, format: date-time}
        rationale: {type: [string, 'null']}
    SubscriptionInput:
      type: object
      required: [url, events]
      properties:
        url: {type: string, format: uri, pattern: '^https://'}
        events: {type: array, minItems: 1, items: {type: string, enum: [rating.completed, certificate.issued, certificate.status_changed, certificate.expired, appeal.resolved]}}
    Subscription:
      allOf:
        - {$ref: '#/components/schemas/SubscriptionInput'}
        - {type: object, required: [subscription_id, status], properties: {subscription_id: {type: string}, status: {type: string, enum: [pending_verification, active, disabled]}}}
    RankEvent:
      type: object
      required: [event_id, event_type, occurred_at, resource_id, status_version]
      properties:
        event_id: {type: string}
        event_type: {type: string}
        occurred_at: {type: string, format: date-time}
        resource_id: {type: string}
        status_version: {type: integer, minimum: 1}
        data: {type: object, additionalProperties: true}
