Where your bookings come from
Every booking records its channel: your Instagram bio link, a counter QR code, your Google listing, your website, your newsletter. Share tagged links, and Channels shows each one’s bookings, money taken and no-shows.
How a booking gets its channel
Section titled “How a booking gets its channel”A link sets its channel with ch: book.tuppence.ai/bellas-barks?ch=instagram. Your page keeps
ch through the booking steps and staff pages. The channel is fixed when the booking is made.
On your page or widget, the first rule that applies wins:
- The link's cha known or set-up code
- Google token
rwg_token→google_reserve - Widgetno
ch→website - Referring site
instagram,google_link… - Nowhere
link
| Rule | Detail |
|---|---|
1. The link’s ch | One of Tuppence’s codes, case-insensitive (?ch=Instagram → instagram), or one you’ve set up. Anything else is other, with the raw value in channel_raw (tiktok-ad-7). |
| 2. Reserve with Google | Google’s rwg_token means google_reserve (not live yet). |
| 3. Your website | Widget bookings without ch are website. |
| 4. The referring site | Instagram → instagram; Facebook, Messenger → facebook; TikTok → tiktok; Bing → bing; Linktree → linktree; WhatsApp → whatsapp; Apple Maps → apple; Google search, Maps and short links → google_link (not Gmail or Docs); Gmail, Outlook or Yahoo webmail → email. Other sites are other. |
| 5. Nowhere | Typed, bookmarked, or from an app that sends no referrer: link. |
Only the referring host is kept, in referrer (l.instagram.com); paths and queries can hold
personal details. Your page passes it on as ?ref= through later steps.
At the desk or through the API, a booking has the channel you set (Walk-in sets
walk_in) or none. The API stores it verbatim, so send instagram, not Instagram, which would
count separately.
source records how it was booked (online, widget, dashboard, api, agent). A client
from your bio link booking on your page is source: online, channel: instagram.
The channels Tuppence knows
Section titled “The channels Tuppence knows”| Code | Called | Its bookings |
|---|---|---|
link | Your page’s link | Your Share link, its service and staff links, and visits with no referrer |
qr | QR code | Scans of your QR code from Share |
website | Your website | Your site’s widget (widget is an alias) |
email | Email signature | Your signature’s Book button; webmail links |
google_link | Google Business Profile | Your Google listing’s booking link; Google search and Maps |
instagram | Instagram bio | Your bio link; Instagram visits |
instagram_story | Instagram story | A story’s link sticker |
facebook | Your Page’s button; Facebook and Messenger | |
apple | Apple Business Connect | The Book action on your Apple Maps place card; Apple Maps |
whatsapp | WhatsApp Business | Your greeting or away message; WhatsApp |
tiktok | TikTok | Your profile’s website; TikTok |
linktree | Linktree | Your Linktree |
bing | Bing | Visits from Bing |
google_reserve | Reserve with Google | Google’s own Book button, once live |
nudge | Your marketing | The button in your marketing messages |
agent | AI agents | A client’s AI assistant booking on your page |
waitlist | The waitlist | A waitlist offer the client took |
walk_in, phone | Walk-in, Phone | Your bookings: Walk-in on the calendar, or phone via the API |
seed | Example bookings | The test-mode example week |
other | Other | Unknown ch or unlisted site |
Share has a link for each of the first twelve; Channels suggests the ones you haven’t used.
The share panel’s links
Section titled “The share panel’s links”| Link | What it is |
|---|---|
| Your link | Your page with ?ch=link (one per location), plus direct links to each service and person: …/bellas-barks/book?service=svc_…&ch=link. |
| QR code | PNG or SVG, for your page or one service. Uses a short link (book.tuppence.ai/w/…, ch=qr) that survives address changes and counts scans (Scanned 12 times). |
| Your website | The widget’s tag, with data-ch="website". |
| Email signature | A Book with … button, ch=email. |
| Where your clients already are | Cards with steps and a link for Google Business Profile, Instagram bio and story, Facebook, Apple Business Connect, WhatsApp Business (with a link auto-reply), TikTok and Linktree. |
Copying a link (or downloading the QR code) sets its channel up: Set up, then Live after the first booking. This needs permission to edit the booking page; the links work for anyone.
Your own channels
Section titled “Your own channels”Give a newsletter, flyer or advert its own channel so its bookings aren’t other.
| How | |
|---|---|
POST /v1/booking_channels | code (1–40 lower-case letters, digits, _ or -, starting with a letter or digit), kind (link, qr, email, instagram, other…) and label. Add ?ch=<code> to its links (case-insensitive). |
| The same code again | Updates it: a new label renames; for your own, a new kind changes it. other and none are reserved. |
| One of Tuppence’s | Only code (what Share does on copy). |
| A QR code of your own | POST /v1/booking_page/short_links with your ch (qr_window) returns a stable short link and sets up QR code (qr_window). |
| In your widget | data-ch="spring-offer" instead of website. |
Setting a channel up needs booking_page:write. Custom channels are created via the API only;
Channels lists them with the rest.
The Channels page
Section titled “The Channels page”Channels (after Booking reports in the menu) is for the team, not staff logins:
- The days. From/To, or 7, 30 or 90 days to the To day, plus location if you have several. Defaults to 30 days; bookmarkable via the URL. Bookings count on the visit day at their location, not the booking day, so future days show what’s ahead.
- Four figures. Bookings (completed, didn’t come, cancelled), Taken on them, Didn’t come, and From your channels (5 of 6: you made the rest).
- By channel. Status, bookings, money taken, no-shows (25.00% (1 of 4)) and latest booking date; then Not from a channel and All bookings. Every count links to those bookings in the bookings list (which also shows uncounted holds and requests). Cards on a phone.
- Google finds you by name, address and phone. Your location’s three, what’s missing, and Check your location details. Google needs an exact match with your Business Profile, or it shows no Book button, or someone else’s.
- Put your link in more places. Unused places in Share’s order, with steps and Get the link.
| Status | status | Means |
|---|---|---|
| Not set up | suggested | A place you haven’t used yet. |
| Set up | set_up | Link handed out; no booking yet. |
| Live | live | Has bookings; last_booking_at is the latest. A request counts when made, not again on approval. |
| Needs attention | error | For partner integrations, not built yet. |
Live is permanent.
What the numbers mean
Section titled “What the numbers mean”| Figure | Counts |
|---|---|
| Bookings | All bookings for those days except holds, waiting requests, declined and lapsed. Cancelled ones count and are labelled. |
| Taken | Paid towards the price, by card and at the desk, less refunds. Excludes tips, late-cancellation and no-show fees, gift vouchers, packages and memberships (paid when sold). |
| Didn’t come | No-shows as a share of completed or no-show visits, to two decimal places; a dash if none. |
| From your channels | Bookings with a channel, including other. |
These match the booking reports’ Where bookings came from figures.
Find the bookings from a channel
Section titled “Find the bookings from a channel”On Bookings, Came from filters by channel or Not from a channel (kept in the URL). Each
booking shows how and where from (The booking page · Instagram bio); other shows its raw value
(Other · tiktok-ad-7) or host. Its history starts Booked online from Instagram bio.
API: GET /v1/bookings?channel= takes an exact code, other or none, plus ?from=&to= and
?location=. Bookings include channel, channel_raw (when other) and referrer.
Bookings from your marketing
Section titled “Bookings from your marketing”Every marketing message (rebooking nudge, birthday message, campaign,
last-minute slot) has a button that books with ch=nudge (Your marketing on Channels).
Marketing counts them per campaign; See them opens the bookings list at channel=nudge. No
setup needed.
Through the API
Section titled “Through the API”GET /v1/booking_channels (bookings:read) returns Channels as data. Parameters: ?from=&to=
(YYYY-MM-DD visit days; default last 30, max 366) and ?location=.
| Field | |
|---|---|
channels | Yours, unused suggestions (status: "suggested", id: null) and any others in those bookings, busiest first, each with stats: bookings, completed, no_shows, cancelled, revenue (minor units of the summary’s currency), no_show_rate ("25.00", or null with no outcomes). |
without_channel | Bookings without one. |
totals | All of them, matching the bookings list. |
Create a channel, book from it, and check that day’s channels, on an account with a staffed service (Set up bookings):
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}# A channel of your own, before its link goes out: your newsletter's links carry ?ch=newsletter.api /v1/booking_channels '{"code": "newsletter", "kind": "email", "label": "Newsletter"}' \ | jq -r '"\(.label) \(.id | startswith("chan_"))"' # Newsletter true
# One of Tuppence's needs only its code.api /v1/booking_channels '{"code": "instagram_story"}' | jq -r '"\(.kind) · \(.label)"' # instagram · Instagram story
# A newsletter reader rings to book: at the desk, name the channel they came from.location=$(api /v1/locations | jq -r '.data[0].id')service=$(api "/v1/services?status=active&limit=1" | jq -r '.data[0].id')starts_at=$(api "/v1/availability/next?service=$service&location=$location" | jq -r .starts_at)booking=$(api /v1/bookings "{\"location\": \"$location\", \"items\": [{\"service\": \"$service\"}], \"starts_at\": \"$starts_at\", \"client\": {\"name\": \"Nia Clarke\", \"email\": \"nia@example.test\"}, \"channel\": \"newsletter\"}")id=$(echo "$booking" | jq -r .id)day=$(echo "$booking" | jq -r .local_date)
# Where that day's bookings came from: the newsletter is live.api "/v1/booking_channels?from=$day&to=$day" \ | jq -r '.channels[] | select(.code == "newsletter") | "\(.label) \(.status)"' # Newsletter live
# Its count is a list: the bookings list, by channel.api "/v1/bookings?channel=newsletter" | jq --arg id "$id" '[.data[].id] | index($id) != null' # trueimport { Tuppence } from "@tuppence/node";
const tuppence = new Tuppence(process.env.TUPPENCE_SECRET_KEY);
// A channel of your own, before its link goes out: your newsletter's links carry ?ch=newsletter.const newsletter = await tuppence.request("POST", "/v1/booking_channels", { body: { code: "newsletter", kind: "email", label: "Newsletter" },});console.log(newsletter.label, newsletter.id.startsWith("chan_")); // Newsletter true
// One of Tuppence's needs only its code.const story = await tuppence.request("POST", "/v1/booking_channels", { body: { code: "instagram_story" } });console.log(story.kind, "·", story.label); // instagram · Instagram story
// A newsletter reader rings to book: at the desk, name the channel they came from.const [location] = (await tuppence.locations.list()).data;const [service] = (await tuppence.services.list({ status: "active", limit: 1 })).data;const slot = await tuppence.availability.next({ service: service.id, location: location.id });const booking = await tuppence.bookings.create({ location: location.id, items: [{ service: service.id }], starts_at: slot.starts_at, client: { name: "Nia Clarke", email: "nia@example.test" }, channel: "newsletter",});
// Where that day's bookings came from: the newsletter is live.const day = booking.local_date;const summary = await tuppence.request("GET", "/v1/booking_channels", { query: { from: day, to: day } });const line = summary.channels.find((c) => c.code === "newsletter");console.log(line.label, line.status); // Newsletter live
// Its count is a list: the bookings list, by channel.const listed = await tuppence.bookings.list({ channel: "newsletter" });console.log(listed.data.some((b) => b.id === booking.id)); // trueimport osfrom tuppence import Tuppence
tuppence = Tuppence(os.environ["TUPPENCE_SECRET_KEY"])
# A channel of your own, before its link goes out: your newsletter's links carry ?ch=newsletter.newsletter = tuppence.request( "POST", "/v1/booking_channels", body={"code": "newsletter", "kind": "email", "label": "Newsletter"},)print(newsletter["label"], newsletter["id"].startswith("chan_")) # Newsletter True
# One of Tuppence's needs only its code.story = tuppence.request("POST", "/v1/booking_channels", body={"code": "instagram_story"})print(story["kind"], "·", story["label"]) # instagram · Instagram story
# A newsletter reader rings to book: at the desk, name the channel they came from.location = tuppence.locations.list().data[0]service = tuppence.services.list(status="active", limit=1).data[0]slot = tuppence.availability.next(service=service.id, location=location.id)booking = tuppence.bookings.create( location=location.id, items=[{"service": service.id}], starts_at=slot.starts_at, client={"name": "Nia Clarke", "email": "nia@example.test"}, channel="newsletter",)
# Where that day's bookings came from: the newsletter is live.day = booking.local_datesummary = tuppence.request("GET", "/v1/booking_channels", params={"from": day, "to": day})line = next(c for c in summary["channels"] if c["code"] == "newsletter")print(line["label"], line["status"]) # Newsletter live
# Its count is a list: the bookings list, by channel.listed = tuppence.bookings.list(channel="newsletter")print(booking.id in [b.id for b in listed.data]) # True| Call | |
|---|---|
GET /v1/booking_channels | Bookings by channel over a date range (bookings:read). |
POST /v1/booking_channels | Sets a channel up: code, plus kind and label for your own (booking_page:write). |
POST /v1/bookings | channel: the client’s source. |
GET /v1/bookings?channel= | Bookings from a channel: its code, other, or none. |
POST /v1/booking_page/short_links | A QR short link with its ch; a custom ch sets the channel up. |
The Bookings reference has every field.
Not built yet
Section titled “Not built yet”Reserve with Google. Google’s Book button needs Tuppence in Google’s partner programme (a
person applies; Google reviews), so no bookings come from it yet. Arrivals are handled: a visit with
rwg_token is google_reserve, and the token is stripped from the address, held in the tab and
kept with the booking (page only, not the widget). It never appears in the API or logs; a malformed
token is dropped and the booking proceeds. 30-day retention and conversion reporting come with the
integration. Until then, use Share’s Google Business Profile card (google_link).
Native Book buttons elsewhere. Not built: Instagram’s and Facebook’s Book now (Meta
chooses providers; a person asks), Apple’s Book on your Maps card (a person registers Tuppence),
or booking inside WhatsApp. Use your link there today. external_ref and Needs attention are
reserved for these and unused.