Airwave

Webhooks

Get a signed event pushed to your endpoint when someone starts or stops watching.

Airwave can push a signed event to any HTTPS endpoint the moment something happens, so an integration reacts instantly instead of polling. Deliveries use the Standard Webhooks scheme, so you can verify them with an off-the-shelf library in most languages.

Add a webhook

In the admin, go to Settings → Webhooks → New webhook:

  1. Enter the delivery URL (where Airwave POSTs events).
  2. Tick the events you want, or leave them all unchecked to receive every event.
  3. Save. The signing secret (whsec_…) is shown once, right then. Copy it now, you can't see it again (you can rotate it later).

You can also manage webhooks over the API (/api/public/v1/webhooks) with an admin API key, which is how an integration like n8n registers its own endpoint.

Events

EventWhen
session.startedA viewer starts watching a channel
session.endedA viewer stops watching (also fires when a session goes stale, e.g. the client crashed or dropped off the network)
channel.tunedA viewer changes channel without leaving the player
playback.startedPlayback begins, the first play of a session (or content resuming from an idle/loading state)
playback.pausedThe viewer pauses
playback.resumedThe viewer resumes after a pause
playback.stoppedPlayback ends together with the session
pingA test event you trigger from the webhook's Send test button

The playback.* events fire only for clients that report their real play/pause state (the Airwave TV apps do). An older or third-party client that only heartbeats a channel still produces session.started / session.ended, just without the playback.* detail, so nothing breaks if a client doesn't send it.

How a channel change surfaces depends on the client. The apps that keep the player open across channels (the desktop, mobile, and Roku clients) emit a single channel.tuned. The browser/TV-web client tears the player down and rebuilds it per channel, so there it arrives as a session.ended followed by a session.started.

The payload

Every delivery is a POST with a JSON envelope. Session and playback events carry the same rich data the "Now Watching" view has, so you can react straight off the payload without a follow-up call:

{
  "id": "evt_2a9f…",
  "type": "playback.paused",
  "timestamp": "2026-09-28T18:04:11.000Z",
  "data": {
    "userId": "clx9user01",
    "deviceId": "roku-3f9c1a2b7d40",
    "user": "James",
    "channel": { "id": "clx9ch01", "number": 101, "name": "90s Sitcoms", "callsign": "SITCOM" },
    "program": {
      "ratingKey": "45210",
      "title": "The One with the Blackout",
      "showTitle": "Friends",
      "season": 1,
      "episode": 7,
      "year": 1994
    },
    "playbackState": "paused",
    "delaySeconds": 42,
    "progress": { "positionSeconds": 610, "durationSeconds": 1380 }
  }
}

id is unique per event (also sent as the webhook-id header) so you can dedupe. type is the dotted event name. In data:

  • deviceId — the specific device this session is on (see Per-device sessions).
  • channel — id, number, name, and callsign.
  • program — the current program, with showTitle / season / episode for episodic content (movies leave those null). null if nothing is scheduled.
  • playbackState — playing, paused, buffering, or idle. null from a client that doesn't report it.
  • delaySeconds — how far behind the live edge the viewer is (0 at the live edge, higher when paused or rewound).
  • progress — positionSeconds into a program of durationSeconds. null when there's no resolvable program.

Fields resolve on a best-effort basis: anything Airwave can't determine comes through as null rather than being omitted, so your parser can rely on the shape.

Per-device sessions

Sessions are keyed per (user, device), so the same account watching two different things on two TVs is two sessions, each with its own deviceId and its own events. The deviceId is the stable id the client already uses (the same one the capability system keys on). A client that doesn't send a deviceId collapses to a single "legacy" session for that user, unchanged from older behavior.

Verify the signature

Each POST carries three headers:

webhook-id: evt_2a9f…
webhook-timestamp: 1790000651
webhook-signature: v1,g0hM9SsE…

The signature is v1, plus the base64 HMAC-SHA256 of {id}.{timestamp}.{body}, keyed by the secret's bytes (the part after whsec_, base64-decoded). Verify with a Standard Webhooks library:

# pip install standardwebhooks
from standardwebhooks import Webhook

wh = Webhook("whsec_your_secret")
payload = wh.verify(request_body_bytes, dict(request.headers))  # raises on a bad signature

Reject anything that fails verification, and optionally reject a webhook-timestamp more than a few minutes old to prevent replay.

Home Assistant

Create an automation with a webhook trigger, which gives you a URL like https://ha.example.com/api/webhook/airwave-xyz. Add that URL as an Airwave webhook subscribed to session.started and session.ended, then act on the payload:

# automation: dim the lights when someone starts watching
automation:
  - alias: Airwave – lights down on watch
    trigger:
      - platform: webhook
        webhook_id: airwave-xyz
        allowed_methods: [POST]
        local_only: false
    condition:
      - "{{ trigger.json.type == 'session.started' }}"
    action:
      - service: light.turn_off
        target: { entity_id: light.living_room }

Delivery attempts (and retries) show up per endpoint under Settings → Webhooks, where you can also send a test event or redeliver a past one.

On this page