Foyer

SMS plus web chat. Postgres-enforced no-overlap. Owner confirms before the lock.

A booking agent that holds the slot for fifteen minutes — and asks before it commits.

Foyer runs an intake conversation over SMS or an embedded web widget, validates the address and service area, proposes a slot inside business hours, holds it against a Postgres EXCLUDE constraint, and waits for one-click approval from the shop owner before locking it in Google Calendar. No auto-booking. No ghost appointments. No drift.

The wedge

Every “AI booking bot” demo on the market either auto-books — which turns the shop into a ghost-appointment factory — or chats without touching a calendar, which produces nothing. Foyer is the honest middle. It holds the slot for fifteen minutes against a Postgres exclusion constraint, asks the owner to confirm, then locks it in Calendar. The owner stays in the loop, the customer gets a real confirmation, the shop stops paying for no-shows.

Customer side

Plain text — no app, no link, no signup.

Local-service customers don’t install apps. They text. Foyer runs the intake over SMS or web chat with the same agent behind both — service type, address, timing, photos if the issue needs them — then proposes a slot and stops. The owner is the only thing that can turn a hold into a booking.

Messages

(303) 555-0142

9:47 AM

hi can you clear my kitchen drain
Hi, this is the booking line for Anchor Plumbing. I can help with that. What's the service address?
1432 Oak St, Boulder CO
Got it — 1432 Oak St, Boulder CO 80301. When would you like us to come out? We're open Mon-Fri 8am-5pm; the earliest available is Thursday 6/19 at 10am.
thursday works
Holding Thursday 6/19 at 10am for you. I'll text once Sam confirms — usually within fifteen minutes during business hours.
Text Message

Three surfaces, one conversation.

The customer texts or types. The agent collects what it needs, proposes a slot, and stops. The owner is the only thing that turns a hold into a booking.

SMS

Customer texts the shop's number.

Twilio webhook validates the signature, dedupes on MessageSid, dispatches a turn job to Redis, and returns 200 in under 500ms. STOP, START, and HELP are caught before any agent dispatch.

Web widget

Embedded chat on the shop's site.

Preact widget under 30KB gzipped, Shadow-DOM isolated so the host page's CSS can't leak in or out. Cross-channel resume requires phone OTP — number-match alone is a spoofing vector.

Owner inbox

One click confirms. One click rejects.

Filament admin shows the transcript, the address with a map preview, the photos, and the proposed slot. Confirm fires an Idempotency-Key-guarded request; the slot locks in Calendar, the customer gets a confirmation SMS, the conversation completes.

Owner side

The card that turns a hold into a booking.

One pending card per request, with the transcript, the geocoded address, the customer’s photos, and the slot the agent proposed. Confirm fires an idempotent request — double-clicks collapse to one Calendar update and one SMS. Reject releases the slot and re-engages the agent with alternatives.

Pending booking— Anchor Plumbing
hold expires in 12:47

Customer

+1 (303) 555-0142

SMS — phone verified

Service

Drain clearing — kitchen

Proposed slot

Thu Jun 19, 10:00 – 11:30 AM (MDT)

Address

1432 Oak St

Boulder, CO 80301

Geocoded · in service area

map preview
Idempotency-Key required

Try it

Text Anchor Plumbing's booking line to schedule.

Anchor Plumbing serves Boulder, CO (ZIPs 80301-80310). Open Mon-Fri 8am-5pm Mountain. Services: drain clearing, leak repair, water heater install. Text the number below to request service — the booking assistant will ask for the service type, address, and preferred time, then send a confirmation once the slot is held.

Text to book

(970) 469-8922

Send any message describing the service you need — the assistant will reply to gather the details. Reply HELP for help, STOP to opt out at any time.

Message frequency is conversational and reply-only (typically 4-10 messages per booking session). Msg & data rates may apply. By texting this number you agree to the terms and privacy policy.

Web chat

The same widget that ships to customer sites — embedded directly on the demo page.

Open the demo →

Pricing — illustrative

One shop, one number, one calendar.

Foyer is a portfolio demo, not a product on sale. The numbers below sketch what a real engagement would look like — a fixed-fee setup that replaces the blank page with a working agent, then a monthly retainer sized to one shop with one Twilio number and one Google Calendar.

Setup

$1,800

one-time

  • — Twilio number provisioning + 10DLC walkthrough
  • — Google Calendar OAuth wiring
  • — Scope configuration — services, hours, area, lead time
  • — Agent persona + welcome / confirm / out-of-scope copy
  • — Owner inbox access, kill switch, runbook hand-off

Monthly

$240

per shop, per month

  • — Hosting, monitoring, uptime watch
  • — LLM token cost passed through at cost — typical $4-$12 / mo
  • — Twilio SMS cost passed through at cost
  • — Scope and persona edits — one round per month
  • — Quarterly review of out-of-scope log for service expansion

These numbers assume one Twilio number, one Google Calendar, one human owner. Multi-location shops or shared-calendar shops are scoping work before they are pricing work.

Questions you'd ask before you'd buy.

Why a fifteen-minute hold and not auto-book?+

Auto-booking is the failure mode for this category — the bot agrees to anything, the calendar fills with appointments the owner never sees, the shop ends up sending a tech to an address where nobody's home. The fifteen-minute hold sits in the middle: the slot is reserved against a Postgres exclusion constraint so a second customer can't take it, and the owner has a window to confirm or reject before it becomes a real booking.

What happens if the owner doesn't confirm in fifteen minutes?+

The hold expires, the Calendar event is deleted, and the customer is texted that the slot is no longer available with an offer to check another time. Quiet hours apply — if the expiry would land at 11pm, the message is queued until morning.

What stops the bot from agreeing to something out-of-scope?+

Scope is configured per business and validated at write time — service types, service area as ZIPs or radius, business hours, blocked dates, lead-time bounds, quiet hours. The agent is a state machine constrained to those rules and refuses out-of-scope requests with a documented fallback. Garbage configs cannot be saved.

What about TCPA and 10DLC?+

STOP, START, HELP are caught before any agent dispatch and consent is keyed on the (customer phone, Twilio number) pair. Conversational reply is exempt from express-written-consent rules, but reactivation requires explicit START — no override. 10DLC brand and campaign registration are walked through in the setup docs.

What happens if the owner moves the Calendar event from their phone?+

Google Calendar events.watch push subscription notices the change and a reconciliation job updates the booking. If the push channel expires, a five-minute fallback poll catches it. The owner dashboard surfaces a sync-health indicator if drift exceeds threshold.

Why is there a Python sidecar?+

The agent state machine and LLM adapter live in FastAPI to keep retry, backoff, and circuit-breaker logic out of the request cycle, to make async LLM streaming additive without re-plumbing PHP, and because portfolio coverage of Python alongside PHP is the point. A documented Laravel-only fallback path is described in docs/architecture/why-fastapi.md for buyers who want to fork without the Python half.