Set up bookings
A bookable page needs where, what, who, the terms and somewhere to book. Every account starts with a location, an unpublished page and a default policy: add services and people, then publish.
- Wherea location (already there)
- What
POST /v1/services - Who
POST /v1/staff+ hours - Terms
POST /v1/booking_policy - Publish
book.tuppence.ai/<address>
Switching? Fresha, Booksy, Square, Vagaro and Treatwell exports import as they are; other spreadsheets are matched column by column. Imports dry-run first and can be undone for 24 hours.
What it costs
Section titled “What it costs”| Bookings | Free: no monthly, per-booking or new-client fees |
| Payments | The card rate on deposits, fees and payments on the day |
| Texts (UK business) | 6p a segment (160 characters, or 70 with an emoji or curly quote) |
| WhatsApp (UK business) | 3p a message |
| Free |
Each message’s cost is in the log; monthly totals are on Messages and your statement.
In the dashboard
Section titled “In the dashboard”- Where. Location: display name; clients come to you, you go to them, or online; timezone; phone, address and opening hours.
- What. Services → New service: name, duration and price. Optional: description, buffers, processing time (the person is free meanwhile), options with their own time and price, class size, who the visit is for, online bookability, required resources (a room, a chair) and who performs it.
- Who. Staff → New staff member: bio and photo, bookable online (off hides them from the page; you can still book them), services (with their own time or price), and hours: a week or two-week rotation from a date, plus time off, breaks and extra hours.
- The terms. Booking policy: deposit, card on file, free-cancellation window, late and no-show fees. What a client reads previews the wording for a £45.00$45.00€45.00 service. Booking policies →
- Somewhere to book. Booking page: address (
book.tuppence.ai/<address>), look, words, step order, questions, after-booking behaviour and SEO. Preview before going live; Publish puts it live. - Share it. Share: your link, service and staff links, a QR code, the website widget, and places to post your link. Each link carries its channel, so every booking records where its client came from.
Through the API
Section titled “Through the API”The dashboard uses the same calls. Create a service, a person with weekly hours and a deposit, then publish:
api() { # GET a path, or POST it with a body, using your key and a fresh Idempotency-Key if [ $# -eq 1 ]; then curl -sS "https://api.tuppence.ai$1" -H "Authorization: Bearer $TUPPENCE_SECRET_KEY" else curl -sS "https://api.tuppence.ai$1" -H "Authorization: Bearer $TUPPENCE_SECRET_KEY" \ -H "Idempotency-Key: $(uuidgen)" -H "Content-Type: application/json" -d "$2" fi}# Where: every account has a location from the first look.location=$(api /v1/locations | jq -r '.data[0].id')
# What, and who: Monday to Friday, nine till half past five.api /v1/services '{"name": "Intro call", "duration_minutes": 30, "amount": 4500}' > /dev/nullstaff=$(api /v1/staff "{\"name\": \"Bella\", \"locations\": [\"$location\"]}" | jq -r .id)day='[["09:00", "17:30"]]'api "/v1/staff/$staff/schedules" "{\"location\": \"$location\", \"effective_from\": \"$(date +%F)\", \"pattern\": [{\"1\": $day, \"2\": $day, \"3\": $day, \"4\": $day, \"5\": $day}]}" > /dev/null
# The terms: a £10 deposit, refunded if they cancel in time.api /v1/booking_policy '{"deposit": {"type": "fixed", "value": 1000, "refundable": true}}' > /dev/null
# Somewhere to book: a line about you, then live.api /v1/booking_page '{"content": {"about": "Product strategy calls for early-stage startups."}}' > /dev/nullapi /v1/booking_page/publish '{}' | jq -r '"\(.published) \(.url)"' # true https://book.tuppence.ai/test/…api() { # GET a path, or POST it with a body, using your key and a fresh Idempotency-Key if [ $# -eq 1 ]; then curl -sS "https://api.tuppence.ai$1" -H "Authorization: Bearer $TUPPENCE_SECRET_KEY" else curl -sS "https://api.tuppence.ai$1" -H "Authorization: Bearer $TUPPENCE_SECRET_KEY" \ -H "Idempotency-Key: $(uuidgen)" -H "Content-Type: application/json" -d "$2" fi}# Where: every account has a location from the first look.location=$(api /v1/locations | jq -r '.data[0].id')
# What, and who: Monday to Friday, nine till half past five.api /v1/services '{"name": "Intro call", "duration_minutes": 30, "amount": 4500}' > /dev/nullstaff=$(api /v1/staff "{\"name\": \"Bella\", \"locations\": [\"$location\"]}" | jq -r .id)day='[["09:00", "17:30"]]'api "/v1/staff/$staff/schedules" "{\"location\": \"$location\", \"effective_from\": \"$(date +%F)\", \"pattern\": [{\"1\": $day, \"2\": $day, \"3\": $day, \"4\": $day, \"5\": $day}]}" > /dev/null
# The terms: a $10 deposit, refunded if they cancel in time.api /v1/booking_policy '{"deposit": {"type": "fixed", "value": 1000, "refundable": true}}' > /dev/null
# Somewhere to book: a line about you, then live.api /v1/booking_page '{"content": {"about": "Product strategy calls for early-stage startups."}}' > /dev/nullapi /v1/booking_page/publish '{}' | jq -r '"\(.published) \(.url)"' # true https://book.tuppence.ai/test/…api() { # GET a path, or POST it with a body, using your key and a fresh Idempotency-Key if [ $# -eq 1 ]; then curl -sS "https://api.tuppence.ai$1" -H "Authorization: Bearer $TUPPENCE_SECRET_KEY" else curl -sS "https://api.tuppence.ai$1" -H "Authorization: Bearer $TUPPENCE_SECRET_KEY" \ -H "Idempotency-Key: $(uuidgen)" -H "Content-Type: application/json" -d "$2" fi}# Where: every account has a location from the first look.location=$(api /v1/locations | jq -r '.data[0].id')
# What, and who: Monday to Friday, nine till half past five.api /v1/services '{"name": "Intro call", "duration_minutes": 30, "amount": 4500}' > /dev/nullstaff=$(api /v1/staff "{\"name\": \"Bella\", \"locations\": [\"$location\"]}" | jq -r .id)day='[["09:00", "17:30"]]'api "/v1/staff/$staff/schedules" "{\"location\": \"$location\", \"effective_from\": \"$(date +%F)\", \"pattern\": [{\"1\": $day, \"2\": $day, \"3\": $day, \"4\": $day, \"5\": $day}]}" > /dev/null
# The terms: a €10 deposit, refunded if they cancel in time.api /v1/booking_policy '{"deposit": {"type": "fixed", "value": 1000, "refundable": true}}' > /dev/null
# Somewhere to book: a line about you, then live.api /v1/booking_page '{"content": {"about": "Product strategy calls for early-stage startups."}}' > /dev/nullapi /v1/booking_page/publish '{}' | jq -r '"\(.published) \(.url)"' # true https://book.tuppence.ai/test/…import { Tuppence } from "@tuppence/node";
const tuppence = new Tuppence(process.env.TUPPENCE_SECRET_KEY);
// Where: every account has a location from the first look.const [location] = (await tuppence.locations.list()).data;
// What, and who: Monday to Friday, nine till half past five.await tuppence.services.create({ name: "Intro call", duration_minutes: 30, amount: 4500 });const bella = await tuppence.staff.create({ name: "Bella", locations: [location.id] });await tuppence.staff.createSchedule(bella.id, { location: location.id, pattern: [Object.fromEntries(["1", "2", "3", "4", "5"].map((d) => [d, [["09:00", "17:30"]]]))], effective_from: new Date().toISOString().slice(0, 10),});
// The terms: a £10 deposit, refunded if they cancel in time.await tuppence.bookingPolicy.update({ deposit: { type: "fixed", value: 1000, refundable: true } });
// Somewhere to book: a line about you, then live.await tuppence.bookingPage.update({ content: { about: "Product strategy calls for early-stage startups." } });const page = await tuppence.bookingPage.publish();console.log(page.published, page.url); // true https://book.tuppence.ai/test/…import { Tuppence } from "@tuppence/node";
const tuppence = new Tuppence(process.env.TUPPENCE_SECRET_KEY);
// Where: every account has a location from the first look.const [location] = (await tuppence.locations.list()).data;
// What, and who: Monday to Friday, nine till half past five.await tuppence.services.create({ name: "Intro call", duration_minutes: 30, amount: 4500 });const bella = await tuppence.staff.create({ name: "Bella", locations: [location.id] });await tuppence.staff.createSchedule(bella.id, { location: location.id, pattern: [Object.fromEntries(["1", "2", "3", "4", "5"].map((d) => [d, [["09:00", "17:30"]]]))], effective_from: new Date().toISOString().slice(0, 10),});
// The terms: a $10 deposit, refunded if they cancel in time.await tuppence.bookingPolicy.update({ deposit: { type: "fixed", value: 1000, refundable: true } });
// Somewhere to book: a line about you, then live.await tuppence.bookingPage.update({ content: { about: "Product strategy calls for early-stage startups." } });const page = await tuppence.bookingPage.publish();console.log(page.published, page.url); // true https://book.tuppence.ai/test/…import { Tuppence } from "@tuppence/node";
const tuppence = new Tuppence(process.env.TUPPENCE_SECRET_KEY);
// Where: every account has a location from the first look.const [location] = (await tuppence.locations.list()).data;
// What, and who: Monday to Friday, nine till half past five.await tuppence.services.create({ name: "Intro call", duration_minutes: 30, amount: 4500 });const bella = await tuppence.staff.create({ name: "Bella", locations: [location.id] });await tuppence.staff.createSchedule(bella.id, { location: location.id, pattern: [Object.fromEntries(["1", "2", "3", "4", "5"].map((d) => [d, [["09:00", "17:30"]]]))], effective_from: new Date().toISOString().slice(0, 10),});
// The terms: a €10 deposit, refunded if they cancel in time.await tuppence.bookingPolicy.update({ deposit: { type: "fixed", value: 1000, refundable: true } });
// Somewhere to book: a line about you, then live.await tuppence.bookingPage.update({ content: { about: "Product strategy calls for early-stage startups." } });const page = await tuppence.bookingPage.publish();console.log(page.published, page.url); // true https://book.tuppence.ai/test/…import osfrom datetime import datefrom tuppence import Tuppence
tuppence = Tuppence(os.environ["TUPPENCE_SECRET_KEY"])
# Where: every account has a location from the first look.location = tuppence.locations.list().data[0]
# What, and who: Monday to Friday, nine till half past five.tuppence.services.create(name="Intro call", duration_minutes=30, amount=4500)bella = tuppence.staff.create(name="Bella", locations=[location.id])tuppence.staff.create_schedule( bella.id, location=location.id, pattern=[{day: [["09:00", "17:30"]] for day in ["1", "2", "3", "4", "5"]}], effective_from=date.today().isoformat(),)
# The terms: a £10 deposit, refunded if they cancel in time.tuppence.booking_policy.update(deposit={"type": "fixed", "value": 1000, "refundable": True})
# Somewhere to book: a line about you, then live.tuppence.booking_page.update(content={"about": "Product strategy calls for early-stage startups."})page = tuppence.booking_page.publish()print(page.published, page.url) # True https://book.tuppence.ai/test/…import osfrom datetime import datefrom tuppence import Tuppence
tuppence = Tuppence(os.environ["TUPPENCE_SECRET_KEY"])
# Where: every account has a location from the first look.location = tuppence.locations.list().data[0]
# What, and who: Monday to Friday, nine till half past five.tuppence.services.create(name="Intro call", duration_minutes=30, amount=4500)bella = tuppence.staff.create(name="Bella", locations=[location.id])tuppence.staff.create_schedule( bella.id, location=location.id, pattern=[{day: [["09:00", "17:30"]] for day in ["1", "2", "3", "4", "5"]}], effective_from=date.today().isoformat(),)
# The terms: a $10 deposit, refunded if they cancel in time.tuppence.booking_policy.update(deposit={"type": "fixed", "value": 1000, "refundable": True})
# Somewhere to book: a line about you, then live.tuppence.booking_page.update(content={"about": "Product strategy calls for early-stage startups."})page = tuppence.booking_page.publish()print(page.published, page.url) # True https://book.tuppence.ai/test/…import osfrom datetime import datefrom tuppence import Tuppence
tuppence = Tuppence(os.environ["TUPPENCE_SECRET_KEY"])
# Where: every account has a location from the first look.location = tuppence.locations.list().data[0]
# What, and who: Monday to Friday, nine till half past five.tuppence.services.create(name="Intro call", duration_minutes=30, amount=4500)bella = tuppence.staff.create(name="Bella", locations=[location.id])tuppence.staff.create_schedule( bella.id, location=location.id, pattern=[{day: [["09:00", "17:30"]] for day in ["1", "2", "3", "4", "5"]}], effective_from=date.today().isoformat(),)
# The terms: a €10 deposit, refunded if they cancel in time.tuppence.booking_policy.update(deposit={"type": "fixed", "value": 1000, "refundable": True})
# Somewhere to book: a line about you, then live.tuppence.booking_page.update(content={"about": "Product strategy calls for early-stage startups."})page = tuppence.booking_page.publish()print(page.published, page.url) # True https://book.tuppence.ai/test/…| Call | Takes |
|---|---|
POST /v1/services | name, duration_minutes, amount (minor units); optionally variants, addons, resources, staff, buffers, max_group_size. Without staff, every active person performs it. |
POST /v1/staff | A person and the locations they work at. |
POST /v1/staff/{id}/schedules | A weekly pattern (ISO weekdays, "1" is Monday), or two patterns for a rotation, from effective_from. |
POST /v1/booking_policy | Any of the policy’s fields; the rest carry over. |
POST /v1/booking_page, …/publish | Patches to slug (the address), theme, content, flow, seo; then publish. |
From your Google listing
Section titled “From your Google listing”In Bookings › Set up bookings › Where and when, Connect Google Business Profile and choose the place, or paste its Google Maps link (Share → copy). Address, phone and opening hours fill in for you to check before saving. If you tick the box, the listing’s photos become your page’s cover and gallery. Disconnect revokes access. The option only appears where it’s available.
API: GET /v1/booking_setup/google_business and …/locations, then POST …/listing and
…/photos.
What’s left
Section titled “What’s left”Home shows a checklist once the page exists (GET /v1/booking_setup/checklist). Three days
after going live, the account owner gets one email naming the next item.
| Item | Done when |
|---|---|
| Take real payments | The account is live |
| Your first booking | A real one (not example or imported) |
| Reminders on | On by default unless switched off in Messages |
| Invite your team | Someone else joins |
| Connect a calendar | Google, Outlook or iCloud is syncing |
| A second channel | Bookings from two places, e.g. your link and Instagram |
Try it in test mode
Section titled “Try it in test mode”No money moves. Your page is at book.tuppence.ai/test/<address>; book yourself with a
test card. POST /v1/test_helpers/bookings/seed_week fills a week
with example bookings from your services, staff and hours. Reseeding the same week adds nothing.
- Booking policies: deposits, cancellations, fees.
- The Bookings API: find a time and take a booking.
- Bookings reference: every field.