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:
- Sign in at timelydo.com.
- Open Settings, then Developers Console, then API Key. You can also go straight to timelydo.com/settings/developers/api_key.
- 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
- Open Settings, then Developers Console, then API Key.
- Click Regenerate key.
- 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. |
| 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
- PNG, JPEG, GIF or WEBP. SVG is not accepted.
- 5 MB or smaller.
- The type is read from the file's content type, so set it correctly (for example
image/png).
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. |