Skip to content

Webhooks

Overview

Floopy webhooks deliver real-time HTTP POST notifications to your endpoints when important events occur in your organization. Use webhooks to integrate Floopy with your incident management, chat, and monitoring tools.

Supported events:

EventDescription
security_alertThreat detected by the AI firewall (prompt injection, anomalous traffic, geo anomaly, etc.)
alert_triggeredCustom alert rule threshold exceeded (e.g., error rate > 5%)
budget_exceededTeam or organization budget limit reached

Every delivery is signed with HMAC-SHA256 so you can verify authenticity. Failed deliveries are retried automatically with exponential backoff.

Payload Schema Reference

All webhook payloads share a common envelope:

{
"event": "security_alert",
"timestamp": "2026-04-10T14:30:00.000Z",
"organization_id": "550e8400-e29b-41d4-a716-446655440000",
"delivery_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"data": { ... }
}
FieldTypeDescription
eventstringEvent type: security_alert, alert_triggered, or budget_exceeded
timestampstringISO 8601 timestamp of when the event was created
organization_idstringUUID of the organization that owns the event
delivery_idstringUUID unique to this delivery attempt — use for deduplication
dataobjectEvent-specific payload (see below)

security_alert

Fired when the AI firewall detects a threat — prompt injection, volume spike, geo anomaly, or other security events.

{
"event": "security_alert",
"timestamp": "2026-04-10T14:30:00.000Z",
"organization_id": "550e8400-e29b-41d4-a716-446655440000",
"delivery_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"data": {
"alert_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"threat_type": "prompt_injection",
"severity": "high",
"title": "Prompt injection detected",
"message": "Malicious prompt injection attempt detected from API key ending in ...x4f2",
"affected_key_id": "d4e5f6a7-b8c9-0123-4567-89abcdef0123",
"country_code": "CN",
"details": {
"score": 0.97,
"blocked": true
}
}
}
FieldTypeRequiredDescription
alert_idstring (UUID)YesUnique identifier for this alert
threat_typestringYesType of threat detected (e.g., prompt_injection, volume_spike, geo_anomaly, quality_regression)
severitystringYesSeverity level: low, medium, high, critical
titlestringYesHuman-readable alert title
messagestringYesDetailed description of the threat
affected_key_idstring (UUID)NoAPI key involved in the threat, if applicable
country_codestringNoISO 3166-1 alpha-2 country code of the request origin
detailsobjectNoAdditional context (varies by threat type)

alert_triggered

Fired when a custom alert rule threshold is exceeded.

{
"event": "alert_triggered",
"timestamp": "2026-04-10T14:30:00.000Z",
"organization_id": "550e8400-e29b-41d4-a716-446655440000",
"delivery_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"data": {
"alert_id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"rule_name": "High Error Rate",
"metric": "error_rate",
"threshold": 5.0,
"current_value": 8.3,
"window_minutes": 15
}
}
FieldTypeRequiredDescription
alert_idstring (UUID)YesUnique identifier for this triggered alert
rule_namestringYesName of the custom alert rule that fired
metricstringYesMetric being monitored (e.g., error_rate, latency_p99, request_count)
thresholdnumberYesThreshold value that was exceeded
current_valuenumberYesActual metric value that triggered the alert
window_minutesintegerYesTime window in minutes over which the metric was evaluated

budget_exceeded

Fired when a team or organization exceeds its configured budget limit.

{
"event": "budget_exceeded",
"timestamp": "2026-04-10T14:30:00.000Z",
"organization_id": "550e8400-e29b-41d4-a716-446655440000",
"delivery_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"data": {
"budget_id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"team_name": "ML Engineering",
"limit_type": "monthly",
"limit_cents": 100000,
"spent_cents": 100247
}
}
FieldTypeRequiredDescription
budget_idstring (UUID)YesUnique identifier for the budget
team_namestringYesName of the team that exceeded the budget
limit_typestringYesBudget period type (e.g., monthly, daily)
limit_centsintegerYesBudget limit in US cents
spent_centsintegerYesAmount spent in US cents at time of event

HMAC Signature Verification

Every webhook delivery includes an X-Floopy-Signature header containing an HMAC-SHA256 signature of the raw request body. Always verify this signature before processing the payload to ensure the request came from Floopy.

Header format:

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

Verification steps:

  1. Extract the hex signature from the X-Floopy-Signature header (strip the sha256= prefix)
  2. Compute HMAC-SHA256 of the raw request body using your webhook signing secret
  3. Compare the computed signature with the received signature using a timing-safe comparison
import { createHmac, timingSafeEqual } from "crypto";
function verifyWebhookSignature(
body: string,
signature: string,
secret: string
): boolean {
const expected = createHmac("sha256", secret)
.update(body)
.digest("hex");
const sig = signature.replace("sha256=", "");
if (expected.length !== sig.length) return false;
return timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(sig, "hex")
);
}
// Express.js example
app.post("/webhooks/floopy", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.headers["x-floopy-signature"] as string;
const body = req.body.toString();
if (!verifyWebhookSignature(body, signature, process.env.FLOOPY_WEBHOOK_SECRET!)) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(body);
console.log(`Received ${event.event} event:`, event.data);
res.status(200).send("OK");
});

Setup Guide

1. Check your plan

Webhooks are available on Pro plans and above. Navigate to Settings > Billing to verify your plan includes webhooks.

2. Create a webhook endpoint

  1. Go to Webhooks in the dashboard sidebar
  2. Click Create Endpoint
  3. Enter a name for the endpoint (e.g., “PagerDuty Alerts”)
  4. Enter the URL where Floopy should send events (must be publicly accessible HTTPS)
  5. Select the events you want to receive (security_alert, alert_triggered, budget_exceeded)
  6. Optionally add custom headers (e.g., authorization tokens for your endpoint)
  7. Click Create

3. Save your signing secret

After creating the endpoint, your signing secret is displayed once. Copy it and store it securely — you will not be able to view it again. This secret is used to verify webhook signatures.

The secret has the format: whsec_<64 hex characters>

4. Verify your endpoint

Click the Send Test button on the endpoint card to send a test payload. Verify that:

  • Your endpoint receives the request
  • The HMAC signature validates correctly
  • Your endpoint returns a 2xx status code

5. Go live

Once your endpoint passes the test delivery, it will start receiving real events. Monitor the Delivery Log section on the webhooks page to track successful and failed deliveries.

Integration Guides

Slack

Forward Floopy alerts to a Slack channel using an Incoming Webhook:

  1. Create a Slack Incoming Webhook for your channel
  2. Create a small relay service that receives Floopy webhooks and formats them for Slack:
app.post("/webhooks/floopy", express.raw({ type: "application/json" }), async (req, res) => {
// Verify signature first (see HMAC section above)
const event = JSON.parse(req.body.toString());
const slackMessage = {
text: `*Floopy ${event.event}*`,
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: event.event === "security_alert"
? `:rotating_light: *${event.data.title}*\nSeverity: ${event.data.severity}\n${event.data.message}`
: event.event === "alert_triggered"
? `:warning: *${event.data.rule_name}* triggered\n${event.data.metric}: ${event.data.current_value} (threshold: ${event.data.threshold})`
: `:money_with_wings: *Budget exceeded*\nTeam: ${event.data.team_name}\nSpent: $${(event.data.spent_cents / 100).toFixed(2)} / $${(event.data.limit_cents / 100).toFixed(2)}`
}
}
]
};
await fetch(process.env.SLACK_WEBHOOK_URL!, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(slackMessage),
});
res.status(200).send("OK");
});

PagerDuty

Route Floopy security alerts directly to PagerDuty using the Events API v2:

  1. Create a PagerDuty Events API v2 integration and copy the Integration Key
  2. Create a relay that maps Floopy events to PagerDuty alerts:
const event = JSON.parse(req.body.toString());
if (event.event === "security_alert") {
await fetch("https://events.pagerduty.com/v2/enqueue", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
routing_key: process.env.PAGERDUTY_INTEGRATION_KEY,
event_action: "trigger",
payload: {
summary: `[Floopy] ${event.data.title}`,
severity: event.data.severity === "critical" ? "critical" : "warning",
source: "floopy-gateway",
component: "ai-firewall",
custom_details: event.data,
},
}),
});
}

OpsGenie

Forward alerts to OpsGenie using the Alert API:

const event = JSON.parse(req.body.toString());
await fetch("https://api.opsgenie.com/v2/alerts", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `GenieKey ${process.env.OPSGENIE_API_KEY}`,
},
body: JSON.stringify({
message: `[Floopy] ${event.data.title || event.data.rule_name || "Budget exceeded"}`,
priority: event.data.severity === "critical" ? "P1" : "P3",
description: JSON.stringify(event.data, null, 2),
tags: ["floopy", event.event],
}),
});

Discord

Send alerts to a Discord channel using a webhook URL:

const event = JSON.parse(req.body.toString());
const embed = {
title: `Floopy: ${event.event.replace("_", " ")}`,
color: event.event === "security_alert" ? 0xff0000 : event.event === "alert_triggered" ? 0xffa500 : 0x0099ff,
fields: Object.entries(event.data).map(([key, value]) => ({
name: key,
value: String(value).substring(0, 1024),
inline: true,
})),
timestamp: event.timestamp,
};
await fetch(process.env.DISCORD_WEBHOOK_URL!, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ embeds: [embed] }),
});

Custom Endpoint

For custom integrations, implement an HTTP endpoint that:

  1. Accepts POST requests with Content-Type: application/json
  2. Verifies the X-Floopy-Signature header (see HMAC Signature Verification)
  3. Returns a 2xx status code within 10 seconds
  4. Handles deduplication using delivery_id (at-least-once delivery means you may receive the same event more than once)
app.post("/webhooks/floopy", express.raw({ type: "application/json" }), async (req, res) => {
const signature = req.headers["x-floopy-signature"] as string;
if (!verifyWebhookSignature(req.body.toString(), signature, SECRET)) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(req.body.toString());
// Deduplicate by delivery_id
if (await isAlreadyProcessed(event.delivery_id)) {
return res.status(200).send("Already processed");
}
// Process the event
switch (event.event) {
case "security_alert":
await handleSecurityAlert(event.data);
break;
case "alert_triggered":
await handleAlertTriggered(event.data);
break;
case "budget_exceeded":
await handleBudgetExceeded(event.data);
break;
}
await markAsProcessed(event.delivery_id);
res.status(200).send("OK");
});

Retry Behavior & Delivery Guarantees

At-Least-Once Delivery

Floopy guarantees at-least-once delivery. In rare cases (e.g., your endpoint returns 200 but Floopy’s network drops before receiving the response), you may receive the same event more than once. Use the delivery_id field to deduplicate.

Retry Schedule

If your endpoint returns a non-2xx status code or the connection fails, Floopy retries with exponential backoff:

AttemptDelayCumulative Time
1Immediate0s
21 second~1s
35 seconds~6s
425 seconds~31s

Maximum 4 attempts. After 4 consecutive failures, the delivery is dead-lettered (no further retries).

Timeout

Each delivery attempt has a 10-second timeout. If your endpoint does not respond within 10 seconds, the attempt is considered failed and will be retried.

Dead Letter

After 4 failed attempts, the delivery is permanently marked as failed. You can see dead-lettered deliveries in the Delivery Log on the webhooks page. To replay failed events, use the Send Test button or re-trigger the event.

SSRF Protection

Webhook URLs are validated against internal and private IP ranges. Endpoints pointing to loopback addresses (127.0.0.1, ::1), private networks (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), or link-local addresses are rejected.

Troubleshooting

Delivery failures in the log

Check the Delivery Log on the webhooks page. Each attempt shows:

  • Status code — your endpoint’s HTTP response code
  • Error — connection error message if the request failed
  • Latency — how long the request took
  • Attempt — which retry attempt (1–4)

Signature verification fails

  • Ensure you are comparing against the raw request body (before any JSON parsing or middleware transformation)
  • Verify you are using the correct signing secret — the one shown at endpoint creation time
  • Check that your framework is not modifying the body (e.g., Express.js needs express.raw() to get the unmodified body)

Endpoint not receiving events

  • Verify the endpoint is active (toggle is enabled on the webhooks page)
  • Check that the endpoint is subscribed to the correct event types
  • Ensure your endpoint URL is publicly accessible from the internet
  • Check your firewall or load balancer is not blocking requests from Floopy’s IP range

Timeout errors

  • Your endpoint must respond within 10 seconds
  • If processing takes longer, acknowledge the webhook immediately with 200 and process asynchronously
  • Example: write the event to a queue, return 200, then process from the queue

Duplicate events

  • Floopy uses at-least-once delivery — duplicates are possible
  • Use the delivery_id field to deduplicate events in your handler
  • Store processed delivery_id values in a cache or database with a TTL matching the retry window (~1 minute)