Webhooks

Subscribe to events: screen status changes, content publishes, POS sync results.

Updated May 18, 2026·4 min read

Register a webhook

Shell
curl -X POST https://api.menupi.com/v1/public/webhooks \
  -H "Authorization: Bearer mpi_live_••••••" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.com/menupi-webhook",
    "events": ["screen.offline", "screen.online", "content.published"]
  }'

Available events

FieldTypeDescription
screen.onlineeventScreen came back online after being offline. Alias of device.online.
screen.offlineeventScreen has been offline for 2+ minutes. Alias of device.offline.
content.publishedeventA design was published (directly, or on approval).
design.approvedeventA design passed the approval workflow.
design.rejectedeventA design was rejected in the approval workflow.
pos.sync.completedeventPOS sync finished. Payload includes counts. Alias of pos.sync_succeeded.
pos.sync.failedeventPOS sync errored. Payload includes reason. Alias of pos.sync_failed.
team.member.addedeventA new team member accepted their invite.
subscription.updatedeventBilling subscription plan or status changed.

Signature verification

Each delivery carries three headers: `X-Webhook-Event` (the event name), `X-Webhook-Timestamp` (ISO 8601), and `X-MenuPi-Signature`. The signature is Stripe-style — `t=<unixSeconds>,v1=<hmacSha256>` — where the HMAC is computed over `` `${unixSeconds}.${rawBody}` `` using your endpoint secret. Reject the delivery if the timestamp is older than 5 minutes, then compare with a constant-time check.

verify.ts·TypeScript
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(secret: string, header: string, body: string): boolean {
  const m = /t=(\d+),v1=([a-f0-9]+)/.exec(header);
  if (!m) return false;
  const [, ts, sig] = m;
  // Reject replays older than 5 minutes
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const expected = createHmac("sha256", secret)
    .update(`${ts}.${body}`)
    .digest("hex");
  return timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}

Retries

A non-2xx response is retried up to 3 times with exponential backoff (≈60s base). After repeated failures the delivery is logged as failed and surfaced under Settings → Developer → Deliveries, where it can be replayed. An endpoint that fails 10 deliveries in a row is automatically disabled until you re-enable it.