For people done fighting embeds
Build your own front door.
Funal runs the rest.
Everything a scheduling and routing tool does, as an API. Qualify the visitor, route them to the right person, book real calendar time, and follow up until the call is held. You build the screen, in an evening, with the coding agent you already use.
Why / instead of an embed
The booking tool,
without the embed.
Scheduling and routing tools work. Installing them is the painful part: a widget that never matches your site, a flow you cannot change, and data that lives somewhere else. Building the screen yourself used to be the expensive option. It no longer is.
The screen
EmbedAn iframe or a script with a few colour settings. It never quite matches.
FunalYour own components, your own design. There is nothing of ours on the page.
The flow
EmbedTheir steps, their order, their redirect, their confirmation page.
FunalAny order you like. Ask first and book after, or book first. You decide.
Qualifying
EmbedStatic rules on form fields, often a higher plan or a second product.
FunalQuestions written per answer, turned into facts, with rules you edit as data.
The data
EmbedA webhook, then glue code to copy it into your own system.
FunalOne record: the person, their answers, the booking, how they arrived.
Testing
EmbedOne booking experience. You cannot compare two.
FunalRoute a share of traffic to a new front door and see which holds more calls.
After booking
EmbedA confirmation and a reminder.
FunalHello text, reminders, a brief for whoever takes the call, no-show recovery.
The core job is the same: qualify, route, book, follow up. If you rely on deep CRM routing rules or paid bookings, check the reference first. Payments are not live yet.
How it works / one family, end to end
From first question
to a held call.
Your app draws every screen. Funal decides what to ask, who qualifies, which times to offer, and what happens after they book. Nobody on your team updates a record.
Step 01 / Track
Know where
they came from.
page, track, identify: the Segment spec you already know, first-party on your own domain. One snippet instruments the whole journey, ad click to booked call to renewal, and stitches every anonymous session to the person behind it.
click_id and link_variant ride through redirects, hosted form tools, and cross-domain hops, so conversion by variant, or by anything you emit, is one query. First-party, invisible, never blocks a paint.
One line in your <head>
<script async src="https://go.funal.link/t.js" data-write-key="YOUR_WRITE_KEY"></script>Paste into your site's global <head>, on every page. Your write key (starts with fnl_wk_) namespaces events to your org: any agent connected in step two can fetch it for you, or ask the Funal team. Wix: Settings > Custom Code, apply to all pages, load in head. Webflow: Site Settings > Custom Code > Head Code. Async and runs once, so it's safe to ship anywhere.
Or hand it to your agent
The full setup prompt covers the platform quirks, the hidden-field carriage through hosted form tools, and the checks that prove events are landing.
# Setup prompt: install the Funal site tag
You are installing the Funal site tag on a business's marketing website. The
tag connects the site to Funal's ad-click attribution and journey tracking:
the ad click, page views, form submissions, and the eventual booking join
into one identity.
## What you need from the human before starting
1. Their Funal write key. It starts with fnl_wk_. If YOU have Funal's MCP
server connected, fetch it yourself with the track_write_key tool (it
also returns the exact snippet). Otherwise the human gets it from the
Funal team. Never invent one.
2. Which website platform they use (Wix, Webflow, or other).
3. If their forms live on a hosted form tool with its own domain (FormCrafts,
Typeform, etc.): which one.
## Install
Add this to the site's global <head>, on ALL pages, with their real write
key in data-write-key:
<script async src="https://go.funal.link/t.js" data-write-key="THEIR_WRITE_KEY"></script>
The write key namespaces every event to their organization. It is public by
design (like a Segment write key) but revocable: if it ever leaks or gets
spammed, rotate it (track_write_key with rotate:true) and update the
snippet. Events whose click attribution proves a different org are dropped
by the server, so a wrong key means silently lost data: copy it exactly.
go.funal.link is Funal's shared link domain and works for every org. If the
human has a custom link domain configured in Funal (looks like
go.their-brand.com), swap it in for go.funal.link here and in every URL
below; otherwise do not ask, just use the default. Keep data-write-key
either way.
- Wix: Settings -> Custom Code -> Add Custom Code -> paste, apply to All
Pages, load in Head.
- Webflow: Site Settings -> Custom Code -> Head Code -> paste -> Save and
publish.
- If their call-to-action links out to a hosted form tool, add the tool's
domain to the tag: data-decorate="formcrafts.com" (comma-separate several).
## If they use a hosted form tool
In the form builder, add two hidden fields named exactly click_id and
link_variant, each prefilled from the URL parameter of the same name. The
tag decorates the CTA link so those parameters arrive on the form URL; the
hidden fields carry them through the form's own webhooks back to Funal.
If the form tool redirects back to the site after submit with the
visitor's email as a query parameter (FormCrafts thank-you pages often
do), add data-identify-param="email" (the actual parameter name) to the
script tag. The tag then hashes that one parameter's value in the browser
(SHA-256; the raw email is never sent) and ties the visitor's session to
their lead. Only the named parameter is ever read, and only when this
attribute is present.
## Optional JavaScript API
The tag exposes window.funal for pages that know more than the tag can
observe. Add the queue line so calls made before the tag loads still work:
<script>window.funal=window.funal||function(){(funal.q=funal.q||[]).push(arguments)}</script>
funal('page') // SPA route change
funal('track', 'quiz_completed', { score: 12 }) // custom moment
funal('identify', { email: userEmail }) // hashed in-browser
track names are stored namespaced (custom:quiz_completed). identify never
sends the raw email — it is hashed before any network call. Most installs
need none of this; the tag's automatic behavior covers normal sites.
## Verify (do not skip)
1. Open any page of the site with ?click_id=brief-test-123&link_variant=a
appended to the URL.
2. In the browser console, confirm localStorage has _fnl_cid =
"brief-test-123" and _fnl_anon = "c_brief-test-123".
3. Confirm the tag loaded: fetch https://go.funal.link/t.js and expect
HTTP 200 with JavaScript.
4. Confirm the beacon sink accepts and namespaces: POST to
https://go.funal.link/t with body
{"writeKey":"THEIR_WRITE_KEY","events":[{"name":"page_viewed","anonymous_id":"a_brief_test"}]}
and expect HTTP 202 with {"accepted":1,"dropped":0}. accepted:0 almost
always means the write key is wrong or was rotated; re-check it.
5. If forms are native to the site: submit a test entry and confirm the
submission contains hidden click_id / link_variant / anonymous_id fields.
6. Report each check's result to the human, then delete the test entry if
one was created.
The tag is safe to install twice (it runs once) and never blocks or alters
the site; a failed network or blocked script is a silent no-op.
Reference / the tracking SDK
Three verbs,
nothing else.
With zero calls the tag already tracks pageviews, outbound clicks, and form submits, captures click_id, and decorates your links. When your own page code knows something the tag can't see, it speaks in three verbs.
Install with the command queue
<script async src="https://go.funal.link/t.js" data-write-key="fnl_wk_..."></script>
<script>window.funal=window.funal||function(){(funal.q=funal.q||[]).push(arguments)}</script>The second line is the command queue: calls made before the script loads accumulate and drain on arrival. No load-order bugs, no ready callbacks. window.funal is callable forever, even if the script never loads.
funal('page')report a pageviewAmbient on every document load, so multi-page sites never call it. Single-page apps call it on route change.
funal('track', name, properties?)a moment the tag cannot seeA quiz finished, a calculator used, a video watched. Stored namespaced as custom:{name}, so page code can never forge a reserved event. Properties are flat JSON.
funal('identify', traits)identity the page has learnedPass an email; the SDK lowercases and hashes it in the browser with SHA-256 before anything leaves. The raw value never touches the wire. Stitches this browser’s anonymous trail to the known person.
Identity is asserted, never inferred
The tag never harvests emails from URLs on its own. To trust a redirect param from a hosted form tool, name it once: data-identify-param="email". Then it hashes client-side and fires identify.
Failure is silence
Every call is wrapped. A hostile DOM, a full localStorage, a blocked beacon: all no-ops. The tag can never break your page, and it never blocks a paint.
Step 02 / Intake
An interview,
not a form.
There is no field list to fetch. You send the answers so far and Funal sends back the next question, written for this person. You hold the state, so back, edit and resume are yours to design.
Your screen
Question 3 of about 6
How long has that been going on?
Something else…
{
"turn": 3,
"answers": {
"situation": "He stopped turning in work",
"who": "my son, Marcus, 15"
},
"known": { "intent": "exploring" }
}{
"question": "How long has that been going on?",
"field_type": "select",
"options": [
"A few weeks",
"This semester",
"Years"
],
"key": "duration",
"done": false
}/public/forms/:org/:formThe copy, the opener, the contact card, where qualified people book, and how long the interview runs.
/public/forms/:org/:form/nextSend the answers so far. Get the next question, or done.
/public/forms/:org/:form/submitCreates the record, qualifies it, and returns the token that unlocks booking.
/public/forms/:org/:form/draftPick up an unfinished interview where they left it.
Step 03 / Schedule
Real calendars,
drawn your way.
Availability comes from the team’s actual calendars, across a whole pool of people. Ask for two weeks in one call, draw the picker however you like, and book. The best fits see the soonest times.
Your picker
Pick a time with admissions
Times shown in your timezone · 30 minutes on Zoom
{
"pool": { "slug": "admissions" },
"duration": 30,
"timezone": "America/New_York",
"days": [
{ "date": "2026-10-02",
"slots": ["14:00Z", "14:30Z", "20:30Z"] },
{ "date": "2026-10-03",
"slots": ["15:30Z"] },
{ "date": "2026-10-04",
"slots": [] }
]
}/public/booking/:org/pool/:slugOpen times across a team, up to 14 days in one call.
/public/booking/:org/availabilityThe same, for one person.
/public/booking/:org/suggestThree good times and a one-line invitation, for a lighter screen than a calendar.
/public/booking/:org/bookingsBooks the slot. Returns a token the visitor can use to move or cancel it.
/public/booking/:org/action/:token/rescheduleMove it. Cancel is the same shape.
Step 04 / Follow through
The part
everyone else skips.
A booked call is not a held call. Once your app books, Funal keeps working: the hello text, the confirmation, the reminders, the brief for whoever takes the call, and the recovery when someone does not show. All of it knows what was said in the interview.
Right after booking
A text from the person they will meet, and a confirmation with the join link and a way to reschedule.
Before the call
Reminders timed to the meeting, in the visitor’s own timezone. They restart on a reschedule and stop on a cancel.
For your team
A short brief before each call: who they are, what they answered, what to cover.
When it goes quiet
A missed call or a no-show starts its own recovery, so nobody has to remember to chase.
Events out
Booked, held, cancelled and more as named events you can send to any tool. Every delivery can be replayed.
One record
The person, their answers, the booking and how they arrived are one record your team works from.
The graph / ask and automate
Then ask it
anything.
Everything your front door produces lands in one graph: clicks, answers, bookings, calls and emails hang off the people they belong to. One hop from a held call sits the ad click that caused it.
One MCP endpoint hands that graph to Claude Code, Cursor, or Codex: search it, walk a lead's neighborhood, pull funnel analytics, wire automations, scoped to your org. Pull conversion numbers or trace a journey without leaving your editor.
Same move as step one: copy the setup prompt, paste it into the tool you want connected. It configures itself and proves the connection with a live call. You'll need your Funal API key from onboarding.
# Setup prompt: connect Funal MCP to this coding agent
You are connecting Funal's MCP server to the agent environment you are
running in (Claude Code, Cursor, Codex, or another MCP client). Funal
exposes the business's working graph (records, search, analytics,
forms, workflows) as MCP tools, scoped to the business's organization.
## What you need from the human before starting
1. Their Funal API key. It is issued during onboarding; if they do not have
one, they should ask the Funal team. Treat it like a password.
2. Which agent they run: Claude Code, Cursor, Codex, or something else.
## Connect
Endpoint (Streamable HTTP): https://funal-api.alex-boquis.workers.dev/mcp
Auth: an "Authorization: Bearer <API key>" header on every request.
- Claude Code:
claude mcp add --transport http funal \
https://funal-api.alex-boquis.workers.dev/mcp \
--header "Authorization: Bearer THEIR_KEY"
- Cursor: add to .cursor/mcp.json (project) or ~/.cursor/mcp.json (global):
{ "mcpServers": { "funal": {
"url": "https://funal-api.alex-boquis.workers.dev/mcp",
"headers": { "Authorization": "Bearer THEIR_KEY" } } } }
- Codex: add to ~/.codex/config.toml (Codex speaks stdio, so bridge the
HTTP endpoint with mcp-remote):
[mcp_servers.funal]
command = "npx"
args = ["-y", "mcp-remote",
"https://funal-api.alex-boquis.workers.dev/mcp",
"--header", "Authorization: Bearer THEIR_KEY"]
- Any other client without native Streamable HTTP support: use the same
mcp-remote bridge as Codex.
## Verify (do not skip)
1. Restart or reload the agent so it picks up the new server.
2. Confirm the tool list includes atoms_list, atoms_search, and search.
3. Call atoms_count with kind "client". Any number, even 0, proves auth
and org scoping are live. A 401 means the key is wrong or revoked; stop
and tell the human.
4. Report the connected tool count and the atoms_count result to the human.
Never echo the API key back into chat output, logs, or committed files;
config files holding it (~/.cursor/mcp.json, ~/.codex/config.toml) should
stay out of version control.
For example
you > which front door books more held calls this month?
front_door started submitted booked held start→held new app 412 251 138 96 23.3% hosted form 1,204 597 241 141 11.7%
The new app turns starts into held calls at twice the rate. Most of the gap is before submit. Want me to trace where the hosted form loses people?
Illustrative numbers.
Concepts / the data model
A graph,
not tables.
Tracking doesn't fill rows. It builds a knowledge graph, and every query on this page reads it. Three ideas make the rest legible.
person · sarah k.
├─ click ────── fb-cold
├─ pageview ─── /webinar
├─ form_fill ── webinar-intake
├─ booking ──── strategy-call
└─ email ────── recapone node, its edges, the whole journey
Everything hangs off a person
Clicks, pageviews, form fills, emails, calls, bookings, payments. Not scattered across tables you join, but hung on the human they belong to. One record carries the whole story.
Relationships are first-class
The edge from a lead to the ad click that made them, from a client to their family, from a booking to its source, is a real object you walk. Not a foreign key you remember to set.
One hop from the answer
From a booked call, the ad click that caused it is a single step away. That is why "which ad produced clients" is a query, not a data project.
Reference / tool surface
Everything your
agent can reach.
One endpoint, one API key, scoped to your org. The same guarded, validated tools Funal's own AI staff use. The full list loads the moment you connect.
Records & graph
Search, read, and write the whole book of business. Walk a lead’s neighborhood edge by edge.
Analytics & funnels
Conversion, cohorts, and cause and effect down the chain. Trace exactly where a funnel leaks.
Links
Tracked links you own, split variants, attribution that survives every hop.
Forms
Author guided intake, preview it, activate it. The form is your funnel’s front door.
Scheduling
Availability, fresh booking links, booking-specific manage links, and no-show recovery, all attached to the source.
Automations
Durable agent workflows — resolvers, one judgment pass, idempotent effects, self-checks — on a cron, an event, or on demand.
Events & webhooks
A catalog of named business events. Subscribe your other tools; every delivery replays.
And more: client memory, e-sign templates, pages, calendar, voice. Every tool is validated before it writes, attributed to whoever ran it, and inspectable after.
Cookbook / ask your agent
Say it,
ship it.
16 prompts to paste at your agent once you're connected. Every one lands in the same graph tracking feeds, attributed the moment it exists.
you > track a custom event when someone finishes the pricing calculator
funal('track', 'calculator_done', { plan: 'pro' })
stored custom:calculator_doneNow on their journey. Ask later: who used the calculator but never booked?
you > identify a visitor by email after they log in
funal('identify', { email })
hashed in-browser, sha-256The raw email never leaves the page. This browser’s anonymous trail joins the known person.
you > spin up two link variants for the fb campaign, split 50/50
go.funal.link/fb-webinar live A "book a call" 50% B "get the plan" 50%
Both carry click_id end to end. Conversion by variant is one query, whenever.
you > point my main ad link at the new lander without touching the ad
go.funal.link/q4-ad → /lander-v2 swapped 0 ad edits, attribution intact
The ad platform never resets its learning. You moved the destination, not the link.
you > run deposit-required against no-deposit and call the winner
funnel intake-ab live A deposit 12 booked / 41 B no-deposit 15 booked / 44
Too early to call: roughly 38 more leads to significance. I’ll ping you when it’s decided.
you > trace where fb-cold leads drop off between click and booking
click 2,987 → form 201 → booked 41 biggest leak click to form, 93%
The lander is losing them before the form. Want me to diff it against newsletter’s?
you > what if we cut the intake from 8 fields to 4
whatif fields 8 → 4 modeled completion +18%
Modeled on your real drop-off curve. A projection, not a promise. Ship it as a test?
you > build a 5-question intake form for the webinar lander and give me the link
form webinar-intake live 5 fields · funal.ai/f/webinar-intake
Wired to tracking: every submission joins the click that brought it.
you > add a qualifying question that routes low-fit leads to a nurture page
form webinar-intake updated budget < $2k → /nurture
High-fit leads still hit book-now. The form behaves like your best intake rep.
you > give hot leads a booking link that only offers this week
go.funal.link/strategy-call live window mon-fri, 30 min holds
Bookings land in the graph attached to their source. No Calendly, no sync.
you > protect my 9-11am slots for leads scored above 80
target prime-am reserved score 80+, else after 11
Your best hours go to your best leads, automatically.
you > when someone books, post to #sales and email a confirmation
workflow booking-alert live trigger booking.created steps slack #sales → email
Runs on every booking, logs every run. Change it by asking.
you > every morning, draft me a brief on leads that went quiet
routine quiet-leads 8:00 am reads memory + last touch → brief
Lands in your inbox before you’re at your desk. A draft, for your approval.
you > send every booking to our ops webhook
subscription booking.* → ops every delivery logged, replayable
Named events, one catalog. Your other tools subscribe; nothing is a mystery.
you > which ad produced actual clients, not just clicks
source clicks clients google-brand 1,204 11 fb-cold 2,987 3
Only the system that holds the bookings and payments can answer this. Clicks lie; clients don’t.
you > show me leads who booked but never showed, grouped by source
no-shows this month 22 fb-cold 14 · google 5 · ref 3
fb-cold no-shows 3x the rest. Want the confirmation flow tightened for that source?
Reference / the API
Every endpoint,
one page.
The visitor side needs no key: the organization is in the path and any site can call it. The operator side takes your Funal API key and belongs on your server. Every field, example and error code is in the full reference, written as plain markdown so your agent can read it.
https://funal.ai/docs/api.md/public/forms/:org/:formLoad a form/public/forms/:org/:form/nextNext question/public/forms/:org/:form/submitSubmit and qualify/public/forms/:org/:form/draftResume/public/forms/:org/:form/uploadAttach a document/public/booking/:org/configBranding, people, products/public/booking/:org/availabilityOne person, up to 14 days/public/booking/:org/pool/:slugA team, up to 14 days/public/booking/:org/suggestThree good times/public/booking/:org/bookingsBook/public/booking/:org/action/:tokenRead it, with the join link/public/booking/:org/action/:token/rescheduleMove it/public/booking/:org/action/:token/cancelCancel it/scheduling/bookingsBook for a client/scheduling/seriesBook a recurring series/scheduling/reschedule-batchMove many at once/mcpEvery tool, for your agentStart here / one prompt
Hand it
to your agent.
Paste one prompt into the coding agent that is building your app. It reads the reference, wires your screens to the real API, proves the whole flow with a test booking, and tells you what to click when it needs you.
Or point it at the same prompt as a URL and say “read this and do what it says”: https://funal.ai/docs/build.md
# Setup prompt: build a front door on Funal
You are building the visitor-facing screens for a business that runs on
Funal: an application, a quiz, a booking page, or all three in one flow. The
business owns the design. Funal is the backend: it writes the questions,
qualifies the answers, offers real calendar time, keeps the record, and
follows up after the booking. Nothing of Funal's renders on the page.
## What you need from the human before starting
1. Their Funal organization id (starts with org_) and which form to build
against. If YOU have Funal's MCP server connected, find both yourself
with form_list and scheduling_targets. Use the form that is already
active. Do not create or activate a form unless the human asks.
2. Their designs (a Figma file, screenshots, or a description).
3. Nothing else. The visitor flow needs NO API key. Never put a Funal API
key in browser code, in a NEXT_PUBLIC_ variable, or in git.
## Read the reference first
Fetch https://funal.ai/docs/api.md and read all of it. Endpoint shapes there
are taken from the source code. Follow them exactly.
## Build in this order
1. Prove the API before styling. With plain fetch calls:
- GET the form. Note its opener, contact fields, booking target, and
the interview length.
- Run the interview: POST /next with turn, answers, known, and a stable
random anonymous_id, until done is true. Turn 1 (the opener) is yours
to render; the server never sends it. Pass known back unchanged.
- POST /submit. Read action (book or nurture), the reflection, and the
token that unlocks booking.
- Read availability for the form's booking target with days=14.
- Make a test booking (see Test safely).
2. Then draw it from the designs:
- A progress indicator from the interview length ("question 3 of about
6"; closers are only asked when needed, so never show an exact count).
- A waiting state between questions and a processing screen on submit.
Each is a model call that takes one to a few seconds. No streaming.
- Back (drop later answers, step turn back, call /next again) and resume
(GET /draft with the stored anonymous_id on load).
- A date picker that disables days with no openings, in the visitor's
own timezone.
- A confirmation screen that uses the booking's action token to show the
join link, an add-to-calendar link, and reschedule and cancel.
3. Add tracking: install the Funal site tag (see the tracking setup prompt
on https://funal.ai/docs), call funal('page') on each step, and pass the
tag's click_id, link_variant and anonymous id on submit so the record
keeps its attribution.
## Rules
- Always pass duration explicitly on availability and booking calls.
- On a 409 from booking, refetch availability and ask again.
- Treat interview, days, actionToken and meetingUrl as optional in
responses, and fall back without crashing when one is absent.
- Error bodies are { error, code? }. Show a retry on a network error.
## Test safely
- Only ever test with an email ending in @qa.funal.test.
- Pass dryRun: true on every test booking. A dry run makes no calendar
event and returns no action token, so reschedule and cancel need one real
booking to verify. Ask the human before making a real one, and cancel it
after.
- Tell the human the ids of any test records you created so they can be
removed.
## Verify (do not skip)
1. A full interview runs against the real form, start to done, with no
mock data.
2. Submit returns a record id and an action, and the right end state shows.
3. The picker shows real openings and a dry-run booking returns success.
4. No Funal API key appears anywhere in the built browser bundle.
5. Report each check to the human, with the test record ids.
Ship / verify it works
Prove it,
then ship.
Three setups, three proofs. Don't trust any of them until you've seen it land.
Tracking is live
- Load your site and watch the network tab: a POST to /t returns 202.
- Or just ask your agent: did events land for my write key in the last hour?
- Submit a test lead and confirm click_id rode through to the record, hosted form and all.
The connection is live
- The tool list now includes atoms_search, funnels_compare, and search.
- atoms_count with kind "client" returns a number, even 0. A 401 means the key is wrong or revoked.
Your front door is live
- A full interview runs against your real form, start to done, with no mock data.
- A test booking with a @qa.funal.test email and dryRun: true returns success.
- No Funal API key appears anywhere in your browser bundle.
Three ways it bites, and the fix
A wrong write key loses data silently
Events whose click proves a different org are dropped by the server, with no error. Copy the key exactly.
Hosted form tools need decoration
Add the tool’s domain with data-decorate and let the tag inject the hidden click_id field, or the journey breaks at the form hop.
Custom domain, everywhere or nowhere
If go.yourbrand.com is configured, swap it into the snippet and every URL below. Otherwise use go.funal.link. Never mix the two.