NAV
API Reference
Shell Ruby Python JavaScript

Introduction

About TimelyDo

TimelyDo is a scheduling platform. You publish a booking page, share the link, and people pick a time from your real availability. No back and forth emails, and no double bookings.

TimelyDo syncs with Google Calendar, so events already on your calendar block those time slots. It connects to Zoom and Google Meet for the meetings people book with you.

Using TimelyDo is free. Learn more on the features pages or in the Help Center.

About the API

The TimelyDo API lets you work with your TimelyDo account from your own applications, scripts and integrations. It uses the same data you see in the app, so anything you read or change through the API shows up in TimelyDo straight away.

Need help? Email support@timelydo.com.

Every example on this page comes in Shell (curl), Ruby, Python and JavaScript. Switch languages with the tabs at the top right.

Base URL

All endpoints live under:

https://timelydo.com/api/v1

Response format

Every response, success or failure, has the same shape:

{
  "success": true,
  "message": "user details",
  "data": {},
  "status": 200
}

Requests and responses are JSON. Every response carries the same four fields:

Field Type Description
success boolean true when the request worked, false otherwise.
message string A short human readable description of the result.
data object or array The payload. An empty object when there is nothing to return.
status integer or string Mirrors the HTTP status. Successful responses use the number (200). Errors use the status name ("unauthorized", "forbidden").

Always check the HTTP status code or success before reading data.

Authentication

The TimelyDo API uses API keys. Every request must send your key in the X-API-KEY header. Requests without a valid key are rejected with 401 Unauthorized.

Get your API key

Every TimelyDo account has one API key, created when the account is created. To find it:

  1. Sign in at timelydo.com.
  2. Open Settings, then Developers Console, then API Key. You can also go straight to timelydo.com/settings/developers/api_key.
  3. Click the copy button next to Your API Key.

Keys are 73 characters long. They do not expire. A key keeps working until you regenerate it or delete your account.

The examples on this page read the key from an environment variable called TIMELYDO_API_KEY, so it never appears in your code:

export TIMELYDO_API_KEY="paste your key here"

Authenticate your requests

curl "https://timelydo.com/api/v1/user" \
  -H "X-API-KEY: $TIMELYDO_API_KEY" \
  -H "Accept: application/json"
require 'net/http'
require 'json'

uri = URI('https://timelydo.com/api/v1/user')

request = Net::HTTP::Get.new(uri)
request['X-API-KEY'] = ENV.fetch('TIMELYDO_API_KEY')
request['Accept'] = 'application/json'

response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end

body = JSON.parse(response.body)

if body['success']
  puts "Authenticated as #{body['data']['email']}"
else
  warn "#{response.code}: #{body['message']}"
end
import os
import requests

response = requests.get(
    "https://timelydo.com/api/v1/user",
    headers={
        "X-API-KEY": os.environ["TIMELYDO_API_KEY"],
        "Accept": "application/json",
    },
    timeout=10,
)

body = response.json()

if body["success"]:
    print("Authenticated as", body["data"]["email"])
else:
    print(f"{response.status_code}: {body['message']}")
// Node.js 18 or newer (built in fetch). Run this on your server, never in a browser.
async function getCurrentUser() {
  const response = await fetch("https://timelydo.com/api/v1/user", {
    headers: {
      "X-API-KEY": process.env.TIMELYDO_API_KEY,
      "Accept": "application/json",
    },
  });

  const body = await response.json();

  if (body.success) {
    console.log(`Authenticated as ${body.data.email}`);
  } else {
    console.error(`${response.status}: ${body.message}`);
  }
}

getCurrentUser();

A valid key returns the account it belongs to (trimmed):

{
  "success": true,
  "message": "user details",
  "data": {
    "id": "3f6c2a9e-8b1d-4c7a-9e21-5d0b7f4a1c33",
    "email": "jane@example.com",
    "full_name": "Jane Doe",
    "url": "jane",
    "avatar_name": "JD",
    "timezone": "Berlin",
    "date_timezone": "Europe/Berlin",
    "locale": "en",
    "time_format": "12_hours",
    "date_format": "31/12/2024",
    "currency": "eur",
    "country_iso_code": "de",
    "api_key": "YOUR_API_KEY",
    "created_at": "2026-08-09T04:23:16.260Z"
  },
  "status": 200
}

A missing or wrong key returns 401:

{
  "success": false,
  "message": "Invalid api_key",
  "data": {},
  "status": "unauthorized"
}

Send the key in the X-API-KEY header on every request:

X-API-KEY: YOUR_API_KEY

The header name is not case sensitive. The key itself must match exactly.

The quickest way to check a key is GET /api/v1/user. It returns the account the key belongs to.

HTTP Request

GET https://timelydo.com/api/v1/user

Authentication errors

HTTP status message Cause
401 api_key is missing No X-API-KEY header was sent.
401 Invalid api_key The key does not match any active account. It may have been regenerated, or the account was deleted.
403 depends on the endpoint The endpoint only works from a signed in browser session. See below.

Endpoints that need a signed in session

A few account security settings cannot be changed with an API key, even a valid one. This protects your account if a key ever leaks. These endpoints return 403 Forbidden for API key requests:

Endpoint 403 message
/api/v1/two_factor (all methods) and /api/v1/two_factor/backup_codes Sign in to the app to manage two factor authentication.
PUT /api/v1/users/change_password and PUT /api/v1/user/destroy_me Sign in to the app to change your password or delete your account.
/api/v1/hellobar and its actions A signed in session is required

Manage these from the TimelyDo app instead.

Regenerate your API key

Regenerate your key if it may have leaked, or to rotate it on a schedule. The old key stops working immediately, so update every integration that uses it right away.

You can regenerate the key in two ways.

In the app

  1. Open Settings, then Developers Console, then API Key.
  2. Click Regenerate key.
  3. Copy the new key shown on the page.

With the API

curl -X PUT "https://timelydo.com/api/v1/users/regenerate_api_key" \
  -H "X-API-KEY: $TIMELYDO_API_KEY" \
  -H "Accept: application/json"
require 'net/http'
require 'json'

uri = URI('https://timelydo.com/api/v1/users/regenerate_api_key')

request = Net::HTTP::Put.new(uri)
request['X-API-KEY'] = ENV.fetch('TIMELYDO_API_KEY')
request['Accept'] = 'application/json'

response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end

body = JSON.parse(response.body)

if body['success']
  new_api_key = body['data']['api_key']
  # Save new_api_key somewhere safe now. The old key has already stopped working.
  puts "New key received (#{new_api_key.length} characters)"
else
  warn "#{response.code}: #{body['message']}"
end
import os
import requests

response = requests.put(
    "https://timelydo.com/api/v1/users/regenerate_api_key",
    headers={
        "X-API-KEY": os.environ["TIMELYDO_API_KEY"],
        "Accept": "application/json",
    },
    timeout=10,
)

body = response.json()

if body["success"]:
    new_api_key = body["data"]["api_key"]
    # Save new_api_key somewhere safe now. The old key has already stopped working.
    print(f"New key received ({len(new_api_key)} characters)")
else:
    print(f"{response.status_code}: {body['message']}")
// Node.js 18 or newer (built in fetch). Run this on your server, never in a browser.
async function regenerateApiKey() {
  const response = await fetch("https://timelydo.com/api/v1/users/regenerate_api_key", {
    method: "PUT",
    headers: {
      "X-API-KEY": process.env.TIMELYDO_API_KEY,
      "Accept": "application/json",
    },
  });

  const body = await response.json();

  if (body.success) {
    const newApiKey = body.data.api_key;
    // Save newApiKey somewhere safe now. The old key has already stopped working.
    console.log(`New key received (${newApiKey.length} characters)`);
  } else {
    console.error(`${response.status}: ${body.message}`);
  }
}

regenerateApiKey();

The response contains the new key:

{
  "success": true,
  "message": "api_key is updated",
  "data": {
    "api_key": "YOUR_NEW_API_KEY"
  },
  "status": 200
}

Authenticate this request with your current key. The response returns the new key, and from that moment only the new key works.

HTTP Request

PUT https://timelydo.com/api/v1/users/regenerate_api_key

This endpoint takes no parameters.

If you lost your key

You cannot get a key back through the API without a working key. Sign in to TimelyDo and copy it from the API Key page, or regenerate a new one there.

User

Read and update the account your API key belongs to.

The user object

A user object (trimmed):

{
  "id": "3f6c2a9e-8b1d-4c7a-9e21-5d0b7f4a1c33",
  "email": "jane@example.com",
  "full_name": "Jane Doe",
  "url": "jane",
  "avatar_name": "JD",
  "bio": "Career coach helping people change jobs with confidence.",
  "timezone": "Berlin",
  "date_timezone": "Europe/Berlin",
  "time_format": "12_hours",
  "date_format": "31/12/2024",
  "locale": "en",
  "currency": "eur",
  "country_iso_code": "de",
  "calendar_view": "week",
  "dob": null,
  "show_custom_policy": false,
  "created_at": "2026-08-09T04:23:15.560Z",
  "api_key": "YOUR_API_KEY"
}
Field Type Description
id string Unique id of the account.
email string Sign in email address.
full_name string Name shown on your booking pages.
url string Your booking page address: https://timelydo.com/<url>.
avatar_name string Initials used when there is no profile picture.
bio string or null Short introduction shown on your profile.
timezone string Your time zone, as a TimelyDo time zone name such as Berlin or Eastern Time (US & Canada).
date_timezone string The same time zone as a standard IANA name, such as Europe/Berlin.
time_format string 12_hours or 24_hours.
date_format string How dates are written. See Update the current user.
locale string Language of the app and your emails.
currency string Lowercase ISO currency code, such as usd or eur.
country_iso_code string or null Lowercase two letter country code.
calendar_view string Default calendar view: day, week or month.
created_at string When the account was created, in ISO 8601 UTC.
api_key string Your API key. Treat every response that contains it as secret.

Get the current user

curl "https://timelydo.com/api/v1/user" \
  -H "X-API-KEY: $TIMELYDO_API_KEY" \
  -H "Accept: application/json"
require 'net/http'
require 'json'

uri = URI('https://timelydo.com/api/v1/user')

request = Net::HTTP::Get.new(uri)
request['X-API-KEY'] = ENV.fetch('TIMELYDO_API_KEY')
request['Accept'] = 'application/json'

response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end

body = JSON.parse(response.body)

if body['success']
  puts "#{body['data']['full_name']} (#{body['data']['timezone']})"
else
  warn "#{response.code}: #{body['message']}"
end
import os
import requests

response = requests.get(
    "https://timelydo.com/api/v1/user",
    headers={"X-API-KEY": os.environ["TIMELYDO_API_KEY"], "Accept": "application/json"},
    timeout=10,
)

body = response.json()

if body["success"]:
    print(body["data"]["full_name"], body["data"]["timezone"])
else:
    print(f"{response.status_code}: {body['message']}")
// Node.js 18 or newer. Run this on your server, never in a browser.
async function getCurrentUser() {
  const response = await fetch("https://timelydo.com/api/v1/user", {
    headers: { "X-API-KEY": process.env.TIMELYDO_API_KEY, "Accept": "application/json" },
  });

  const body = await response.json();

  if (body.success) {
    console.log(`${body.data.full_name} (${body.data.timezone})`);
  } else {
    console.error(`${response.status}: ${body.message}`);
  }
}

getCurrentUser();

Returns the user object for the account the API key belongs to.

HTTP Request

GET https://timelydo.com/api/v1/user

Get account settings

curl "https://timelydo.com/api/v1/user/settings" \
  -H "X-API-KEY: $TIMELYDO_API_KEY" \
  -H "Accept: application/json"
require 'net/http'
require 'json'

uri = URI('https://timelydo.com/api/v1/user/settings')

request = Net::HTTP::Get.new(uri)
request['X-API-KEY'] = ENV.fetch('TIMELYDO_API_KEY')
request['Accept'] = 'application/json'

response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end

body = JSON.parse(response.body)

if body['success']
  settings = body['data']
  puts "Week starts on day #{settings['start_day_of_week']}, verified: #{settings['verified']}"
else
  warn "#{response.code}: #{body['message']}"
end
import os
import requests

response = requests.get(
    "https://timelydo.com/api/v1/user/settings",
    headers={"X-API-KEY": os.environ["TIMELYDO_API_KEY"], "Accept": "application/json"},
    timeout=10,
)

body = response.json()

if body["success"]:
    settings = body["data"]
    print("Week starts on day", settings["start_day_of_week"], "verified:", settings["verified"])
else:
    print(f"{response.status_code}: {body['message']}")
// Node.js 18 or newer. Run this on your server, never in a browser.
async function getSettings() {
  const response = await fetch("https://timelydo.com/api/v1/user/settings", {
    headers: { "X-API-KEY": process.env.TIMELYDO_API_KEY, "Accept": "application/json" },
  });

  const body = await response.json();

  if (body.success) {
    console.log(`Week starts on day ${body.data.start_day_of_week}, verified: ${body.data.verified}`);
  } else {
    console.error(`${response.status}: ${body.message}`);
  }
}

getSettings();

Settings add these fields to the user object (trimmed):

{
  "start_day_of_week": 1,
  "designation": "Career coach",
  "label_for_primary_timezone": null,
  "verified": false,
  "need_to_set_password": false,
  "show_message_button_on_profile": true,
  "show_message_button_on_events": true,
  "enable_branding": false,
  "profile_picture_url": "https://timelydo.com/rails/active_storage/...",
  "profile_embed_url": "https://timelydo.com/embed/jane",
  "active_policy": { "id": "...", "content": "<p>Booking policy</p>" },
  "branding": null,
  "social_library": null
}

Returns everything in the user object, plus the settings that shape your profile and booking pages: your profile picture, verification status, booking policy, branding and social links.

Use this endpoint when you need profile_picture_url. The plain user object does not include it.

HTTP Request

GET https://timelydo.com/api/v1/user/settings

Update the current user

curl -X PATCH "https://timelydo.com/api/v1/user" \
  -H "X-API-KEY: $TIMELYDO_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"user": {"time_format": "24_hours", "date_format": "2023-12-31"}}'
require 'net/http'
require 'json'

uri = URI('https://timelydo.com/api/v1/user')

request = Net::HTTP::Patch.new(uri)
request['X-API-KEY'] = ENV.fetch('TIMELYDO_API_KEY')
request['Accept'] = 'application/json'
request['Content-Type'] = 'application/json'
request.body = { user: { time_format: '24_hours', date_format: '2023-12-31' } }.to_json

response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end

body = JSON.parse(response.body)

if body['success']
  puts "Time format is now #{body['data']['time_format']}"
else
  warn "#{response.code}: #{body['message']}"
end
import os
import requests

response = requests.patch(
    "https://timelydo.com/api/v1/user",
    headers={"X-API-KEY": os.environ["TIMELYDO_API_KEY"], "Accept": "application/json"},
    json={"user": {"time_format": "24_hours", "date_format": "2023-12-31"}},
    timeout=10,
)

body = response.json()

if body["success"]:
    print("Time format is now", body["data"]["time_format"])
else:
    print(f"{response.status_code}: {body['message']}")
// Node.js 18 or newer. Run this on your server, never in a browser.
async function updateUser() {
  const response = await fetch("https://timelydo.com/api/v1/user", {
    method: "PATCH",
    headers: {
      "X-API-KEY": process.env.TIMELYDO_API_KEY,
      "Accept": "application/json",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ user: { time_format: "24_hours", date_format: "2023-12-31" } }),
  });

  const body = await response.json();

  if (body.success) {
    console.log(`Time format is now ${body.data.time_format}`);
  } else {
    console.error(`${response.status}: ${body.message}`);
  }
}

updateUser();

A value that is not allowed returns 422:

{
  "success": false,
  "message": "Timezone is not included in the list",
  "data": {},
  "status": "unprocessable_entity"
}

Updates the fields you send and leaves everything else as it is. Send the fields inside a user object. Returns the updated user object.

HTTP Request

PATCH https://timelydo.com/api/v1/user

Body parameters

Parameter Allowed values
full_name Required once set. Up to 80 characters.
url 2 to 80 characters. Must be unique, and cannot start with http, https or /, or contain ., ? or #. Check it first.
bio Up to 500 characters.
designation Up to 180 characters.
timezone A TimelyDo time zone name, such as Berlin, Karachi or Eastern Time (US & Canada). Standard names like Europe/Berlin are not accepted here.
time_format 12_hours or 24_hours.
date_format 31/12/2024, 12/31/2024, 2023-12-31, 31.12.2024, 2024/12/31 or 31-12-2024.
locale en, fr, de, es, nl, ru, zh, ja, ar, hi or it.
currency A lowercase ISO currency code, such as usd, eur or gbp.
start_day_of_week First day of the week in calendars: 0 (Sunday) to 6 (Saturday).
events_layout grid_layout or table_layout.
show_message_button_on_profile true or false.
show_message_button_on_events true or false.

Check a booking page URL

curl -X POST "https://timelydo.com/api/v1/users/validate_url_availability" \
  -H "X-API-KEY: $TIMELYDO_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"user": {"url": "jane-doe-coaching"}}'
require 'net/http'
require 'json'

uri = URI('https://timelydo.com/api/v1/users/validate_url_availability')

request = Net::HTTP::Post.new(uri)
request['X-API-KEY'] = ENV.fetch('TIMELYDO_API_KEY')
request['Accept'] = 'application/json'
request['Content-Type'] = 'application/json'
request.body = { user: { url: 'jane-doe-coaching' } }.to_json

response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end

body = JSON.parse(response.body)

if body['success']
  puts body['data']['available'] ? 'Available' : 'Already taken'
else
  warn "#{response.code}: #{body['message']}"
end
import os
import requests

response = requests.post(
    "https://timelydo.com/api/v1/users/validate_url_availability",
    headers={"X-API-KEY": os.environ["TIMELYDO_API_KEY"], "Accept": "application/json"},
    json={"user": {"url": "jane-doe-coaching"}},
    timeout=10,
)

body = response.json()

if body["success"]:
    print("Available" if body["data"]["available"] else "Already taken")
else:
    print(f"{response.status_code}: {body['message']}")
// Node.js 18 or newer. Run this on your server, never in a browser.
async function checkUrl() {
  const response = await fetch("https://timelydo.com/api/v1/users/validate_url_availability", {
    method: "POST",
    headers: {
      "X-API-KEY": process.env.TIMELYDO_API_KEY,
      "Accept": "application/json",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ user: { url: "jane-doe-coaching" } }),
  });

  const body = await response.json();

  if (body.success) {
    console.log(body.data.available ? "Available" : "Already taken");
  } else {
    console.error(`${response.status}: ${body.message}`);
  }
}

checkUrl();

The response:

{
  "success": true,
  "message": "url is available to use",
  "data": { "available": true },
  "status": 200
}

Tells you whether a booking page URL is free before you update your url. Your own current URL counts as available. URLs are compared in lowercase.

This only checks that nobody else uses the URL. The format rules are applied when you save it.

HTTP Request

POST https://timelydo.com/api/v1/users/validate_url_availability

Body parameters

Parameter Description
user[url] The URL to check.

Upload a profile picture

curl -X PUT "https://timelydo.com/api/v1/users/profile_picture" \
  -H "X-API-KEY: $TIMELYDO_API_KEY" \
  -H "Accept: application/json" \
  -F "user[profile_picture]=@avatar.png;type=image/png"
require 'net/http'
require 'json'

uri = URI('https://timelydo.com/api/v1/users/profile_picture')

request = Net::HTTP::Put.new(uri)
request['X-API-KEY'] = ENV.fetch('TIMELYDO_API_KEY')
request['Accept'] = 'application/json'

File.open('avatar.png', 'rb') do |image|
  request.set_form(
    [['user[profile_picture]', image, { filename: 'avatar.png', content_type: 'image/png' }]],
    'multipart/form-data'
  )

  response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
    http.request(request)
  end

  body = JSON.parse(response.body)

  if body['success']
    puts body['message']
  else
    warn "#{response.code}: #{body['message']}"
  end
end
import os
import requests

with open("avatar.png", "rb") as image:
    response = requests.put(
        "https://timelydo.com/api/v1/users/profile_picture",
        headers={"X-API-KEY": os.environ["TIMELYDO_API_KEY"], "Accept": "application/json"},
        files={"user[profile_picture]": ("avatar.png", image, "image/png")},
        timeout=30,
    )

body = response.json()

if body["success"]:
    print(body["message"])
else:
    print(f"{response.status_code}: {body['message']}")
// Node.js 18 or newer. Run this on your server, never in a browser.
const fs = require("fs");

async function uploadProfilePicture() {
  const form = new FormData();
  const image = new Blob([fs.readFileSync("avatar.png")], { type: "image/png" });
  form.append("user[profile_picture]", image, "avatar.png");

  const response = await fetch("https://timelydo.com/api/v1/users/profile_picture", {
    method: "PUT",
    headers: { "X-API-KEY": process.env.TIMELYDO_API_KEY, "Accept": "application/json" },
    body: form,
  });

  const body = await response.json();

  if (body.success) {
    console.log(body.message);
  } else {
    console.error(`${response.status}: ${body.message}`);
  }
}

uploadProfilePicture();

A file that is not an allowed image returns 422:

{
  "success": false,
  "message": "unsupported image type. Use PNG, JPEG, GIF, or WEBP.",
  "data": {},
  "status": "unprocessable_entity"
}

Replaces your profile picture. Send the image as multipart/form-data in the user[profile_picture] field. Returns the updated user object. To get the new picture's address, call Get account settings.

HTTP Request

PUT https://timelydo.com/api/v1/users/profile_picture

Rules

Upload errors

HTTP status message Cause
400 missing params No user[profile_picture] field was sent.
422 profile_picture must be an uploaded image file The field was sent as text instead of a file.
422 unsupported image type. Use PNG, JPEG, GIF, or WEBP. The file is not an allowed image type.
422 image must be 5MB or smaller The file is too large.

Remove the profile picture

curl -X DELETE "https://timelydo.com/api/v1/users/remove_profile_picture" \
  -H "X-API-KEY: $TIMELYDO_API_KEY" \
  -H "Accept: application/json"
require 'net/http'
require 'json'

uri = URI('https://timelydo.com/api/v1/users/remove_profile_picture')

request = Net::HTTP::Delete.new(uri)
request['X-API-KEY'] = ENV.fetch('TIMELYDO_API_KEY')
request['Accept'] = 'application/json'

response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end

body = JSON.parse(response.body)

if body['success']
  puts body['message']
else
  warn "#{response.code}: #{body['message']}"
end
import os
import requests

response = requests.delete(
    "https://timelydo.com/api/v1/users/remove_profile_picture",
    headers={"X-API-KEY": os.environ["TIMELYDO_API_KEY"], "Accept": "application/json"},
    timeout=10,
)

body = response.json()

if body["success"]:
    print(body["message"])
else:
    print(f"{response.status_code}: {body['message']}")
// Node.js 18 or newer. Run this on your server, never in a browser.
async function removeProfilePicture() {
  const response = await fetch("https://timelydo.com/api/v1/users/remove_profile_picture", {
    method: "DELETE",
    headers: { "X-API-KEY": process.env.TIMELYDO_API_KEY, "Accept": "application/json" },
  });

  const body = await response.json();

  if (body.success) {
    console.log(body.message);
  } else {
    console.error(`${response.status}: ${body.message}`);
  }
}

removeProfilePicture();

The response:

{
  "success": true,
  "message": "profile picture removed",
  "data": { "id": "3f6c2a9e-8b1d-4c7a-9e21-5d0b7f4a1c33", "avatar_name": "JD" },
  "status": 200
}

Removes your profile picture. Your initials (avatar_name) are shown instead. Returns the updated user object. Calling it when there is no picture also succeeds.

HTTP Request

DELETE https://timelydo.com/api/v1/users/remove_profile_picture

Events

An event is a bookable meeting type on your booking page, for example a 30 minute intro call. People pick a time from your availability and book it.

The examples below use the event id 7a1c33e2-4b8d-4f6a-9c21-5d0b7f4a1e90. Replace it with one of your own from List your events.

The event object

An event object:

{
  "id": "7a1c33e2-4b8d-4f6a-9c21-5d0b7f4a1e90",
  "name": "Intro call",
  "url": "intro-call",
  "absolute_url": "/jane/intro-call",
  "active": true,
  "is_public": true,
  "color": "peacock",
  "slot_duration": "30 Minutes",
  "padding_duration": "0 Minutes",
  "location_name": "Google Meet",
  "description": "A quick call to see if we are a good fit.",
  "connected_calendar_email": "jane@example.com",
  "created_at": "17 August, 2026",
  "created_at_ago": "1 month ago",
  "formated_created_at_for_host": "Monday, August 17, 2026 at 09:20 AM",
  "active_event_questions": []
}
Field Type Description
id string Unique id of the event.
name string Name people see when booking.
url string Last part of the booking link.
absolute_url string Path of the booking page, for example /jane/intro-call.
active boolean Whether the event accepts bookings.
is_public boolean Whether the event is listed on your public profile.
color string Colour name used in calendars.
slot_duration string Meeting length, already written for display, such as 30 Minutes.
padding_duration string Gap kept after each meeting, such as 15 Minutes.
location_name string Where the meeting happens, such as Google Meet or No Location.
description string or null Description shown on the booking page. Line breaks come back as <br>.
connected_calendar_email string or null Calendar the bookings are written to.
created_at string Date the event was created.
active_event_questions array Questions on the booking form. See Booking form questions.

Booking form questions

An entry in active_event_questions:

{
  "id": "6d2b1f4a-9c3e-4a71-8f52-1b7d9e0c4a38",
  "required": true,
  "order_number": 1,
  "conditions": [],
  "created_at_ago": "3 weeks ago",
  "question": {
    "id": "21f8c6b4-7e19-4d3a-95cf-8b204e7a1d66",
    "title": "What would you like to cover?",
    "question_type": "multi_line",
    "state": "active",
    "question_details": {},
    "events_count": 2,
    "created_at": "Monday, August 17, 2026 at 09:22 AM"
  }
}

Each entry links one of your questions to this event.

Field Type Description
id string Id of the link between the event and the question. Use it when updating or removing the question from this event.
required boolean Whether the person booking must answer.
order_number integer Position on the form, lowest first.
conditions array Conditional logic rules, if any.
question.title string The question text.
question.question_type string One of one_line, multi_line, radio_button, check_box, drop_down, phone_number, email, number, boolean, url, date, hidden, rating, file_upload, signature.
question.state string draft, active or archived.
question.question_details object Type specific settings, such as the list of options.
question.events_count integer How many of your events use this question.

List your events

curl "https://timelydo.com/api/v1/events" \
  -H "X-API-KEY: $TIMELYDO_API_KEY" \
  -H "Accept: application/json"
require 'net/http'
require 'json'

uri = URI('https://timelydo.com/api/v1/events')

request = Net::HTTP::Get.new(uri)
request['X-API-KEY'] = ENV.fetch('TIMELYDO_API_KEY')
request['Accept'] = 'application/json'

response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end

body = JSON.parse(response.body)

if body['success']
  body['data'].each { |event| puts "#{event['name']} (#{event['slot_duration']})" }
else
  warn "#{response.code}: #{body['message']}"
end
import os
import requests

response = requests.get(
    "https://timelydo.com/api/v1/events",
    headers={"X-API-KEY": os.environ["TIMELYDO_API_KEY"], "Accept": "application/json"},
    timeout=10,
)

body = response.json()

if body["success"]:
    for event in body["data"]:
        print(event["name"], event["slot_duration"])
else:
    print(f"{response.status_code}: {body['message']}")
// Node.js 18 or newer. Run this on your server, never in a browser.
async function listEvents() {
  const response = await fetch("https://timelydo.com/api/v1/events", {
    headers: { "X-API-KEY": process.env.TIMELYDO_API_KEY, "Accept": "application/json" },
  });

  const body = await response.json();

  if (body.success) {
    body.data.forEach((event) => console.log(`${event.name} (${event.slot_duration})`));
  } else {
    console.error(`${response.status}: ${body.message}`);
  }
}

listEvents();

Lists your events, newest first. Deleted events are left out.

HTTP Request

GET https://timelydo.com/api/v1/events

Get an event

curl "https://timelydo.com/api/v1/events/7a1c33e2-4b8d-4f6a-9c21-5d0b7f4a1e90" \
  -H "X-API-KEY: $TIMELYDO_API_KEY" \
  -H "Accept: application/json"
require 'net/http'
require 'json'

uri = URI('https://timelydo.com/api/v1/events/7a1c33e2-4b8d-4f6a-9c21-5d0b7f4a1e90')

request = Net::HTTP::Get.new(uri)
request['X-API-KEY'] = ENV.fetch('TIMELYDO_API_KEY')
request['Accept'] = 'application/json'

response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end

body = JSON.parse(response.body)

if body['success']
  puts "#{body['data']['name']} has #{body['data']['active_event_questions'].length} questions"
else
  warn "#{response.code}: #{body['message']}"
end
import os
import requests

response = requests.get(
    "https://timelydo.com/api/v1/events/7a1c33e2-4b8d-4f6a-9c21-5d0b7f4a1e90",
    headers={"X-API-KEY": os.environ["TIMELYDO_API_KEY"], "Accept": "application/json"},
    timeout=10,
)

body = response.json()

if body["success"]:
    print(body["data"]["name"], "has", len(body["data"]["active_event_questions"]), "questions")
else:
    print(f"{response.status_code}: {body['message']}")
// Node.js 18 or newer. Run this on your server, never in a browser.
async function getEvent() {
  const response = await fetch(
    "https://timelydo.com/api/v1/events/7a1c33e2-4b8d-4f6a-9c21-5d0b7f4a1e90",
    { headers: { "X-API-KEY": process.env.TIMELYDO_API_KEY, "Accept": "application/json" } },
  );

  const body = await response.json();

  if (body.success) {
    console.log(`${body.data.name} has ${body.data.active_event_questions.length} questions`);
  } else {
    console.error(`${response.status}: ${body.message}`);
  }
}

getEvent();

Returns one event object. An id that is not yours returns 404, never someone else's event.

HTTP Request

GET https://timelydo.com/api/v1/events/<ID>

Create an event

curl -X POST "https://timelydo.com/api/v1/events" \
  -H "X-API-KEY: $TIMELYDO_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"event": {"name": "Intro call", "url": "intro-call", "slot_duration": "30_minutes", "color": "peacock", "location": {"name": "no_location"}}}'
require 'net/http'
require 'json'

uri = URI('https://timelydo.com/api/v1/events')

request = Net::HTTP::Post.new(uri)
request['X-API-KEY'] = ENV.fetch('TIMELYDO_API_KEY')
request['Accept'] = 'application/json'
request['Content-Type'] = 'application/json'
request.body = {
  event: {
    name: 'Intro call',
    url: 'intro-call',
    slot_duration: '30_minutes',
    color: 'peacock',
    location: { name: 'no_location' }
  }
}.to_json

response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end

body = JSON.parse(response.body)

if body['success']
  puts "Created #{body['data']['name']} at #{body['data']['absolute_url']}"
else
  warn "#{response.code}: #{body['message']}"
end
import os
import requests

response = requests.post(
    "https://timelydo.com/api/v1/events",
    headers={"X-API-KEY": os.environ["TIMELYDO_API_KEY"], "Accept": "application/json"},
    json={
        "event": {
            "name": "Intro call",
            "url": "intro-call",
            "slot_duration": "30_minutes",
            "color": "peacock",
            "location": {"name": "no_location"},
        }
    },
    timeout=10,
)

body = response.json()

if body["success"]:
    print("Created", body["data"]["name"], "at", body["data"]["absolute_url"])
else:
    print(f"{response.status_code}: {body['message']}")
// Node.js 18 or newer. Run this on your server, never in a browser.
async function createEvent() {
  const response = await fetch("https://timelydo.com/api/v1/events", {
    method: "POST",
    headers: {
      "X-API-KEY": process.env.TIMELYDO_API_KEY,
      "Accept": "application/json",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      event: {
        name: "Intro call",
        url: "intro-call",
        slot_duration: "30_minutes",
        color: "peacock",
        location: { name: "no_location" },
      },
    }),
  });

  const body = await response.json();

  if (body.success) {
    console.log(`Created ${body.data.name} at ${body.data.absolute_url}`);
  } else {
    console.error(`${response.status}: ${body.message}`);
  }
}

createEvent();

A value that is not allowed returns 422:

{
  "success": false,
  "message": "Slot duration is not included in the list",
  "data": {},
  "status": "unprocessable_entity"
}

Creates an event and returns it. name and url are required; everything else falls back to a sensible default.

HTTP Request

POST https://timelydo.com/api/v1/events

Body parameters

Parameter Allowed values
name Required. Up to 80 characters.
url Required. Up to 80 characters, unique among your events. Cannot start with http, https or /, or contain ., ? or #. Check it first.
slot_duration 15_minutes, 30_minutes, 45_minutes, 1_hour or 2_hours.
padding_duration 0_minutes, 5_minutes, 10_minutes, 15_minutes, 30_minutes, 45_minutes, 1_hour or 2_hours.
color lavender, sage, grape, flamingo, banana, tangerine, peacock, graphite, blueberry, basil or tomato.
description Up to 500 characters.
location An object with a name of no_location, google_meet, zoom, ask_invitee, in_person or phone_call, plus that type's own settings. See Locations.
active true or false. An inactive event takes no bookings.
is_public true or false. Controls whether it appears on your public profile.
enable_guest_emails true or false. Lets the person booking invite guests.
guest_emails_limit How many guests they may add.
connected_calendar_id Id of the calendar bookings are written to, or no_calendar_selected.
event_questions_attributes Booking form questions. See Add questions to an event.

Locations

Location name Extra settings
no_location none
google_meet none. Needs a connected Google calendar.
zoom none. Needs Zoom connected in Integrations.
ask_invitee none. The person booking says where.
in_person details: the address or room.
phone_call selected_option: host_will_call_invitee or invitee_will_call_host, plus host_contact_number or invitee_contact_number.

Update an event

curl -X PATCH "https://timelydo.com/api/v1/events/7a1c33e2-4b8d-4f6a-9c21-5d0b7f4a1e90" \
  -H "X-API-KEY: $TIMELYDO_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"event": {"name": "Intro call", "url": "intro-call", "description": "A quick 30 minute chat.", "color": "basil"}}'
require 'net/http'
require 'json'

uri = URI('https://timelydo.com/api/v1/events/7a1c33e2-4b8d-4f6a-9c21-5d0b7f4a1e90')

request = Net::HTTP::Patch.new(uri)
request['X-API-KEY'] = ENV.fetch('TIMELYDO_API_KEY')
request['Accept'] = 'application/json'
request['Content-Type'] = 'application/json'
request.body = {
  event: {
    name: 'Intro call',
    url: 'intro-call',
    description: 'A quick 30 minute chat.',
    color: 'basil'
  }
}.to_json

response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end

body = JSON.parse(response.body)

if body['success']
  puts "Updated #{body['data']['name']}"
else
  warn "#{response.code}: #{body['message']}"
end
import os
import requests

response = requests.patch(
    "https://timelydo.com/api/v1/events/7a1c33e2-4b8d-4f6a-9c21-5d0b7f4a1e90",
    headers={"X-API-KEY": os.environ["TIMELYDO_API_KEY"], "Accept": "application/json"},
    json={
        "event": {
            "name": "Intro call",
            "url": "intro-call",
            "description": "A quick 30 minute chat.",
            "color": "basil",
        }
    },
    timeout=10,
)

body = response.json()

if body["success"]:
    print("Updated", body["data"]["name"])
else:
    print(f"{response.status_code}: {body['message']}")
// Node.js 18 or newer. Run this on your server, never in a browser.
async function updateEvent() {
  const response = await fetch(
    "https://timelydo.com/api/v1/events/7a1c33e2-4b8d-4f6a-9c21-5d0b7f4a1e90",
    {
      method: "PATCH",
      headers: {
        "X-API-KEY": process.env.TIMELYDO_API_KEY,
        "Accept": "application/json",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        event: {
          name: "Intro call",
          url: "intro-call",
          description: "A quick 30 minute chat.",
          color: "basil",
        },
      }),
    },
  );

  const body = await response.json();

  if (body.success) {
    console.log(`Updated ${body.data.name}`);
  } else {
    console.error(`${response.status}: ${body.message}`);
  }
}

updateEvent();

Updates an event and returns it. Send the whole event object, including name and url, the same way the app does.

HTTP Request

PATCH https://timelydo.com/api/v1/events/<ID>

Body parameters

The same as Create an event.

Add questions to an event

curl -X PATCH "https://timelydo.com/api/v1/events/7a1c33e2-4b8d-4f6a-9c21-5d0b7f4a1e90" \
  -H "X-API-KEY: $TIMELYDO_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"event": {"name": "Intro call", "url": "intro-call", "event_questions_attributes": []}}'
require 'net/http'
require 'json'

uri = URI('https://timelydo.com/api/v1/events/7a1c33e2-4b8d-4f6a-9c21-5d0b7f4a1e90')

request = Net::HTTP::Patch.new(uri)
request['X-API-KEY'] = ENV.fetch('TIMELYDO_API_KEY')
request['Accept'] = 'application/json'
request['Content-Type'] = 'application/json'

# One entry per question the form should have, in the order people see them.
request.body = {
  event: {
    name: 'Intro call',
    url: 'intro-call',
    event_questions_attributes: []
  }
}.to_json

response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end

body = JSON.parse(response.body)

if body['success']
  puts "Form now has #{body['data']['active_event_questions'].length} questions"
else
  warn "#{response.code}: #{body['message']}"
end
import os
import requests

# One entry per question the form should have, in the order people see them.
response = requests.patch(
    "https://timelydo.com/api/v1/events/7a1c33e2-4b8d-4f6a-9c21-5d0b7f4a1e90",
    headers={"X-API-KEY": os.environ["TIMELYDO_API_KEY"], "Accept": "application/json"},
    json={
        "event": {
            "name": "Intro call",
            "url": "intro-call",
            "event_questions_attributes": [],
        }
    },
    timeout=10,
)

body = response.json()

if body["success"]:
    print("Form now has", len(body["data"]["active_event_questions"]), "questions")
else:
    print(f"{response.status_code}: {body['message']}")
// Node.js 18 or newer. Run this on your server, never in a browser.
async function setEventQuestions() {
  const response = await fetch(
    "https://timelydo.com/api/v1/events/7a1c33e2-4b8d-4f6a-9c21-5d0b7f4a1e90",
    {
      method: "PATCH",
      headers: {
        "X-API-KEY": process.env.TIMELYDO_API_KEY,
        "Accept": "application/json",
        "Content-Type": "application/json",
      },
      // One entry per question the form should have, in the order people see them.
      body: JSON.stringify({
        event: {
          name: "Intro call",
          url: "intro-call",
          event_questions_attributes: [],
        },
      }),
    },
  );

  const body = await response.json();

  if (body.success) {
    console.log(`Form now has ${body.data.active_event_questions.length} questions`);
  } else {
    console.error(`${response.status}: ${body.message}`);
  }
}

setEventQuestions();

One entry per question, for example:

{
  "event_questions_attributes": [
    { "question_id": "21f8c6b4-7e19-4d3a-95cf-8b204e7a1d66", "required": true, "order_number": 1 },
    { "id": "6d2b1f4a-9c3e-4a71-8f52-1b7d9e0c4a38", "required": false, "order_number": 2 },
    { "id": "5c1a0e39-3b77-4d02-9a6e-7f1c8d25b430", "_destroy": true }
  ]
}

Questions belong to your account and are reused across events. This endpoint decides which of them appear on one event's booking form, in what order, and which are required.

Key Use it to
question_id Add one of your questions to this event.
id Change a question already on this event. It is the entry's own id from active_event_questions, not the question id.
required Whether an answer is needed.
order_number Position on the form, lowest first.
_destroy Set to true with the entry's id to take the question off this event. The question itself is kept.

A question_id that is not yours is ignored rather than rejected, so the rest of the update still applies.

HTTP Request

PATCH https://timelydo.com/api/v1/events/<ID>

Duplicate an event

curl -X POST "https://timelydo.com/api/v1/events/7a1c33e2-4b8d-4f6a-9c21-5d0b7f4a1e90/clone" \
  -H "X-API-KEY: $TIMELYDO_API_KEY" \
  -H "Accept: application/json"
require 'net/http'
require 'json'

uri = URI('https://timelydo.com/api/v1/events/7a1c33e2-4b8d-4f6a-9c21-5d0b7f4a1e90/clone')

request = Net::HTTP::Post.new(uri)
request['X-API-KEY'] = ENV.fetch('TIMELYDO_API_KEY')
request['Accept'] = 'application/json'

response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end

body = JSON.parse(response.body)

if body['success']
  puts "Copy created: #{body['data']['name']}"
else
  warn "#{response.code}: #{body['message']}"
end
import os
import requests

response = requests.post(
    "https://timelydo.com/api/v1/events/7a1c33e2-4b8d-4f6a-9c21-5d0b7f4a1e90/clone",
    headers={"X-API-KEY": os.environ["TIMELYDO_API_KEY"], "Accept": "application/json"},
    timeout=10,
)

body = response.json()

if body["success"]:
    print("Copy created:", body["data"]["name"])
else:
    print(f"{response.status_code}: {body['message']}")
// Node.js 18 or newer. Run this on your server, never in a browser.
async function cloneEvent() {
  const response = await fetch(
    "https://timelydo.com/api/v1/events/7a1c33e2-4b8d-4f6a-9c21-5d0b7f4a1e90/clone",
    { method: "POST", headers: { "X-API-KEY": process.env.TIMELYDO_API_KEY, "Accept": "application/json" } },
  );

  const body = await response.json();

  if (body.success) {
    console.log(`Copy created: ${body.data.name}`);
  } else {
    console.error(`${response.status}: ${body.message}`);
  }
}

cloneEvent();

Copies an event, including its booking form questions. The copy is named [CLONE] <name> and gets its own URL. Rename it with Update an event.

Names are limited to 80 characters, so cloning an event whose name is already near the limit returns 422.

HTTP Request

POST https://timelydo.com/api/v1/events/<ID>/clone

Delete an event

curl -X DELETE "https://timelydo.com/api/v1/events/7a1c33e2-4b8d-4f6a-9c21-5d0b7f4a1e90" \
  -H "X-API-KEY: $TIMELYDO_API_KEY" \
  -H "Accept: application/json"
require 'net/http'
require 'json'

uri = URI('https://timelydo.com/api/v1/events/7a1c33e2-4b8d-4f6a-9c21-5d0b7f4a1e90')

request = Net::HTTP::Delete.new(uri)
request['X-API-KEY'] = ENV.fetch('TIMELYDO_API_KEY')
request['Accept'] = 'application/json'

response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end

body = JSON.parse(response.body)

puts body['success'] ? body['message'] : "#{response.code}: #{body['message']}"
import os
import requests

response = requests.delete(
    "https://timelydo.com/api/v1/events/7a1c33e2-4b8d-4f6a-9c21-5d0b7f4a1e90",
    headers={"X-API-KEY": os.environ["TIMELYDO_API_KEY"], "Accept": "application/json"},
    timeout=10,
)

body = response.json()
print(body["message"] if body["success"] else f"{response.status_code}: {body['message']}")
// Node.js 18 or newer. Run this on your server, never in a browser.
async function deleteEvent() {
  const response = await fetch(
    "https://timelydo.com/api/v1/events/7a1c33e2-4b8d-4f6a-9c21-5d0b7f4a1e90",
    { method: "DELETE", headers: { "X-API-KEY": process.env.TIMELYDO_API_KEY, "Accept": "application/json" } },
  );

  const body = await response.json();

  if (body.success) {
    console.log(body.message);
  } else {
    console.error(`${response.status}: ${body.message}`);
  }
}

deleteEvent();

Removes the event from your booking page. Existing bookings are kept, and the event stops appearing in List your events. Its URL becomes free for a new event.

HTTP Request

DELETE https://timelydo.com/api/v1/events/<ID>

List bookings for an event

curl "https://timelydo.com/api/v1/events/7a1c33e2-4b8d-4f6a-9c21-5d0b7f4a1e90/bookings?category=upcoming" \
  -H "X-API-KEY: $TIMELYDO_API_KEY" \
  -H "Accept: application/json"
require 'net/http'
require 'json'

uri = URI('https://timelydo.com/api/v1/events/7a1c33e2-4b8d-4f6a-9c21-5d0b7f4a1e90/bookings')
uri.query = URI.encode_www_form(category: 'upcoming')

request = Net::HTTP::Get.new(uri)
request['X-API-KEY'] = ENV.fetch('TIMELYDO_API_KEY')
request['Accept'] = 'application/json'

response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end

body = JSON.parse(response.body)

if body['success']
  puts "#{body['data']['page']['total_records']} bookings"
else
  warn "#{response.code}: #{body['message']}"
end
import os
import requests

response = requests.get(
    "https://timelydo.com/api/v1/events/7a1c33e2-4b8d-4f6a-9c21-5d0b7f4a1e90/bookings",
    headers={"X-API-KEY": os.environ["TIMELYDO_API_KEY"], "Accept": "application/json"},
    params={"category": "upcoming"},
    timeout=10,
)

body = response.json()

if body["success"]:
    print(body["data"]["page"]["total_records"], "bookings")
else:
    print(f"{response.status_code}: {body['message']}")
// Node.js 18 or newer. Run this on your server, never in a browser.
async function listEventBookings() {
  const response = await fetch(
    "https://timelydo.com/api/v1/events/7a1c33e2-4b8d-4f6a-9c21-5d0b7f4a1e90/bookings?category=upcoming",
    { headers: { "X-API-KEY": process.env.TIMELYDO_API_KEY, "Accept": "application/json" } },
  );

  const body = await response.json();

  if (body.success) {
    console.log(`${body.data.page.total_records} bookings`);
  } else {
    console.error(`${response.status}: ${body.message}`);
  }
}

listEventBookings();

The response (trimmed):

{
  "success": true,
  "message": "Bookings found",
  "data": {
    "page": { "limit": 10, "number": 1, "total_pages": 1, "total_records": 2, "current_page": 1 },
    "records": [
      {
        "id": "b4f1c07a-52d8-4e6b-93a1-6c28d0e7f915",
        "status": "confirmed",
        "event_name": "Intro call",
        "color": "peacock",
        "attendee_name": "Sam Rivera",
        "attendee_email": "sam@example.com",
        "host_ui_booking_range_with_date": "Wednesday, 23 September 2026, 10:00am to 10:30am",
        "ui_duration": "30 Minutes",
        "formatted_location_name": "Google Meet",
        "internal_notes": null,
        "can_be_rescheduled": true,
        "private_key": "8c2f5b19-77ad-4e30-9c6b-1f0a2d3e4b58"
      }
    ]
  },
  "status": 200
}

Lists the bookings made for one event, grouped into the same categories the app shows.

HTTP Request

GET https://timelydo.com/api/v1/events/<ID>/bookings

Query parameters

Parameter Default Description
category upcoming upcoming, recent, past, canceled or archived. Anything else falls back to upcoming.
date_from today Earliest day to include, as YYYY-MM-DD.
date_to in 15 days Latest day to include, as YYYY-MM-DD.
page[number] 1 Page to return.
page[limit] 10 Bookings per page, up to 500.

Dates that cannot be read fall back to the default range rather than failing.

Category What it holds
upcoming Confirmed bookings from now on, soonest first.
recent The same bookings, newest booked first.
past Confirmed bookings that already happened.
canceled Canceled bookings still in the future.
archived Bookings you archived, whatever their status.

Count bookings for an event

curl "https://timelydo.com/api/v1/events/7a1c33e2-4b8d-4f6a-9c21-5d0b7f4a1e90/bookings_counts" \
  -H "X-API-KEY: $TIMELYDO_API_KEY" \
  -H "Accept: application/json"
require 'net/http'
require 'json'

uri = URI('https://timelydo.com/api/v1/events/7a1c33e2-4b8d-4f6a-9c21-5d0b7f4a1e90/bookings_counts')

request = Net::HTTP::Get.new(uri)
request['X-API-KEY'] = ENV.fetch('TIMELYDO_API_KEY')
request['Accept'] = 'application/json'

response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end

body = JSON.parse(response.body)

if body['success']
  puts "#{body['data']['upcoming']} upcoming, #{body['data']['past']} past"
else
  warn "#{response.code}: #{body['message']}"
end
import os
import requests

response = requests.get(
    "https://timelydo.com/api/v1/events/7a1c33e2-4b8d-4f6a-9c21-5d0b7f4a1e90/bookings_counts",
    headers={"X-API-KEY": os.environ["TIMELYDO_API_KEY"], "Accept": "application/json"},
    timeout=10,
)

body = response.json()

if body["success"]:
    print(body["data"]["upcoming"], "upcoming,", body["data"]["past"], "past")
else:
    print(f"{response.status_code}: {body['message']}")
// Node.js 18 or newer. Run this on your server, never in a browser.
async function countEventBookings() {
  const response = await fetch(
    "https://timelydo.com/api/v1/events/7a1c33e2-4b8d-4f6a-9c21-5d0b7f4a1e90/bookings_counts",
    { headers: { "X-API-KEY": process.env.TIMELYDO_API_KEY, "Accept": "application/json" } },
  );

  const body = await response.json();

  if (body.success) {
    console.log(`${body.data.upcoming} upcoming, ${body.data.past} past`);
  } else {
    console.error(`${response.status}: ${body.message}`);
  }
}

countEventBookings();

The response:

{
  "success": true,
  "message": "Counts fetched",
  "data": { "recent": 2, "upcoming": 2, "past": 5, "canceled": 1, "archived": 0 },
  "status": 200
}

Returns how many bookings sit in each category, for building tabs without fetching every list. Takes the same date_from and date_to parameters as the bookings list.

HTTP Request

GET https://timelydo.com/api/v1/events/<ID>/bookings_counts

List attendees for an event

curl "https://timelydo.com/api/v1/events/7a1c33e2-4b8d-4f6a-9c21-5d0b7f4a1e90/attendees" \
  -H "X-API-KEY: $TIMELYDO_API_KEY" \
  -H "Accept: application/json"
require 'net/http'
require 'json'

uri = URI('https://timelydo.com/api/v1/events/7a1c33e2-4b8d-4f6a-9c21-5d0b7f4a1e90/attendees')

request = Net::HTTP::Get.new(uri)
request['X-API-KEY'] = ENV.fetch('TIMELYDO_API_KEY')
request['Accept'] = 'application/json'

response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end

body = JSON.parse(response.body)

if body['success']
  body['data'].each { |person| puts "#{person['email']}: #{person['bookings_count']} bookings" }
else
  warn "#{response.code}: #{body['message']}"
end
import os
import requests

response = requests.get(
    "https://timelydo.com/api/v1/events/7a1c33e2-4b8d-4f6a-9c21-5d0b7f4a1e90/attendees",
    headers={"X-API-KEY": os.environ["TIMELYDO_API_KEY"], "Accept": "application/json"},
    timeout=10,
)

body = response.json()

if body["success"]:
    for person in body["data"]:
        print(f"{person['email']}: {person['bookings_count']} bookings")
else:
    print(f"{response.status_code}: {body['message']}")
// Node.js 18 or newer. Run this on your server, never in a browser.
async function listEventAttendees() {
  const response = await fetch(
    "https://timelydo.com/api/v1/events/7a1c33e2-4b8d-4f6a-9c21-5d0b7f4a1e90/attendees",
    { headers: { "X-API-KEY": process.env.TIMELYDO_API_KEY, "Accept": "application/json" } },
  );

  const body = await response.json();

  if (body.success) {
    body.data.forEach((person) => console.log(`${person.email}: ${person.bookings_count} bookings`));
  } else {
    console.error(`${response.status}: ${body.message}`);
  }
}

listEventAttendees();

The response (trimmed):

{
  "success": true,
  "message": "Attendees found",
  "data": [
    {
      "email": "sam@example.com",
      "full_name": "Sam Rivera",
      "avatar_name": "SR",
      "bookings_count": 3,
      "canceled_count": 1,
      "first_appeared": "Monday, 17 August 2026",
      "last_appeared": "Wednesday, 23 September 2026",
      "first_appeared_at": "2026-08-17T09:20:11Z",
      "last_appeared_at": "2026-09-23T08:02:47Z",
      "is_timelydo_user": false,
      "logo": null,
      "ids": ["c71e0a24-9d3b-4f85-a6e2-0b8c15d97f43"]
    }
  ],
  "status": 200
}

Lists everyone who has booked this event, one entry per email address, with how many times they booked and when they first and last did. Rescheduled bookings are left out, and canceled ones are counted separately in canceled_count.

is_timelydo_user says whether that person also has a TimelyDo account.

HTTP Request

GET https://timelydo.com/api/v1/events/<ID>/attendees

Check an event URL

curl -X POST "https://timelydo.com/api/v1/events/validate_url_availability" \
  -H "X-API-KEY: $TIMELYDO_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"event": {"url": "discovery-call"}}'
require 'net/http'
require 'json'

uri = URI('https://timelydo.com/api/v1/events/validate_url_availability')

request = Net::HTTP::Post.new(uri)
request['X-API-KEY'] = ENV.fetch('TIMELYDO_API_KEY')
request['Accept'] = 'application/json'
request['Content-Type'] = 'application/json'
request.body = { event: { url: 'discovery-call' } }.to_json

response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end

body = JSON.parse(response.body)

puts body['data']['available'] ? 'Available' : 'Already taken'
import os
import requests

response = requests.post(
    "https://timelydo.com/api/v1/events/validate_url_availability",
    headers={"X-API-KEY": os.environ["TIMELYDO_API_KEY"], "Accept": "application/json"},
    json={"event": {"url": "discovery-call"}},
    timeout=10,
)

body = response.json()
print("Available" if body["data"]["available"] else "Already taken")
// Node.js 18 or newer. Run this on your server, never in a browser.
async function checkEventUrl() {
  const response = await fetch("https://timelydo.com/api/v1/events/validate_url_availability", {
    method: "POST",
    headers: {
      "X-API-KEY": process.env.TIMELYDO_API_KEY,
      "Accept": "application/json",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ event: { url: "discovery-call" } }),
  });

  const body = await response.json();
  console.log(body.data.available ? "Available" : "Already taken");
}

checkEventUrl();

The response:

{
  "success": true,
  "message": "url is available to use",
  "data": { "available": true },
  "status": 200
}

Tells you whether an event URL is free before you create or rename an event. URLs only need to be unique among your own events, so someone else using the same word does not block you.

Send event[id] as well when renaming an existing event, so its current URL counts as available.

HTTP Request

POST https://timelydo.com/api/v1/events/validate_url_availability

Body parameters

Parameter Description
event[url] The URL to check.
event[id] Optional. The event being renamed.

Errors

Errors use the same response shape as successful requests:

{
  "success": false,
  "message": "missing params",
  "data": {
    "user": ["profile_picture"]
  },
  "status": 400
}

When a request fails, success is false, message says what went wrong, and status repeats the HTTP status. For some errors data carries extra detail, such as the list of missing fields.

HTTP status status field Meaning
400 400 Required parameters are missing. data lists them, grouped by object.
401 unauthorized The X-API-KEY header is missing, or the key is not valid. See Authentication.
403 forbidden Your key is valid, but this endpoint only works from a signed in browser session.
404 not_found The endpoint does not exist (endpoint does not exists), or the record you asked for was not found.
422 unprocessable_entity The request was understood, but the values failed validation. message explains which ones.
500 internal_server_error Something went wrong on our side. Try again later. If it keeps happening, contact support.