Resources
Webhooks
Floo POSTs JSON events to your endpoint as calls progress. Configure the URL, secret, and event subscriptions per agent from the dashboard.
Headers we send
Content-Type: application/jsonUser-Agent: FlooAI-Webhook/1.0X-Floo-Event- the event name, e.g.call_endedX-Floo-Timestamp- ISO 8601 send timeX-Floo-Signature-sha256=<hex>HMAC of the raw body (only when a secret is configured)
Signature verification
We sign every request with HMAC-SHA256 using the agent's webhook secret. Recompute the HMAC server-side from the raw body and compare in constant time.
700 font-semibold">import crypto 700 font-semibold">from "node:crypto";
700 font-semibold">export 700 font-semibold">function verifyFloo(
rawBody: string,
signatureHeader: string | 700 font-semibold">null,
secret: string,
): boolean {
700 font-semibold">if (!signatureHeader) 700 font-semibold">return 700 font-semibold">false;
700 font-semibold">const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
700 font-semibold">const provided = signatureHeader.replace(/^sha256=/, "");
700 font-semibold">const a = Buffer.700 font-semibold">from(expected, "hex");
700 font-semibold">const b = Buffer.700 font-semibold">from(provided, "hex");
700 font-semibold">if (a.length !== b.length) 700 font-semibold">return 700 font-semibold">false;
700 font-semibold">return crypto.timingSafeEqual(a, b);
}700 font-semibold">import hmac
700 font-semibold">import hashlib
700 font-semibold">def verify_floo(raw_body: bytes, signature_header: str, secret: str) -> bool:
700 font-semibold">if 700 font-semibold">not signature_header:
700 font-semibold">return 700 font-semibold">False
expected = hmac.new(
secret.encode("utf-8"),
raw_body,
hashlib.sha256,
).hexdigest()
provided = signature_header.removeprefix("sha256=")
700 font-semibold">return hmac.compare_digest(expected, provided)Retry policy
We expect a 2xx response within 5 seconds. Any 5xx or network/timeout failure is retried once after a 5-minute delay; non-2xx 4xx responses are marked failed and not retried (your endpoint rejected it for a reason). Every attempt is logged in the dashboard and replayable from the deliveries log.
Events
call_startedFired when an agent connects to a caller. Use to log the call into your CRM and start any real-time UI.
Payload
{
"event": "call_started",
"agent_id": "agt_2k9f1a",
"workspace_id": "ws_4p2j1k",
"timestamp": "2026-05-15T17: 55: 00Z",
"data": {
"call_id": "call_z8h2k1",
"direction": "inbound",
"from_number": "+15551234567",
"to_number": "+15557654321",
"started_at": "2026-05-15T17: 55: 00Z"
}
}call_endedFired when a call disconnects for any reason. Includes duration and end_reason. Sentiment may be null if extraction has not yet completed.
Payload
{
"event": "call_ended",
"agent_id": "agt_2k9f1a",
"workspace_id": "ws_4p2j1k",
"timestamp": "2026-05-15T17: 56: 27Z",
"data": {
"call_id": "call_z8h2k1",
"duration_seconds": 87,
"end_reason": "hangup",
"sentiment": "positive",
"successful": true
}
}transcript_turnFired for each finalized turn (one per speaker change). High volume - only subscribe if you need live transcription.
Payload
{
"event": "transcript_turn",
"agent_id": "agt_2k9f1a",
"workspace_id": "ws_4p2j1k",
"timestamp": "2026-05-15T17: 55: 03Z",
"data": {
"call_id": "call_z8h2k1",
"role": "user",
"text": "Hello, I'd like to book an appointment.",
"started_at_ms": 1200
}
}extraction_completeFired after the post-call LLM extraction finishes. The `fields` shape mirrors whatever extraction schema you configured on the agent.
Payload
{
"event": "extraction_complete",
"agent_id": "agt_2k9f1a",
"workspace_id": "ws_4p2j1k",
"timestamp": "2026-05-15T17: 56: 45Z",
"data": {
"call_id": "call_z8h2k1",
"fields": {
"summary": "Caller asked about pricing and booked a demo.",
"success": true,
"sentiment": "positive"
}
}
}transfer_initiatedFired when the agent attempts a live transfer (warm or cold). The destination number is included so you can log routing.
Payload
{
"event": "transfer_initiated",
"agent_id": "agt_2k9f1a",
"workspace_id": "ws_4p2j1k",
"timestamp": "2026-05-15T17: 55: 42Z",
"data": {
"call_id": "call_z8h2k1",
"to_number": "+15559876543",
"reason": "caller_requested_human"
}
}call_failedFired when an outbound dial cannot connect: no_answer, busy, voicemail (when AMD is on), or carrier error.
Payload
{
"event": "call_failed",
"agent_id": "agt_2k9f1a",
"workspace_id": "ws_4p2j1k",
"timestamp": "2026-05-15T17: 55: 15Z",
"data": {
"call_id": "call_z8h2k1",
"error": "no_answer"
}
}