openapi: 3.1.0

info:
  title: GETKINETIK Bureau API
  summary: The independent evidence bureau for DePIN — bureau-signed device attestations, offline verification, partner attestation channel.
  description: |
    GETKINETIK is the **independent evidence bureau** for the decentralized
    physical economy. Every sovereign node carries a hardware-sealed Ed25519
    identity. Every uptime claim is hash-chained and signed. The bureau ships
    **signed evidence, not verdicts** — partners map evidence to reward tiers
    with their own policy. Reference policy + methodology:
    https://github.com/Ricolax310/GetKinetik/blob/main/docs/methodology/GENESIS_SCORE.md.

    Four public endpoints:

    - `POST /api/verify-device` — verify a Proof-of-Origin URL and return a
      bureau-signed attestation (plus a convenience `derived` tier).
    - `GET  /api/score/{nodeId}` — look up the most recent cached attestation
      for a node without supplying a fresh proof.
    - `POST /api/attest` — partner attestation channel (Bearer auth) for
      signalling cross-network observations into the bureau.
    - `GET  /api/bureau/stats` — public bureau telemetry powering the live
      ticker on https://getkinetik.app/bureau/.

    **Neutrality.** GETKINETIK never issues a token, never holds equity in
    a graded network, and never accepts payment in a graded network's
    native asset. Charter: https://github.com/Ricolax310/GetKinetik/blob/main/NEUTRALITY.md.
  version: "2.0.0"
  contact:
    name: Eric · GETKINETIK
    email: eric@outfromnothingllc.com
    url: https://getkinetik.app
  license:
    name: MIT
    url: https://github.com/Ricolax310/GetKinetik/blob/main/LICENSE
  termsOfService: https://github.com/Ricolax310/GetKinetik/blob/main/NEUTRALITY.md

externalDocs:
  description: Genesis Score methodology (public, versioned)
  url: https://github.com/Ricolax310/GetKinetik/blob/main/docs/methodology/GENESIS_SCORE.md

servers:
  - url: https://getkinetik.app
    description: Production bureau

tags:
  - name: Verification
    description: Hardware-rooted device verification.
  - name: Score
    description: Genesis Score lookup.
  - name: Attestation
    description: Partner cross-network signals.
  - name: Telemetry
    description: Public bureau stats.

security: []

paths:
  /api/verify-device:
    post:
      tags: [Verification]
      summary: Verify a Proof of Origin and return a bureau-signed attestation
      operationId: verifyDevice
      description: |
        Posts a Proof-of-Origin URL (from the GETKINETIK app's **Share**
        action, or a partner SDK) and returns whether the proof is valid,
        plus a bureau-signed `attestation` (verifiable offline via
        `@getkinetik/verify`) and a convenience `derived` tier. No
        authentication required — anyone can verify any proof.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/VerifyDeviceRequest"
            examples:
              proofUrl:
                summary: A signed proof URL
                value:
                  proofUrl: "https://getkinetik.app/verify/#proof=<base64url-payload>"
      responses:
        "200":
          description: Verification result (valid or invalid — both 200).
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/VerifyDeviceValid"
                  - $ref: "#/components/schemas/VerifyDeviceInvalid"
              examples:
                valid:
                  summary: Valid signature, bureau-signed attestation
                  value:
                    valid: true
                    nodeId: "KINETIK-NODE-A49C9E14"
                    pubkey: "3062f3ba272db9204afe1fe11dd3cb086e06a3292e299e33f0450bcf30434c22"
                    schema: "proof-of-origin:v2"
                    attestation:
                      payload: { v: 1, kind: "attestation" }
                      message: "<canonical bytes that were signed>"
                      signature: "<128-char hex bureau Ed25519 signature>"
                      hash: "<16-char hex sha256(message)[:16]>"
                    derived:
                      tier: "STANDING"
                      score: 636
                      flagged: false
                      flags: []
                      policyVersion: "v2.0.2"
                      contributingFactors:
                        baseline: 200
                        bureauObservedAge: 204
                        lifetimeBeats: 232
                        sensorCoherence: 0
                    asOf: "2026-05-13T13:34:22.674Z"
                invalid:
                  summary: Signature mismatch
                  value:
                    valid: false
                    reason: "signature_invalid"
        "400":
          description: Malformed request.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiError" }
    get:
      tags: [Verification]
      summary: Discovery — humans pasting the URL in a browser
      operationId: verifyDeviceDiscovery
      description: |
        Browser GETs return JSON documentation rather than a 405. Useful
        for partner engineers who paste the webhook URL into a tab while
        wiring it up.
      responses:
        "200":
          description: Discovery JSON.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/EndpointDiscovery" }

  /api/score/{nodeId}:
    get:
      tags: [Score]
      summary: Look up the most recent cached attestation for a node
      operationId: getScore
      description: |
        Returns the cached bureau-signed attestation from the bureau's last
        verification of this node (same `attestation` + `derived` shape as
        `verify-device`). If no partner has ever called `verify-device` for
        this nodeId, the bureau has no record and returns 404. Attestations
        are cached for 30 days after the last verification.
      parameters:
        - name: nodeId
          in: path
          required: true
          description: Sovereign node ID, format `KINETIK-NODE-XXXXXXXX` (8 uppercase hex).
          schema:
            type: string
            pattern: "^KINETIK-NODE-[0-9A-F]{8}$"
            example: "KINETIK-NODE-A49C9E14"
      responses:
        "200":
          description: Cached Genesis Score for the node.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CachedScore" }
        "400":
          description: Invalid node ID format.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiError" }
        "404":
          description: Node not found in the bureau cache.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiError" }
        "503":
          description: Score storage temporarily unavailable.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiError" }

  /api/attest:
    post:
      tags: [Attestation]
      summary: Submit a partner attestation about a node
      operationId: attest
      description: |
        Partner networks (DIMO, Hivemapper, WeatherXM, Geodnet, etc.) post
        typed observations here — "this node performed expected work",
        "this node failed our integrity check", "this node was flagged for
        fraud" — and the bureau records them for cross-network reputation.

        v1.1 records attestations; v1.2 folds them into the live Genesis
        Score. Partner API keys are issued by emailing
        eric@outfromnothingllc.com.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AttestRequest" }
            examples:
              engagement:
                summary: Positive engagement signal
                value:
                  nodeId: "KINETIK-NODE-A49C9E14"
                  kind: "engagement"
                  network: "dimo"
                  detail: "Trip uploaded successfully (vehicle ID matched)."
                  weight: 1
              fraud:
                summary: Fraud flag
                value:
                  nodeId: "KINETIK-NODE-A49C9E14"
                  kind: "fraud"
                  network: "hivemapper"
                  detail: "GPS coordinates outside claimed coverage region."
                  weight: -3
      responses:
        "200":
          description: Attestation recorded.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AttestResponse" }
        "400":
          description: Validation failed (bad nodeId, kind, weight, etc.).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiError" }
        "401":
          description: Missing or invalid Bearer token.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiError" }
        "503":
          description: Attestation channel offline (no partner key configured on this deployment).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ApiError" }

  /api/webhooks/tier-changed:
    x-webhook-direction: outbound
    description: |
      **Outbound webhook (bureau → partner).** Documented here as a path
      for tooling clarity, but this endpoint is not served by the bureau —
      it's the *shape* of the POST the bureau sends to receivers
      registered via `SCORE_WEBHOOK_URLS`. See `docs/api/webhooks.md` for
      registration, signature verification, and delivery semantics.
    post:
      tags: [Verification]
      summary: Outbound — tier.changed event the bureau POSTs to your URL
      operationId: tierChangedWebhook
      description: |
        Fired when a node's derived `tier` transitions. Embeds the full
        bureau-signed attestation. Best-effort delivery with HMAC-SHA256
        signature in `X-GETKINETIK-Signature`.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/TierChangedEvent" }
      responses:
        "200":
          description: Partner acknowledged delivery.

  /api/bureau/stats:
    get:
      tags: [Telemetry]
      summary: Public bureau telemetry counters
      operationId: bureauStats
      description: |
        Aggregate verification counters powering the live ticker on
        https://getkinetik.app/bureau/. Eventually consistent; do not use
        for billing or compliance.
      responses:
        "200":
          description: Bureau counters.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BureauStats" }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: opaque-string
      description: |
        Partner API key (issued by emailing eric@outfromnothingllc.com).
        Send as `Authorization: Bearer <partner-api-key>`.

  schemas:
    NodeId:
      type: string
      pattern: "^KINETIK-NODE-[0-9A-F]{8}$"
      example: "KINETIK-NODE-A49C9E14"
      description: Sovereign node identifier — 8 uppercase hex characters of `sha256(pubkey)`.

    ScoreBand:
      type: string
      enum: [TAMPERED, NEW, STANDING, STRONG, PREMIER]
      description: |
        - **TAMPERED** (≤ 199): impossible inputs detected; do not pay.
        - **NEW** (200–499): insufficient evidence yet.
        - **STANDING** (500–749): real-device operation confirmed.
        - **STRONG** (750–899): long, sensor-coherent record.
        - **PREMIER** (900–999): top tier; multi-input attested.

    TamperFlag:
      type: string
      enum:
        - beat_rate_implausible
        - chain_rewind
        - lux_implausible
        - motion_implausible
        - pressure_implausible
      description: Specific failure modes documented in methodology §3.6.

    VerifyDeviceRequest:
      type: object
      required: [proofUrl]
      properties:
        proofUrl:
          type: string
          format: uri
          maxLength: 8192
          description: |
            Full proof URL from the GETKINETIK app's **Share** action, or a
            bare base64url payload. Both `https://getkinetik.app/verify/#proof=...`
            and the raw fragment are accepted.

    Attestation:
      type: object
      description: |
        Bureau-signed evidence bundle. A standard `SignedArtifact` shape,
        verifiable offline with `@getkinetik/verify`. **This is the
        authoritative output partners should integrate against.** Null when
        the bureau signing key is not configured on the deployment.
      required: [payload, message, signature, hash]
      properties:
        payload:
          type: object
          description: The typed AttestationPayload (subject, bureauObserved, chainClaim, sensorCoherence, flags, witnesses).
        message:
          type: string
          description: The canonical bytes that were signed.
        signature:
          type: string
          pattern: "^[0-9a-f]{128}$"
          description: 128-char hex bureau Ed25519 signature.
        hash:
          type: string
          pattern: "^[0-9a-f]{16}$"
          description: 16-char hex sha256(message)[:16].

    Derived:
      type: object
      description: |
        Convenience tier computed with the default policy from
        `@getkinetik/evidence-mapping`. Partners running custom policy should
        ignore this and map `attestation.payload` themselves.
      required: [tier, score, flagged, flags, policyVersion, contributingFactors]
      properties:
        tier:    { $ref: "#/components/schemas/ScoreBand" }
        score:   { type: integer, minimum: 0, maximum: 1000 }
        flagged: { type: boolean, description: "True if any flag was present; forces tier to TAMPERED." }
        flags:
          type: array
          items: { $ref: "#/components/schemas/TamperFlag" }
        policyVersion:
          type: string
          example: "v2.0.2"
          description: Pin when caching to detect policy upgrades that would shift tiers.
        contributingFactors:
          type: object
          description: "Audit breakdown — baseline + bureauObservedAge + lifetimeBeats + sensorCoherence = score (before flag floor)."

    VerifyDeviceValid:
      type: object
      required: [valid, nodeId, pubkey, schema, derived, asOf]
      properties:
        valid:  { type: boolean, enum: [true] }
        nodeId: { $ref: "#/components/schemas/NodeId" }
        pubkey: { type: string, pattern: "^[0-9a-f]{64}$" }
        schema: { type: string, enum: ["proof-of-origin:v2"] }
        attestation:
          oneOf:
            - { $ref: "#/components/schemas/Attestation" }
            - { type: "null" }
          description: Bureau-signed attestation, or null when signing key unconfigured.
        derived: { $ref: "#/components/schemas/Derived" }
        asOf:
          type: string
          format: date-time

    VerifyDeviceInvalid:
      type: object
      required: [valid, reason]
      properties:
        valid: { type: boolean, enum: [false] }
        reason:
          type: string
          enum:
            - malformed_envelope
            - missing_fields
            - attribution_mismatch
            - hash_mismatch
            - invalid_pubkey
            - pubkey_decode_failed
            - signature_invalid
            - internal_error

    CachedScore:
      type: object
      required: [nodeId, derived, asOf, source]
      properties:
        nodeId: { $ref: "#/components/schemas/NodeId" }
        attestation:
          oneOf:
            - { $ref: "#/components/schemas/Attestation" }
            - { type: "null" }
        derived:
          oneOf:
            - { $ref: "#/components/schemas/Derived" }
            - { type: "null" }
        asOf:     { type: string, format: date-time }
        cachedAt: { type: string, format: date-time }
        source:   { type: string, enum: [kv] }

    AttestRequest:
      type: object
      required: [nodeId, kind, network, weight]
      properties:
        nodeId:  { $ref: "#/components/schemas/NodeId" }
        kind:
          type: string
          enum: [engagement, fault, fraud]
          description: |
            - **engagement** — positive observation (work performed, expected behaviour).
            - **fault** — node failed an integrity / data-quality check.
            - **fraud** — strong evidence of misbehaviour.
        network:
          type: string
          minLength: 1
          maxLength: 32
          example: "dimo"
          description: Partner network identifier (free-form, lowercased).
        detail:
          type: string
          maxLength: 256
          description: Optional partner-defined detail string.
        weight:
          type: integer
          minimum: -3
          maximum: 3
          description: Signed magnitude. Clamped to ±3 server-side.

    AttestResponse:
      type: object
      required: [ok, receipt, recordedAt]
      properties:
        ok:         { type: boolean, enum: [true] }
        receipt:
          type: string
          description: KV key the attestation is stored under.
          example: "attest:KINETIK-NODE-A49C9E14:1778679539654"
        recordedAt: { type: string, format: date-time }
        attestor:
          type: string
          description: |
            Partner identity derived from the Bearer key. In dict-key mode
            (`ATTEST_API_KEYS`), this is the partner name. In legacy
            single-key mode (`ATTEST_API_KEY`), this is `"unknown"`.
          example: "dimo"
        note:       { type: string }

    BureauStats:
      type: object
      required: [ok, stats, methodologyVersion, asOf]
      properties:
        ok: { type: boolean, enum: [true] }
        stats:
          type: object
          required: [total, valid, invalid, flagged, signedAttestations, byTier]
          properties:
            total:              { type: integer, minimum: 0 }
            valid:              { type: integer, minimum: 0 }
            invalid:            { type: integer, minimum: 0 }
            flagged:
              type: integer
              minimum: 0
              description: Verifications where the bureau set a tamper / chain-integrity flag.
            signedAttestations:
              type: integer
              minimum: 0
              description: Verifications that returned a bureau-signed attestation envelope.
            byTier:
              type: object
              required: [NEW, STANDING, STRONG, PREMIER, TAMPERED]
              properties:
                NEW:      { type: integer, minimum: 0 }
                STANDING: { type: integer, minimum: 0 }
                STRONG:   { type: integer, minimum: 0 }
                PREMIER:  { type: integer, minimum: 0 }
                TAMPERED: { type: integer, minimum: 0 }
            firstVerifyAt:
              type: [string, "null"]
              format: date-time
            lastVerifyAt:
              type: [string, "null"]
              format: date-time
        methodologyVersion: { type: string, example: "v2.0.2" }
        asOf:               { type: string, format: date-time }

    EndpointDiscovery:
      type: object
      properties:
        documentation: { type: boolean, enum: [true] }
        summary:       { type: string }
        method:        { type: string }
        contentType:   { type: string }
        bodyExample:   { type: object }
        spec:          { type: string, format: uri }

    TierChangedEvent:
      type: object
      required: [event, nodeId, toTier, score, policyVersion, asOf, delivery]
      properties:
        event:    { type: string, enum: [tier.changed] }
        nodeId:   { $ref: "#/components/schemas/NodeId" }
        fromTier:
          oneOf:
            - { $ref: "#/components/schemas/ScoreBand" }
            - { type: "null" }
          description: Previous tier, or `null` on first sighting.
        toTier:   { $ref: "#/components/schemas/ScoreBand" }
        score:    { type: integer, minimum: 0, maximum: 1000 }
        flags:
          type: array
          items: { $ref: "#/components/schemas/TamperFlag" }
        policyVersion: { type: string, example: "v2.0.2" }
        attestation:
          oneOf:
            - { $ref: "#/components/schemas/Attestation" }
            - { type: "null" }
          description: Full bureau-signed attestation — re-verify offline before trusting the transition.
        asOf:     { type: string, format: date-time }
        delivery:
          type: string
          description: UUID for deduplication on the receiver.

    ApiError:
      type: object
      required: [error]
      properties:
        error: { type: string }
        hint:  { type: string }
