Repository navigation
feat(#562): a booking finds the click that started it - #1165
Merged
Merged
Conversation
The webhook half of Phase 3b. An earlier pass concluded this could not be built
on Cal.com. That was wrong, and the correction is measured: `utm_content` really
does not survive, but a HIDDEN custom booking field does, and it is a carrier we
control.
THE LOOP, DRIVEN END TO END ON PRODUCTION BEFORE THIS WAS COMMITTED:
click Book a call -> create-lead returns an id
/schedule?lead=ID -> embed prefills the hidden `lead_ref` booking field
visitor books -> Cal.com POSTs BOOKING_CREATED
calcom-webhook -> lead advances to `scheduled`
leads row after: status=scheduled name="End To End"
email=admin@scripthammer.com
scheduled_at=2026-09-14 13:00:00+00
booking_ref=cp8JzQ2am3bJPVgnUcN8S5
ledger row after: provider=calcom signature_verified=true processed=true
THE SIGNATURE IS MEASURED, NOT DOCUMENTED. A live delivery carries
`x-cal-signature-256` with 64 hex characters and NO prefix. This matters because
`agents/skills/calcom-api/references/webhooks.md` inside Cal.com's OWN repository
compares against `sha256=${expected}` and would reject every real delivery — and
it is the document an agent researching this is most likely to find. A test
pins the bare form so nobody "fixes" it to match.
Five rejection paths, all driven live:
no signature header -> 400
the literal "no-secret-provided" -> 400
a prefixed sha256= signature -> 400
valid signature over a tampered body -> 400
correctly signed replay -> 200 {handled:false,"already processed"}
The second is the sharpest trap in this integration and the reason it is called
out by name. A webhook configured WITHOUT a secret still sends the header, valued
`no-secret-provided`, so `if (!sig) return 401` accepts unsigned traffic. The
literal is rejected explicitly.
A booking with no `lead_ref` is NOT an error. Somebody can always book from a
Cal.com link that never touched this site, and answering that with a 4xx would
make the operator's webhook log red for people booking calls correctly. It is
recorded, acknowledged 200, and never retried.
THE CTA NOW AWAITS, AND THAT IS THE FEATURE. The id only exists in create-lead's
response, so fire-and-forget cannot carry it. The wait is capped at 1200ms and
every failure — slow, offline, rate-limited, unconfigured — falls through to the
plain navigation. Worst case is an unattributed booking, which is what happened
for every booking before this existed. It stays a real anchor underneath, so
no-JS and middle-click still reach the calendar.
T036 widens `webhook_events.provider` to accept BOTH schedulers, not just the one
this deployment runs — calendar.config.ts supports Calendly by design and a fork
may be on it. DROP+ADD, and applied to production: an inline edit to the CREATE
TABLE is a silent no-op on a provisioned database.
Also live now, outside the repo: the hidden `lead_ref` field on both bookable
event types, and the BOOKING_CREATED webhook. CALCOM_WEBHOOK_SECRET is documented
in .env.example, including why leaving it unset fails closed.
Closes #562.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Sep 10, 2026
TortoiseWolfe
added a commit
that referenced
this pull request
Sep 10, 2026
#1165 shipped booking attribution and it worked — when the function was warm. Driving the real browser path on production, minutes apart: click 1 (cold) -> lead recorded, landed on /schedule/ no ?lead= click 2 (warm) -> lead recorded, landed on /schedule/?lead=93a0305b-... embed: app.cal.com/turtle-wolfe/15min/embed?...&lead_ref=93a0305b-... Both clicks wrote a lead. Only the second could ever be matched to a booking. On a low-traffic site the cold one is the common case, so the feature would have looked built and mostly not worked — and failed silently, because the row looks perfect and simply never leaves `link_opened`. The API run that shipped #1165 could not have caught this: it warmed the function itself on the first call. THE WAIT COMES OUT RATHER THAN GETTING LONGER. Raising the cap punishes the visitor; pinging the function on a schedule buys warmth with a recurring cost and still loses the genuinely-first click. `leads.id` is a UUID primary key and an INSERT may supply it, so BookingCta now mints one with crypto.randomUUID(), puts it on the URL immediately, and records in the background with keepalive. No await, no cap, no cold-start sensitivity, and no schema change. Generated at CLICK time, not render time: this page is statically exported, so an id minted during render would be baked into the HTML and every visitor would share one. What it concedes, stated plainly: the id is caller-supplied. A collision is a unique violation, now answered 409 rather than 500. A CHOSEN id lets somebody mark a lead they invented as `scheduled` — a row they created about themselves. Neither is worth a mechanism. Shape is not conceded: anything that is not a v4 UUID is refused before Postgres sees it. Driven live after redeploying: caller-supplied id -> 201 with that id the same id again -> 409 "That lead id already exists" "'; drop table leads; --" -> 400 "id is not a uuid" The FR-024a case needed care rather than a number bump: it smuggled an `id` that happens to be a valid v4 UUID, so that key is now legitimately accepted. It keeps the exact key list, which is what catches a NEW field leaking in, and names utm_*/name/email/status explicitly so the rule survives the list changing again. Closes #1166. Co-authored-by: TurtleWolfe <TurtleWolfe@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
8 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The webhook half of Phase 3b. An earlier pass in this same session concluded it could not be
built on Cal.com. That was wrong, and the correction is measured rather than argued:
utm_contentreally does not survive, but a hidden custom booking field does — and that is a carrier we
control.
The loop, driven end to end on production before this was committed
The row afterwards:
The signature is measured, not documented
A live delivery carries
x-cal-signature-256with 64 hex characters and no prefix.That matters more than usual:
agents/skills/calcom-api/references/webhooks.mdinside Cal.com'sown repository compares against
`sha256=${expected}`— which would reject 100% of realdeliveries — and it is the document an agent researching this problem is most likely to find. A test
pins the bare form specifically so nobody "fixes" this to match it.
Five rejection paths, all driven live
400no-secret-provided400sha256=signature400400200 {handled:false, "already processed"}The second is the sharpest trap in this integration. A webhook configured without a secret still
sends the header, valued
no-secret-provided— so a receiver writtenif (!sig) return 401sees apresent, non-empty string and accepts unsigned traffic. The literal is rejected by name.
A booking with no
lead_refis not an error. Somebody can always book from a Cal.com link thatnever touched this site, and answering that with a 4xx would make the operator's webhook log red for
people booking calls correctly. It is recorded, acknowledged
200, and never retried.The CTA now awaits, and that is the feature
The id only exists in
create-lead's response, so fire-and-forget cannot carry it. The wait iscapped at 1200ms and every failure — slow, offline, rate-limited, unconfigured — falls through to
the plain navigation. Worst case is an unattributed booking, which is what happened for every booking
before this existed. It stays a real
<a href>underneath, so no-JS and middle-click still reach thecalendar.
Schema
T036 widens
webhook_events.providerto accept both schedulers, not just the one this deploymentruns —
calendar.config.tssupports Calendly by design and a fork may be on it. DROP+ADD, andapplied to production: an inline edit to the
CREATE TABLEis a silent no-op on a provisioneddatabase. The other three provider CHECKs are deliberately left alone; they already disagree with
each other and harmonising them is a separate decision.
Live outside the repo
The hidden
lead_reffield on both bookable event types, and the BOOKING_CREATED webhook pointing atcalcom-webhook(v1, ACTIVE,verify_jwt: false).CALCOM_WEBHOOK_SECRETis documented in.env.exampleincluding why leaving it unset fails closed. All test data was removed afterwards —booking cancelled, lead and ledger rows deleted.
5,831 unit tests, 961 script tests,
deno checkclean across 18 functions.Closes #562.
🤖 Generated with Claude Code