Custom webhook tools
Turn your own HTTP API into agent tools — build connections and tools in the dashboard, then verify the signed request on your server.
Experimental feature
Custom webhooks are in controlled rollout. If you don't see the Custom webhooks card under Settings → Integrations, ask your Fibly contact to enable it for your workspace.
Custom webhooks let you turn any HTTP API you already run into a tool your bot can call during a conversation — checking an order's status, looking up a customer record, opening a support ticket, or anything else your backend can do over HTTP. Unlike the ready-made integrations (Calendly, PrestaShop), this is a self-serve, no-Fibly-engineering way to connect your own systems — but it does need a developer on your side to build the receiving endpoint and, for the parts covered in this guide, to verify that a call genuinely came from Fibly.
When to use it
Reach for a custom webhook tool when you want the bot to call a system that has no dedicated Fibly integration: your own order management API, an internal ticketing system, a booking backend, a CRM, anything reachable over HTTPS. If the data or action already fits an existing integration (Calendly, PrestaShop), use that instead — it needs no code on your end.
Building a connection
A connection is one of your backend services: its base URL and the static credentials the bot uses to call it. Every tool you define lives under a connection and calls a path relative to its base URL.
- Open Settings → Integrations, find the Custom webhooks card, and click Manage.
- Click Add connection.
- Fill in:
- Name — a label for your own reference (e.g. "Order lookup API").
- Base URL — your API's root, e.g.
https://api.example.com. It must be a publicly reachable HTTPS URL — local or private addresses, and unencrypted (non-HTTPS) traffic, are rejected. - Authentication — how Fibly authenticates to your API:
- No authentication — no credentials sent.
- Basic auth — a username and password, sent as an
Authorization: Basicheader. - Bearer token — a single token, sent as
Authorization: Bearer <token>.
- Send verification token — off by default. Enable this if you want Fibly to send the
signed
X-Fibly-Verificationheader on every call to this connection (see Verifying the caller below). When off, Fibly never attaches the header to calls on this connection.
- Save. The password/token field is write-only: once saved, it's never shown again in the dashboard.
The first connection you create also generates your workspace's signing key automatically (see Verifying the caller below).
Building a tool
Under a connection, add one or more tools — each maps to a single HTTP call:
- Click Add tool on the connection's page.
- Fill in:
- Tool name — the identifier the agent uses internally to call this tool, e.g.
get_order_status. Letters, numbers, and underscores only, and it must be unique across all your connections. - HTTP method —
GET,POST,PUT,PATCH, orDELETE. - Path — relative to the connection's base URL, e.g.
/orders/{order_ref}. Wrap a segment in{ }to reference a parameter (see below) — it's substituted with the argument value and URL-encoded before the call. - Description — the prompt the AI model reads to decide when and how to call this tool. Describe what it does and when it's useful; the model never sees this text rendered to the customer.
- Parameters — the arguments the model can fill in. Add each with a name, a type
(
String,Number,Integer,Boolean, orEnumwith a fixed list of values), whether it's required, and a description. - Requires confirmation — see below.
- Requires identity verification — see below.
- Tool name — the identifier the agent uses internally to call this tool, e.g.
- The tool editor has an English/Polish language switch: the description and each parameter's description are localized, so the model reasons in whichever language the conversation is in. An English description is required; other languages fall back to it if left blank.
- Save, and toggle Enabled on when you're ready for the bot to use it live.
How the agent calls your webhook
When the model decides to use a tool, Fibly builds and sends the HTTP request for you:
- Any
{name}placeholders in the path are filled from the matching argument (URL-encoded). - Whatever arguments are left over go to the query string for
GET/DELETE, or the JSON body forPOST/PUT/PATCH. - Your connection's authentication (Basic/Bearer) is applied automatically.
- If the connection's Send verification token setting is on, a signed
X-Fibly-Verificationheader is attached — see the next section. It's off by default, so unless you've turned it on, your webhook won't receive this header at all. - The call has a fixed ~10 second timeout, and Fibly does not follow redirects — if your API 30x-redirects, update the connection's base URL instead of relying on a redirect.
What the agent sees back
- A 2xx response with a JSON body is handed to the model as structured data it can use in its answer. A 2xx with a non-JSON body is passed through as plain text.
- A non-2xx response, a timeout, or a connection error is treated as a tool failure — the model is told the call didn't succeed and adapts its answer accordingly (it doesn't see your raw status code or body in that case).
Design your webhook's success responses to be small and directly useful — whatever you return in a 2xx JSON body is what the model works with to answer the customer.
Verifying the caller (X-Fibly-Verification)
Turn on Send verification token on a connection (off by default — see
Building a connection above) and every call on that connection carries a
signed JSON Web Token in the X-Fibly-Verification header. This is the part your developer needs
to implement, and it matters even if your API also requires its own Basic/Bearer credentials: those
credentials only prove the request came from Fibly's backend somewhere; the signed token proves
it came from your tenant's configured connection, and carries the conversation and customer
context you need to make authorization decisions. Once you turn the toggle on, Fibly sends the
header on every call to the connection — your server verifying it is a separate step, covered
below; until you build that verification, the header just arrives unused.
Why it exists. Fibly cannot know your data model, so it cannot check, for example, that the order a customer is asking about actually belongs to them. What it can do is hand you a cryptographically signed, tamper-proof statement of who the visitor is (as far as Fibly's own verification goes) and what conversation the call belongs to — so your server can perform that ownership check itself.
The JWKS endpoint
Fibly generates an RSA-2048 signing keypair for your workspace the first time you create a
connection. The private key never leaves Fibly; the public key is published at your workspace's
JWKS endpoint — a public, unauthenticated URL that returns your active (and any recently
rotated) public keys in the standard {"keys": [...]} JWKS format.
Copy your JWKS URL from the dashboard — don't try to build it yourself. It's fixed for your
workspace, shown alongside the Key ID and Connection ID on the connection's Signing
keys panel, next to a Download public key (PEM) button and a Rotate key action. Copy the
JWKS URL into the JWKS_URL constant and the Connection ID into the CONNECTION_ID constant in
the code samples below — both belong in your server's hardcoded configuration, not something you
compute from a request. Rotating immediately starts signing new tokens with a new key, but the
previous key stays published in the JWKS for a short overlap window so any tokens already in
flight still verify — fetch keys by kid and don't hardcode a single key's contents, or
rotation will break your verification.
Claims reference
The token is a compact JWS, algorithm RS256, with the signing key's id in the JWT header as
kid. Its payload:
| Claim | Type | Always present? | Meaning |
|---|---|---|---|
iss | string | always | Always the literal string "fibly". |
aud | string (UUID) | always | The id of the connection the call belongs to. |
tenant_id | string (UUID) | always | Your workspace id. Useful as an optional extra check that a token was signed for your workspace — don't use it to build the JWKS URL; that URL is fixed, copy it from the dashboard (see The JWKS endpoint above). |
iat | number (Unix timestamp) | always | When the token was issued. |
nbf | number (Unix timestamp) | always | Same as iat — the token isn't valid before this time. |
exp | number (Unix timestamp) | always | Expiry, about 120 seconds after iat. The token is meant to authenticate this one call, not to be replayed later. |
jti | string (hex) | always | A unique token id, useful if you want to reject an exact replay. |
conversation_id | string (UUID) or null | always (value may be null) | The conversation thread this call happened in. null for calls made outside a real conversation, such as the dashboard's Test action. |
language | string | always | The bot's active language for this call, e.g. en or pl. |
tool_name | string | always | The name of the tool that was called. |
tool_call_id | string (hex) | always | A unique id for this specific call, useful for correlating with your own logs. |
channel | string | only inside a real conversation | The channel the conversation is on: chat (the website widget), email, whatsapp, facebook, or instagram. |
email_verified | boolean | only inside a real conversation | Whether the visitor's email is trusted overall: they completed Fibly's own verification (magic link, including a returning visitor recognized via their browser's saved verification for up to 90 days), or the conversation came in over the email channel (an email sender is inherently a stronger identity signal). This is the single flag the Requires identity verification tool gate itself checks — see below. |
channel_data | object | only inside a real conversation | Per-channel context, nested one level under the key matching channel — see Channel data below. |
customer_id | string (UUID) | only when the conversation has an identified customer | Fibly's internal id for this customer. |
name | string or null | only when the conversation has an identified customer | The customer's name, if known. |
email | string or null | only when the conversation has an identified customer | The customer's email on file. This is not the same as verified — see the warning below. |
phone | string or null | only when the conversation has an identified customer | The customer's phone number, if known. |
The message transcript is never included, only conversation/customer metadata.
Channel data (channel_data)
channel_data carries context specific to the channel the conversation happened on. Only the
one key matching the channel claim is present — read it as
claims["channel_data"][claims["channel"]] rather than checking every channel's shape:
channel | key in channel_data | fields |
|---|---|---|
chat (website widget) | chat | ip, user_agent, browser_name, browser_version, os_name, os_version, and geo country_code/country/city |
email | email | email |
whatsapp | whatsapp | phone |
facebook | facebook | psid (the Messenger page-scoped user id) |
instagram | instagram | igsid (the Instagram-scoped user id) |
Only the chat channel carries IP address, user agent, browser/OS, and geolocation — that data
comes from the visitor's browser session on your website, which doesn't exist for the other
channels. Any field Fibly doesn't have for the call is null rather than omitted, so the shape for
a given channel is always the same.
channel_datais personal data. IP address, browser/OS fingerprint, email, phone number, and the Messenger/Instagram scoped ids are all personal data under most privacy regimes. Handle, store, and log it accordingly, and don't forward it anywhere you wouldn't forward the rest of the customer's contact details.
The
email_verifiedistrue. If your tool is marked Requires identity verification in the dashboard, Fibly guaranteesemail_verifiedistrueby the time your webhook is ever called for that tool — see the next section.
Verifying the token, step by step
- Read the
X-Fibly-Verificationheader from the incoming request. If it's missing, treat the call as unauthenticated — decide your own policy (reject it, or only serve non-sensitive data). - Fetch (and cache) your fixed JWKS URL — the one you copied from the Signing keys panel, not
one you compute — and, from the token's header, pick the key whose
kidmatches. - Verify the token's RS256 signature against that key.
- Validate
exp(not expired),iss(equals"fibly"), andaud(equals this connection's Connection ID, also copied from the Signing keys panel). Always checkaud, not just when you want to "pin" verification — it's what stops a token signed for a different connection from validating here. Optionally also check thetenant_idclaim equals your workspace id, as extra defense. - Only once the signature and standard claims check out, trust the rest of the payload.
- Perform your own resource-ownership check using the verified claims — Fibly does not know your data model and cannot do this for you.
Because a rotated key stays valid for a short overlap window, your JWKS client should cache
responses briefly and refetch on a kid it doesn't recognize, rather than caching a single key
forever.
Security note: never let the token tell you which keys to verify it with. Reading a tenant_id
(or any other claim) out of an unverified token and using it to build the JWKS URL you then fetch
is unsafe — a token signed with a different Fibly tenant's key would fetch and validate against
that tenant's JWKS, letting anyone who can obtain a signed token from their own (attacker-owned)
workspace impersonate calls to your webhook. Always verify against the one fixed JWKS URL you
copied from your connection's Signing keys panel, never a URL built from the token itself.
Worked example: an order-status tool
Say you build a get_order_status tool: GET /orders/{order_ref} on your Orders API connection,
with one required order_ref string parameter and Requires identity verification turned on.
The connection also has Send verification token turned on — without it, no
X-Fibly-Verification header would arrive at all, and steps 3-4 below wouldn't be possible.
- A customer asks "where's my order 12345?" The model has
order_ref="12345"but the visitor hasn't verified their email yet on this conversation. Because the tool requires verification, Fibly never calls your webhook — it returns averification_requiredstatus to the model, which asks the customer to verify their email (Fibly's own magic-link flow) and retries once they do. - Once verified, Fibly calls:
GET https://api.example.com/orders/12345 Authorization: Bearer <your connection's token> X-Fibly-Verification: <jwt> - Your server verifies the JWT as above. Because this tool requires verification,
email_verifiedis guaranteedtrue, andemailis the visitor's verified address, e.g.jane@example.com. - Your server looks up order
12345in your own database and compares its owner's email to the token's verifiedemail. If they match, return the status:
If they don't match — or the order doesn't exist — return a 404, not a 403: a 403 would confirm to an attacker that order 12345 exists at all.{ "order_ref": "12345", "status": "shipped" }
Code samples: verifying the token
Each sample below reads the header, fetches the JWKS (caching it), verifies the RS256 signature
by kid, checks exp/iss/aud, and performs the same email-ownership check as the worked
example. Replace JWKS_URL and CONNECTION_ID with the values shown in this connection's Signing
keys panel — both are hardcoded configuration, never derived from the token (see the security note
above).
import jwt
from jwt import PyJWKClient
# Copy both values from this connection's Signing keys panel and hardcode them as
# configuration — never derive JWKS_URL from a claim inside the (unverified) token
# itself, or a token signed by a different tenant's key could validate here.
JWKS_URL = "https://app.fibly.io/api/tool-integrations/custom-webhooks/<your-workspace-id>/jwks.json" # copy from the dashboard
CONNECTION_ID = "c1a2e3f4-5678-4abc-9def-0123456789ab" # "Connection ID" in the Signing keys panel
# PyJWKClient caches the JWKS response and refetches when it meets an unknown `kid`.
_jwks_client = PyJWKClient(JWKS_URL, cache_keys=True)
def verify_fibly_token(token: str) -> dict:
signing_key = _jwks_client.get_signing_key_from_jwt(token)
claims = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience=CONNECTION_ID,
issuer="fibly",
options={"require": ["exp", "iat", "iss", "aud"]},
)
return claims
def handle_get_order_status(order_ref: str, headers: dict) -> tuple[dict, int]:
token = headers.get("X-Fibly-Verification", "")
if not token:
return {"error": "missing verification token"}, 401
try:
claims = verify_fibly_token(token)
except Exception:
return {"error": "invalid verification token"}, 401
order = get_order(order_ref) # your own lookup
if order is None:
return {"error": "not found"}, 404
verified_email = (claims.get("email") or "").lower()
if not claims.get("email_verified") or order.customer_email.lower() != verified_email:
return {"error": "not found"}, 404 # 404, not 403 — don't confirm the order exists
return {"order_ref": order_ref, "status": order.status}, 200
Identity verification & confirmation
Two independent, per-tool toggles control how cautiously a tool is called:
Identity verification (requires_verification)
Turn on Requires identity verification for a tool that returns private data (order details,
account information, anything tied to one customer). While it's on, the agent can only call your
webhook once the visitor has proven ownership of their email — either via Fibly's own
verification link, or because the conversation itself came in over a verified email channel.
Until then, calling the tool returns a verification_required status to the model, which asks
the customer to verify and retries — your webhook is never called for an unverified attempt.
This only proves the caller controls an email address. It does not prove that the specific
resource being requested (e.g. this particular order) belongs to them — that check is always
yours to make, using the verified email/customer_id claims in the token, as shown in the
worked example above.
Confirmation (requires_confirmation)
Turn on Requires confirmation for a tool that changes something (creating a ticket, cancelling an order, anything you don't want fired silently). While it's on, the widget pauses and asks the customer to explicitly approve the specific call before it reaches your webhook. With it off, the tool fires immediately whenever the model decides to use it — appropriate for read-only lookups.
Testing your tool
Before enabling a tool for real conversations, open it in the builder and use the Test this
tool panel: enter sample arguments as JSON, then click Run test. This sends a real call
through the exact same path production traffic uses — SSRF-checked, and with a live
X-Fibly-Verification token attached if the connection's Send verification token setting is
on — and shows you the resulting status and response inline, so you can confirm your webhook is
wired up correctly before turning it on.
Security checklist
- Always verify the signature. Never trust the claims in
X-Fibly-Verificationuntil the RS256 signature checks out against your JWKS. - Check
exp. Reject expired tokens; most JWT libraries do this automatically once you decode with verification. - Enforce resource ownership yourself. Fibly proves who the verified visitor is; it does not know your data model, so it cannot confirm that the record they're asking about belongs to them.
- Don't trust
emailon its own. Only treat it as verified whenemail_verifiedistrue. - Use HTTPS on your endpoint — Fibly requires it (except for a workspace's own local development, which isn't reachable from the internet anyway).
- Protect your connection's secret. Treat your Basic/Bearer credential like a password; Fibly encrypts it at rest and never displays it again after you save it.
- Prefer 404 over 403 when an ownership check fails, so you don't confirm to a caller that a resource exists.
Related articles
- Tools overview: all the tools your bot can use.
- Conversations: see the actions the bot took, including custom tool calls.