Skip to main content

Receive real-time notifications when flags change — delivery, signatures, retry logic, and verification

Webhooks

Webhooks notify your systems when flag events occur. Flaggr delivers signed HTTP POST requests to your endpoint with the event payload.

Events

EventTrigger
flag.createdA new flag is created
flag.updatedA flag's configuration changes
flag.deletedA flag is removed
flag.toggledA flag's enabled state is flipped

Registering a Webhook

POST /api/webhooks
Content-Type: application/json
Authorization: Bearer flg_your_token
 
{
  "url": "https://your-app.com/webhooks/flaggr",
  "secret": "whsec_your_signing_secret",
  "events": ["flag.created", "flag.updated", "flag.deleted", "flag.toggled"],
  "active": true
}

Store your secret securely — you'll use it to verify incoming signatures.

Payload Format

Every webhook delivery is an HTTP POST with Content-Type: application/json:

{
  "event": "flag.updated",
  "timestamp": "2025-07-20T10:30:00Z",
  "data": {
    "flagKey": "checkout-v2",
    "projectId": "proj-1",
    "serviceId": "web-app",
    "changes": {
      "enabled": { "from": false, "to": true }
    }
  }
}

Signature Verification

Every delivery includes an X-Flaggr-Signature header containing an HMAC-SHA256 signature of the raw request body.

Header format:

X-Flaggr-Signature: sha256=a1b2c3d4e5f6...

Verifying in Node.js

import crypto from 'crypto'
 
function verifyWebhook(body: string, signature: string, secret: string): boolean {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(body)
    .digest('hex')
 
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  )
}
 
// In your webhook handler
app.post('/webhooks/flaggr', (req, res) => {
  const signature = req.headers['x-flaggr-signature']
  const rawBody = req.body // Must be raw string, not parsed JSON
 
  if (!verifyWebhook(rawBody, signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' })
  }
 
  const event = JSON.parse(rawBody)
  console.log(`Flag ${event.data.flagKey}: ${event.event}`)
  res.status(200).json({ received: true })
})

Verifying with the SDK

import { verifySignature } from '@flaggr/sdk'
 
const isValid = verifySignature(rawBody, secret, signatureHeader)

Important: Always verify signatures using timing-safe comparison to prevent timing attacks.

Delivery & Retry

Flaggr uses exponential backoff for failed deliveries:

AttemptDelayTotal Elapsed
1stImmediate0s
2nd1 second1s
3rd2 seconds3s
4th (final)4 seconds7s

Timeout: Each attempt has a 10-second timeout.

Retry conditions:

  • 5xx responses — retried (server error)
  • 429 responses — retried (rate limited)
  • Timeouts — retried
  • 4xx responses (except 429) — not retried (client error, fix your endpoint)

After all retry attempts are exhausted, the delivery is marked as failed.

Best Practices

Return 200 quickly

Process webhook payloads asynchronously. Return 200 OK immediately and handle the event in a background job:

app.post('/webhooks/flaggr', async (req, res) => {
  // Verify signature first
  if (!verifyWebhook(req.body, req.headers['x-flaggr-signature'], secret)) {
    return res.status(401).end()
  }
 
  // Acknowledge immediately
  res.status(200).json({ received: true })
 
  // Process asynchronously
  await queue.add('process-flag-event', JSON.parse(req.body))
})

Handle duplicate deliveries

Network issues can cause the same event to be delivered more than once. Use the event + timestamp + data.flagKey combination as an idempotency key:

const idempotencyKey = `${event.event}:${event.data.flagKey}:${event.timestamp}`
if (await cache.has(idempotencyKey)) return
await cache.set(idempotencyKey, true, { ttl: 3600 })

Use HTTPS endpoints

Always use HTTPS URLs for production webhooks. HTTP endpoints are accepted for local development but are not recommended.

Managing Webhooks

Update a webhook

PATCH /api/webhooks/{id}
 
{
  "events": ["flag.toggled"],
  "active": false
}

Delete a webhook

DELETE /api/webhooks/{id}

Disable temporarily

Set active: false to pause deliveries without deleting the webhook configuration:

PATCH /api/webhooks/{id}
{ "active": false }

Use Cases

  • Cache invalidation — Bust your application cache when a flag changes
  • Audit trail — Feed flag changes into your own audit system
  • Slack notifications — Alert your team when production flags are toggled
  • CI/CD triggers — Kick off deploys when feature flags are updated
  • Analytics — Track flag change frequency and patterns