Webhooks
Flip sends webhooks to your server when key events occur — demand response events, device commands, enrollment changes, and settings updates. This lets your integration react in real time without polling the API.
Setup
Register your webhook URL in the Flip Developer Console under your webhook settings. Flip will provide a signing key that you'll use to verify incoming requests.
You can subscribe to all event types or filter to specific ones.
Event types
Events
| Event | Description |
|---|---|
event.created | A new demand response event has been created targeting one of your sites. |
event.updated | An existing event has been updated (e.g. schedule change, participation status). |
event.canceled | An event has been canceled. |
Commands
| Event | Description |
|---|---|
command.created | A per-device command has been created for an upcoming event. |
command.started | A command has started — your device should begin executing it. |
command.ended | A command has ended. |
command.canceled | A command has been canceled before completion. |
command.updated | A command has been updated (e.g. status change). |
Enrollments
| Event | Description |
|---|---|
enrollment.created | A site has been enrolled in a program. |
enrollment.updated | An enrollment status has changed (e.g. activated, paused, unenrolled). |
Settings
| Event | Description |
|---|---|
settings.apply | Device settings need to be applied. Read the payload and push the configuration to the device. |
Payload structure
Every webhook request body follows the same envelope:
{
"event_type": "event.created",
"event_object": { ... },
"timestamp": "2025-01-15T12:00:00.000Z"
}The event_object contains the full resource data and includes an object_type field (event, command, enrollment, or settings_request) that identifies which schema applies. See the schema reference pages in the Webhooks section for exact field definitions.
Headers
Every webhook request includes three headers following the Standard Webhooks specification:
| Header | Description |
|---|---|
Webhook-Id | Unique identifier for this delivery. Use it to detect and discard duplicates. |
Webhook-Timestamp | Unix timestamp (seconds) when the webhook was created. Reject any request where this value is more than 5 minutes from your server's current time. |
Webhook-Signature | HMAC-SHA256 signature for verifying the request came from Flip. Format: v1,<base64-encoded-digest>. |
Verifying signatures
To verify a webhook request:
- Parse the three headers from the incoming request.
- Strip the
whsec_prefix from your signing key and base64-decode the remainder. - Concatenate
{Webhook-Id}.{Webhook-Timestamp}.{body}(the raw request body as a string). - Compute an HMAC-SHA256 digest using the decoded key.
- Compare your result against the signature in the header.
import { createHmac, timingSafeEqual } from 'node:crypto'
async function verifyWebhook(request: Request, signingKey: string): Promise<boolean> {
const id = request.headers.get('Webhook-Id')
const timestamp = request.headers.get('Webhook-Timestamp')
const signature = request.headers.get('Webhook-Signature')
if (!id || !timestamp || !signature) return false
const body = await request.text()
// Reject stale requests (replay protection)
const ts = Number(timestamp)
if (!Number.isFinite(ts)) return false
const age = Math.abs(Date.now() / 1000 - ts)
if (age > 300) return false
const key = Buffer.from(signingKey.replace('whsec_', ''), 'base64')
const expected = `v1,${createHmac('sha256', key)
.update(`${id}.${timestamp}.${body}`)
.digest('base64')}`
const a = Buffer.from(signature)
const b = Buffer.from(expected)
return a.length === b.length && timingSafeEqual(a, b)
}Best practices
- Handle duplicates. Track the
Webhook-Idof processed deliveries and skip any you've already seen. - Respond quickly. Return an HTTP
200before doing any heavy processing. Use a queue to handle payloads asynchronously. - Act on
settings.applypromptly. This webhook tells your integration to push settings to the physical device. Delays may affect demand response performance.