# ─────────────────────────────────────────────────────────────────────────
# GENERATED FILE — DO NOT EDIT BY HAND.
#
# Regenerate with:  node scripts/generate-webhook-spec.js
# Derived from THREE sources, because no single one of them is the contract:
#   services/partner/WebhookDispatcher.js          EVENTS      (producer catalogue)
#   controllers/partner/submodules/partner-webhooks.js  validEvents (subscribable)
#   services/partner/screenGroupEventDispatcher.js EVENT       (queued => retried)
# tests/unit/webhookEventSpecParity.test.js fails if this file drifts by a byte.
#
# OpenAPI 3.1.0 is REQUIRED here: the top-level `webhooks:` keyword does not
# exist in 3.0.x, and redocly rejects it there (`struct` is an error rule).
# ─────────────────────────────────────────────────────────────────────────
openapi: 3.1.0
info:
  title: Trillboards Partner Webhooks
  version: 1.0.0
  contact:
    name: Trillboards Developer Support
    email: developers@trillboards.com
  description: |
    Trillboards POSTs these events to any active webhook subscription whose
    `events` list includes the event type. **22** event types are
    subscribable, plus the `test` delivery you can trigger yourself.

    Subscriptions are managed through the Partner API — `POST /v1/partner/webhooks`,
    `PATCH /v1/partner/webhooks/{webhookId}`, `POST /v1/partner/webhooks/{webhookId}/test`
    and `GET /v1/partner/webhooks/{webhookId}/deliveries`. This document describes
    what we send you; that one describes how to subscribe.

    ## ⚠️ Delivery guarantees differ per event. Read this before you rely on one.

    There are two delivery paths, and they do not offer the same thing. The
    difference is not configurable and you cannot detect it at runtime — it is a
    property of the event type, listed below.

    | | Queued (16 events) | Synchronous (7 events) |
    |---|---|---|
    | Retries | **3 attempts**, exponential backoff from **60 seconds** | **None — at most once** |
    | In `GET /webhooks/{id}/deliveries` | Yes | **No — no audit row is written** |
    | Signature header | `t=,v0=,v1=` (compound) | `sha256=<hex>` (legacy) |
    | Envelope `id` | Present | Absent |

    **Queued:** `creative.rewards_ready`, `audience.spike`, `audience.demographics_update`, `audience.purchase_intent`, `audience.venue_busy`, `venue.traffic_summary`, `sensing.threshold_crossed`, `screen_group.created`, `screen_group.updated`, `screen_group.deleted`, `screen_group.member.added`, `screen_group.member.removed`, `screen_group.content_policy.updated`, `screen_group.sensing_config.updated`, `fleet_command.completed`, `fleet_command.failed`.

    **Every other event on this page is synchronous.** A non-2xx response or a
    timeout on a synchronous event means the notification is **permanently lost** —
    nothing retries it and nothing records that it failed. If you need durability
    for one of those events, return `2xx` immediately, persist the body yourself,
    and process it after; and reconcile from the REST API rather than treating the
    webhook as a ledger.

    > Separately, 8 further event types exist in the server's internal
    > catalogue but are **rejected** by `POST`/`PATCH /v1/partner/webhooks` with a 400,
    > so they cannot be subscribed to and are deliberately not documented as webhooks
    > here: `screen.content_preferences_changed`, `screen.settings_updated`, `screen.display_settings_changed`, `screen.programmatic_settings_changed`, `programmatic.ad_started`, `programmatic.ad_ended`, `programmatic.no_fill`, `programmatic.error`.

    ## Delivery headers

    ```
    Content-Type:               application/json
    User-Agent:                 Trillboards-Webhook/1.0
    X-Trillboards-Event:        <event type>
    X-Trillboards-Timestamp:    <unix seconds>
    X-Trillboards-Signature:    <see below>
    X-Trillboards-Webhook-Id:   <your subscription id>
    ```

    ## Signature verification

    Every delivery is signed with HMAC-SHA256 using your subscription secret.

    > **Two signature header formats are live. A verifier must accept both.**
    > Which one you receive follows the delivery path in the table above.

    **Compound (Stripe-style)** — the queued path:

    ```
    X-Trillboards-Signature: t=<unix_seconds>,v0=<hex>,v1=<hex>
    ```

    - `v1 = HMAC-SHA256(secret, "<timestamp>.<raw body>")` — canonical, replay-protected. Prefer it.
    - `v0 = HMAC-SHA256(secret, "<raw body>")` — legacy body-only digest, emitted during the migration window.

    **Legacy single-value** — the synchronous path:

    ```
    X-Trillboards-Signature: sha256=<hex>
    ```

    where `<hex> = HMAC-SHA256(secret, "<timestamp>.<raw body>")` — the same
    computation as `v1`, using `X-Trillboards-Timestamp` for the timestamp.

    To verify:

    1. Read the raw request body **before any JSON parsing**. Sign the exact bytes received.
    2. If the header contains `t=`, parse `t`/`v0`/`v1`; otherwise take the value after `sha256=`
       and read the timestamp from `X-Trillboards-Timestamp`.
    3. Reject if `|now - timestamp| > 300` seconds (the default tolerance; configurable per partner).
    4. Compare using a **constant-time** comparison. Prefer `v1`, fall back to `v0`.

    A worked, copy-pasteable implementation lives at
    `https://api.trillboards.com/docs/integrations/webhook-signature-verification.md`,
    and the official client exposes `Webhooks.constructEvent(body, headers, secret)`.

    ## Retries (queued events only)

    A delivery succeeds on any 2xx. For a **queued** event anything else — including
    a timeout — is retried by the queue with **3 total attempts** and exponential
    backoff starting at **60 seconds**, and each outcome is readable via
    `GET /v1/partner/webhooks/{webhookId}/deliveries`.

    > The audit row's `next_retry_at` is computed on a different curve
    > (`5^(n-1)` minutes: 1 min, 5 min, 25 min) from the queue's own backoff. The
    > queue schedule is what actually governs redelivery; treat `next_retry_at` as
    > an indication, not a promise.

    Deliveries are **not ordered** and may arrive more than once. Make your handler
    idempotent.

    ## Envelope

    The 22 subscribable events share `EventEnvelope`; only queued events carry its
    optional `id`. The `test` delivery does **not** use the envelope at all — see
    `TestEvent`. There are 23 documented deliveries in total.

servers:
  - url: https://api.trillboards.com
    description: Production

tags:
  - name: Webhook Events
    description: |
      Events Trillboards POSTs to a subscriber URL. Manage subscriptions with
      the `/v1/partner/webhooks` endpoints documented in the Partner API.

# No request paths: this document describes only outbound webhook deliveries.
paths: {}

webhooks:
  device.online:
    post:
      tags: [Webhook Events]
      operationId: onDeviceOnline
      summary: Device online
      security: []
      description: |
        A registered device transitioned to online.

        Sent with `X-Trillboards-Event: device.online`.

        Delivered **synchronously, at most once**: a failure is NOT retried and does NOT appear in your delivery history. Signed with the legacy `sha256=<hex>` header.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/EventEnvelope'
                - type: object
                  properties:
                    event:
                      const: "device.online"
                    data:
                      type: object
                      description: |
                        Fields observed on this event's dispatch call site. Additional fields may
                        be added over time — parse defensively and ignore unknown keys.
                      properties:
                        device_id: {}
                        fingerprint: {}
                        external_device_id: {}
                        name: {}
                        screen_id: {}
                        timestamp: {}
      responses:
        '2XX':
          description: |
            Any 2xx marks the delivery successful. Return one promptly and do your own
            processing asynchronously — the request times out server-side.
        '4XX':
          description: |
            Treated as a failed delivery. This event is NOT retried — the notification is
            lost. Return 2xx and process asynchronously if you need it.
        '5XX':
          description: Treated as a failed delivery. NOT retried.
  device.offline:
    post:
      tags: [Webhook Events]
      operationId: onDeviceOffline
      summary: Device offline
      security: []
      description: |
        A registered device stopped reporting and is now considered offline.

        Sent with `X-Trillboards-Event: device.offline`.

        Delivered **synchronously, at most once**: a failure is NOT retried and does NOT appear in your delivery history. Signed with the legacy `sha256=<hex>` header.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/EventEnvelope'
                - type: object
                  properties:
                    event:
                      const: "device.offline"
                    data:
                      type: object
                      description: |
                        Fields observed on this event's dispatch call site. Additional fields may
                        be added over time — parse defensively and ignore unknown keys.
                      properties:
                        device_id: {}
                        fingerprint: {}
                        external_device_id: {}
                        name: {}
                        screen_id: {}
                        last_seen_at: {}
                        timestamp: {}
      responses:
        '2XX':
          description: |
            Any 2xx marks the delivery successful. Return one promptly and do your own
            processing asynchronously — the request times out server-side.
        '4XX':
          description: |
            Treated as a failed delivery. This event is NOT retried — the notification is
            lost. Return 2xx and process asynchronously if you need it.
        '5XX':
          description: Treated as a failed delivery. NOT retried.
  device.requirements_updated:
    post:
      tags: [Webhook Events]
      operationId: onDeviceRequirementsUpdated
      summary: Device requirements updated
      security: []
      description: |
        The playback or hardware requirements for a device changed.

        Sent with `X-Trillboards-Event: device.requirements_updated`.

        Delivered **synchronously, at most once**: a failure is NOT retried and does NOT appear in your delivery history. Signed with the legacy `sha256=<hex>` header.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/EventEnvelope'
                - type: object
                  properties:
                    event:
                      const: "device.requirements_updated"
                    data:
                      type: object
                      description: |
                        Fields observed on this event's dispatch call site. Additional fields may
                        be added over time — parse defensively and ignore unknown keys.
                      properties:
                        device_id: {}
                        external_device_id: {}
                        screen_id: {}
                        name: {}
                        trigger: {}
                        timestamp: {}
      responses:
        '2XX':
          description: |
            Any 2xx marks the delivery successful. Return one promptly and do your own
            processing asynchronously — the request times out server-side.
        '4XX':
          description: |
            Treated as a failed delivery. This event is NOT retried — the notification is
            lost. Return 2xx and process asynchronously if you need it.
        '5XX':
          description: Treated as a failed delivery. NOT retried.
  impression.recorded:
    post:
      tags: [Webhook Events]
      operationId: onImpressionRecorded
      summary: Impression recorded
      security: []
      description: |
        An impression was recorded against one of your devices.

        Sent with `X-Trillboards-Event: impression.recorded`.

        Delivered **synchronously, at most once**: a failure is NOT retried and does NOT appear in your delivery history. Signed with the legacy `sha256=<hex>` header.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/EventEnvelope'
                - type: object
                  properties:
                    event:
                      const: "impression.recorded"
                    data:
                      type: object
                      description: |
                        Fields observed on this event's dispatch call site. Additional fields may
                        be added over time — parse defensively and ignore unknown keys.
                      properties:
                        impression_id: {}
                        ad_id: {}
                        device_id: {}
                        device_fingerprint: {}
                        duration_ms: {}
                        completed: {}
                        timestamp: {}
      responses:
        '2XX':
          description: |
            Any 2xx marks the delivery successful. Return one promptly and do your own
            processing asynchronously — the request times out server-side.
        '4XX':
          description: |
            Treated as a failed delivery. This event is NOT retried — the notification is
            lost. Return 2xx and process asynchronously if you need it.
        '5XX':
          description: Treated as a failed delivery. NOT retried.
  campaign.allocated:
    post:
      tags: [Webhook Events]
      operationId: onCampaignAllocated
      summary: Campaign allocated
      security: []
      description: |
        A campaign was allocated to one of your devices.

        Sent with `X-Trillboards-Event: campaign.allocated`.

        Delivered **synchronously, at most once**: a failure is NOT retried and does NOT appear in your delivery history. Signed with the legacy `sha256=<hex>` header.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/EventEnvelope'
                - type: object
                  properties:
                    event:
                      const: "campaign.allocated"
                    data:
                      type: object
                      description: |
                        Fields observed on this event's dispatch call site. Additional fields may
                        be added over time — parse defensively and ignore unknown keys.
                      properties:
                        allocation_id: {}
                        campaign_id: {}
                        device_id: {}
                        device_fingerprint: {}
                        start_date: {}
                        end_date: {}
                        impressions_allocated: {}
                        timestamp: {}
      responses:
        '2XX':
          description: |
            Any 2xx marks the delivery successful. Return one promptly and do your own
            processing asynchronously — the request times out server-side.
        '4XX':
          description: |
            Treated as a failed delivery. This event is NOT retried — the notification is
            lost. Return 2xx and process asynchronously if you need it.
        '5XX':
          description: Treated as a failed delivery. NOT retried.
  creative.rewards_ready:
    post:
      tags: [Webhook Events]
      operationId: onCreativeRewardsReady
      summary: Creative rewards ready
      security: []
      description: |
        Native play refresh has measured states ready for an owned creative experiment. This queued hint contains no reward rows. Follow reward_reader_url with a caller-owned idempotency key; the canonical reward page is separately metered and uses latest-state rewind and revision deduplication.

        Sent with `X-Trillboards-Event: creative.rewards_ready`.

        Delivered through the **queue**: retried, and recorded in your delivery history. Signed with the compound `t=,v0=,v1=` header.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/EventEnvelope'
                - type: object
                  properties:
                    event:
                      const: "creative.rewards_ready"
      responses:
        '2XX':
          description: |
            Any 2xx marks the delivery successful. Return one promptly and do your own
            processing asynchronously — the request times out server-side.
        '4XX':
          description: |
            Treated as a failed delivery and retried. A 4xx is NOT interpreted as "stop
            sending"; disable the subscription to stop delivery.
        '5XX':
          description: Treated as a failed delivery and retried.
  payout.processed:
    post:
      tags: [Webhook Events]
      operationId: onPayoutProcessed
      summary: Payout processed
      security: []
      description: |
        A payout was processed for your account.

        Sent with `X-Trillboards-Event: payout.processed`.

        Delivered **synchronously, at most once**: a failure is NOT retried and does NOT appear in your delivery history. Signed with the legacy `sha256=<hex>` header.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/EventEnvelope'
                - type: object
                  properties:
                    event:
                      const: "payout.processed"
                    data:
                      type: object
                      description: |
                        Fields observed on this event's dispatch call site. Additional fields may
                        be added over time — parse defensively and ignore unknown keys.
                      properties:
                        payout_id: {}
                        amount_cents: {}
                        currency: {}
                        period_start: {}
                        period_end: {}
                        status: {}
                        timestamp: {}
      responses:
        '2XX':
          description: |
            Any 2xx marks the delivery successful. Return one promptly and do your own
            processing asynchronously — the request times out server-side.
        '4XX':
          description: |
            Treated as a failed delivery. This event is NOT retried — the notification is
            lost. Return 2xx and process asynchronously if you need it.
        '5XX':
          description: Treated as a failed delivery. NOT retried.
  audience.spike:
    post:
      tags: [Webhook Events]
      operationId: onAudienceSpike
      summary: Audience spike
      security: []
      description: |
        A Sense Anything Route on a viewer-count signal crossed its threshold.

        Sent with `X-Trillboards-Event: audience.spike`.

        Delivered through the **queue**: retried, and recorded in your delivery history. Signed with the compound `t=,v0=,v1=` header.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: |
                The route-fire body is sent AS the request body — there is no envelope and
                no `data` wrapper. Additional fields may be added over time — parse
                defensively and ignore unknown keys.
              properties:
                route_id: {}
                route_name: {}
                owner_kind: {}
                screen_id: {}
                field: {}
                op: {}
                threshold: {}
                observed: {}
                fired_at: {}
      responses:
        '2XX':
          description: |
            Any 2xx marks the delivery successful. Return one promptly and do your own
            processing asynchronously — the request times out server-side.
        '4XX':
          description: |
            Treated as a failed delivery and retried. A 4xx is NOT interpreted as "stop
            sending"; disable the subscription to stop delivery.
        '5XX':
          description: Treated as a failed delivery and retried.
  audience.demographics_update:
    post:
      tags: [Webhook Events]
      operationId: onAudienceDemographicsUpdate
      summary: Audience demographics update
      security: []
      description: |
        Updated audience demographics are available for a screen.

        Sent with `X-Trillboards-Event: audience.demographics_update`.

        Delivered through the **queue**: retried, and recorded in your delivery history. Signed with the compound `t=,v0=,v1=` header.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: |
                The route-fire body is sent AS the request body — there is no envelope and
                no `data` wrapper. Additional fields may be added over time — parse
                defensively and ignore unknown keys.
              properties:
                route_id: {}
                route_name: {}
                owner_kind: {}
                screen_id: {}
                field: {}
                op: {}
                threshold: {}
                observed: {}
                fired_at: {}
      responses:
        '2XX':
          description: |
            Any 2xx marks the delivery successful. Return one promptly and do your own
            processing asynchronously — the request times out server-side.
        '4XX':
          description: |
            Treated as a failed delivery and retried. A 4xx is NOT interpreted as "stop
            sending"; disable the subscription to stop delivery.
        '5XX':
          description: Treated as a failed delivery and retried.
  audience.purchase_intent:
    post:
      tags: [Webhook Events]
      operationId: onAudiencePurchaseIntent
      summary: Audience purchase intent
      security: []
      description: |
        A purchase-intent signal crossed its threshold.

        Sent with `X-Trillboards-Event: audience.purchase_intent`.

        Delivered through the **queue**: retried, and recorded in your delivery history. Signed with the compound `t=,v0=,v1=` header.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: |
                The route-fire body is sent AS the request body — there is no envelope and
                no `data` wrapper. Additional fields may be added over time — parse
                defensively and ignore unknown keys.
              properties:
                route_id: {}
                route_name: {}
                owner_kind: {}
                screen_id: {}
                field: {}
                op: {}
                threshold: {}
                observed: {}
                fired_at: {}
      responses:
        '2XX':
          description: |
            Any 2xx marks the delivery successful. Return one promptly and do your own
            processing asynchronously — the request times out server-side.
        '4XX':
          description: |
            Treated as a failed delivery and retried. A 4xx is NOT interpreted as "stop
            sending"; disable the subscription to stop delivery.
        '5XX':
          description: Treated as a failed delivery and retried.
  audience.venue_busy:
    post:
      tags: [Webhook Events]
      operationId: onAudienceVenueBusy
      summary: Audience venue busy
      security: []
      description: |
        A venue-busy signal crossed its threshold.

        Sent with `X-Trillboards-Event: audience.venue_busy`.

        Delivered through the **queue**: retried, and recorded in your delivery history. Signed with the compound `t=,v0=,v1=` header.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: |
                The route-fire body is sent AS the request body — there is no envelope and
                no `data` wrapper. Additional fields may be added over time — parse
                defensively and ignore unknown keys.
              properties:
                route_id: {}
                route_name: {}
                owner_kind: {}
                screen_id: {}
                field: {}
                op: {}
                threshold: {}
                observed: {}
                fired_at: {}
      responses:
        '2XX':
          description: |
            Any 2xx marks the delivery successful. Return one promptly and do your own
            processing asynchronously — the request times out server-side.
        '4XX':
          description: |
            Treated as a failed delivery and retried. A 4xx is NOT interpreted as "stop
            sending"; disable the subscription to stop delivery.
        '5XX':
          description: Treated as a failed delivery and retried.
  venue.traffic_summary:
    post:
      tags: [Webhook Events]
      operationId: onVenueTrafficSummary
      summary: Venue traffic summary
      security: []
      description: |
        A periodic venue traffic summary is available.

        Sent with `X-Trillboards-Event: venue.traffic_summary`.

        Delivered through the **queue**: retried, and recorded in your delivery history. Signed with the compound `t=,v0=,v1=` header.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: |
                The route-fire body is sent AS the request body — there is no envelope and
                no `data` wrapper. Additional fields may be added over time — parse
                defensively and ignore unknown keys.
              properties:
                route_id: {}
                route_name: {}
                owner_kind: {}
                screen_id: {}
                field: {}
                op: {}
                threshold: {}
                observed: {}
                fired_at: {}
      responses:
        '2XX':
          description: |
            Any 2xx marks the delivery successful. Return one promptly and do your own
            processing asynchronously — the request times out server-side.
        '4XX':
          description: |
            Treated as a failed delivery and retried. A 4xx is NOT interpreted as "stop
            sending"; disable the subscription to stop delivery.
        '5XX':
          description: Treated as a failed delivery and retried.
  sensing.threshold_crossed:
    post:
      tags: [Webhook Events]
      operationId: onSensingThresholdCrossed
      summary: Sensing threshold crossed
      security: []
      description: |
        A Sense Anything Route's threshold was crossed on one of your screens (custom sensed field).

        Sent with `X-Trillboards-Event: sensing.threshold_crossed`.

        Delivered through the **queue**: retried, and recorded in your delivery history. Signed with the compound `t=,v0=,v1=` header.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: |
                The route-fire body is sent AS the request body — there is no envelope and
                no `data` wrapper. Additional fields may be added over time — parse
                defensively and ignore unknown keys.
              properties:
                route_id: {}
                route_name: {}
                owner_kind: {}
                screen_id: {}
                field: {}
                op: {}
                threshold: {}
                observed: {}
                fired_at: {}
      responses:
        '2XX':
          description: |
            Any 2xx marks the delivery successful. Return one promptly and do your own
            processing asynchronously — the request times out server-side.
        '4XX':
          description: |
            Treated as a failed delivery and retried. A 4xx is NOT interpreted as "stop
            sending"; disable the subscription to stop delivery.
        '5XX':
          description: Treated as a failed delivery and retried.
  screen_group.created:
    post:
      tags: [Webhook Events]
      operationId: onScreenGroupCreated
      summary: Screen group created
      security: []
      description: |
        A screen group was created.

        Sent with `X-Trillboards-Event: screen_group.created`.

        Delivered through the **queue**: retried, and recorded in your delivery history. Signed with the compound `t=,v0=,v1=` header.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/EventEnvelope'
                - type: object
                  properties:
                    event:
                      const: "screen_group.created"
      responses:
        '2XX':
          description: |
            Any 2xx marks the delivery successful. Return one promptly and do your own
            processing asynchronously — the request times out server-side.
        '4XX':
          description: |
            Treated as a failed delivery and retried. A 4xx is NOT interpreted as "stop
            sending"; disable the subscription to stop delivery.
        '5XX':
          description: Treated as a failed delivery and retried.
  screen_group.updated:
    post:
      tags: [Webhook Events]
      operationId: onScreenGroupUpdated
      summary: Screen group updated
      security: []
      description: |
        A screen group was updated.

        Sent with `X-Trillboards-Event: screen_group.updated`.

        Delivered through the **queue**: retried, and recorded in your delivery history. Signed with the compound `t=,v0=,v1=` header.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/EventEnvelope'
                - type: object
                  properties:
                    event:
                      const: "screen_group.updated"
      responses:
        '2XX':
          description: |
            Any 2xx marks the delivery successful. Return one promptly and do your own
            processing asynchronously — the request times out server-side.
        '4XX':
          description: |
            Treated as a failed delivery and retried. A 4xx is NOT interpreted as "stop
            sending"; disable the subscription to stop delivery.
        '5XX':
          description: Treated as a failed delivery and retried.
  screen_group.deleted:
    post:
      tags: [Webhook Events]
      operationId: onScreenGroupDeleted
      summary: Screen group deleted
      security: []
      description: |
        A screen group was deleted.

        Sent with `X-Trillboards-Event: screen_group.deleted`.

        Delivered through the **queue**: retried, and recorded in your delivery history. Signed with the compound `t=,v0=,v1=` header.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/EventEnvelope'
                - type: object
                  properties:
                    event:
                      const: "screen_group.deleted"
      responses:
        '2XX':
          description: |
            Any 2xx marks the delivery successful. Return one promptly and do your own
            processing asynchronously — the request times out server-side.
        '4XX':
          description: |
            Treated as a failed delivery and retried. A 4xx is NOT interpreted as "stop
            sending"; disable the subscription to stop delivery.
        '5XX':
          description: Treated as a failed delivery and retried.
  screen_group.member.added:
    post:
      tags: [Webhook Events]
      operationId: onScreenGroupMemberAdded
      summary: Screen group member added
      security: []
      description: |
        A screen was added to a screen group.

        Sent with `X-Trillboards-Event: screen_group.member.added`.

        Delivered through the **queue**: retried, and recorded in your delivery history. Signed with the compound `t=,v0=,v1=` header.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/EventEnvelope'
                - type: object
                  properties:
                    event:
                      const: "screen_group.member.added"
      responses:
        '2XX':
          description: |
            Any 2xx marks the delivery successful. Return one promptly and do your own
            processing asynchronously — the request times out server-side.
        '4XX':
          description: |
            Treated as a failed delivery and retried. A 4xx is NOT interpreted as "stop
            sending"; disable the subscription to stop delivery.
        '5XX':
          description: Treated as a failed delivery and retried.
  screen_group.member.removed:
    post:
      tags: [Webhook Events]
      operationId: onScreenGroupMemberRemoved
      summary: Screen group member removed
      security: []
      description: |
        A screen was removed from a screen group.

        Sent with `X-Trillboards-Event: screen_group.member.removed`.

        Delivered through the **queue**: retried, and recorded in your delivery history. Signed with the compound `t=,v0=,v1=` header.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/EventEnvelope'
                - type: object
                  properties:
                    event:
                      const: "screen_group.member.removed"
      responses:
        '2XX':
          description: |
            Any 2xx marks the delivery successful. Return one promptly and do your own
            processing asynchronously — the request times out server-side.
        '4XX':
          description: |
            Treated as a failed delivery and retried. A 4xx is NOT interpreted as "stop
            sending"; disable the subscription to stop delivery.
        '5XX':
          description: Treated as a failed delivery and retried.
  screen_group.content_policy.updated:
    post:
      tags: [Webhook Events]
      operationId: onScreenGroupContentPolicyUpdated
      summary: Screen group content policy updated
      security: []
      description: |
        A screen group's content policy was updated.

        Sent with `X-Trillboards-Event: screen_group.content_policy.updated`.

        Delivered through the **queue**: retried, and recorded in your delivery history. Signed with the compound `t=,v0=,v1=` header.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/EventEnvelope'
                - type: object
                  properties:
                    event:
                      const: "screen_group.content_policy.updated"
      responses:
        '2XX':
          description: |
            Any 2xx marks the delivery successful. Return one promptly and do your own
            processing asynchronously — the request times out server-side.
        '4XX':
          description: |
            Treated as a failed delivery and retried. A 4xx is NOT interpreted as "stop
            sending"; disable the subscription to stop delivery.
        '5XX':
          description: Treated as a failed delivery and retried.
  screen_group.sensing_config.updated:
    post:
      tags: [Webhook Events]
      operationId: onScreenGroupSensingConfigUpdated
      summary: Screen group sensing config updated
      security: []
      description: |
        A screen group's sensing configuration was updated.

        Sent with `X-Trillboards-Event: screen_group.sensing_config.updated`.

        Delivered through the **queue**: retried, and recorded in your delivery history. Signed with the compound `t=,v0=,v1=` header.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/EventEnvelope'
                - type: object
                  properties:
                    event:
                      const: "screen_group.sensing_config.updated"
      responses:
        '2XX':
          description: |
            Any 2xx marks the delivery successful. Return one promptly and do your own
            processing asynchronously — the request times out server-side.
        '4XX':
          description: |
            Treated as a failed delivery and retried. A 4xx is NOT interpreted as "stop
            sending"; disable the subscription to stop delivery.
        '5XX':
          description: Treated as a failed delivery and retried.
  fleet_command.completed:
    post:
      tags: [Webhook Events]
      operationId: onFleetCommandCompleted
      summary: Fleet command completed
      security: []
      description: |
        A group fleet command finished successfully.

        Sent with `X-Trillboards-Event: fleet_command.completed`.

        Delivered through the **queue**: retried, and recorded in your delivery history. Signed with the compound `t=,v0=,v1=` header.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/EventEnvelope'
                - type: object
                  properties:
                    event:
                      const: "fleet_command.completed"
      responses:
        '2XX':
          description: |
            Any 2xx marks the delivery successful. Return one promptly and do your own
            processing asynchronously — the request times out server-side.
        '4XX':
          description: |
            Treated as a failed delivery and retried. A 4xx is NOT interpreted as "stop
            sending"; disable the subscription to stop delivery.
        '5XX':
          description: Treated as a failed delivery and retried.
  fleet_command.failed:
    post:
      tags: [Webhook Events]
      operationId: onFleetCommandFailed
      summary: Fleet command failed
      security: []
      description: |
        A group fleet command failed.

        Sent with `X-Trillboards-Event: fleet_command.failed`.

        Delivered through the **queue**: retried, and recorded in your delivery history. Signed with the compound `t=,v0=,v1=` header.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/EventEnvelope'
                - type: object
                  properties:
                    event:
                      const: "fleet_command.failed"
      responses:
        '2XX':
          description: |
            Any 2xx marks the delivery successful. Return one promptly and do your own
            processing asynchronously — the request times out server-side.
        '4XX':
          description: |
            Treated as a failed delivery and retried. A 4xx is NOT interpreted as "stop
            sending"; disable the subscription to stop delivery.
        '5XX':
          description: Treated as a failed delivery and retried.
  test:
    post:
      tags: [Webhook Events]
      operationId: onTest
      summary: Test
      security: []
      description: |
        A synthetic event emitted by `POST /v1/partner/webhooks/{webhookId}/test`. It is delivered through the same signing and retry path as a real event, so it is a valid end-to-end check of a subscriber. It carries no production data.

        Sent with `X-Trillboards-Event: test`.

        Delivered **synchronously, at most once**: a failure is NOT retried and does NOT appear in your delivery history. Signed with the legacy `sha256=<hex>` header.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TestEvent'
      responses:
        '2XX':
          description: |
            Any 2xx marks the delivery successful. Return one promptly and do your own
            processing asynchronously — the request times out server-side.
        '4XX':
          description: |
            Treated as a failed delivery. This event is NOT retried — the notification is
            lost. Return 2xx and process asynchronously if you need it.
        '5XX':
          description: Treated as a failed delivery. NOT retried.

components:
  schemas:
    EventEnvelope:
      type: object
      description: |
        The delivery body. Every webhook shares this envelope; only `data` varies.

        ```json
        {
          "event": "device.online",
          "timestamp": "2026-01-01T00:00:00.000Z",
          "data": { "device_id": "..." }
        }
        ```

        Two producers build this envelope and they do NOT agree on `id`: the
        screen-group dispatcher adds one, the general dispatch path does not. Treat
        `id` as optional.
      required: [event, timestamp, data]
      properties:
        id:
          type: string
          description: |
            Stable event id, `evt_<base36 ts>_<hex>`.

            **Present only on queued events** (the screen-group producer sets it). The
            synchronous path does not, so do NOT key idempotency on this field alone —
            treat it as optional and fall back to your own dedupe key.
          example: evt_m1a2b3c4_9f8e7d
        event:
          type: string
          description: The event type. Mirrors the `X-Trillboards-Event` header.
        timestamp:
          type: string
          format: date-time
          description: |
            ISO-8601 time the event was built. This is NOT the value signed in the
            signature header — that is `X-Trillboards-Timestamp`, in unix seconds.
        data:
          type: object
          description: Event-specific payload.
          additionalProperties: true
    TestEvent:
      type: object
      description: |
        The body of a `POST /v1/partner/webhooks/{webhookId}/test` delivery.

        **This is NOT the standard envelope.** `sendTestWebhook()` posts this object
        directly, so there is no `event`, `timestamp` or `data` member — only the
        `X-Trillboards-Event: test` header identifies it. A validator that assumes
        `EventEnvelope` here will reject a genuine test delivery.
      required: [test, message, partner_id, webhook_id]
      properties:
        test:
          type: boolean
          const: true
        message:
          type: string
          example: This is a test webhook from Trillboards
        partner_id:
          type: string
        webhook_id:
          type: string

