API
Drive your TimeTuna account from your own code. Create booking pages, read availability, book and cancel meetings, and get a webhook when anything changes. Everything the API touches belongs to the account that owns the key.
The API lives under https://timetuna.com/api/v1. Requests and responses are JSON. Every response carries Cache-Control: no-store.
Authentication
Create a key in the dashboard under API and webhooks. The key is shown once, when it is created: we store only a hash of it, so there is no way to read it back. If you lose it, revoke it and create another.
Authorization: Bearer tt_sk_...- A key is scoped to one account and grants what that account can do in the dashboard.
- You can hold 5 active keys at once. Revoking is immediate.
- An OAuth access token from a connected app (such as Zapier) works on the same endpoints, so you can script against an existing connection.
Your first call
GET /api/v1/me verifies a key and returns the account behind it. Start here when wiring up an integration.
curl https://timetuna.com/api/v1/me \
-H "Authorization: Bearer $TIMETUNA_API_KEY"{
"id": "0f2c...",
"email": "you@example.com",
"name": "Your Name",
"plan": "executive",
"booking_page_count": 3
}Errors and limits
Every error has the same shape: a stable machine code and a sentence for a human.
{ "error": "not_found", "message": "No booking page with that id." }| Error | What it means |
|---|---|
400 invalid_request | A field is missing or malformed. The message names it. |
401 unauthorized | No Bearer header, or the key is revoked or wrong. |
402 upgrade_required | The account is not on Executive and has no lifetime deal. |
403 forbidden | The account is not a member of the team you named. |
404 not_found | No such object in this account. A stranger's id looks the same as a wrong one. |
409 slug_taken, slot_unavailable, already_cancelled | The request is valid but the world says no. |
429 rate_limited | More than 120 requests in a minute on one key. Retry after 60 seconds. |
Booking pages
GET /api/v1/booking-pages
Every page the account owns, newest first. Takes limit (default 50, max 100).
curl https://timetuna.com/api/v1/booking-pages \
-H "Authorization: Bearer $TIMETUNA_API_KEY"{
"data": [
{
"id": "6a1b...",
"slug": "pavel",
"url": "https://timetuna.com/pavel",
"name": "Pavel",
"title": "Book a call",
"subheading": null,
"bio": null,
"job_title": null,
"timezone": "Europe/Amsterdam",
"language": "auto",
"location_type": "google_meet",
"duration_minutes": 30,
"durations": [30, 60],
"minimum_gap_minutes": 0,
"time_slot_increment": 15,
"booking_window": {
"mode": "rolling",
"days": 14,
"active_from": null,
"active_until": null,
"minimum_notice_hours": 8
},
"weekly_availability": { "monday": { "available": true, "hours": [{ "start": "09:00", "end": "17:00" }] } },
"search_indexable": false,
"team_id": null,
"created_at": "2026-09-01T10:04:11.000Z"
}
]
}POST /api/v1/booking-pages
Creates a page. Everything is optional: with an empty body you get the same page the New page button creates, wired to the calendars the account has connected, on a slug derived from the host's name.
| Field | What it does |
|---|---|
slug | String. The page URL: 3 to 50 lowercase letters, digits or hyphens. Returns 409 slug_taken rather than silently renaming it. |
name | String. The host name shown on the page. |
title, subheading | Strings. Headline copy on the page. |
timezone | String. IANA name, for example Europe/Amsterdam. Defaults to the connected calendar's timezone. |
language | String. The guest-facing language of the page. auto follows the guest's browser. See Language. |
duration_minutes | Number, or an array of numbers, from 5 to 480. An array offers the guest a choice; the first is the default. |
team_id | String. Shares the page with a team the account belongs to. |
curl -X POST https://timetuna.com/api/v1/booking-pages \
-H "Authorization: Bearer $TIMETUNA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"slug": "acme-intro",
"name": "Acme Sales",
"title": "Book an intro call",
"timezone": "Europe/Amsterdam",
"duration_minutes": [15, 30]
}'Returns 201 with the same object GET returns. The new page uses the calendars already connected to the account, so it is bookable immediately if at least one calendar is connected.
GET, PATCH and DELETE /api/v1/booking-pages/{id}
PATCH takes any of name, title, subheading, bio, job_title, timezone, language, duration_minutes, minimum_gap_minutes, minimum_notice_hours, booking_window_days, search_indexable and weekly_availability. Fields you leave out are untouched.
curl -X PATCH https://timetuna.com/api/v1/booking-pages/6a1b... \
-H "Authorization: Bearer $TIMETUNA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"minimum_notice_hours": 24,
"weekly_availability": {
"monday": { "available": true, "hours": [{ "start": "09:00", "end": "12:00" }] },
"tuesday": { "available": true, "hours": [{ "start": "09:00", "end": "17:00" }] },
"wednesday": { "available": true, "hours": [{ "start": "09:00", "end": "17:00" }] },
"thursday": { "available": true, "hours": [{ "start": "09:00", "end": "17:00" }] },
"friday": { "available": true, "hours": [{ "start": "09:00", "end": "15:00" }] },
"saturday": { "available": false, "hours": [] },
"sunday": { "available": false, "hours": [] }
}
}'weekly_availability must carry all seven days, and every range needs start and end as HH:MM in the page timezone.
DELETE refuses with 409 has_upcoming_bookings while the page still has confirmed meetings in the future. Cancel them first, so nobody is left holding an invite to a page that no longer exists.
GET /api/v1/booking-pages/{id}/available-times
Real availability, with the account's calendars, buffers, notice period and booking window already applied. This is the same computation the public page runs.
date(optional,YYYY-MM-DD): only that day, in the page timezone. Without it you get the whole booking window.duration_minutes(optional): which of the page's durations to fit.
{
"timezone": "Europe/Amsterdam",
"data": [
{ "start": "2026-09-21T09:00:00.000Z", "end": "2026-09-21T09:30:00.000Z" },
{ "start": "2026-09-21T09:30:00.000Z", "end": "2026-09-21T10:00:00.000Z" }
]
}Bookings
GET /api/v1/bookings
The account's bookings, newest first. Filters: status (confirmed, cancelled, rescheduled), booking_page_id, starts_after and starts_before (ISO 8601), limit (default 50, max 100).
curl "https://timetuna.com/api/v1/bookings?status=confirmed&starts_after=2026-09-16T00:00:00Z" \
-H "Authorization: Bearer $TIMETUNA_API_KEY"POST /api/v1/bookings
Books a slot on one of your pages. It runs the same path as a guest booking, so the calendar event is created and the confirmation emails go out.
curl -X POST https://timetuna.com/api/v1/bookings \
-H "Authorization: Bearer $TIMETUNA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"booking_page_id": "6a1b...",
"start_time": "2026-09-21T09:00:00Z",
"guest_name": "Mark Kochan",
"guest_email": "mark@example.com",
"purpose": "Intro call",
"duration_minutes": 30
}'booking_page_id, start_time and guest_email are required. If the slot went in the meantime you get 409 slot_unavailable: ask for availability again rather than retrying the same time.
GET /api/v1/bookings/{id}
{
"data": {
"id": "b7d2...",
"booking_page_id": "6a1b...",
"booking_page_name": "Acme Sales",
"booking_page_url": "https://timetuna.com/acme-intro",
"guest_name": "Mark Kochan",
"guest_email": "mark@example.com",
"start_time": "2026-09-21T09:00:00.000Z",
"end_time": "2026-09-21T09:30:00.000Z",
"timezone": "Europe/Amsterdam",
"purpose": "Intro call",
"meeting_link": "https://meet.google.com/abc-defg-hij",
"form_responses": { "name": "Mark Kochan", "email": "mark@example.com" },
"status": "confirmed",
"created_at": "2026-09-16T08:12:44.000Z"
}
}POST /api/v1/bookings/{id}/cancel
Cancels the meeting, removes the calendar event and notifies the guest, the host and any cohosts. No body. Returns 409 already_cancelled if it was already cancelled.
POST /api/v1/bookings/{id}/reschedule
Moves the meeting to start_time (ISO 8601), optionally with a new duration_minutes. The new time is checked against live availability, so this can return 409 slot_unavailable.
curl -X POST https://timetuna.com/api/v1/bookings/b7d2.../reschedule \
-H "Authorization: Bearer $TIMETUNA_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "start_time": "2026-09-22T14:00:00Z" }'Webhooks
Rather than polling, register a URL and we POST to it when a booking changes. Webhooks registered here are the same ones the dashboard shows, with the same delivery log, so you can debug a failing endpoint at API and webhooks.
GET and POST /api/v1/webhooks
event is one of new_booking, booking_cancelled, booking_rescheduled. target_url must use https, because the payload carries a guest's name and email. Up to 20 active webhooks per account.
curl -X POST https://timetuna.com/api/v1/webhooks \
-H "Authorization: Bearer $TIMETUNA_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "event": "new_booking", "target_url": "https://example.com/hooks/timetuna" }'DELETE /api/v1/webhooks/{id}
Stops delivery immediately.
What we send you
POST https://example.com/hooks/timetuna
Content-Type: application/json
X-Webhook-Event: new_booking
{
"event": "new_booking",
"data": { ...the booking object above }
}Reply 2xx. Anything else is logged as a failure and is visible in the delivery log. Delivery is a single attempt, so treat the webhook as a fast signal and reconcile with GET /api/v1/bookings if you need a guarantee.
Building an agent on this
The usual loop for a tool that books on a host's behalf:
POST /api/v1/booking-pagesonce per client or campaign, and keep theidandurl.GET /api/v1/booking-pages/{id}/available-times?date=...to offer real times.POST /api/v1/bookingsto take the slot, and handle409 slot_unavailableby asking for availability again.- A
new_bookingwebhook to keep your own system in step without polling.