Docs/Getting started

For people done fighting embeds

Build your own front door.
Funal runs the rest.

connect >

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.

Read the API reference
No iframe, no embed scriptYour steps, your order, your copyQualify, route and book in one APIReschedule and cancel built inTest one front door against anotherThe record is yours, no glue code

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.

the reyes familyyour appfunalyour team
mon 19:02screenWho is this for? Parent picks "my son", types Marcus, 15
mon 19:02▸questionnext question written from that answer · 1 of about 6
mon 19:04answer"He stopped turning in work in October"
mon 19:06▸qualifiedanswers turned into facts · fit: yes · tier: hot
mon 19:06▸recordthe family is created · parent and child on it · source kept
mon 19:07calendartwo weeks of real openings, drawn your way
mon 19:08▸bookedthu 4:30 pm with Khloe · Zoom made · invite sent
mon 19:08▸follow-uphello text · confirmation · reminders queued · no-show watch armed
thu 16:15▸briefwho they are, what they said, what to cover
thu 16:30calladmissions call held
first question → held call · your screens, one backend

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 / tracking
# 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 pageview

Ambient 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 see

A 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 learned

Pass 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?

A few weeks
This semester
Years

Something else…

Continue
You send · POST /next
{
  "turn": 3,
  "answers": {
    "situation": "He stopped turning in work",
    "who": "my son, Marcus, 15"
  },
  "known": { "intent": "exploring" }
}
Funal replies
{
  "question": "How long has that been going on?",
  "field_type": "select",
  "options": [
    "A few weeks",
    "This semester",
    "Years"
  ],
  "key": "duration",
  "done": false
}
GET
/public/forms/:org/:form

The copy, the opener, the contact card, where qualified people book, and how long the interview runs.

POST
/public/forms/:org/:form/next

Send the answers so far. Get the next question, or done.

POST
/public/forms/:org/:form/submit

Creates the record, qualifies it, and returns the token that unlocks booking.

GET
/public/forms/:org/:form/draft

Pick 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

Thu2
Fri3
Sat4
Sun5
Mon6
Tue7
10:00 AM10:30 AM1:15 PM2:00 PM4:30 PM

Times shown in your timezone · 30 minutes on Zoom

GET /pool/admissions?date=2026-10-02&days=14
{
  "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": [] }
  ]
}
GET
/public/booking/:org/pool/:slug

Open times across a team, up to 14 days in one call.

GET
/public/booking/:org/availability

The same, for one person.

POST
/public/booking/:org/suggest

Three good times and a one-line invitation, for a lighter screen than a calendar.

POST
/public/booking/:org/bookings

Books the slot. Returns a token the visitor can use to move or cancel it.

POST
/public/booking/:org/action/:token/reschedule

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

01

Right after booking

A text from the person they will meet, and a confirmation with the join link and a way to reschedule.

02

Before the call

Reminders timed to the meeting, in the visitor’s own timezone. They restart on a reschedule and stop on a cancel.

03

For your team

A short brief before each call: who they are, what they answered, what to cover.

04

When it goes quiet

A missed call or a no-show starts its own recovery, so nobody has to remember to chase.

05

Events out

Booked, held, cancelled and more as named events you can send to any tool. Every delivery can be replayed.

06

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 / MCP
# 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

funal

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 ────── recap

one node, its edges, the whole journey

01

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.

02

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.

03

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.

atoms_searchatoms_getgraph_neighborhoodedges_listsearch

Analytics & funnels

Conversion, cohorts, and cause and effect down the chain. Trace exactly where a funnel leaks.

analytics_queryfunnels_comparefunnels_tracefunnels_segment

Links

Tracked links you own, split variants, attribution that survives every hop.

links_createlinks_listlinks_updatelinks_delete

Forms

Author guided intake, preview it, activate it. The form is your funnel’s front door.

form_upsertform_previewform_activate

Scheduling

Availability, fresh booking links, booking-specific manage links, and no-show recovery, all attached to the source.

scheduling_linkscheduling_manage_linkscheduling_availabilityscheduling_book

Automations

Durable agent workflows — resolvers, one judgment pass, idempotent effects, self-checks — on a cron, an event, or on demand.

agent_workflows_upsertagent_workflows_runagent_workflows_runs

Events & webhooks

A catalog of named business events. Subscribe your other tools; every delivery replays.

relay_catalogrelay_eventsrelay_deliveries

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.

tracking / custom event

you > track a custom event when someone finishes the pricing calculator

funal('track', 'calculator_done', { plan: 'pro' })
stored   custom:calculator_done

Now on their journey. Ask later: who used the calculator but never booked?

tracking / identify

you > identify a visitor by email after they log in

funal('identify', { email })
hashed   in-browser, sha-256

The raw email never leaves the page. This browser’s anonymous trail joins the known person.

links / variants

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.

links / swap

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.

funnels / ab test

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.

funnels / trace

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?

funnels / what-if

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?

forms / build

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.

forms / route

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.

scheduling / window

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.

scheduling / protect

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.

automations / on booking

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.

automations / daily brief

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.

automations / webhook

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.

analysis / real roi

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.

analysis / no-shows

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.

Open the full reference https://funal.ai/docs/api.md
Forms · no key
GET/public/forms/:org/:formLoad a form
POST/public/forms/:org/:form/nextNext question
POST/public/forms/:org/:form/submitSubmit and qualify
GET/public/forms/:org/:form/draftResume
POST/public/forms/:org/:form/uploadAttach a document
Booking · no key
GET/public/booking/:org/configBranding, people, products
GET/public/booking/:org/availabilityOne person, up to 14 days
GET/public/booking/:org/pool/:slugA team, up to 14 days
POST/public/booking/:org/suggestThree good times
POST/public/booking/:org/bookingsBook
Manage a booking · token
GET/public/booking/:org/action/:tokenRead it, with the join link
POST/public/booking/:org/action/:token/rescheduleMove it
POST/public/booking/:org/action/:token/cancelCancel it
Operator · API key
POST/scheduling/bookingsBook for a client
POST/scheduling/seriesBook a recurring series
POST/scheduling/reschedule-batchMove many at once
POST/mcpEvery tool, for your agent

Start 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 on Funal
# 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.