asyncapi: 3.1.0
info:
  title: Patternode events
  version: 1.0.0-draft
  description: |
    The events Patternode sends to your HTTPS endpoint. Set the endpoint with
    `PATCH /v1/account` (`webhook_url`), or per job in `webhook_url`, and get the signing
    secret with `POST /v1/account/webhook_secret`.

    Every delivery is a POST with a JSON body, signed to the Standard Webhooks
    specification (https://www.standardwebhooks.com):

    - `webhook-id`: the message id, the same on every retry. Deduplicate on it.
    - `webhook-timestamp`: seconds since the epoch. Reject deliveries more than 5 minutes
      from your clock.
    - `webhook-signature`: `v1,<base64 HMAC-SHA256 of "id.timestamp.body">`. During a
      secret rotation there are two, space separated; accept either.

    Answer with any 2xx within 10 seconds. Anything else is retried, with backoff, three
    times. Do the work after you answer, not before.

    Events tell you about work that finishes after the call that started it (jobs) and
    about your account. Segments are never sent as events: every segmentation answers in
    its own response, and Patternode keeps nothing to send later.

    The same events reach you two ways, with the same `id`, `type` and `data`:

    | Channel | Plans | Delivery |
    |---|---|---|
    | `webhook`, an HTTPS POST to your endpoint | Production and above | At least once, retried 24 hours (72 on Mission Critical) |
    | Your event bus (Amazon EventBridge partner source, Kafka topic) | Mission Critical, Dedicated | At least once, as CloudEvents 1.0 structured JSON |

    Every event has a stable `id`: deduplicate on it. Each message carries `x-status` with the same meaning as in
    the OpenAPI document.
  contact:
    name: Patternode partner support
    email: partners@patternode.com
  license:
    name: Apache 2.0
    url: https://www.apache.org/licenses/LICENSE-2.0
defaultContentType: application/json

servers:
  partner:
    host: hooks.example-monitoring.com
    pathname: /patternode
    protocol: https
    description: Your endpoint. The host and path are yours; this is an example.
    security:
      - $ref: "#/components/securitySchemes/standardWebhooks"

channels:
  webhook:
    address: /patternode
    title: Your webhook endpoint
    messages:
      jobSucceeded: { $ref: "#/components/messages/jobSucceeded" }
      jobFailed: { $ref: "#/components/messages/jobFailed" }
      keyExpiring: { $ref: "#/components/messages/keyExpiring" }
      balanceLow: { $ref: "#/components/messages/balanceLow" }

operations:
  receiveJobEvents:
    action: send
    title: Patternode tells you a job finished
    channel: { $ref: "#/channels/webhook" }
    messages:
      - $ref: "#/channels/webhook/messages/jobSucceeded"
      - $ref: "#/channels/webhook/messages/jobFailed"
  receiveAccountEvents:
    action: send
    title: Patternode tells you about your keys and balance
    channel: { $ref: "#/channels/webhook" }
    messages:
      - $ref: "#/channels/webhook/messages/keyExpiring"
      - $ref: "#/channels/webhook/messages/balanceLow"

components:
  securitySchemes:
    bearer:
      type: http
      scheme: bearer
      description: An API key or an OAuth access token, as for the REST API.
    standardWebhooks:
      type: httpApiKey
      name: webhook-signature
      in: header
      description: HMAC-SHA256 with your `whsec_` secret over `webhook-id.webhook-timestamp.body`.

  messageTraits:
    signed:
      headers:
        type: object
        required: [webhook-id, webhook-timestamp, webhook-signature]
        properties:
          webhook-id: { type: string, examples: [msg_job_01JBX8F3TA] }
          webhook-timestamp: { type: string, examples: ["1730254012"] }
          webhook-signature: { type: string, examples: ["v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4="] }
          user-agent: { type: string, examples: [patternode-webhooks/1] }

  schemas:
    EventEnvelope:
      type: object
      required: [id, type, created_at, data]
      properties:
        id: { type: string, description: Stable across retries and channels. Deduplicate on it. }
        type: { type: string }
        created_at: { type: string, format: date-time, description: Microsecond precision. }
        data: { type: object }

  messages:
    jobSucceeded:
      name: segmentation.job.succeeded
      title: A segmentation job succeeded
      summary: The result is in your storage, at the result_url you gave.
      x-status: available
      traits: [{ $ref: "#/components/messageTraits/signed" }]
      payload:
        type: object
        required: [id, type, data]
        properties:
          id: { type: string }
          type: { const: segmentation.job.succeeded }
          created_at: { type: string, format: date-time }
          data: { $ref: "./openapi.yaml#/components/schemas/Job" }
      examples:
        - payload:
            id: evt_01JBX8F3TB
            type: segmentation.job.succeeded
            created_at: "2026-10-30T02:06:52Z"
            data:
              object: segmentation_job
              id: job_01JBX8F3TA
              status: succeeded
              series: exu/7f3a9c/1b02e4/export-kw
              created_at: "2026-10-30T02:06:10Z"
              finished_at: "2026-10-30T02:06:51Z"
              result_written: true
              usage: { samples_billed: 2190000, cost_usd: 4.38 }
              error: null

    jobFailed:
      name: segmentation.job.failed
      title: A segmentation job failed
      summary: Not charged. data.error says why; error.details.problems lists bad rows.
      x-status: available
      traits: [{ $ref: "#/components/messageTraits/signed" }]
      payload:
        type: object
        required: [id, type, data]
        properties:
          id: { type: string }
          type: { const: segmentation.job.failed }
          created_at: { type: string, format: date-time }
          data: { $ref: "./openapi.yaml#/components/schemas/Job" }

    keyExpiring:
      name: key.expiring
      title: A key expires within 7 days
      x-status: planned
      traits: [{ $ref: "#/components/messageTraits/signed" }]
      payload:
        type: object
        required: [type, data]
        properties:
          type: { const: key.expiring }
          created_at: { type: string, format: date-time }
          data: { $ref: "./openapi.yaml#/components/schemas/Key" }

    balanceLow:
      name: account.balance_low
      title: The credit balance fell below your threshold
      x-status: planned
      traits: [{ $ref: "#/components/messageTraits/signed" }]
      payload:
        type: object
        required: [type, data]
        properties:
          type: { const: account.balance_low }
          created_at: { type: string, format: date-time }
          data: { $ref: "./openapi.yaml#/components/schemas/Account" }
