Skip to content

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.

  1. Open Settings → Integrations, find the Custom webhooks card, and click Manage.
  2. Click Add connection.
  3. 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: Basic header.
      • 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-Verification header on every call to this connection (see Verifying the caller below). When off, Fibly never attaches the header to calls on this connection.
  4. 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:

  1. Click Add tool on the connection's page.
  2. 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 methodGET, POST, PUT, PATCH, or DELETE.
    • 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, or Enum with a fixed list of values), whether it's required, and a description.
    • Requires confirmation — see below.
    • Requires identity verification — see below.
  3. 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.
  4. 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 for POST/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-Verification header 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:

ClaimTypeAlways present?Meaning
issstringalwaysAlways the literal string "fibly".
audstring (UUID)alwaysThe id of the connection the call belongs to.
tenant_idstring (UUID)alwaysYour 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).
iatnumber (Unix timestamp)alwaysWhen the token was issued.
nbfnumber (Unix timestamp)alwaysSame as iat — the token isn't valid before this time.
expnumber (Unix timestamp)alwaysExpiry, about 120 seconds after iat. The token is meant to authenticate this one call, not to be replayed later.
jtistring (hex)alwaysA unique token id, useful if you want to reject an exact replay.
conversation_idstring (UUID) or nullalways (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.
languagestringalwaysThe bot's active language for this call, e.g. en or pl.
tool_namestringalwaysThe name of the tool that was called.
tool_call_idstring (hex)alwaysA unique id for this specific call, useful for correlating with your own logs.
channelstringonly inside a real conversationThe channel the conversation is on: chat (the website widget), email, whatsapp, facebook, or instagram.
email_verifiedbooleanonly inside a real conversationWhether 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_dataobjectonly inside a real conversationPer-channel context, nested one level under the key matching channel — see Channel data below.
customer_idstring (UUID)only when the conversation has an identified customerFibly's internal id for this customer.
namestring or nullonly when the conversation has an identified customerThe customer's name, if known.
emailstring or nullonly when the conversation has an identified customerThe customer's email on file. This is not the same as verified — see the warning below.
phonestring or nullonly when the conversation has an identified customerThe 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:

channelkey in channel_datafields
chat (website widget)chatip, user_agent, browser_name, browser_version, os_name, os_version, and geo country_code/country/city
emailemailemail
whatsappwhatsappphone
facebookfacebookpsid (the Messenger page-scoped user id)
instagraminstagramigsid (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_data is 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 claim is not automatically verified. It reflects whatever email Fibly has on file for the customer, whether or not it's been proven. Before you trust an email claim for anything sensitive, check email_verified is true. If your tool is marked Requires identity verification in the dashboard, Fibly guarantees email_verified is true by the time your webhook is ever called for that tool — see the next section.

Verifying the token, step by step

  1. Read the X-Fibly-Verification header 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).
  2. 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 kid matches.
  3. Verify the token's RS256 signature against that key.
  4. Validate exp (not expired), iss (equals "fibly"), and aud (equals this connection's Connection ID, also copied from the Signing keys panel). Always check aud, 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 the tenant_id claim equals your workspace id, as extra defense.
  5. Only once the signature and standard claims check out, trust the rest of the payload.
  6. 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.

  1. 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 a verification_required status to the model, which asks the customer to verify their email (Fibly's own magic-link flow) and retries once they do.
  2. Once verified, Fibly calls:
    GET https://api.example.com/orders/12345
    Authorization: Bearer <your connection's token>
    X-Fibly-Verification: <jwt>
    
  3. Your server verifies the JWT as above. Because this tool requires verification, email_verified is guaranteed true, and email is the visitor's verified address, e.g. jane@example.com.
  4. Your server looks up order 12345 in your own database and compares its owner's email to the token's verified email. If they match, return the status:
    { "order_ref": "12345", "status": "shipped" }
    
    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.

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).

PyJWT
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-Verification until 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 email on its own. Only treat it as verified when email_verified is true.
  • 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.