openapi: 3.0.3
info:
  title: VETS Coin API
  description: |
    Partner + public API for the VETS Coin ecosystem.

    Partner authentication uses an HMAC signature over the request:
    - X-Partner-Key: key id
    - X-Partner-Timestamp: unix epoch seconds
    - X-Partner-Signature: hex(hmac_sha256(secret, "{ts}.{method}.{path}.{body}"))
    - X-Partner-Nonce: required for mutation endpoints by default
    - Idempotency-Key: required for mutation endpoints by default
  version: 0.1.0
servers:
  - url: https://api.vets-coin.com
  - url: https://vets-coin.com
tags:
  - name: Public
  - name: Partner
  - name: Salutes
paths:
  /api/public-stats:
    get:
      tags: [Public]
      summary: Public counters and stats
      operationId: getPublicStats
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              example:
                success: true
                stats:
                  users: 1250
                  wallets: 980
                  faucet_claims_24h: 41

  /api/public/system-status:
    get:
      tags: [Public]
      summary: Public system health and automation status snapshot
      operationId: getPublicSystemStatus
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              example:
                success: true
                status:
                  overall: ok
                  checks:
                    api: ok
                    audit_anomalies: ok
                    alert_monitor: ok
                    ops_jobs: ok

  /status.json:
    get:
      tags: [Public]
      summary: Alias of /api/public/system-status
      operationId: getStatusJsonAlias
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              example:
                success: true
                status:
                  overall: ok

  /api/public/system-status/trend:
    get:
      tags: [Public]
      summary: Rolling status history for uptime trend charts
      operationId: getPublicSystemStatusTrend
      parameters:
        - in: query
          name: limit
          schema: { type: integer, minimum: 10, maximum: 1000 }
          required: false
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              example:
                success: true
                limit: 20
                counts:
                  ok: 18
                  warn: 2
                  bad: 0
                points:
                  - generated_at: "2026-03-06T00:00:00Z"
                    overall: ok

  /api/public/system-status/incidents:
    get:
      tags: [Public]
      summary: Incident windows derived from rolling status history
      operationId: getPublicSystemStatusIncidents
      parameters:
        - in: query
          name: hours
          schema: { type: integer, minimum: 1, maximum: 4320 }
          required: false
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 200 }
          required: false
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              example:
                success: true
                hours: 24
                limit: 10
                incidents:
                  - started_at: "2026-03-05T01:08:08Z"
                    ended_at: "2026-03-05T01:09:39Z"
                    severity: warn

  /api/public/system-status/uptime:
    get:
      tags: [Public]
      summary: Windowed uptime percentages and per-check degraded rates
      operationId: getPublicSystemStatusUptime
      parameters:
        - in: query
          name: hours
          schema: { type: integer, enum: [24, 168, 720] }
          required: false
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              example:
                success: true
                selected_hours: 168
                overall:
                  degraded_pct: 0.0
                checks:
                  api:
                    degraded_pct: 0.0

  /api/transactions/latest:
    get:
      tags: [Public]
      summary: Latest known token transactions
      operationId: getTransactionsLatest
      parameters:
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 200 }
          required: false
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              example:
                success: true
                rows:
                  - signature: "5SdxExampleTxSig111111111111111111111111111111111"
                    block_time: "2026-03-05T20:00:00Z"
                    vets_delta: "125.00"

  /api/partner/capabilities:
    get:
      tags: [Partner]
      summary: Partner API capability discovery
      operationId: getPartnerCapabilities
      parameters:
        - $ref: "#/components/parameters/PartnerKey"
        - $ref: "#/components/parameters/PartnerTimestamp"
        - $ref: "#/components/parameters/PartnerSignature"
        - $ref: "#/components/parameters/PartnerNonce"
        - in: query
          name: include_meta
          schema: { type: boolean, default: true }
          required: false
        - in: header
          name: If-Modified-Since
          schema: { type: string }
          required: false
          description: Optional conditional cache validator for 304 responses.
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              example:
                success: true
                partner: "Acme Partner"
                key_id: "acme_key"
                docs:
                  api_guide: "/developers/api-guide.md"
                  openapi: "/api/openapi.yaml"
                query_support:
                  include_meta: true
                meta:
                  request_id: "abc123"
                  generated_at_utc: "2026-03-08T20:00:00Z"
        "304":
          description: Not modified (If-None-Match matched)

  /api/partner/user-lookup:
    get:
      tags: [Partner]
      summary: Lookup partner-linked users by email/username/wallet
      operationId: getPartnerUserLookup
      parameters:
        - $ref: "#/components/parameters/PartnerKey"
        - $ref: "#/components/parameters/PartnerTimestamp"
        - $ref: "#/components/parameters/PartnerSignature"
        - $ref: "#/components/parameters/PartnerNonce"
        - in: query
          name: q
          schema: { type: string }
          required: true
          deprecated: true
          description: Legacy lookup alias; deprecated in favor of explicit `email`, `username`, or `wallet` filters. Sunset target 2026-06-30 UTC.
        - in: query
          name: email
          schema: { type: string }
          required: false
        - in: query
          name: username
          schema: { type: string }
          required: false
        - in: query
          name: wallet
          schema: { type: string }
          required: false
        - in: query
          name: include_wallets
          schema: { type: boolean, default: false }
          required: false
        - in: query
          name: status
          schema: { type: string, enum: [active, suspended, banned, pending] }
          required: false
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 50, default: 10 }
          required: false
        - in: query
          name: offset
          schema: { type: integer, minimum: 0, maximum: 5000, default: 0 }
          required: false
        - in: query
          name: cursor
          schema: { type: string }
          required: false
          description: Cursor alias (last seen user id) for write-resilient pagination.
        - in: query
          name: include_meta
          schema: { type: boolean, default: true }
          required: false
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              example:
                success: true
                count: 1
                users:
                  - id: 42
                    username: "vet_user"
                    status: "active"
                query:
                  limit: 10
                  offset: 0
                  cursor: null
                  q: null
                  status: "active"
                pagination:
                  has_more: false
                  next_offset: null
                  next_cursor: null
                meta:
                  request_id: "abc123"
                  generated_at_utc: "2026-03-08T20:00:00Z"
        "400":
          description: Invalid query parameters

  /api/partner/users:
    get:
      tags: [Partner]
      summary: List users linked to the partner key
      operationId: getPartnerUsers
      parameters:
        - $ref: "#/components/parameters/PartnerKey"
        - $ref: "#/components/parameters/PartnerTimestamp"
        - $ref: "#/components/parameters/PartnerSignature"
        - $ref: "#/components/parameters/PartnerNonce"
        - in: query
          name: status
          schema: { type: string, enum: [active, suspended, banned, pending] }
          required: false
        - in: query
          name: q
          schema: { type: string }
          required: false
          description: Optional free-text filter across username/display_name/email/external_id.
        - in: query
          name: include_wallets
          schema: { type: boolean, default: false }
          required: false
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 200, default: 25 }
          required: false
        - in: query
          name: offset
          schema: { type: integer, minimum: 0, maximum: 10000, default: 0 }
          required: false
        - in: query
          name: cursor
          schema: { type: string }
          required: false
          description: Cursor alias (last seen user id) for write-resilient pagination.
        - in: query
          name: include_meta
          schema: { type: boolean, default: true }
          required: false
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              example:
                success: true
                count: 1
                users:
                  - id: 2048
                    external_id: "partner-2048"
                    username: "new_user"
                    status: "active"
                query:
                  limit: 25
                  offset: 0
                  cursor: null
                  status: null
                  q: null
                  include_wallets: false
                pagination:
                  has_more: false
                  next_offset: null
                  next_cursor: null
        "400":
          description: Invalid query parameters (for example `invalid_status` or `invalid_cursor`)
    post:
      tags: [Partner]
      summary: Create a user
      operationId: postPartnerUsers
      parameters:
        - $ref: "#/components/parameters/PartnerKey"
        - $ref: "#/components/parameters/PartnerTimestamp"
        - $ref: "#/components/parameters/PartnerSignature"
        - $ref: "#/components/parameters/PartnerNonce"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              example:
                success: true
                external_id: "partner-2048"
                user:
                  id: 2048
                  username: "new_user"
                  status: "active"

  /api/partner/users/{user_id}:
    get:
      tags: [Partner]
      summary: Get a user
      operationId: getPartnerUserById
      parameters:
        - $ref: "#/components/parameters/PartnerKey"
        - $ref: "#/components/parameters/PartnerTimestamp"
        - $ref: "#/components/parameters/PartnerSignature"
        - $ref: "#/components/parameters/PartnerNonce"
        - in: path
          name: user_id
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              example:
                success: true
                user:
                  id: 2048
                  username: "new_user"
                  status: "active"
    patch:
      tags: [Partner]
      summary: Update a user (partial)
      operationId: patchPartnerUserById
      parameters:
        - $ref: "#/components/parameters/PartnerKey"
        - $ref: "#/components/parameters/PartnerTimestamp"
        - $ref: "#/components/parameters/PartnerSignature"
        - $ref: "#/components/parameters/PartnerNonce"
        - $ref: "#/components/parameters/IdempotencyKey"
        - in: path
          name: user_id
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              example:
                success: true
                user_id: 2048
                updated: true

  /api/partner/users/{user_id}/wallets:
    post:
      tags: [Partner]
      summary: Link a wallet to a user
      operationId: postPartnerUserWallets
      parameters:
        - $ref: "#/components/parameters/PartnerKey"
        - $ref: "#/components/parameters/PartnerTimestamp"
        - $ref: "#/components/parameters/PartnerSignature"
        - $ref: "#/components/parameters/PartnerNonce"
        - $ref: "#/components/parameters/IdempotencyKey"
        - in: path
          name: user_id
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              example:
                success: true
                user_id: 2048
                wallet:
                  address: "7e2vExampleWallet11111111111111111111111111111111"
                  linked: true

  /api/partner/webhooks:
    get:
      tags: [Partner]
      summary: List partner webhooks
      operationId: getPartnerWebhooks
      parameters:
        - $ref: "#/components/parameters/PartnerKey"
        - $ref: "#/components/parameters/PartnerTimestamp"
        - $ref: "#/components/parameters/PartnerSignature"
        - $ref: "#/components/parameters/PartnerNonce"
        - in: query
          name: event_type
          schema: { type: string }
          required: false
        - in: query
          name: is_active
          schema: { type: string, enum: ["0", "1", all], default: all }
          required: false
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
          required: false
        - in: query
          name: offset
          schema: { type: integer, minimum: 0, maximum: 10000, default: 0 }
          required: false
        - in: query
          name: cursor
          schema: { type: string }
          required: false
          description: Cursor alias (last seen webhook id) for write-resilient pagination.
        - in: query
          name: include_meta
          schema: { type: boolean, default: true }
          required: false
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              example:
                success: true
                count: 1
                webhooks:
                  - id: 42
                    event_type: "salutes.credit"
                    is_active: 1
                pagination:
                  has_more: false
                  next_offset: null
                  next_cursor: null
                query:
                  limit: 50
                  offset: 0
                  cursor: null
                  event_type: null
                  is_active: null
        "400":
          description: Invalid query parameters (for example `invalid_event_type`, `invalid_is_active`, or `invalid_cursor`)
    post:
      tags: [Partner]
      summary: Create partner webhook
      operationId: postPartnerWebhooks
      parameters:
        - $ref: "#/components/parameters/PartnerKey"
        - $ref: "#/components/parameters/PartnerTimestamp"
        - $ref: "#/components/parameters/PartnerSignature"
        - $ref: "#/components/parameters/PartnerNonce"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              example:
                success: true
                webhook:
                  id: 42
                  event_type: "salutes.credit"
                  target_url: "https://example.com/webhooks/vets"
                  is_active: true
                  created_at: "2026-03-08T20:00:00Z"
                  updated_at: "2026-03-08T20:00:00Z"

  /api/partner/webhooks/{webhook_id}:
    delete:
      tags: [Partner]
      summary: Delete partner webhook
      operationId: deletePartnerWebhookById
      parameters:
        - $ref: "#/components/parameters/PartnerKey"
        - $ref: "#/components/parameters/PartnerTimestamp"
        - $ref: "#/components/parameters/PartnerSignature"
        - $ref: "#/components/parameters/PartnerNonce"
        - $ref: "#/components/parameters/IdempotencyKey"
        - in: path
          name: webhook_id
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: OK

  /api/partner/webhooks/{webhook_id}/test:
    post:
      tags: [Partner]
      summary: Test webhook delivery
      operationId: postPartnerWebhookTest
      parameters:
        - $ref: "#/components/parameters/PartnerKey"
        - $ref: "#/components/parameters/PartnerTimestamp"
        - $ref: "#/components/parameters/PartnerSignature"
        - $ref: "#/components/parameters/PartnerNonce"
        - $ref: "#/components/parameters/IdempotencyKey"
        - in: path
          name: webhook_id
          required: true
          schema: { type: integer }
      responses:
        "200":
          description: OK

  /api/partner/analytics/usage:
    get:
      tags: [Partner]
      summary: Per-key usage analytics and webhook delivery health
      operationId: getPartnerUsageAnalytics
      parameters:
        - $ref: "#/components/parameters/PartnerKey"
        - $ref: "#/components/parameters/PartnerTimestamp"
        - $ref: "#/components/parameters/PartnerSignature"
        - $ref: "#/components/parameters/PartnerNonce"
        - in: query
          name: hours
          schema: { type: integer, minimum: 1, maximum: 720, default: 24 }
          required: false
      responses:
        "200":
          description: OK

  /api/partner/webhook-events:
    get:
      tags: [Partner]
      summary: List partner webhook events with replay-oriented filters
      operationId: getPartnerWebhookEvents
      parameters:
        - $ref: "#/components/parameters/PartnerKey"
        - $ref: "#/components/parameters/PartnerTimestamp"
        - $ref: "#/components/parameters/PartnerSignature"
        - $ref: "#/components/parameters/PartnerNonce"
        - in: query
          name: status
          schema: { type: string, enum: [all, pending, failed, delivered, dead_letter], default: all }
          required: false
        - in: query
          name: reason_tag
          schema: { type: string }
          required: false
        - in: query
          name: from
          schema: { type: string, format: date-time }
          required: false
        - in: query
          name: to
          schema: { type: string, format: date-time }
          required: false
      responses:
        "200":
          description: OK

  /api/partner/webhook-events/replay:
    post:
      tags: [Partner]
      summary: Batch replay webhook events
      operationId: postPartnerWebhookReplayBatch
      parameters:
        - $ref: "#/components/parameters/PartnerKey"
        - $ref: "#/components/parameters/PartnerTimestamp"
        - $ref: "#/components/parameters/PartnerSignature"
        - $ref: "#/components/parameters/PartnerNonce"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                event_ids:
                  type: array
                  items: { type: integer }
                reason_tag:
                  type: string
                note:
                  type: string
              required: [event_ids]
      responses:
        "200":
          description: OK

  /api/partner/webhooks/signature/verify:
    post:
      tags: [Partner]
      summary: Verify webhook signature payloads against a candidate secret
      operationId: postPartnerWebhookSignatureVerify
      parameters:
        - $ref: "#/components/parameters/PartnerKey"
        - $ref: "#/components/parameters/PartnerTimestamp"
        - $ref: "#/components/parameters/PartnerSignature"
        - $ref: "#/components/parameters/PartnerNonce"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                secret: { type: string }
                signature: { type: string }
                timestamp: { type: string }
                payload: { type: object, additionalProperties: true }
              required: [secret, signature, timestamp]
      responses:
        "200":
          description: OK

  /api/partner/webhooks/dlq:
    get:
      tags: [Partner]
      summary: Dead-letter queue summary and remediation guidance
      operationId: getPartnerWebhookDlq
      parameters:
        - $ref: "#/components/parameters/PartnerKey"
        - $ref: "#/components/parameters/PartnerTimestamp"
        - $ref: "#/components/parameters/PartnerSignature"
        - $ref: "#/components/parameters/PartnerNonce"
        - in: query
          name: hours
          schema: { type: integer, minimum: 1, maximum: 2160, default: 168 }
          required: false
      responses:
        "200":
          description: OK

  /api/partner/status:
    get:
      tags: [Partner]
      summary: Partner integration status page payload
      operationId: getPartnerStatus
      parameters:
        - $ref: "#/components/parameters/PartnerKey"
        - $ref: "#/components/parameters/PartnerTimestamp"
        - $ref: "#/components/parameters/PartnerSignature"
        - $ref: "#/components/parameters/PartnerNonce"
      responses:
        "200":
          description: OK

  /api/partner/donation-claim:
    post:
      tags: [Partner]
      summary: Issue a donation claim (if enabled)
      operationId: postPartnerDonationClaim
      parameters:
        - $ref: "#/components/parameters/PartnerKey"
        - $ref: "#/components/parameters/PartnerTimestamp"
        - $ref: "#/components/parameters/PartnerSignature"
        - $ref: "#/components/parameters/PartnerNonce"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        "200":
          description: OK

  /api/salutes/balance:
    get:
      tags: [Salutes]
      summary: Get salutes balance for a partner-scoped user id
      operationId: getSalutesBalance
      parameters:
        - $ref: "#/components/parameters/PartnerKey"
        - $ref: "#/components/parameters/PartnerTimestamp"
        - $ref: "#/components/parameters/PartnerSignature"
        - $ref: "#/components/parameters/PartnerNonce"
        - in: query
          name: user_id
          schema: { type: string }
          required: true
        - in: query
          name: include_meta
          schema: { type: boolean, default: true }
          required: false
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              example:
                success: true
                user_id: "42"
                balance: 1200
                partner_key_id: "acme_key"

  /api/salutes/ledger:
    get:
      tags: [Salutes]
      summary: Get salutes ledger entries with optional filters/pagination
      operationId: getSalutesLedger
      parameters:
        - $ref: "#/components/parameters/PartnerKey"
        - $ref: "#/components/parameters/PartnerTimestamp"
        - $ref: "#/components/parameters/PartnerSignature"
        - $ref: "#/components/parameters/PartnerNonce"
        - in: query
          name: user_id
          schema: { type: string }
          required: true
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 500, default: 200 }
          required: false
        - in: query
          name: offset
          schema: { type: integer, minimum: 0, maximum: 5000, default: 0 }
          required: false
        - in: query
          name: order
          schema: { type: string, enum: [asc, desc], default: desc }
          required: false
        - in: query
          name: reason
          schema: { type: string, maxLength: 64 }
          required: false
        - in: query
          name: source
          schema: { type: string, maxLength: 64 }
          required: false
        - in: query
          name: include_meta
          schema: { type: boolean, default: true }
          required: false
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              example:
                success: true
                user_id: "42"
                balance: 1200
                ledger:
                  - id: 101
                    delta: 100
                    reason: "bonus"
                query:
                  limit: 200
                  offset: 0
                  order: "desc"
                  reason: null
                  source: null
                pagination:
                  has_more: false
                  next_offset: null

  /api/salutes/credit:
    post:
      tags: [Salutes]
      summary: Credit salutes (partner/admin)
      operationId: postSalutesCredit
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              example:
                success: true
                user_id: "42"
                balance: 1300
                delta: 100
                ledger_entry_id: 101
                idempotency_key: "idem_abc123"

  /api/salutes/debit:
    post:
      tags: [Salutes]
      summary: Debit salutes (partner/admin)
      operationId: postSalutesDebit
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
              example:
                success: true
                user_id: "42"
                balance: 1200
                delta: -100
                ledger_entry_id: 102
                idempotency_key: "idem_def456"

  /api/salutes/conversion-policy:
    get:
      tags: [Salutes]
      summary: Fetch current salutes conversion policy
      operationId: getSalutesConversionPolicy
      responses:
        "200":
          description: OK

  /api/salutes/convert:
    post:
      tags: [Salutes]
      summary: Convert salutes to VETS (policy-controlled)
      operationId: postSalutesConvert
      responses:
        "200":
          description: OK

components:
  parameters:
    PartnerKey:
      in: header
      name: X-Partner-Key
      required: true
      schema: { type: string }
      description: Partner key id
    PartnerTimestamp:
      in: header
      name: X-Partner-Timestamp
      required: true
      schema: { type: string }
      description: Unix epoch seconds
    PartnerSignature:
      in: header
      name: X-Partner-Signature
      required: true
      schema: { type: string }
      description: Hex HMAC-SHA256 signature
    PartnerNonce:
      in: header
      name: X-Partner-Nonce
      required: false
      schema: { type: string }
      description: Replay-protection nonce (required for mutation endpoints by default)
    IdempotencyKey:
      in: header
      name: Idempotency-Key
      required: false
      schema: { type: string }
      description: Idempotency key (required for mutation endpoints by default)
