asyncapi: 3.0.0
info:
  title: Boring Order Tracker integration webhooks
  version: 1.0.0
  description: >-
    Optional outbound notifications for approved integrations. Pull sync remains
    canonical. Every delivery is idempotent by event ID and signed with the BOT
    Ed25519 HTTP Message Signature key; revocation events contain no rich data.
    Public verification keys are published at
    https://integration.boringordertracker.com/.well-known/bot-integration-webhook-jwks.json.
    The unsigned endpoint-ownership challenge is a separate setup exchange and
    is documented in webhooks.md, not an event on this channel.
defaultContentType: application/json
servers:
  botProduction:
    host: integration-defined-public-https-endpoint
    protocol: https
    description: >-
      The public HTTPS endpoint registered and verified in the operator portal.
      Redirects and private/reserved destinations are rejected.
channels:
  operatorWebhook:
    address: /
    messages:
      connectionCreated:
        $ref: '#/components/messages/connectionCreated'
      resourceGranted:
        $ref: '#/components/messages/resourceGranted'
      resourceUpdated:
        $ref: '#/components/messages/resourceUpdated'
      resourceRevoked:
        $ref: '#/components/messages/resourceRevoked'
      connectionRevoked:
        $ref: '#/components/messages/connectionRevoked'
      statisticsUpdated:
        $ref: '#/components/messages/statisticsUpdated'
      integrationSuspended:
        $ref: '#/components/messages/integrationSuspended'
operations:
  receiveBotEvent:
    action: receive
    channel:
      $ref: '#/channels/operatorWebhook'
    summary: Receive an idempotent BOT integration event.
    description: >-
      Verify Content-Digest and Signature-Input/Signature before parsing the
      body. Apply the receiver's documented bounded freshness policy to the
      signed created parameter and replay-safe deduplicate the event ID. BOT
      does not publish a numeric receiver clock-skew tolerance. Return any 2xx
      only after the event is durably accepted.
components:
  messageTraits:
    signed:
      headers:
        type: object
        additionalProperties: false
        required:
          - content-type
          - content-digest
          - signature-input
          - signature
          - x-bot-event-id
          - x-bot-event-timestamp
          - user-agent
        properties:
          content-type:
            const: application/json
          content-digest:
            type: string
            pattern: '^sha-256=:[A-Za-z0-9+/]+={0,2}:$'
          signature-input:
            type: string
            description: >-
              The bot label covers @method, @target-uri, content-digest,
              x-bot-event-id and x-bot-event-timestamp in exactly this order.
              The created value is generated for each delivery attempt.
            pattern: '^bot=\("@method" "@target-uri" "content-digest" "x-bot-event-id" "x-bot-event-timestamp"\);created=[0-9]+;keyid="[A-Za-z0-9_.:-]{1,128}";alg="ed25519"$'
          signature:
            type: string
            pattern: '^bot=:[A-Za-z0-9+/]+={0,2}:$'
          x-bot-event-id:
            type: string
            minLength: 12
            maxLength: 128
          x-bot-event-timestamp:
            type: string
            format: date-time
            description: >-
              Time the event occurred. It may be older after retries and is not
              the sole delivery-freshness input.
          user-agent:
            const: BoringOrderTracker-Webhook/1.0
  schemas:
    envelope:
      type: object
      additionalProperties: false
      required: [specversion, id, type, source, time, environment, data]
      properties:
        specversion:
          const: '1.0'
        id:
          type: string
          minLength: 12
          maxLength: 128
        type:
          type: string
          enum:
            - connection.created
            - resource.granted
            - resource.updated
            - resource.revoked
            - connection.revoked
            - statistics.updated
            - integration.suspended
        source:
          const: https://api.integration.boringordertracker.com
        time:
          type: string
          format: date-time
        environment:
          type: string
          enum: [sandbox, production]
        data:
          type: object
          additionalProperties: true
    opaqueId:
      type: string
      pattern: '^[A-Za-z0-9_-]{8,80}$'
    resourceReference:
      type: object
      additionalProperties: false
      required: [id, type]
      properties:
        id:
          type: string
          pattern: '^(ord|veh)_[A-Za-z0-9_-]+$'
        type:
          type: string
          enum: [order, vehicle]
    connectionData:
      type: object
      additionalProperties: false
      required: [connection_id]
      properties:
        connection_id: {$ref: '#/components/schemas/opaqueId'}
    resourceData:
      type: object
      additionalProperties: false
      required: [connection_id, resource]
      properties:
        connection_id: {$ref: '#/components/schemas/opaqueId'}
        resource: {$ref: '#/components/schemas/resourceReference'}
        snapshot:
          description: >-
            Present only for full_payload resource.granted/resource.updated.
            It is the current grant- and allowlist-filtered snapshot, never a
            history page.
          anyOf:
            - {$ref: '../schemas/order-response.json#/$defs/order'}
            - {$ref: '../schemas/vehicle-response.json#/properties/data'}
        history_complete:
          type: boolean
          description: Present with snapshot in full_payload mode.
      dependencies:
        snapshot: [history_complete]
        history_complete: [snapshot]
    resourceRevocationData:
      type: object
      additionalProperties: false
      required: [connection_id, resource, revocation_id, deletion_deadline_at]
      properties:
        connection_id: {$ref: '#/components/schemas/opaqueId'}
        resource: {$ref: '#/components/schemas/resourceReference'}
        revocation_id: {$ref: '#/components/schemas/opaqueId'}
        deletion_deadline_at: {type: string, format: date-time}
    connectionRevocationData:
      type: object
      additionalProperties: false
      required: [connection_id, revocation_id, deletion_deadline_at]
      properties:
        connection_id: {$ref: '#/components/schemas/opaqueId'}
        revocation_id: {$ref: '#/components/schemas/opaqueId'}
        deletion_deadline_at: {type: string, format: date-time}
    statisticsData:
      type: object
      additionalProperties: false
      required: [generation_id]
      properties:
        generation_id: {$ref: '#/components/schemas/opaqueId'}
    emptyData:
      type: object
      maxProperties: 0
    connectionCreatedEnvelope:
      allOf:
        - {$ref: '#/components/schemas/envelope'}
        - type: object
          properties:
            type: {const: connection.created}
            data: {$ref: '#/components/schemas/connectionData'}
    resourceGrantedEnvelope:
      allOf:
        - {$ref: '#/components/schemas/envelope'}
        - type: object
          properties:
            type: {const: resource.granted}
            data: {$ref: '#/components/schemas/resourceData'}
    resourceUpdatedEnvelope:
      allOf:
        - {$ref: '#/components/schemas/envelope'}
        - type: object
          properties:
            type: {const: resource.updated}
            data: {$ref: '#/components/schemas/resourceData'}
    resourceRevokedEnvelope:
      allOf:
        - {$ref: '#/components/schemas/envelope'}
        - type: object
          properties:
            type: {const: resource.revoked}
            data: {$ref: '#/components/schemas/resourceRevocationData'}
    connectionRevokedEnvelope:
      allOf:
        - {$ref: '#/components/schemas/envelope'}
        - type: object
          properties:
            type: {const: connection.revoked}
            data: {$ref: '#/components/schemas/connectionRevocationData'}
    statisticsUpdatedEnvelope:
      allOf:
        - {$ref: '#/components/schemas/envelope'}
        - type: object
          properties:
            type: {const: statistics.updated}
            data: {$ref: '#/components/schemas/statisticsData'}
    integrationSuspendedEnvelope:
      allOf:
        - {$ref: '#/components/schemas/envelope'}
        - type: object
          properties:
            type: {const: integration.suspended}
            data: {$ref: '#/components/schemas/emptyData'}
  messages:
    connectionCreated:
      name: connection.created
      traits: [{$ref: '#/components/messageTraits/signed'}]
      payload: {$ref: '#/components/schemas/connectionCreatedEnvelope'}
    resourceGranted:
      name: resource.granted
      traits: [{$ref: '#/components/messageTraits/signed'}]
      payload: {$ref: '#/components/schemas/resourceGrantedEnvelope'}
    resourceUpdated:
      name: resource.updated
      traits: [{$ref: '#/components/messageTraits/signed'}]
      payload: {$ref: '#/components/schemas/resourceUpdatedEnvelope'}
    resourceRevoked:
      name: resource.revoked
      description: Minimal deletion notice; never contains a snapshot or history.
      traits: [{$ref: '#/components/messageTraits/signed'}]
      payload: {$ref: '#/components/schemas/resourceRevokedEnvelope'}
    connectionRevoked:
      name: connection.revoked
      description: Minimal deletion notice for the entire connection.
      traits: [{$ref: '#/components/messageTraits/signed'}]
      payload: {$ref: '#/components/schemas/connectionRevokedEnvelope'}
    statisticsUpdated:
      name: statistics.updated
      traits: [{$ref: '#/components/messageTraits/signed'}]
      payload: {$ref: '#/components/schemas/statisticsUpdatedEnvelope'}
    integrationSuspended:
      name: integration.suspended
      traits: [{$ref: '#/components/messageTraits/signed'}]
      payload: {$ref: '#/components/schemas/integrationSuspendedEnvelope'}
