Webhooks
Este conteúdo não está disponível em sua língua ainda.
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:
| Event | Description |
|---|---|
security_alert | Threat detected by the AI firewall (prompt injection, anomalous traffic, geo anomaly, etc.) |
alert_triggered | Custom alert rule threshold exceeded (e.g., error rate > 5%) |
budget_exceeded | Team 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": { ... }}| Field | Type | Description |
|---|---|---|
event | string | Event type: security_alert, alert_triggered, or budget_exceeded |
timestamp | string | ISO 8601 timestamp of when the event was created |
organization_id | string | UUID of the organization that owns the event |
delivery_id | string | UUID unique to this delivery attempt — use for deduplication |
data | object | Event-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 } }}| Field | Type | Required | Description |
|---|---|---|---|
alert_id | string (UUID) | Yes | Unique identifier for this alert |
threat_type | string | Yes | Type of threat detected (e.g., prompt_injection, volume_spike, geo_anomaly, quality_regression) |
severity | string | Yes | Severity level: low, medium, high, critical |
title | string | Yes | Human-readable alert title |
message | string | Yes | Detailed description of the threat |
affected_key_id | string (UUID) | No | API key involved in the threat, if applicable |
country_code | string | No | ISO 3166-1 alpha-2 country code of the request origin |
details | object | No | Additional 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 }}| Field | Type | Required | Description |
|---|---|---|---|
alert_id | string (UUID) | Yes | Unique identifier for this triggered alert |
rule_name | string | Yes | Name of the custom alert rule that fired |
metric | string | Yes | Metric being monitored (e.g., error_rate, latency_p99, request_count) |
threshold | number | Yes | Threshold value that was exceeded |
current_value | number | Yes | Actual metric value that triggered the alert |
window_minutes | integer | Yes | Time 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 }}| Field | Type | Required | Description |
|---|---|---|---|
budget_id | string (UUID) | Yes | Unique identifier for the budget |
team_name | string | Yes | Name of the team that exceeded the budget |
limit_type | string | Yes | Budget period type (e.g., monthly, daily) |
limit_cents | integer | Yes | Budget limit in US cents |
spent_cents | integer | Yes | Amount 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:
- Extract the hex signature from the
X-Floopy-Signatureheader (strip thesha256=prefix) - Compute HMAC-SHA256 of the raw request body using your webhook signing secret
- 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 exampleapp.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");});import hmacimport hashlibfrom flask import Flask, request, abort
app = Flask(__name__)
def verify_webhook_signature(body: bytes, signature: str, secret: str) -> bool: expected = hmac.new( secret.encode("utf-8"), body, hashlib.sha256 ).hexdigest()
sig = signature.removeprefix("sha256=")
return hmac.compare_digest(expected, sig)
@app.route("/webhooks/floopy", methods=["POST"])def handle_webhook(): signature = request.headers.get("X-Floopy-Signature", "") body = request.get_data()
if not verify_webhook_signature(body, signature, FLOOPY_WEBHOOK_SECRET): abort(401, "Invalid signature")
event = request.get_json() print(f"Received {event['event']} event:", event["data"])
return "OK", 200package main
import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "io" "net/http" "strings")
func verifyWebhookSignature(body []byte, signature, secret string) bool { mac := hmac.New(sha256.New, []byte(secret)) mac.Write(body) expected := hex.EncodeToString(mac.Sum(nil))
sig := strings.TrimPrefix(signature, "sha256=")
return hmac.Equal([]byte(expected), []byte(sig))}
func webhookHandler(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "Bad request", http.StatusBadRequest) return }
signature := r.Header.Get("X-Floopy-Signature") if !verifyWebhookSignature(body, signature, floopyWebhookSecret) { http.Error(w, "Invalid signature", http.StatusUnauthorized) return }
// Process the webhook event w.WriteHeader(http.StatusOK) w.Write([]byte("OK"))}require "openssl"require "sinatra"require "json"
FLOOPY_WEBHOOK_SECRET = ENV["FLOOPY_WEBHOOK_SECRET"]
def verify_webhook_signature(body, signature, secret) expected = OpenSSL::HMAC.hexdigest("SHA256", secret, body) sig = signature.delete_prefix("sha256=")
Rack::Utils.secure_compare(expected, sig)end
post "/webhooks/floopy" do body = request.body.read signature = request.env["HTTP_X_FLOOPY_SIGNATURE"] || ""
unless verify_webhook_signature(body, signature, FLOOPY_WEBHOOK_SECRET) halt 401, "Invalid signature" end
event = JSON.parse(body) puts "Received #{event['event']} event: #{event['data']}"
status 200 "OK"endSetup 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
- Go to Webhooks in the dashboard sidebar
- Click Create Endpoint
- Enter a name for the endpoint (e.g., “PagerDuty Alerts”)
- Enter the URL where Floopy should send events (must be publicly accessible HTTPS)
- Select the events you want to receive (
security_alert,alert_triggered,budget_exceeded) - Optionally add custom headers (e.g., authorization tokens for your endpoint)
- 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
2xxstatus 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:
- Create a Slack Incoming Webhook for your channel
- 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:
- Create a PagerDuty Events API v2 integration and copy the Integration Key
- 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:
- Accepts
POSTrequests withContent-Type: application/json - Verifies the
X-Floopy-Signatureheader (see HMAC Signature Verification) - Returns a
2xxstatus code within 10 seconds - 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:
| Attempt | Delay | Cumulative Time |
|---|---|---|
| 1 | Immediate | 0s |
| 2 | 1 second | ~1s |
| 3 | 5 seconds | ~6s |
| 4 | 25 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
200and 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_idfield to deduplicate events in your handler - Store processed
delivery_idvalues in a cache or database with a TTL matching the retry window (~1 minute)