Webhooks: events and signatures

Pro

Dislab posts a signed JSON message to your address when a screen goes offline, a file is ready, someone joins your list, and more.

  1. In Settings, under Webhooks, enter your address. It must start with https:// and be on the public internet.
  2. Tick Every event, or untick it and choose the events you want.
  3. Press Add webhook.
  4. Copy the signing secret (whsec_…) into your receiver now: it’s shown once.
  5. Press Send a test. Dislab sends a ping and says whether it arrived.
    Webhooks in Settings: an address already added with Send a test (marked 5), and the form: the address (marked 1), which events (marked 2) and Add webhook (marked 3).

Events

  • screen.paired, screen.unpaired: data.screen with id, name, location_id.
  • screen.offline (30 minutes without checking in, while its location is open) and screen.online: data.screen, since, and until when it’s back.
  • screen.stuck (new content waiting an hour) and screen.caught_up: the same fields.
  • media.ready, media.failed: data.media with id, name, kind, failure_reason.
  • contact.created: someone joined a list from a “join our list” QR code. data.contact with id, list_id, first_name, email, mobile, consent, consent_at.
  • ping: from Send a test only.

What arrives

A POST with Content-Type: application/json, a Dislab-Event header naming the event, a Dislab-Signature header, and:

{
  "id": "EVENT_ID",
  "type": "screen.offline",
  "created": "2026-10-11T09:30:00Z",
  "account_id": "ACCOUNT_ID",
  "data": { "screen": { "id": "...", "name": "Counter",
            "location_id": "..." }, "since": "..." }
}

Answer with any 2xx within 8 seconds. Anything else is tried again after 1, 5 and 30 minutes, then 2, 6 and 12 hours, and then given up. Each address gets its messages one at a time, oldest first. A retry has the same id, so skip ids you’ve already handled.

Checking the signature

Dislab-Signature looks like t=1791700000,v1=5f2b…. v1 is HMAC-SHA256, in hex, of t, a full stop and the raw body, keyed with your secret. Check it against the body exactly as it arrived (before parsing), and check t is within 5 minutes of now.

Node:

import { createHmac, timingSafeEqual } from 'node:crypto'

// rawBody: the request body as a string, exactly as received.
export function fromDislab(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
  const t = Number(parts.t)
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false
  const want = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
  const got = Buffer.from(parts.v1 ?? '', 'hex')
  return got.length === 32 && timingSafeEqual(got, Buffer.from(want, 'hex'))
}

Python:

import hashlib, hmac, time

def from_dislab(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t = int(parts.get("t", "0"))
    if abs(time.time() - t) > 300:
        return False
    want = hmac.new(secret.encode(), f"{t}.".encode() + raw_body,
                    hashlib.sha256).hexdigest()
    return hmac.compare_digest(want, parts.get("v1", ""))

Under each address, Settings shows the last message and whether it was delivered. Switch off pauses an address; Delete removes it. Up to 10 addresses.

Still stuck?

Email support@dislab.app, or send us a message. If it’s about a screen, tell us its name. We answer within one business day.

Contact us