Webhooks
Floe uses webhooks in two directions:
Events Floe sends you — register a URL and Floe POSTs it when something happens on your account (an agent is provisioned, a loan nears liquidation, a call ends, a marketplace payment settles). Use these to drive activation checklists, audit logs, and spend alerts instead of polling.
Cost you report to Floe (Reconcile Mode) — get every call's cost onto one ledger so one budget spans your whole stack. On hosted platforms (Vapi, Retell, Bland) you point the platform's own end-of-call cost webhook at Floe. On self-hosted stacks (Pipecat, LiveKit) there is no such webhook — your agent self-reports each call's cost to Floe. Enforcement then differs by platform (see Connect your orchestrator).
Base URL: https://credit-api.floelabs.xyz
Events Floe sends you
Event catalog
Floe emits 46 events across seven categories. The tables below are a snapshot — the live catalog is GET /v1/developer/webhooks/events (or floe webhooks events from the CLI), which returns every event's name, title, description, category, and scope dimension. Treat that endpoint as the source of truth; new events appear there first.
Every delivery is a JSON POST with the shape { "event": "<name>", ...fields, "firedAt": "<ISO 8601>" }.
Loan events
Routed on the loan ID — subscribe with loan scope to follow one loan, or global for all.
loan.health_warning
A loan's collateral ratio is approaching its liquidation threshold — top up collateral or repay to avoid liquidation
loan.expiry_warning
A loan is nearing its maturity date and is still outstanding
loan.overdue
A loan passed its maturity date without being repaid
loan.liquidated
A loan was liquidated — collateral was seized to cover the outstanding debt
loan.repaid
A loan was fully repaid and closed
Agent lifecycle events
Routed on the agent's wallet address.
agent.created
A new agent finished provisioning and is ready to spend
agent.suspended
An agent was suspended by a spend-policy kill-switch — its requests return 403 until you reactivate it
key.created
A new API key was minted for an agent
key.rotated
An API key was rotated; the previous key no longer authenticates
x402.first_settlement
The agent's first x402 payment settled — the activation milestone for a new integration
provider_key.created
A bring-your-own provider key was stored
provider_key.updated
A stored provider key was enabled, disabled, or otherwise modified
provider_key.deleted
A stored provider key was removed
Credit events
Threshold crossings on an agent's credit utilization, routed on the agent's wallet address.
credit.warning
Credit utilization crossed one of your subscribed thresholds from below
credit.at_limit
Utilization crossed a threshold at or above 95% — the agent is effectively out of credit
credit.recovered
Utilization dropped back below a previously crossed threshold
Call events
Voice call lifecycle, routed on the agent's wallet address. Delivery rows carry the provider call ID (or Twilio CallSid) as the correlation ID.
call.started
A Floe Phone call began (Floe-provisioned numbers only)
call.ended
A voice call finished — includes duration and settled cost legs where available
call.report.ready
The post-call report is available — a sanitized summary and transcript extract from the voice provider
call.recording.ready
The voice provider published a recording URL for a finished call
call.analyzed
Post-call analysis arrived from the voice provider (success evaluation, structured data, sentiment)
call.rejected
An incoming call was denied before it started (spend policy, budget, or a fail-closed guard)
Phone number events
Rented number lifecycle, routed on the agent's wallet address.
phone.number.grace
A rented phone number entered its expiry grace period — renew it to keep the number
phone.number.released
A rented phone number was released and is no longer attached to your agent
Marketplace events
Vendor spend events, routed on the agent's wallet address — except the two marketplace.vendor.* events, which are platform-wide broadcasts delivered to every subscribed webhook regardless of scope.
marketplace.job.completed
An asynchronous marketplace vendor job finished — carries the job ID as the correlation ID
marketplace.payment.settled
A marketplace request's payment settled — fired per call, on either the x402 or credit rail
marketplace.spend_cap.hit
An agent hit a marketplace spend cap and the request was blocked
marketplace.tripwire.triggered
A metering anomaly tripwire fired (for example an STT duration divergence) — informational only, nothing is auto-suspended
marketplace.vendor.degraded
A marketplace vendor's health probe flipped to down — platform-wide broadcast
marketplace.vendor.recovered
A previously degraded marketplace vendor is healthy again — platform-wide broadcast
Payloads never contain plaintext key material — only a masked keyPrefix.
Billing & invoicing events
Account-level events — no agent attribution. They reach a global webhook, or a wallet/agent webhook whose scopeValue is set to your account's own wallet address (the 0x... value the Scopes table describes — for these events, your account wallet rather than an agent's). Payloads carry the public acct_… account ID, never a wallet address.
billing.plan.changed
Your account's effective plan changed — an upgrade, downgrade, cancellation, or an admin-assigned plan starting or expiring
billing.payment_failed
A payment for your Floe plan failed or needs action — update your payment method to keep the plan
billing.invoice.paid
A Floe plan invoice was paid — carries the amount and the hosted invoice link
billing.renewal_upcoming
Your plan renews soon — carries the amount due and the end of the current period
billing.usage_threshold
Month-to-date tracked spend crossed 80% or 100% of your plan's cap — informational, nothing is blocked
client_invoice.sent
A client invoice was sent through your connected Stripe account
client_invoice.paid
A client paid an invoice issued from your connected Stripe account
client_invoice.voided
A client invoice was voided in Stripe and will not be collected
client_invoice.uncollectible
A client invoice was marked uncollectible in Stripe
vendor_actuals.connection.created
A vendor billing connection was added
vendor_actuals.connection.updated
A vendor billing connection was enabled, disabled, or reconfigured
vendor_actuals.connection.deleted
A vendor billing connection was removed
vendor_actuals.invoice.footed
A vendor invoice was footed against your account
vendor_actuals.close_gate_overridden
An account owner closed a billing period while some vendor costs were still unconfirmed
stripe.connected
A Stripe account was connected for client invoicing
stripe.disconnected
The connected Stripe account was disconnected — client invoicing pauses until it is reconnected
Wildcard subscriptions
The events array accepts three forms:
Exact names —
"call.ended","loan.liquidated"The global wildcard —
"*"subscribes to every eventPrefix wildcards —
"<prefix>.*", matched at every dot level:"call.*"covers all six call events (includingcall.report.ready), and"call.report.*"is also valid
A prefix wildcard must cover at least one catalog event — a typo like "lone.*" is rejected at creation instead of silently never matching. Wildcard subscriptions automatically pick up new events added under the prefix later.
Scopes
Each webhook is scoped to control which events reach it:
Scope
Description
scopeValue
global
Every event on your account
Not accepted
wallet
Only events for one agent
The agent's wallet address (0x...)
agent
Only events for one agent (synonym of wallet)
The agent's wallet address (0x...) — never the numeric agent ID
loan
Only events for one loan
The numeric loan ID
wallet and agent behave identically — both filter on a 0x... wallet address; agent is the value the dashboard's per-agent screen uses. For an agent-scoped event that address is the event's agent wallet. For an account-scoped event (the billing & invoicing category) it is your account's own wallet — the single wallet those events resolve to — so to receive billing events on a wallet/agent webhook, set its scopeValue to your account wallet. loan filters on the event's loan ID. A global webhook receives everything it subscribes to, account-scoped events included.
Two exceptions ignore scope entirely: marketplace.vendor.degraded and marketplace.vendor.recovered are platform-wide broadcasts sent to every webhook subscribed to them.
Scope is immutable.
scopeandscopeValuecannot be changed after creation — to re-scope, delete the webhook and create a new one.
Registering webhooks
Via the dashboard
Click Create Webhook
Enter your endpoint URL (must be HTTPS)
Select the events you want to receive — per-category select-all covers a whole group
Choose scope:
global,wallet,agent, orloanClick Create — your webhook secret is displayed once
Copy the secret immediately. You need it to verify webhook signatures.
Via the API
Response:
The secret is shown only at creation (and when you rotate it). Store it securely.
API endpoints
All endpoints require a developer key (floe_live_*) in the Authorization header, or a dashboard session. Agent keys (floe_*) are rejected on every /v1/developer route.
POST /v1/developer/webhooks
Register a new webhook endpoint. Maximum 10 webhooks per account.
url
string
Yes
Endpoint to receive events — max 2048 chars; HTTPS required in production, and private/internal addresses are rejected
events
string[]
Yes
At least one event name, *, or <prefix>.* wildcard
scope
string
Yes
global, wallet, agent, or loan
scopeValue
string
Conditional
Required for non-global scopes: a 0x wallet address for wallet/agent, a numeric loan ID for loan. Must be absent for global
description
string
No
Human-readable label, max 256 chars
GET /v1/developer/webhooks
List all registered webhooks. Secrets are never included.
GET /v1/developer/webhooks/events
The live event catalog — every subscribable event with its name, title, description, category, and scope dimension. This is the source of truth for what you can subscribe to; the CLI equivalent is floe webhooks events.
Response (truncated):
GET /v1/developer/webhooks/:id
Fetch a single webhook, including a rollup of its delivery outcomes.
Response:
PATCH /v1/developer/webhooks/:id
Update a webhook's url, events, active state, or description. scope and scopeValue are not updatable — recreate the webhook to change them. URL updates pass the same HTTPS and private-address checks as creation.
DELETE /v1/developer/webhooks/:id
Delete a webhook. No further events are sent after deletion.
POST /v1/developer/webhooks/:id/test
Send a test delivery to your endpoint. The payload's event is "test" and uses realistic-looking placeholder data. Test deliveries are one-shot — a failed test is never retried (and cannot be retried manually).
POST /v1/developer/webhooks/:id/rotate-secret
Generate a new HMAC secret. The old secret stops working immediately, and the new one is returned only in this response. Update your server's verification logic before rotating in production.
Response:
GET /v1/developer/webhooks/:id/deliveries
Recent deliveries for one endpoint. Query params: limit (default 50, max 100) and offset.
Response:
POST /v1/developer/webhooks/:id/deliveries/:deliveryId/retry
Manually re-send a past delivery. The retry is signed with a fresh timestamp so your replay-protection window accepts it, and it keeps the original delivery's agentWallet and correlation ID so it stays findable under the same log filters. Test deliveries cannot be retried.
Delivery logs
Beyond the per-endpoint list, Floe keeps an account-wide delivery log across all your webhooks — the same data behind the dashboard's Logs tab. Use it to answer "did my server get the call.ended for this call?" without knowing which endpoint it went to. The CLI equivalent is floe webhooks logs, which takes the same filters.
Delivery logs are retained for 30 days.
GET /v1/developer/webhook-deliveries
Filterable, cursor-paginated log of every delivery on your account, newest first.
endpoint
Numeric webhook ID — only deliveries to that endpoint
event
Exact event name (e.g. call.ended — wildcards apply to subscriptions, not this filter)
agent
A 0x agent wallet address
status
pending, success, failed, or retrying
from / to
ISO 8601 timestamp bounds on the delivery time
id
Searches both delivery IDs and correlation IDs — pass a call session ID, Twilio CallSid, job ID, or loan ID to find every delivery for that call, job, or loan
cursor
Opaque pagination cursor — pass a previous page's nextCursor verbatim
limit
Page size, default 50, max 100
A filtered query — failed call.ended deliveries for one agent:
Response:
List rows are deliberately lightweight — they carry no payloads or response bodies. When hasMore is true, pass nextCursor back as cursor for the next page.
GET /v1/developer/webhook-deliveries/:deliveryId
Full detail for one delivery. The path parameter is the hex deliveryId (the value in X-Floe-Delivery-Id and in list rows) — not the numeric row id.
The response includes everything from the list row plus:
payload
Exactly what was POSTed to your endpoint
responseBody
Your server's response body, sanitized and capped at 1 KB
nextRetryAt
When the next automatic retry is scheduled, if the delivery is retrying
Payload format
Every delivery is a JSON POST to your endpoint. Fields are spread at the top level alongside event and firedAt:
Alongside each delivery, the log records the agent wallet and a correlation ID (a call session ID, Twilio CallSid, job ID, or loan ID) so you can trace a delivery back to the thing that fired it.
Signature verification
Every delivery includes three headers:
X-Floe-Signature
Hex HMAC-SHA256 of {timestamp}.{raw_body} using your webhook secret
X-Floe-Timestamp
Unix timestamp (seconds) when the event was sent
X-Floe-Delivery-Id
ID for this delivery, stable across its retry attempts — use it for idempotency
The signature is computed as:
Always verify the signature against the raw request body before processing the event. This prevents forged requests from reaching your business logic.
See the full handler examples: TypeScript | Python
Delivery guarantees
At-least-once delivery. Your endpoint may receive the same event more than once. Use
X-Floe-Delivery-Idfor idempotency.10-second timeout. Your endpoint must respond within 10 seconds or the attempt counts as failed. Return
2xximmediately and process asynchronously.3 attempts maximum. If your endpoint does not respond with a
2xxstatus code, Floe retries: attempt 2 fires 60 seconds after the first failure, attempt 3 fires 300 seconds after that.Delivery statuses:
pending(queued) →success(2xx received), orretrying(a failed attempt with retries remaining) →successorfailed(all 3 attempts exhausted).Test deliveries are one-shot. A failed test stays
failed— it is never retried, automatically or manually.
After a delivery goes failed, re-drive it manually with POST /v1/developer/webhooks/:id/deliveries/:deliveryId/retry. Inspect failures in the dashboard, the per-endpoint list, or the account-wide delivery log — logs are retained for 30 days.
Connect your orchestrator (Reconcile Mode)
The webhooks above are events Floe sends you. Reconcile Mode is the other direction: it gets every call's cost onto one ledger. How the cost reaches Floe depends on where the agent runs:
Hosted (Vapi, Retell, Bland) — Floe ingests the platform's own end-of-call cost webhook. You paste the
call-endURL into the platform.Self-hosted (Pipecat, LiveKit) — there is no platform cost webhook. Your agent self-reports each call's cost to Floe's
call-endURL, signed withX-Floe-Signature.BYOK / off-path LLM & tool spend — for priced calls Floe never routes (your own provider key, a self-hosted model), the
floe-guardlibrary can push its local spend ledger to Reconcile Mode viaPOST /v1/agents/ledger/sync. See the ledger sync guide.
Enforcement at the cap is also platform-specific — Floe can only stop a call where it sits in the path:
Vapi & Retell — a pre-call webhook can deny the next inbound call, but only for the configured number/assistant path and only when you've set the pre-call URL.
Pipecat & LiveKit — the pre-call check is cooperative: your agent must call
preCallUrlbefore dialing and honor the{ "allowed": false }response. Floe cannot enforce it for you, and any leg not on a Floe key can bypass it.Bland — no pre-call hook. A reconciled breach suspends the agent, which hard-blocks its subsequent Floe-keyed actions (LLM / STT / TTS / telephony on a Floe key) rather than rejecting the inbound call.
So a runaway campaign is stopped before the next call on Vapi/Retell, at the next Floe-keyed action on Bland, and on Pipecat/LiveKit only if the agent honors the cooperative check.
This works with Vapi, Retell, Bland, Pipecat, and LiveKit. The full per-platform setup — where to paste each URL, provider-specific fields, and the pre-call deny responses — lives in the guide:
→ Voice orchestrators — connect & reconcile
What follows is the reference.
1. Register a connection
Connecting an orchestrator is an admin action — do it from the dashboard, or via the API with an admin dashboard session:
agentId
number
Yes
The agent this connection meters against
provider
string
Yes
vapi, retell, bland, pipecat, or livekit
secret
string
Conditional
The platform's signing secret — required for retell and bland; for vapi, pipecat, and livekit Floe mints one and returns it once
label
string
No
Human-readable label
Response (201):
Manage connections with GET /v1/developer/orchestrators, POST /v1/developer/orchestrators/:id/rotate (mints a fresh token + secret), PATCH /v1/developer/orchestrators/:id ({ "active": false }), and DELETE /v1/developer/orchestrators/:id. Rotating invalidates the old URL token and secret immediately, so update the URLs everywhere you pasted them — and for retell/bland, update the signing credential in the provider's dashboard — before the next call, or deliveries fail 401.
2. Paste the URLs into your platform
The token in each URL identifies the connection — treat the URLs as secrets and rotate if leaked.
Call-end
POST /v1/webhooks/{provider}/call-end/{token}
Ingests the call's cost at end-of-call; meters it onto the ledger (counts against policies, does not debit balance)
Pre-call
POST /v1/webhooks/{provider}/pre-call/{token}
Admission gate before a call connects. Returns whether the agent may proceed. null for Bland (no pre-call hook)
Ingest is idempotent per call ID — safe for provider retries.
3. Inbound signature verification
Authentication is provider-specific. The path token always identifies the connection; how the request is authenticated depends on the provider — Retell, Bland, Pipecat, and LiveKit sign the raw request body with HMAC, while Vapi compares a shared-secret header (X-Vapi-Secret) independent of the body. A bad or missing signature is rejected 401 with no ledger write; an unknown or disabled token is 404.
vapi
X-Vapi-Secret
Equality against your stored secret
retell
x-retell-signature
v={ts},d={hex} — HMAC-SHA256 of body + ts, within a ±5-minute window
bland
x-webhook-signature
Hex HMAC-SHA256 of the raw body with your stored secret
pipecat / livekit
x-floe-signature
Hex HMAC-SHA256 of the raw body with the Floe-minted secret
4. Pre-call gate response
The pre-call endpoint decides admission from server-side state (agent status + budget). Deny reasons include unknown_agent, agent_suspended, and budget_exceeded; it fails closed if the check can't run. Responses are shaped to each platform's contract — e.g. Retell receives { "call_inbound": { "reject": true } } on deny, and Pipecat/LiveKit receive { "allowed": false, "reason": "budget_exceeded" }. On Vapi, admission applies to the configured assistant/number path (an admit returns { "assistantId": … }). On self-hosted stacks (Pipecat, LiveKit) the gate only bites if your agent actually calls preCallUrl before dialing and honors the response — Floe cannot block the call for you. See the guide for the exact per-platform bodies.
Testing
Dashboard
Click Send Test Event on any webhook in the dashboard. The test payload looks like a real event but does not correspond to an actual one.
webhook.site
For quick testing without deploying a server:
Go to webhook.site and copy your unique URL
Register that URL as a webhook endpoint
Send a test event from the dashboard
Inspect the payload and headers on webhook.site
Best practices
Always verify signatures. Never process a payload without checking
X-Floe-Signatureagainst the raw body. This prevents spoofed requests.Respond with
2xxquickly. Return a200as soon as you receive the payload — the delivery times out after 10 seconds. Process the event asynchronously in a background job or queue.Implement idempotency. Store
X-Floe-Delivery-Idand skip duplicates. Retries can send the same event more than once.Use HTTPS endpoints. Floe only delivers to HTTPS URLs in production.
Monitor delivery logs. Check the dashboard,
GET /v1/developer/webhooks/:id/deliveries, or the account-wideGET /v1/developer/webhook-deliverieslog (floe webhooks logs) periodically for failed deliveries.Prefer wildcards over long event lists. Subscribing to
call.*keeps you current as new call events ship; enumerate exact names only when you need to exclude some.Rotate secrets with a short window.
rotate-secretreturns the new secret and invalidates the old one immediately — there is no overlap period. Install the returned secret in your verifier right away; any deliveries signed with the old secret during the swap fail signature and retry (3 attempts over ~6 minutes), so keep the window short.
Next steps
Voice orchestrators — the full connect-and-reconcile walkthrough for Vapi / Retell / Bland / Pipecat / LiveKit.
Webhook Handler (TypeScript) — Express.js example with signature verification.
Webhook Handler (Python) — Flask example with HMAC verification.
Floe CLI —
floe webhooksmanages endpoints, tests deliveries, and tails logs from the terminal.API Keys — create your developer key to register webhooks.
Developer Dashboard — manage webhooks through the web UI.
Last updated
