TimeTuna

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.

What you need
An Executive plan or an AppSumo lifetime deal, and an API key from API and webhooks in your dashboard.

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.

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

bash
curl https://timetuna.com/api/v1/me \
  -H "Authorization: Bearer $TIMETUNA_API_KEY"
JSON
{
  "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.

JSON
{ "error": "not_found", "message": "No booking page with that id." }
ErrorWhat it means
400 invalid_requestA field is missing or malformed. The message names it.
401 unauthorizedNo Bearer header, or the key is revoked or wrong.
402 upgrade_requiredThe account is not on Executive and has no lifetime deal.
403 forbiddenThe account is not a member of the team you named.
404 not_foundNo such object in this account. A stranger's id looks the same as a wrong one.
409 slug_taken, slot_unavailable, already_cancelledThe request is valid but the world says no.
429 rate_limitedMore 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).

bash
curl https://timetuna.com/api/v1/booking-pages \
  -H "Authorization: Bearer $TIMETUNA_API_KEY"
JSON
{
  "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.

FieldWhat it does
slugString. The page URL: 3 to 50 lowercase letters, digits or hyphens. Returns 409 slug_taken rather than silently renaming it.
nameString. The host name shown on the page.
title, subheadingStrings. Headline copy on the page.
timezoneString. IANA name, for example Europe/Amsterdam. Defaults to the connected calendar's timezone.
languageString. The guest-facing language of the page. auto follows the guest's browser. See Language.
duration_minutesNumber, or an array of numbers, from 5 to 480. An array offers the guest a choice; the first is the default.
team_idString. Shares the page with a team the account belongs to.
bash
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.

Design is not in the API
Backgrounds, logos, colours and the confirmation screen are set in the page editor, not here. The API exists to provision and schedule; the look of a page is where a host's taste goes, and a REST field is a bad place to put it.

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.

bash
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.
JSON
{
  "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).

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

bash
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}

JSON
{
  "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.

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

bash
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

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

Webhooks are not signed yet
There is no signature header today. Keep your endpoint URL secret, treat it as unauthenticated input, and confirm anything that matters by reading it back from the API.

Building an agent on this

The usual loop for a tool that books on a host's behalf:

  1. POST /api/v1/booking-pages once per client or campaign, and keep the id and url.
  2. GET /api/v1/booking-pages/{id}/available-times?date=... to offer real times.
  3. POST /api/v1/bookings to take the slot, and handle 409 slot_unavailable by asking for availability again.
  4. A new_booking webhook to keep your own system in step without polling.
Prefer a no-code path
If you would rather not write code, the Zapier integration covers the same triggers and actions.