Personalized booking links on top of Cal.com. Instead of sending everyone the same cal.com/you link, you generate a unique URL per person, like book.yourdomain.com/book/marcus-0e2e. Each one carries the recipient's name, a short note about why you're reaching out, a themed background, and a native calendar that pulls live availability from Cal.com. Link previews (WhatsApp, LinkedIn, iMessage) render a custom Open Graph image with both your names.
It's a small FastAPI service. You bring a Cal.com account, a Postgres database, and somewhere to run a container.
A raw scheduling link is fine, but a page that greets someone by name and says "here's why I want to talk" converts a lot better for cold outreach and warm intros. This wraps Cal.com so the booking experience feels personal without leaving Cal.com to manage your actual calendar, reminders, and video links.
visitor → /book/{slug}
│
FastAPI (this service)
│
┌────────┴─────────┐
│ booking.html │ left panel: name, note, avatar
│ rendered server- │ right panel: calendar fed by Cal.com slots
│ side per invite │ OG image for link previews
└────────┬─────────┘
│
Cal.com API v2 availability + booking creation + calendar invites
Postgres booking_invites table (invite data, view + booking tracking)
The service never touches your calendar directly. Cal.com owns availability and sends the invites; this layer owns the presentation and a small table of who you invited.
- Per-recipient pages with a custom greeting, note, and closing line
- Three meeting types out of the box (discovery call, coffee chat, deep dive), each mapped to its own Cal.com event type
- Two visual themes (a warm "coffee" theme and a green "nature" theme), switched by meeting type
- Server-side slot prefetch so the calendar paints instantly instead of waiting on round trips
- Dynamic Open Graph images so shared links preview nicely
- A password-protected admin dashboard at
/book/adminto create, copy, and retire links - An always-on generic link (
/book) and a permanent coffee link (/book/coffee) for email signatures - Links auto-expire, and one-shot links flip to "booked" after use
- A Cal.com account with an API key (Settings → Developer → API keys) and at least one event type.
- A PostgreSQL database. Supabase, Neon, and Railway all work; so does plain Postgres.
- Docker, or Python 3.11+ if you'd rather run it directly.
Run schema.sql against your database once. In Supabase, paste it into the SQL editor. With psql:
psql "$DATABASE_URL" -f schema.sqlCreate the event types you want in Cal.com (for example a 20-minute discovery call and a 30-minute coffee chat). For each, note two things: the slug (in the event's URL) and the numeric event type ID (visible in the event type settings). You'll paste these into the env file next.
Copy the example env file and fill it in:
cp .env.example .envKey values:
| Variable | What it is |
|---|---|
DATABASE_URL |
Postgres connection string |
OWNER_NAME |
Your name, shown on the page and OG images |
CAL_API_KEY |
Cal.com API v2 key |
CAL_USERNAME |
Your Cal.com username |
CAL_*_EVENT_ID / CAL_*_SLUG |
The event type IDs and slugs from step 2 |
BOOKING_API_KEY |
Secret for the admin API (generate a random string) |
ADMIN_PASSWORD |
Password for the /book/admin dashboard |
BASE_URL |
Public URL where the service runs |
SCHEDULE_TIMEZONE |
IANA timezone of your Cal.com availability schedule |
With Docker:
docker build -t booking-pages .
docker run --rm -p 8080:8080 --env-file .env booking-pagesOr directly:
pip install -r requirements.txt
uvicorn main:app --host 0.0.0.0 --port 8080Open http://localhost:8080/book for the generic page and http://localhost:8080/book/admin to sign in with your ADMIN_PASSWORD.
Go to /book/admin, sign in, switch to "Create New", and fill in the recipient's name, meeting type, and an optional note. You get a link back, copied to your clipboard.
import httpx
r = httpx.post(
"https://your-domain.example/api/v1/invites",
json={
"recipient_name": "Marcus Lee",
"recipient_first_name": "Marcus",
"meeting_type": "coffee_chat", # or discovery_call, deep_dive, none
"context_quote": "Loved your talk on edge caching. Would enjoy comparing notes.",
"closing": "Looking forward to it.",
"expires_days": 30,
},
headers={"X-API-Key": "YOUR_BOOKING_API_KEY"},
)
print(r.json()["url"]) # https://your-domain.example/book/marcus-0e2e- Your photo: drop a square image into
static/and pointOWNER_AVATARat it (a neutral placeholder ships by default). - Backgrounds: replace
static/nature.{png,webp}andstatic/cafe-scene.{png,webp}. The.pngis the source for OG images; the.webpis the page background. Keep both in sync. - Themes and colors: the palette lives in CSS custom properties at the top of
templates/booking.html. Edit--accent-*to recolor. - Meeting types: add or change entries in
MEETING_TYPESinapp/config.pyand wire their event type IDs through env vars.
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /book/{slug} |
public | Personalized booking page |
| GET | /book |
public | Generic booking page |
| GET | /book/coffee |
public | Permanent coffee-chat page |
| GET | /book/og/{slug}.jpg |
public | Dynamic OG image |
| GET | /book/admin |
public | Admin dashboard (password login) |
| POST | /api/v1/invites |
API key | Create an invite |
| GET | /api/v1/invites |
API key | List invites |
| PATCH | /api/v1/invites/{id} |
API key | Update status or content |
| DELETE | /api/v1/invites/{id} |
API key | Delete an invite |
| GET | /api/v1/cal/slots |
public | Proxy Cal.com availability |
| POST | /api/v1/cal/book |
public | Create a booking via Cal.com |
| GET | /health |
public | Health check |
After your first deploy, call POST /api/v1/admin/fix-cal-titles once (with your API key) to set calendar event titles to the Name & You | Type format.
The container runs anywhere. A genericized cloudbuild.yaml is included for Google Cloud Run, but Fly, Render, Railway, or a plain VM all work the same way: build the image, set the environment variables, expose port 8080. Put a domain in front and set BASE_URL to match.
.
├── main.py FastAPI app, CORS, health
├── app/
│ ├── config.py env-driven configuration
│ ├── routes.py all endpoints + template rendering
│ ├── cal_client.py Cal.com API v2 client
│ └── og_image.py OG image generation (Pillow)
├── templates/
│ ├── booking.html the booking page
│ └── admin.html the admin dashboard
├── static/ avatar + background images
├── schema.sql database table
└── Dockerfile
MIT. See LICENSE.