Webhooks
Subscribe to events: screen status changes, content publishes, POS sync results.
Register a webhook
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
| Field | Type | Description |
|---|---|---|
| screen.online | event | Screen came back online after being offline. Alias of device.online. |
| screen.offline | event | Screen has been offline for 2+ minutes. Alias of device.offline. |
| content.published | event | A design was published (directly, or on approval). |
| design.approved | event | A design passed the approval workflow. |
| design.rejected | event | A design was rejected in the approval workflow. |
| pos.sync.completed | event | POS sync finished. Payload includes counts. Alias of pos.sync_succeeded. |
| pos.sync.failed | event | POS sync errored. Payload includes reason. Alias of pos.sync_failed. |
| team.member.added | event | A new team member accepted their invite. |
| subscription.updated | event | Billing 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.
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.