TidyCal API (1.0)

Download OpenAPI specification:Download

Introduction

TidyCal's REST API provides a handful of endpoints which can be used to get information about your account and bookings. It uses conventional OAuth 2.0 protocol for authentication.

Authentication

Personal Access Token

Create a personal access token from Integrations -> Advanced -> "Manage API keys" -> "Personal tokens" tab at https://tidycal.com/integrations/advanced (the API is included on every TidyCal plan, including Free). Once created, it can be used to authenticate requests by passing it in the Authorization header.

Authorization: Bearer {TOKEN}

OAuth 2.0 Client

If you're building a custom integration to TidyCal which requires users to authenticate in order to get access tokens to make API requests on their behalf, you'll need to create an OAuth 2.0 client. This is easy to do from Integrations -> Advanced -> "Manage API keys" -> "OAuth clients" tab found here https://tidycal.com/integrations/advanced

Using the authorization_code grant type to authenticate users using OAuth 2.0 to retrieve an access token is fairly conventional, more information on that process can be found here: https://www.oauth.com/oauth2-servers/server-side-apps/authorization-code/

AI Assistants (MCP)

TidyCal ships an MCP server that lets AI assistants such as Claude and ChatGPT work with your TidyCal scheduling data through natural language — ask things like "what bookings do I have next week?" and the assistant answers using your live account data. It can read your account and, only if you approve the write permission when you connect, create and change things: book a meeting, cancel a booking, create a booking type, or add a contact.

Full setup guide, OAuth details, and the current tool list: https://tidycal.com/mcp-docs

Requirements

  • A TidyCal account. The MCP server is included on every TidyCal plan, including Free.
  • A supported AI client: Claude.ai (custom connector) or ChatGPT (custom connector, on a plan that supports them).

Connecting Claude

  1. In Claude.ai, open Settings -> Connectors and choose Add custom connector.
  2. Paste the TidyCal MCP server URL: https://tidycal.com/mcp
  3. Claude redirects you to TidyCal to sign in (OAuth 2.1). Log in and approve access — TidyCal requests two scopes: mcp:scheduling:read (see your bookings, booking types, availability, and teams) and mcp:scheduling:write (create and change bookings and booking types). Approve only the first for a read-only connection.
  4. Once connected, ask Claude about your schedule in plain language.

The connection uses OAuth 2.1 with Dynamic Client Registration; you never paste an API key or token into Claude.

Connecting ChatGPT

  1. In ChatGPT, open Settings -> Connectors -> Add and choose a custom connector.
  2. Paste the same server URL: https://tidycal.com/mcp
  3. ChatGPT redirects you to TidyCal to sign in and approve the same two scopes.
  4. Ask ChatGPT about your schedule. Disconnect any time from ChatGPT's connector settings or from TidyCal under Integrations -> Advanced -> Manage API keys -> Authorized clients.

Available tools

The server exposes tools to read your profile, teams, booking types, bookings, availability, busy times, locations, and Store orders. With the write scope it also exposes tools to book a meeting, cancel a booking, create a booking type, and add a contact. The current list, with what each tool returns, is on the setup page: https://tidycal.com/mcp-docs — it is generated from the server itself, so it is always up to date.

Scope and limitations

  • Changes need the write scope. Without mcp:scheduling:write, tools that create or cancel refuse and say so. Even with it, no tool edits or deletes existing booking types, issues refunds, or changes account settings.
  • Cancelling a booking cannot be undone, and always emails the booker and host. Booking types that charge a price cannot be booked from MCP.
  • An MCP connection's token works only with the MCP server, not this REST API. It lasts one hour and is refreshed by the assistant; the refresh token lasts 30 days. Use a personal access token or your own OAuth client for the REST API.
  • Exposed data: your profile, booking types, bookings, availability, busy times, teams, locations, and Store orders.
  • list-busy-times covers every connected provider — Google, Microsoft and Apple — for calendars you have enabled for conflict checking.
  • Dates/times use ISO 8601 (e.g. 2026-06-01T14:30:00Z); returned times are UTC unless a resource notes otherwise.

Example prompts

  • "What bookings do I have next week?"
  • "When am I free for a 30-minute consult over the next few days?"
  • "Show my cancelled bookings from last month."
  • "Which booking types do I offer, and how long is each?"

Pagination

List endpoints return 100 items per page (booking types: 30, a team's booking types: 50). Use the page query parameter to move through pages. Responses include the data array plus pagination fields: either at the top level (current_page, last_page, per_page, total, next_page_url, prev_page_url, from, to, first_page_url, last_page_url, path, links) or, for endpoints that return resource collections, in links and meta objects. Keep requesting the next page until next_page_url (or links.next) is null.

Dates

Date filters (starts_at, ends_at) accept ISO 8601 timestamps, with or without milliseconds, for example 2026-10-20T14:30:00Z, 2026-10-20T14:30:00.000Z or 2026-10-20T09:30:00-05:00.

Rate limits

The API allows 60 requests per minute per user. Requests over the limit return 429 Too Many Requests with a Retry-After header (seconds to wait). Each response includes X-RateLimit-Limit and X-RateLimit-Remaining.

Errors

Errors are always JSON. Validation failures return 422 with a message and an errors object keyed by field name.

Webhooks

Webhooks send a signed HTTPS POST to your URL when something happens in your account. Manage them with the /webhooks endpoints below, or subscribe from Zapier or Make, which use the same endpoints. Up to 10 webhooks per account.

Payload

Every event has the same envelope. data is a webhook object with exactly the fields shown in the examples below: it never includes the booker's IP address, payment processor ids, or internal host and booking type settings.

{
  "id": "6f1b7c52-0f4e-4d0e-9d5e-0a4a8b1f2c33",
  "type": "booking.created",
  "created_at": "2026-10-21T15:04:05+00:00",
  "data": { ... }
}

id is unique per event and is the same on every retry and resend, so use it to dedupe. created_at is when the event happened. data is built by a background worker a few seconds later, so it reflects the record as it was then, and can be newer than created_at suggests if the record changed in between. Headers on every request: Content-Type: application/json, User-Agent: TidyCal-Webhooks/1.0, TidyCal-Event (the event type), TidyCal-Event-Id (the event id, your dedupe key), TidyCal-Delivery-Id (the id of this webhook's delivery record, the id in the delivery log) and TidyCal-Signature.

Receiving webhooks

  • Delivery is at least once. The same event can reach you more than once (a retry after a timeout, or a resend from the dashboard). Dedupe on the event id, which is also sent in the TidyCal-Event-Id header and never changes across retries and resends. (TidyCal-Delivery-Id is different: it identifies this webhook's copy of the event, so it is the same across retries but not a dedupe key across webhooks.) Store the ids you have handled and skip repeats.
  • Verify every request before you trust it (see below), then answer fast. Respond with a 2xx and do the real work afterwards, for example in a queue.
  • Order is not guaranteed. A retried booking.created can arrive after a later booking.cancelled. Use the timestamps inside data, not arrival order.
  • Test it with POST /webhooks/{id}/test, which sends a signed webhook.test event through the same path as real events.

Verifying the signature

Every request carries TidyCal-Signature: t=<unix timestamp>,v1=<hex digest>.

  1. Read the raw request body bytes. Do not parse and re-serialize the JSON, because that changes the bytes.
  2. Split the header on , to get t and v1.
  3. Compute HMAC-SHA256 of the string t + "." + raw_body, keyed with the full webhook secret including the whsec_ prefix, as lowercase hex.
  4. Compare it with v1 in constant time: hash_equals in PHP, crypto.timingSafeEqual in Node. Never use == or === on signatures.
  5. Reject the request if t is more than 5 minutes (300 seconds) away from your clock. This stops someone replaying a captured request. Each retry is signed again with a fresh t, so a legitimate retry always passes.
    expected = hex(hmac_sha256(secret, t + "." + raw_body))
    
    The secret is shown once, when you create the webhook or rotate its secret (POST /webhooks/{id}/rotate-secret). Rotating stops the old secret from verifying immediately.

PHP:

$secret = getenv('TIDYCAL_WEBHOOK_SECRET'); // the full whsec_... string
$body   = file_get_contents('php://input'); // raw bytes, before any json_decode

parse_str(str_replace(',', '&', $_SERVER['HTTP_TIDYCAL_SIGNATURE'] ?? ''), $parts);
$t  = $parts['t'] ?? '';
$v1 = $parts['v1'] ?? '';

if (!ctype_digit($t) || abs(time() - (int) $t) > 300) {
    http_response_code(400); // missing, malformed or stale timestamp
    exit;
}

$expected = hash_hmac('sha256', $t . '.' . $body, $secret);

if (!hash_equals($expected, $v1)) {
    http_response_code(401); // bad signature
    exit;
}

$eventId = $_SERVER['HTTP_TIDYCAL_EVENT_ID']; // dedupe on this
$event   = json_decode($body, true);
http_response_code(204);

Node.js (Express):

const crypto = require('crypto');
const express = require('express');
const app = express();

const secret = process.env.TIDYCAL_WEBHOOK_SECRET; // the full whsec_... string

// express.raw keeps the body as a Buffer of the exact bytes TidyCal sent.
app.post('/tidycal', express.raw({ type: 'application/json' }), (req, res) => {
  const parts = Object.fromEntries(
    (req.get('TidyCal-Signature') || '').split(',').map((part) => part.split('=', 2))
  );
  const t = parts.t || '';
  const v1 = parts.v1 || '';

  if (!/^\d+$/.test(t) || Math.abs(Date.now() / 1000 - Number(t)) > 300) {
    return res.sendStatus(400); // missing, malformed or stale timestamp
  }

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${t}.`)
    .update(req.body)
    .digest('hex');

  const a = Buffer.from(expected);
  const b = Buffer.from(v1);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.sendStatus(401); // bad signature
  }

  const eventId = req.get('TidyCal-Event-Id'); // dedupe on this
  const event = JSON.parse(req.body.toString('utf8'));
  res.sendStatus(204);
});

Delivery and retries

Respond with any 2xx within 10 seconds for the whole request (5 seconds to connect). Anything else, or a timeout, is retried up to 8 attempts in all, after 1 minute, 5 minutes, 15 minutes, 1 hour, 3 hours, 6 hours and 12 hours (about 22 hours from the first attempt to the last). After the 8th failed attempt the delivery is marked failed; you can resend it from the dashboard or with POST /webhooks/deliveries/{id}/resend, which keeps the same event id.

Your URL must be https and must resolve only to public addresses (private, loopback, link-local and cloud metadata addresses are refused). Redirects are not followed, so a 301 or 302 counts as a failure. Only ports 443 (the default) and 8443 are allowed; any other port is rejected when you save the webhook and before every send. Only the status code of your response is used: the body is never read, and the delivery log shows an outcome such as http_5xx, timeout, connection_failed, tls_error or blocked_destination instead. Deliveries to one host are rate limited across all accounts; over the limit they are delayed, not dropped.

When a webhook is turned off

  • A 410 Gone response turns the webhook off straight away and that delivery is not retried. Use it to tell TidyCal you no longer want events.
  • A webhook is turned off only when it has been failing continuously for more than 72 hours and at least 8 delivery attempts in a row have failed. Any 2xx resets the count, and so does a gap of more than 24 hours between failures. A short outage, or a webhook that gets very few events, is not turned off.
  • In both cases the owner is emailed, enabled becomes false and disabled_reason is gone or failing (manual when you turn it off yourself). Events that happen while a webhook is off are not queued for it. Turn it back on with PATCH /webhooks/{id} {"enabled": true} or from Integrations, Automations in the dashboard.
  • A team webhook is also turned off (team_access_removed) when the team owner or admin who created it stops managing the team, and a webhook created by a third-party app when you revoke that app (app_revoked). The first can be turned back on once its creator manages the team again; the second cannot: delete it and create a new one.
  • Delivery history is kept for 30 days.

Events

Event Sent when data
booking.created A booking is confirmed (paid bookings, after payment) Booking
booking.cancelled A booking is cancelled Booking
booking.rescheduled A booking is moved to a new time Booking
booking.approved A booking that needed approval is approved Booking
payment.completed A booking payment is completed Payment
payment.refunded A booking payment is refunded (in full or in part) Payment, with the refund fields
contact.created A new contact is created Contact
store_order.completed A Store order is paid Store order
subscription.started A contact starts a booking subscription Subscription
subscription.renewed A subscription renews Subscription
subscription.cancelled A subscription is cancelled Subscription

Booking events carry the booking, contact.created the contact and store_order.completed the Store order, in the webhook shapes shown below (hosts are {id, name}, booking_type is a short summary). Payment and subscription events carry their own objects, shown below. Payment amounts are in major units (for example dollars), like bookings. Payment, charge and Stripe or PayPal ids, card details and the subscription manage link are never included.

Example: booking.created

{
  "id": "6f1b7c52-0f4e-4d0e-9d5e-0a4a8b1f2c33",
  "type": "booking.created",
  "created_at": "2026-10-21T15:04:05+00:00",
  "data": {
    "id": 101,
    "booking_type_id": 7,
    "starts_at": "2026-10-28T16:00:00.000000Z",
    "ends_at": "2026-10-28T16:30:00.000000Z",
    "timezone": "America/Chicago",
    "location": null,
    "booking_type": { "id": 7, "title": "Intro call", "url_slug": "intro-call", "url": "https://tidycal.com/ada/intro-call",
                      "duration_minutes": 30, "price": null, "currency_code": "USD", "team": null },
    "contact_id": 55,
    "contact": { "id": 55, "name": "Ada Lovelace", "email": "[email protected]", "phone_number": null, "timezone": "Europe/London",
                 "source": null, "created_at": "2026-10-21T15:04:04.000000Z", "updated_at": "2026-10-21T15:04:04.000000Z" },
    "payment": null,
    "add_ons": [],
    "hosts": [{ "id": 3, "name": "Grace Hopper" }],
    "questions": [],
    "meeting_url": "https://zoom.us/j/949494949494",
    "approved_at": null,
    "cancelled_at": null,
    "created_at": "2026-10-21T15:04:04.000000Z"
  }
}

booking.cancelled is the same with cancelled_at set. booking.approved has approved_at set. booking.rescheduled has the new starts_at and ends_at and an extra previous_starts_at.

Example: payment.completed

status is completed, pending or failed. On payment.refunded it is refunded or partially_refunded, and refunded_amount (cumulative, major units) is added; refunded_amount is absent on every other payment event.

{
  "id": "2c7e...", "type": "payment.completed", "created_at": "2026-10-21T15:04:05+00:00",
  "data": { "id": 9, "booking_id": 101, "type": "booking", "amount": 25.0, "currency": "USD", "status": "completed",
            "platform_fee": null, "completed_at": "2026-10-21T15:04:00.000000Z", "created_at": "2026-10-21T15:03:30.000000Z" }
}

Example: contact.created

{
  "id": "0b9c...", "type": "contact.created", "created_at": "2026-10-21T15:04:05+00:00",
  "data": { "id": 55, "name": "Ada Lovelace", "email": "[email protected]", "phone_number": null,
            "timezone": "Europe/London", "source": null,
            "created_at": "2026-10-21T15:04:04.000000Z", "updated_at": "2026-10-21T15:04:04.000000Z" }
}

Example: store_order.completed

{
  "id": "4d2a...", "type": "store_order.completed", "created_at": "2026-10-21T15:04:05+00:00",
  "data": { "id": 31, "booking_id": null, "product_id": 4, "product": { "id": 4, "title": "Planner", "slug": "planner" },
            "buyer_name": "Ada Lovelace", "buyer_email": "[email protected]", "amount": 19.0, "currency": "USD",
            "formatted_amount": "$19.00", "status": "completed",
            "completed_at": "2026-10-21T15:04:00.000000Z", "refunded_at": null, "created_at": "2026-10-21T15:03:30.000000Z" }
}

Example: subscription.started

subscription.renewed and subscription.cancelled have the same shape. renews_at is the end of the current period while the subscription is active and not cancelled, otherwise null.

{
  "id": "8e1f...", "type": "subscription.started", "created_at": "2026-10-21T15:04:05+00:00",
  "data": { "id": 3, "status": "active",
            "plan": { "id": 2, "booking_type_id": 7, "booking_type": "Coaching", "credits_per_period": 4, "period_unit": "month" },
            "price": 99.0, "currency": "USD", "credits_used": 0,
            "current_period_start": "2026-10-21T15:00:00.000000Z", "current_period_end": "2026-11-21T15:00:00.000000Z",
            "renews_at": "2026-11-21T15:00:00.000000Z", "cancelled_at": null, "ends_at": null,
            "contact": { "id": 55, "name": "Ada Lovelace", "email": "[email protected]" },
            "created_at": "2026-10-21T15:00:00.000000Z" }
}

Test events

POST /webhooks/{id}/test sends {"id": "...", "type": "webhook.test", "created_at": "...", "data": {"message": "This is a test event from TidyCal.", "webhook_id": 12}}. You cannot subscribe to webhook.test.

Account

Account information

Get account details.

Responses

Response samples

Content type
application/json
{}

Bookings

List bookings

Get a list of bookings.

query Parameters
starts_at
string <date-time>

Get bookings starting from a specific date.

ends_at
string <date-time>

Get bookings ending before a specific date.

cancelled
boolean

Get only cancelled bookings.

page
number

Page number.

include_teams
boolean

Include team bookings.

order_by
string
Example: order_by=id desc

Sort column, optionally followed by a space and asc or desc (for example "id desc"). Sortable columns: id, starts_at, ends_at, created_at, updated_at, cancelled_at. Any other value returns 422.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "current_page": 0,
  • "first_page_url": "string",
  • "from": 0,
  • "last_page": 0,
  • "last_page_url": "string",
  • "links": [
    ],
  • "next_page_url": "string",
  • "path": "string",
  • "per_page": 0,
  • "prev_page_url": "string",
  • "to": 0,
  • "total": 0
}

Get booking

Get a booking by ID.

path Parameters
booking
required
integer

The ID of the booking.

Responses

Response samples

Content type
application/json
{
  • "id": 1,
  • "contact_id": 1,
  • "booking_type_id": 1,
  • "starts_at": "2022-01-01T00:00:00Z",
  • "ends_at": "2022-02-01T00:00:00Z",
  • "cancelled_at": "2022-02-01T00:00:00Z",
  • "created_at": "2022-02-01T00:00:00Z",
  • "updated_at": "2022-02-01T00:00:00Z",
  • "timezone": "America/Los_Angeles",
  • "meeting_id": "fw44lkj48fks",
  • "questions": [
    ],
  • "contact": {
    },
  • "contact_subscription_id": null,
  • "payment": {
    },
  • "coupon_code": "SPRING25",
  • "add_ons": [
    ],
  • "refund": {
    },
  • "hosts": [
    ]
}

Delete booking

Archive (soft-delete) a booking by ID. Only unpaid bookings that are cancelled or have already ended can be deleted; cancel an upcoming booking first. Bookings with a payment, add-on purchase, cancellation fee or subscription cannot be deleted, and neither can a booking in a recurring series or guest group that is still active. Deleting never sends email and never changes any connected calendar. A deleted booking no longer appears in the bookings list or can be fetched.

path Parameters
booking
required
integer

The ID of the booking to delete.

Responses

Cancel booking

Cancel a booking by ID.

path Parameters
booking
required
integer

The ID of the booking to cancel.

Request Body schema: application/json
reason
string

Optional reason for cancellation

notify
boolean
Default: true

Email the booker and the host about the cancellation. Send false to cancel without any notification.

Responses

Request samples

Content type
application/json
{
  • "reason": "Client requested cancellation",
  • "notify": true
}

Response samples

Content type
application/json
{
  • "id": 1,
  • "contact_id": 1,
  • "booking_type_id": 1,
  • "starts_at": "2022-01-01T00:00:00Z",
  • "ends_at": "2022-02-01T00:00:00Z",
  • "cancelled_at": "2022-02-01T00:00:00Z",
  • "created_at": "2022-02-01T00:00:00Z",
  • "updated_at": "2022-02-01T00:00:00Z",
  • "timezone": "America/Los_Angeles",
  • "meeting_id": "fw44lkj48fks",
  • "questions": [
    ],
  • "contact": {
    },
  • "contact_subscription_id": null,
  • "payment": {
    },
  • "coupon_code": "SPRING25",
  • "add_ons": [
    ],
  • "refund": {
    },
  • "hosts": [
    ]
}

Reschedule booking

Move a booking to a new start time. The new slot is checked exactly as on the public reschedule page: the booking type's availability, conflicts with the host's connected calendars and other bookings (this booking is ignored), and team host assignment (a round-robin booking may be reassigned to another available host). The booking keeps the booking type's current duration. If the booking type requires approval, the booking goes back to pending. The booker and host are notified unless notify is false. Sending the booking's current start time returns 200 and changes nothing. Guest bookings must be rescheduled through their parent booking. Rate limited with booking creation (10 per minute).

path Parameters
booking
required
integer

The ID of the booking to reschedule.

Request Body schema: application/json
starts_at
required
string <date-time>

New start time, ISO 8601. A value without a UTC offset is read as UTC. Use /booking-types/{bookingType}/timeslots to find open slots.

notify
boolean
Default: true

Send the TidyCal reschedule (or re-approval) emails and notifications. When false, TidyCal sends nothing, but the host's calendar provider (Google Calendar invite updates) and meeting provider (Zoom, Microsoft Teams) may still notify the booker of the new time.

Responses

Request samples

Content type
application/json
{
  • "starts_at": "2026-04-22T09:00:00Z",
  • "notify": true
}

Response samples

Content type
application/json
{
  • "id": 1,
  • "contact_id": 1,
  • "booking_type_id": 1,
  • "starts_at": "2022-01-01T00:00:00Z",
  • "ends_at": "2022-02-01T00:00:00Z",
  • "cancelled_at": "2022-02-01T00:00:00Z",
  • "created_at": "2022-02-01T00:00:00Z",
  • "updated_at": "2022-02-01T00:00:00Z",
  • "timezone": "America/Los_Angeles",
  • "meeting_id": "fw44lkj48fks",
  • "questions": [
    ],
  • "contact": {
    },
  • "contact_subscription_id": null,
  • "payment": {
    },
  • "coupon_code": "SPRING25",
  • "add_ons": [
    ],
  • "refund": {
    },
  • "hosts": [
    ]
}

List rescheduled bookings

Get a list of reschedules of your bookings, newest first. Each item has the rescheduled booking (with its hosts) and the time it was previously scheduled for.

query Parameters
page
number

Page number.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "current_page": 0,
  • "first_page_url": "string",
  • "from": 0,
  • "last_page": 0,
  • "last_page_url": "string",
  • "links": [
    ],
  • "next_page_url": "string",
  • "path": "string",
  • "per_page": 0,
  • "prev_page_url": "string",
  • "to": 0,
  • "total": 0
}

Add guest to booking

Add a guest to an existing booking. The guest is created as its own booking linked to the original one: it shares the original booking's time, location and meeting link (it never gets a meeting link or calendar event of its own) and receives a confirmation email. The number of guests per booking is capped by the booking type's guest invite limit (max_guest_invites_per_booker). Guests do not take a seat on a group booking slot. Only the original booking can take guests, and only while it is upcoming, not cancelled, paid, and (for approval-required booking types) not yet approved. Guests are free: on a priced booking type the guest is not charged, the same as guests invited on the booking form (the original booker has already paid).

path Parameters
booking
required
integer

The ID of the booking to add the guest to.

Request Body schema: application/json
name
required
string <= 80 characters

Guest's name

email
required
string <email> <= 191 characters

Guest's email address. Must not already be the booker or a guest on this booking.

Responses

Request samples

Content type
application/json
{}

Response samples

Content type
application/json
{
  • "id": 1,
  • "contact_id": 1,
  • "booking_type_id": 1,
  • "starts_at": "2022-01-01T00:00:00Z",
  • "ends_at": "2022-02-01T00:00:00Z",
  • "cancelled_at": "2022-02-01T00:00:00Z",
  • "created_at": "2022-02-01T00:00:00Z",
  • "updated_at": "2022-02-01T00:00:00Z",
  • "timezone": "America/Los_Angeles",
  • "meeting_id": "fw44lkj48fks",
  • "questions": [
    ],
  • "contact": {
    },
  • "contact_subscription_id": null,
  • "payment": {
    },
  • "coupon_code": "SPRING25",
  • "add_ons": [
    ],
  • "refund": {
    },
  • "hosts": [
    ]
}

Booking Types

List booking types

Get a list of booking types.

query Parameters
page
number

Page number.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "current_page": 0,
  • "first_page_url": "string",
  • "from": 0,
  • "last_page": 0,
  • "last_page_url": "string",
  • "links": [
    ],
  • "next_page_url": "string",
  • "path": "string",
  • "per_page": 0,
  • "prev_page_url": "string",
  • "to": 0,
  • "total": 0
}

Create booking type

Create a new booking type.

Request Body schema: application/json
title
required
string <= 191 characters
description
required
string <html>
duration_minutes
required
integer >= 1
url_slug
required
string <= 191 characters
padding_minutes
integer >= 0
Default: 0
latest_availability_days
integer [ 0 .. 36500 ]
Default: 60
private
boolean
Default: false
max_bookings
integer >= 1
Default: 1
max_guest_invites_per_booker
integer [ 0 .. 10 ]
Default: 0
display_seats_remaining
boolean
Default: false
booking_availability_interval_minutes
integer [ 15 .. 1440 ]

If omitted, defaults to 30 for meetings of at least 30 minutes owned by new signups assigned to the new-booking-type-interval treatment; otherwise 15. Explicit values are preserved.

redirect_url
string or null <= 60000 characters
approval_required
boolean or null
in_gallery_at
string or null

Whether to list the booking type in the public TidyCal directory. Any non-empty value lists it; an explicit null or empty value opts out. When omitted, new booking types are listed by default. A listed booking type only appears (and is only indexed by search engines) once it meets the directory eligibility rules.

booking_type_category_id
integer or null
price
number <float> [ 0 .. 99999999.99 ]
Default: 0

Price for the booking type. Set to 0 for free booking types. How many decimal places are accepted depends on the currency: a currency with no sub-unit (JPY, KRW, CLP, VND and the other zero-decimal currencies) takes whole amounts only, every other currency takes at most 2. Trailing zeros are notation rather than precision, so 2200.00 is a valid JPY price. The currency judged is the one sent in currency_code, or your account currency when that is omitted.

payment_platform
string
Enum: "stripe" "paypal" "tidycal"

Payment platform to use. The platform must be connected in your account Integrations settings. Required when price > 0.

currency_code
string

ISO 4217 currency code (e.g. USD, EUR, GBP). Defaults to your account currency.

booking_threshold
integer [ 0 .. 15000 ]

Minimum notice required before a booking, in minutes. Omit for the default of 120 (2 hours); set to 0 for no minimum notice.

Responses

Request samples

Content type
application/json
{
  • "title": "30 Minute Meeting",
  • "description": "Book a 30 minute meeting with me",
  • "duration_minutes": 30,
  • "url_slug": "30-minute-meeting",
  • "padding_minutes": 15,
  • "latest_availability_days": 90,
  • "private": false,
  • "max_bookings": 1,
  • "max_guest_invites_per_booker": 0,
  • "display_seats_remaining": false,
  • "booking_availability_interval_minutes": 30,
  • "approval_required": false,
  • "in_gallery_at": null,
  • "booking_type_category_id": 1,
  • "price": 25,
  • "payment_platform": "stripe",
  • "currency_code": "USD",
  • "booking_threshold": 120
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

List booking type questions

Get the ordered custom questions asked on a booking type's booking form.

path Parameters
bookingType
required
integer

The ID of the booking type.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Replace booking type questions

Replace the full ordered set of questions on a booking type. Array order becomes the display order. Include an existing question's id to update it in place (its id stays stable); questions without an id are created, and existing questions left out of the array are deleted. Answers already stored on past bookings are copies of the question text and are not affected. Send an empty array to remove all questions.

path Parameters
bookingType
required
integer
Request Body schema: application/json
required
Array of objects <= 50 items

Responses

Request samples

Content type
application/json
{
  • "questions": [
    ]
}

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Replace questions on many booking types

Apply the same ordered question set to up to 50 booking types. All-or-nothing: if any id is not a booking type you can update, the request fails with 403 and nothing changes. Existing questions with the same type and text keep their id; others are created or deleted. Question id is not accepted here.

Request Body schema: application/json
required
Array of objects <= 50 items
booking_type_ids
required
Array of integers [ 1 .. 50 ] items

Responses

Request samples

Content type
application/json
{
  • "questions": [
    ],
  • "booking_type_ids": [
    ]
}

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Get booking type

Get a single booking type.

path Parameters
bookingType
required
integer

The ID of the booking type.

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Update booking type

Partially update a booking type. Only the fields you send change; omitted fields keep their current value. Accepts the same fields as create. Returns the updated booking type.

path Parameters
bookingType
required
integer

The ID of the booking type.

Request Body schema: application/json
object

Any subset of the fields accepted when creating a booking type.

Responses

Request samples

Content type
application/json
{
  • "title": "Renamed call"
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

Delete booking type

Delete a booking type. Team-managed copies cannot be deleted (422).

path Parameters
bookingType
required
integer

The ID of the booking type.

Responses

Response samples

Content type
application/json
{
  • "message": "string"
}

Disable booking type

Disable a booking type. Idempotent: calling it when the booking type is already disabled returns it unchanged. Returns the booking type.

path Parameters
bookingType
required
integer

The ID of the booking type.

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Enable booking type

Enable a booking type. Idempotent: calling it when the booking type is already enabled returns it unchanged. Returns the booking type.

path Parameters
bookingType
required
integer

The ID of the booking type.

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

List available timeslots

Get a list of available timeslots for a specific booking type. These are the times a client can book, so when the booking type has "Look busy" turned on (and the owner can use it), the hidden times are left out here too.

path Parameters
bookingType
required
integer

The ID of the booking type.

query Parameters
starts_at
required
string <date-time>

Start date to get timeslots from (UTC).

ends_at
required
string <date-time>

End date to get timeslots until (UTC). Must not be before starts_at, and the range may be at most 90 days; a longer range returns 422.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Create booking

Create a new booking for a specific booking type.

Single vs Package Bookings:

  • For single bookings, use starts_at to specify the booking time
  • For package/multi-session bookings, use the bookings array to specify multiple time slots

Response Format:

  • Single booking: Returns {"data": {Booking}}
  • Multiple bookings (package): Returns {"data": [{Booking}, ...]}

Priced booking types: A booking type with a price can be booked through the API only with a coupon_code that brings the price to 0 (a 100%-off coupon, or a fixed coupon worth at least the price). The booking is then created and confirmed exactly as on the booking page: no payment is created and the coupon use is recorded. Any other request for a priced type returns 400, including a coupon that leaves something to pay, a recurring (weekly or monthly) booking type, add_on_product_ids and contact_subscription_id. There is no way to record a payment taken outside TidyCal.

path Parameters
bookingType
required
integer

The ID of the booking type.

Request Body schema: application/json
starts_at
string <date-time>

The start time of the booking in UTC. Required for single bookings. Ignored if bookings array is provided.

Array of objects

Array of booking times for package/multi-session bookings. If provided, starts_at is ignored.

name
required
string <= 191 characters

Name of the person making the booking

email
required
string <email> <= 191 characters

Email of the person making the booking

timezone
required
string <= 191 characters

The timezone of the booking

Array of objects

Answers to booking type questions

coupon_code
string <= 191 characters

One of the host's coupon codes (case-insensitive). Required to book a priced booking type, and it must bring the price to 0. Ignored for a free booking type.

payment_link
boolean

Reserved. Holding a priced booking awaiting payment and returning a URL the booker pays at is not supported yet: true on a priced booking type returns 400.

Responses

Request samples

Content type
application/json
{
  • "starts_at": "2024-03-20T10:00:00Z",
  • "bookings": [
    ],
  • "name": "John Doe",
  • "email": "[email protected]",
  • "timezone": "America/Los_Angeles",
  • "booking_questions": [
    ],
  • "coupon_code": "VIPFREE",
  • "payment_link": false
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

Get booking type

Get a single booking type.

path Parameters
team
required
integer

The ID of the team.

bookingType
required
integer

The ID of the booking type.

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Update booking type

Partially update a booking type. Only the fields you send change; omitted fields keep their current value. Accepts the same fields as create. Returns the updated booking type.

path Parameters
team
required
integer

The ID of the team.

bookingType
required
integer

The ID of the booking type.

Request Body schema: application/json
object

Any subset of the fields accepted when creating a booking type.

Responses

Request samples

Content type
application/json
{
  • "title": "Renamed call"
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

Delete booking type

Delete a booking type. Team-managed copies cannot be deleted (422).

path Parameters
team
required
integer

The ID of the team.

bookingType
required
integer

The ID of the booking type.

Responses

Response samples

Content type
application/json
{
  • "message": "string"
}

Disable booking type

Disable a booking type. Idempotent: calling it when the booking type is already disabled returns it unchanged. Returns the booking type.

path Parameters
team
required
integer

The ID of the team.

bookingType
required
integer

The ID of the booking type.

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Enable booking type

Enable a booking type. Idempotent: calling it when the booking type is already enabled returns it unchanged. Returns the booking type.

path Parameters
team
required
integer

The ID of the team.

bookingType
required
integer

The ID of the booking type.

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Contacts

List contacts

Get a list of contacts.

query Parameters
search
string <= 255 characters

Return only contacts whose name or email starts with this text (case-insensitive).

page
number

Page number.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "links": {
    },
  • "meta": {
    }
}

Create contact

Create a new contact.

Request Body schema: application/json
name
required
string
email
required
string <email>
timezone
string

Responses

Request samples

Content type
application/json
{}

Response samples

Content type
application/json
{
  • "data": {
    }
}

Get contact

Get a single contact belonging to the authenticated user.

path Parameters
contact
required
integer

Contact ID.

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Update contact

Update a contact belonging to the authenticated user. Only the fields sent are changed.

path Parameters
contact
required
integer

Contact ID.

Request Body schema: application/json
name
string
email
string <email>
timezone
string or null
phone_number
string or null

Responses

Request samples

Content type
application/json
{
  • "name": "John Doe",
  • "email": "[email protected]",
  • "timezone": "America/Los_Angeles",
  • "phone_number": "+15125550100"
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

Store

List Store orders

Get Store sales and booking add-ons for the authenticated user, newest completed first (pending and failed orders last), 100 per page with data, links and meta. Use status=completed to pick up new paid sales only.

query Parameters
status
string
Enum: "pending" "completed" "refunded" "failed"

Only orders with this status.

page
number

Page number.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "links": {
    },
  • "meta": {
    }
}

Subscriptions

List subscriptions

Get your contacts' subscriptions to your booking type subscription plans, newest first, 100 per page with data, links and meta.

query Parameters
status
string
Enum: "active" "past_due" "paused" "cancelled"

Only subscriptions with this status. A subscription set to cancel at period end keeps status active (see cancel_at_period_end) until the period ends.

contact_id
integer

Only subscriptions of this contact.

booking_type_id
integer

Only subscriptions to this booking type's plan.

page
number

Page number.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Get subscription

Get one of your contacts' subscriptions by ID.

path Parameters
subscription
required
integer

The ID of the subscription.

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Cancel subscription

Cancel a subscription at the end of its current billing period, the same as the contact cancelling it from their manage page: the contact is not charged again and keeps any remaining credits until current_period_end. The status stays active (with cancel_at_period_end true) until then. Nothing is refunded. The contact is emailed unless notify is false.

path Parameters
subscription
required
integer

The ID of the subscription to cancel.

Request Body schema: application/json
notify
boolean
Default: true

Email the contact that the subscription was cancelled.

Responses

Request samples

Content type
application/json
{
  • "notify": true
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

Teams

List teams

Get a list of teams the authenticated user has access to.

query Parameters
page
number

Page number.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "links": {
    },
  • "meta": {
    }
}

Get team

Get details of a specific team.

path Parameters
team
required
integer

The ID of the team.

Responses

Response samples

Content type
application/json
{
  • "id": 1,
  • "name": "Sales Team",
  • "created_at": "2024-01-01T00:00:00Z",
  • "updated_at": "2024-01-01T00:00:00Z"
}

List team bookings

Get a list of bookings for a specific team. The team owner and admins see every team booking; other members see bookings they host or whose booking type they own.

path Parameters
team
required
integer

The ID of the team.

query Parameters
page
number

Page number.

start_date
string <date>

Get bookings starting from a specific date.

end_date
string <date>

Get bookings ending before a specific date.

email
string <email>

Get bookings for a specific email address.

host_id
integer

Get bookings for a specific user ID.

cancelled
boolean

Filter by cancellation status. Use true for cancelled bookings, false for non-cancelled bookings, or omit for all bookings.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "links": {
    },
  • "meta": {
    }
}

List team users

Get a list of users in a specific team. Member email addresses (invitation_email and user.email) are returned to the team owner and admins only; other members get null for everyone but themselves, and filtering by email returns 403 for them.

path Parameters
team
required
integer

The ID of the team.

query Parameters
page
number

Page number.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "links": {
    },
  • "meta": {
    }
}

Add user to team

Add a user to a team by sending an invitation email.

path Parameters
team
required
integer

The ID of the team.

Request Body schema: application/json
email
required
string <email>

Email address of the user to invite

role_name
string
Enum: "admin" "user"

Role name for the user in the team

Responses

Request samples

Content type
application/json
{}

Response samples

Content type
application/json
{
  • "message": "Invitation sent successfully",
  • "team_user_id": 123
}

Remove user from team

Remove a user from a team.

path Parameters
team
required
integer

The ID of the team.

teamUser
required
integer

The ID of the team user to remove.

Responses

Response samples

Content type
application/json
{
  • "message": "User removed from team"
}

List team booking types

Get a list of booking types for a specific team.

path Parameters
team
required
integer

The ID of the team.

query Parameters
page
number

Page number.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ],
  • "links": {
    },
  • "meta": {
    }
}

Create team booking type

Create a new booking type for a specific team.

path Parameters
team
required
integer

The ID of the team.

Request Body schema: application/json
title
required
string <= 191 characters
description
required
string <html>
duration_minutes
required
integer >= 1
url_slug
required
string <= 191 characters
padding_minutes
integer >= 0
Default: 0
latest_availability_days
integer [ 0 .. 36500 ]
Default: 60
private
boolean
Default: false
max_bookings
integer >= 1
Default: 1
max_guest_invites_per_booker
integer [ 0 .. 10 ]
Default: 0
display_seats_remaining
boolean
Default: false
booking_availability_interval_minutes
integer [ 15 .. 1440 ]
Default: 15
redirect_url
string or null <= 60000 characters
approval_required
boolean or null
in_gallery_at
string or null

Whether to list the booking type in the public TidyCal directory. Any non-empty value lists it; an explicit null or empty value opts out. When omitted, new booking types are listed by default. A listed booking type only appears (and is only indexed by search engines) once it meets the directory eligibility rules.

booking_type_category_id
integer or null
price
number <float> [ 0 .. 99999999.99 ]
Default: 0

Price for the booking type. Set to 0 for free booking types. How many decimal places are accepted depends on the currency: a currency with no sub-unit (JPY, KRW, CLP, VND and the other zero-decimal currencies) takes whole amounts only, every other currency takes at most 2. Trailing zeros are notation rather than precision, so 2200.00 is a valid JPY price. The currency judged is the one sent in currency_code, or your account currency when that is omitted.

payment_platform
string
Enum: "stripe" "paypal" "tidycal"

Payment platform to use. The platform must be connected in your account Integrations settings. Required when price > 0.

currency_code
string

ISO 4217 currency code (e.g. USD, EUR, GBP). Defaults to your account currency.

booking_threshold
integer [ 0 .. 15000 ]

Minimum notice required before a booking, in minutes. Omit for the default of 120 (2 hours); set to 0 for no minimum notice.

Responses

Request samples

Content type
application/json
{
  • "title": "30 Minute Meeting",
  • "description": "Book a 30 minute meeting with me",
  • "duration_minutes": 30,
  • "url_slug": "30-minute-meeting",
  • "padding_minutes": 15,
  • "latest_availability_days": 90,
  • "private": false,
  • "max_bookings": 1,
  • "max_guest_invites_per_booker": 0,
  • "display_seats_remaining": false,
  • "booking_availability_interval_minutes": 30,
  • "approval_required": false,
  • "in_gallery_at": null,
  • "booking_type_category_id": 1,
  • "price": 25,
  • "payment_platform": "stripe",
  • "currency_code": "USD",
  • "booking_threshold": 120
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

Webhooks

List webhooks

Webhook endpoints you own, plus every endpoint scoped to a team you own or administer. 50 per page. The signing secret is never included.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Create webhook

Subscribe a URL to events. This is also the REST-hook subscribe call used by Zapier and Make: send {url, events} and keep the returned id; DELETE /webhooks/{id} unsubscribes.

The response includes the signing secret. It is shown only here and by rotate-secret; store it. You can have at most 10 webhooks (deleted ones do not count). The URL must be https and resolve only to public addresses; it is checked again before every delivery.

Scope: leave team_id and booking_type_id out for every event on your account. team_id limits the webhook to that team's booking types and needs team owner or admin. booking_type_id limits it to one booking type of yours, or of a team you administer. Team-scoped webhooks are stored under the team owner (they count toward the owner's 10) and are visible to every team owner and admin. The scope cannot be changed later.

Access follows the team: a team-scoped webhook is turned off when the owner or admin who created it is removed from the team or loses the admin role, and an account-wide webhook receives a team's booking events only while its owner owns or administers that team. A webhook created through a connected app (such as Zapier) is turned off when you revoke that app. One webhook per URL per account.

Request Body schema: application/json
url
required
string <uri>
events
required
Array of strings (WebhookEventType) non-empty
Items Enum: "booking.created" "booking.cancelled" "booking.rescheduled" "booking.approved" "payment.completed" "payment.refunded" "contact.created" "store_order.completed" "subscription.started" "subscription.renewed" "subscription.cancelled"
team_id
integer or null
booking_type_id
integer or null
description
string or null <= 255 characters

Responses

Request samples

Content type
application/json
{}

Response samples

Content type
application/json
{
  • "data": {
    }
}

Get webhook

path Parameters
webhook
required
integer

The ID of the webhook.

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Update webhook

Change the URL, events or description, or turn the webhook on or off with enabled. team_id and booking_type_id cannot be changed and are rejected with 422. Setting enabled to true on a webhook that was turned off after failures clears the failure state.

path Parameters
webhook
required
integer

The ID of the webhook.

Request Body schema: application/json
url
string <uri>
events
Array of strings (WebhookEventType) non-empty
Items Enum: "booking.created" "booking.cancelled" "booking.rescheduled" "booking.approved" "payment.completed" "payment.refunded" "contact.created" "store_order.completed" "subscription.started" "subscription.renewed" "subscription.cancelled"
description
string or null
enabled
boolean

Responses

Request samples

Content type
application/json
{
  • "events": [
    ],
  • "description": "string",
  • "enabled": true
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

Delete webhook

Delete the webhook. This is the REST-hook unsubscribe call.

path Parameters
webhook
required
integer

The ID of the webhook.

Responses

Rotate webhook secret

Replace the signing secret. The new secret is returned once and the old one stops verifying immediately. Only the webhook owner, or the team owner or admin who created a team webhook, can rotate it.

path Parameters
webhook
required
integer

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Send test event

Queue a sample webhook.test event to the webhook. It is delivered, signed and retried like any other event and appears in the delivery log. Limited to 10 requests per minute.

path Parameters
webhook
required
integer

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

List deliveries

Delivery log for the webhook, newest first, 50 per page. Deliveries are kept for 30 days.

path Parameters
webhook
required
integer

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Resend delivery

Send a finished (delivered or failed) delivery again. The same event id is used so receivers can dedupe. The delivery is reset (attempts, status and response cleared) and queued. Limited to 10 requests per minute.

path Parameters
delivery
required
integer

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}