openapi: 3.1.0
info:
  title: PayeeProof API
  version: v1
  summary: Pre-send verification for stablecoin payouts.
  description: >
    Public contract for the current PayeeProof API surface.
    This contract documents the pre-send verification surface used to stop
    wrong-network and wrong-destination mistakes before funds move.
    Recovery guidance can exist as a separate module, but public API positioning
    stays pre-send first.
servers:
  - url: https://payeeproof-api.onrender.com
    description: Current production base URL
tags:
  - name: Preflight
    description: Pre-send verification before payout approval
  - name: Records
    description: Verification history and searchable records for authenticated clients
  - name: Account
    description: Client account metadata and usage reporting for authenticated clients
  - name: Webhooks
    description: Delivery acknowledgements for outbound verification events
paths:

  /api/account:
    get:
      tags: [Account]
      operationId: getAccountSummary
      summary: Get client account summary
      description: >
        Returns basic account information for the current API key together with
        the current-month usage snapshot, plan, environment, and webhook state.
      security:
        - ApiKeyAuth: []
      responses:
        '200':
          description: Client account summary
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  trace_id: { type: string }
                  client:
                    type: object
                    properties:
                      tenant_id: { type: string }
                      client_label: { type: string }
                      display_name: { type: string }
                      environment: { type: string }
                      role: { type: string }
                      plan: { type: string }
                      scopes:
                        type: array
                        items: { type: string }
                  usage:
                    type: object
                  webhooks:
                    type: object
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/usage-summary:
    get:
      tags: [Account]
      operationId: getUsageSummary
      summary: Get usage summary
      description: >
        Returns usage totals and breakdowns for the authenticated client.
        Useful for account reporting and future usage-based billing.
      security:
        - ApiKeyAuth: []
      parameters:
        - in: query
          name: period
          schema: { type: string, example: 30d }
          description: Supported values include 7d, 30d, 90d, and this_month.
      responses:
        '200':
          description: Usage summary
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  trace_id: { type: string }
                  client: { type: string }
                  environment: { type: string }
                  period: { type: string }
                  since: { type: string }
                  totals: { type: object }
                  by_event: { type: object }
                  by_service: { type: object }
                  by_status: { type: object }
                  by_network: { type: object }
                  by_reason_code: { type: object }
                  by_verdict: { type: object }
                  daily: { type: object }
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/preflight-check:
    post:
      tags: [Preflight]
      operationId: runPreflightCheck
      summary: Run a pre-send verification check
      description: >
        Compares expected and provided payout details, validates the destination,
        and returns one decision outcome plus a stable reason code.
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PreflightRequest'
            examples:
              clean_match:
                summary: Clean match
                value:
                  policy_profile: payout_strict
                  expected:
                    network: ethereum
                    asset: USDC
                    address: "0x59d779BED4dB1E734D3fDa3172d45bc3063eCD69"
                    memo: null
                  provided:
                    network: ethereum
                    asset: USDC
                    address: "0x59d779BED4dB1E734D3fDa3172d45bc3063eCD69"
                    memo: null
                  context:
                    reference_id: payout_102948
                    flow_type: payout_approval
      responses:
        '200':
          description: Verification result returned
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PreflightResponse'
              examples:
                safe:
                  summary: Safe payout approval
                  value:
                    ok: true
                    api_version: v1
                    request_id: ppf_req_01HZZZZZZZZZZZZZZZZZZZZZZ
                    checked_at: '2026-03-26T10:12:08Z'
                    verdict: SAFE
                    reason_code: OK
                    confidence: High
                    destination_type: personal_wallet
                    next_action: SAFE_TO_PROCEED
                    why_this_verdict: Network, asset, address, and memo checks matched. The destination classified as a personal wallet.
                    scope:
                      network: ethereum
                      asset: USDC
                    policy_profile: payout_strict
                    checks:
                      network_match: true
                      asset_match: true
                      address_match: true
                      expected_address_valid: true
                      provided_address_valid: true
                      memo_match: true
                block_network_mismatch:
                  summary: Wrong network
                  value:
                    ok: true
                    api_version: v1
                    request_id: ppf_req_01HYYYYYYYYYYYYYYYYYYYYYY
                    checked_at: '2026-03-26T10:15:41Z'
                    verdict: BLOCK
                    reason_code: NETWORK_MISMATCH
                    confidence: High
                    destination_type: unknown
                    next_action: BLOCK_AND_REVERIFY
                    why_this_verdict: The provided network does not match the approved payout route.
                    scope:
                      network: ethereum
                      asset: USDC
                    checks:
                      network_match: false
                      asset_match: true
                      address_match: true
                      expected_address_valid: true
                      provided_address_valid: true
                      memo_match: true
                reverify_deposit_route:
                  summary: Deposit-like destination
                  value:
                    ok: true
                    api_version: v1
                    request_id: ppf_req_01HXXXXXXXXXXXXXXXXXXXXXX
                    checked_at: '2026-03-26T10:17:02Z'
                    verdict: REVERIFY
                    reason_code: DESTINATION_REQUIRES_MEMO_OR_VENUE_CHECK
                    confidence: Medium
                    destination_type: exchange_like_deposit
                    next_action: RECHECK_MEMO_OR_TAG
                    why_this_verdict: The destination looks like a platform-style deposit route and may depend on venue and memo/tag.
                    scope:
                      network: ethereum
                      asset: USDT
                    checks:
                      network_match: true
                      asset_match: true
                      address_match: true
                      expected_address_valid: true
                      provided_address_valid: true
                      memo_match: true
        '400':
          description: Invalid request body or malformed input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          description: Rate limited
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Temporary service failure
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/verification-records:
    get:
      tags: [Records]
      operationId: listVerificationRecords
      summary: List verification records
      description: >
        Returns searchable verification history for the authenticated client.
        Only records available to the current API key are returned.
      security:
        - ApiKeyAuth: []
      parameters:
        - in: query
          name: service
          schema: { type: string, enum: [preflight-check, recovery-copilot] }
        - in: query
          name: network
          schema: { type: string }
        - in: query
          name: reason_code
          schema: { type: string }
        - in: query
          name: status
          schema: { type: string }
        - in: query
          name: request_id
          schema: { type: string }
        - in: query
          name: reference_id
          schema: { type: string }
        - in: query
          name: address
          schema: { type: string }
        - in: query
          name: tx_hash
          schema: { type: string }
        - in: query
          name: q
          schema: { type: string }
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
        - in: query
          name: offset
          schema: { type: integer, minimum: 0, default: 0 }
      responses:
        '200':
          description: Verification history page
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerificationRecordListResponse'
        '401':
          description: Missing or invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/verification-records/{record_id}:
    get:
      tags: [Records]
      operationId: getVerificationRecord
      summary: Get one verification record
      security:
        - ApiKeyAuth: []
      parameters:
        - in: path
          name: record_id
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Verification record detail with delivery history
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerificationRecordDetailResponse'
        '404':
          description: Record not found for this client
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
  /api/webhooks/ack:
    post:
      tags: [Webhooks]
      operationId: acknowledgeWebhookDelivery
      summary: Confirm webhook processing
      description: >
        Optional application-level acknowledgement after the receiver accepts
        a webhook event. HTTP 2xx from the receiver still counts as delivery.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookAckRequest'
      responses:
        '200':
          description: Delivery acknowledgement accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookAckResponse'
        '403':
          description: Invalid delivery token or unknown delivery
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

webhooks:
  verificationEvent:
    post:
      tags: [Webhooks]
      summary: Verification event delivery
      description: >
        Sent by PayeeProof to the client webhook URL when a verification record
        is ready. Signatures use HMAC-SHA256 over `${timestamp}.${raw_body}`.
      parameters:
        - in: header
          name: X-PayeeProof-Event
          required: true
          schema: { type: string, enum: [preflight_run, recovery_run] }
        - in: header
          name: X-PayeeProof-Delivery-ID
          required: true
          schema: { type: string }
        - in: header
          name: X-PayeeProof-Record-ID
          required: true
          schema: { type: string }
        - in: header
          name: X-PayeeProof-Timestamp
          required: true
          schema: { type: string, format: date-time }
        - in: header
          name: X-PayeeProof-Signature
          required: true
          schema: { type: string, example: sha256=<hex_digest> }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VerificationWebhookEvent'
      responses:
        '200':
          description: Delivery accepted by receiver

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
  schemas:
    Party:
      type: object
      additionalProperties: false
      required: [network, asset, address]
      properties:
        network:
          type: string
          examples: [ethereum, arbitrum, base, polygon, bsc, solana]
        asset:
          type: string
          examples: [USDC, USDT]
        address:
          type: string
          description: Destination address for the selected network
        memo:
          type:
            - string
            - 'null'
          description: Optional memo or destination tag
    RequestContext:
      type: object
      additionalProperties: false
      properties:
        reference_id:
          type:
            - string
            - 'null'
        flow_type:
          type:
            - string
            - 'null'
          examples: [payout_approval, treasury_review, otc_release]
    PolicyProfile:
      type: string
      enum: [standard, payout_strict, deposit_review, treasury_review]
    PreflightRequest:
      type: object
      additionalProperties: false
      required: [expected, provided]
      properties:
        policy_profile:
          $ref: '#/components/schemas/PolicyProfile'
        expected:
          $ref: '#/components/schemas/Party'
        provided:
          $ref: '#/components/schemas/Party'
        context:
          $ref: '#/components/schemas/RequestContext'
    Scope:
      type: object
      additionalProperties: false
      properties:
        network:
          type: string
        asset:
          type: string
    Checks:
      type: object
      additionalProperties: false
      properties:
        network_match:
          type: boolean
        asset_match:
          type: boolean
        address_match:
          type: boolean
        expected_address_valid:
          type: boolean
        provided_address_valid:
          type: boolean
        memo_match:
          type: boolean
    Verdict:
      type: string
      enum: [SAFE, BLOCK, REVERIFY, TEST_FIRST, UNAVAILABLE]
    ReasonCode:
      type: string
      enum:
        - OK
        - NETWORK_MISMATCH
        - ASSET_MISMATCH
        - ADDRESS_MISMATCH
        - INVALID_ADDRESS
        - ZERO_ADDRESS
        - MEMO_MISMATCH
        - DESTINATION_IS_CONTRACT_OR_APP
        - DESTINATION_IS_BRIDGE_ROUTER
        - DESTINATION_REQUIRES_MEMO_OR_VENUE_CHECK
        - DESTINATION_NOT_CLASSIFIED
        - DESTINATION_LOOKUP_UNAVAILABLE
        - UNSUPPORTED_NETWORK
        - UNSUPPORTED_ASSET
        - UNSUPPORTED_ASSET_OR_NETWORK
        - REQUEST_FAILED
        - RENDER_FAILED
    NextAction:
      type: string
      enum:
        - SAFE_TO_PROCEED
        - BLOCK_AND_REVERIFY
        - CONFIRM_DESTINATION
        - RECHECK_MEMO_OR_TAG
        - REVERIFY_DESTINATION
        - RETRY_OR_ESCALATE
    DestinationType:
      type: string
      enum:
        - personal_wallet
        - contract_or_app
        - exchange_like_deposit
        - bridge_router
        - unknown
        - unavailable
    Confidence:
      type: string
      enum: [High, Medium, Low]
    PreflightResponse:
      type: object
      additionalProperties: true
      required:
        - ok
        - api_version
        - request_id
        - checked_at
        - verdict
        - reason_code
        - confidence
        - destination_type
        - next_action
      properties:
        ok:
          type: boolean
        api_version:
          type: string
          example: v1
        request_id:
          type: string
        checked_at:
          type: string
          format: date-time
        verdict:
          $ref: '#/components/schemas/Verdict'
        reason_code:
          $ref: '#/components/schemas/ReasonCode'
        confidence:
          $ref: '#/components/schemas/Confidence'
        destination_type:
          $ref: '#/components/schemas/DestinationType'
        next_action:
          $ref: '#/components/schemas/NextAction'
        policy_profile:
          $ref: '#/components/schemas/PolicyProfile'
        why_this_verdict:
          type:
            - string
            - 'null'
        scope:
          $ref: '#/components/schemas/Scope'
        checks:
          $ref: '#/components/schemas/Checks'
        expected:
          $ref: '#/components/schemas/Party'
        provided:
          $ref: '#/components/schemas/Party'
    ErrorResponse:
      type: object
      additionalProperties: true
      required: [ok]
      properties:
        ok:
          type: boolean
          example: false
        error:
          type:
            - string
            - 'null'
        message:
          type:
            - string
            - 'null'
        request_id:
          type:
            - string
            - 'null'

    WebhookDeliverySummary:
      type: object
      additionalProperties: true
      properties:
        delivery_id: { type: string }
        event_name: { type: string }
        delivery_status: { type: string }
        ack_status: { type: string }
        attempt_count: { type: integer }
        max_attempts: { type: integer }
        next_attempt_at: { type: [string, 'null'], format: date-time }
        last_attempt_at: { type: [string, 'null'], format: date-time }
        delivered_at: { type: [string, 'null'], format: date-time }
        acknowledged_at: { type: [string, 'null'], format: date-time }
        last_response_code: { type: [integer, 'null'] }
        last_response_excerpt: { type: [string, 'null'] }
        last_error: { type: [string, 'null'] }
    VerificationRecordSummary:
      type: object
      additionalProperties: true
      properties:
        record_id: { type: string }
        created_at: { type: string, format: date-time }
        service: { type: string }
        network: { type: string }
        status: { type: string }
        reason_code: { type: string }
        request_id: { type: string }
        reference_id: { type: [string, 'null'] }
        address: { type: [string, 'null'] }
        tx_hash: { type: [string, 'null'] }
        webhook_delivery_status: { type: [string, 'null'] }
        webhook_attempt_count: { type: [integer, 'null'] }
        webhook_delivered_at: { type: [string, 'null'], format: date-time }
        webhook_acknowledged_at: { type: [string, 'null'], format: date-time }
    VerificationRecordListResponse:
      type: object
      additionalProperties: true
      properties:
        ok: { type: boolean }
        trace_id: { type: string }
        total: { type: integer }
        limit: { type: integer }
        offset: { type: integer }
        items:
          type: array
          items:
            $ref: '#/components/schemas/VerificationRecordSummary'
    VerificationRecord:
      type: object
      additionalProperties: true
      properties:
        record_id: { type: string }
        created_at: { type: string, format: date-time }
        request_id: { type: string }
        service: { type: string }
        event_name: { type: string }
        client_label: { type: string }
        access_mode: { type: string }
        source_ip: { type: [string, 'null'] }
        reference_id: { type: [string, 'null'] }
        network: { type: [string, 'null'] }
        status: { type: [string, 'null'] }
        verdict: { type: [string, 'null'] }
        reason_code: { type: [string, 'null'] }
        address: { type: [string, 'null'] }
        counterparty_address: { type: [string, 'null'] }
        tx_hash: { type: [string, 'null'] }
        payload:
          type: object
          additionalProperties: true
        response:
          type: object
          additionalProperties: true
        webhook_deliveries:
          type: array
          items:
            $ref: '#/components/schemas/WebhookDeliverySummary'
    VerificationRecordDetailResponse:
      type: object
      additionalProperties: true
      properties:
        ok: { type: boolean }
        trace_id: { type: string }
        record:
          $ref: '#/components/schemas/VerificationRecord'
    WebhookAckRequest:
      type: object
      additionalProperties: false
      required: [delivery_id, token]
      properties:
        delivery_id: { type: string }
        token: { type: string }
        status: { type: string, example: processed }
        detail: { type: [string, 'null'] }
    WebhookAckResponse:
      type: object
      additionalProperties: true
      properties:
        ok: { type: boolean }
        delivery_id: { type: string }
        status: { type: string }
        acknowledged_at: { type: string, format: date-time }
        trace_id: { type: string }
    VerificationWebhookEvent:
      type: object
      additionalProperties: true
      properties:
        delivery_id: { type: string }
        event: { type: string }
        record_id: { type: string }
        request_id: { type: string }
        created_at: { type: string, format: date-time }
        service: { type: string }
        network: { type: string }
        status: { type: string }
        reason_code: { type: [string, 'null'] }
        client_label: { type: string }
        verification:
          type: object
          additionalProperties: true
        ack:
          type: object
          additionalProperties: true
          properties:
            url: { type: string }
            delivery_id: { type: string }
            token: { type: string }
