Skip to content

Repository files navigation

Booking Pages

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.

Why this exists

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.

How it works

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.

Features

  • 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/admin to 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

Prerequisites

  1. A Cal.com account with an API key (Settings → Developer → API keys) and at least one event type.
  2. A PostgreSQL database. Supabase, Neon, and Railway all work; so does plain Postgres.
  3. Docker, or Python 3.11+ if you'd rather run it directly.

Setup

1. Database

Run schema.sql against your database once. In Supabase, paste it into the SQL editor. With psql:

psql "$DATABASE_URL" -f schema.sql

2. Cal.com event types

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

3. Configuration

Copy the example env file and fill it in:

cp .env.example .env

Key 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

4. Run it

With Docker:

docker build -t booking-pages .
docker run --rm -p 8080:8080 --env-file .env booking-pages

Or directly:

pip install -r requirements.txt
uvicorn main:app --host 0.0.0.0 --port 8080

Open http://localhost:8080/book for the generic page and http://localhost:8080/book/admin to sign in with your ADMIN_PASSWORD.

Creating invites

From the dashboard

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.

From the API

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

Customizing

  • Your photo: drop a square image into static/ and point OWNER_AVATAR at it (a neutral placeholder ships by default).
  • Backgrounds: replace static/nature.{png,webp} and static/cafe-scene.{png,webp}. The .png is the source for OG images; the .webp is 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_TYPES in app/config.py and wire their event type IDs through env vars.

Endpoints

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.

Deploying

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.

Project layout

.
├── 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

License

MIT. See LICENSE.

About

Personalized Cal.com booking links: each contact gets a unique page with a custom note, themed background, link-preview image, and native calendar. Self-hostable FastAPI service.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages