curl --request GET \
--url https://api.cal.com/v2/discover/listings/search \
--header 'Authorization: <authorization>'import requests
url = "https://api.cal.com/v2/discover/listings/search"
headers = {"Authorization": "<authorization>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: '<authorization>'}};
fetch('https://api.cal.com/v2/discover/listings/search', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.cal.com/v2/discover/listings/search",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: <authorization>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.cal.com/v2/discover/listings/search"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "<authorization>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api.cal.com/v2/discover/listings/search")
.header("Authorization", "<authorization>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.cal.com/v2/discover/listings/search")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = '<authorization>'
response = http.request(request)
puts response.read_body{
"status": "success",
"data": {
"areaNeeded": true,
"areaNotFound": true,
"totalMatches": 3,
"results": [
{
"listingId": 12,
"title": "Skin fade",
"description": "A sharp fade with a hot-towel finish.",
"provider": "Fade House",
"avatarUrl": "https://cal.com/api/avatar/fade-house.png",
"category": "BARBERS",
"subcategory": "haircuts",
"city": "Seattle",
"neighborhood": "Capitol Hill",
"timeZone": "America/Los_Angeles",
"latitude": 47.62,
"longitude": -122.32,
"distanceMiles": 1.4,
"durationMinutes": 30,
"priceInCents": 4500,
"currency": "usd",
"price": "45.00 USD",
"rating": 4.8,
"reviewCount": 132,
"openNow": true,
"verified": true,
"newClientDiscountPercent": 15,
"path": "/discover/fade-house/skin-fade",
"bookingUrl": "https://cal.com/fade-house/skin-fade?utm_source=discover&utm_medium=api&utm_campaign=barbers&utm_content=listing%3A12",
"nextAvailableSlot": "2026-11-10T17:00:00.000Z",
"nextAvailableSlotLocal": "Today, 9:00 AM PST",
"nextAvailableSlotTimeZone": "PST"
}
],
"searchedArea": "within 10 miles of Seattle, WA",
"areaSource": "saved",
"ignoredFilters": [
"neighborhood"
],
"widenedFrom": "Capitol Hill",
"broadenedFrom": {
"subcategory": "haircuts",
"query": "skin fade"
},
"webBusinesses": [
{
"name": "Example Barbers",
"url": "https://barbers.example/book",
"description": "Fades and beard trims",
"appointmentEmails": [
"bookings@barbers.example"
],
"appointmentPhones": [
"(415) 555-0101"
]
}
]
}
}Search Discover listings
Searches the published Discover listings the way Cal.ai does. In-person results stay within the area you name, else around your saved ZIP code; with neither, nothing is searched and areaNeeded is returned. When the named area or the saved ZIP code cannot be found, nothing is searched and areaNeeded comes with areaNotFound. Online-only categories and onlineOnly searches are searched everywhere. An empty neighborhood widens to its whole region (widenedFrom); an empty search retries with the category alone (broadenedFrom); when the whole area has nothing, the web is searched there once and businesses not on Discover come back in webBusinesses. Availability is judged in your saved time zone. Searches are limited to 20 per minute per user, and web searches to 10 per hour per user, each shared across all of the user’s API keys and access tokens; past the web search limit a search still returns its listings, without webBusinesses. The web search runs on your own AI access: when one of your organizations has turned AI off, it is skipped and the listings still return. A place lookup that is rate limited answers 429 and one that fails answers 503, each with a message starting Place lookup; neither means the place does not exist. Other 429s are the request limits above. Each result’s bookingUrl is attributed to Discover. If accessed using an OAuth access token, the EVENT_TYPE_READ scope is required.
curl --request GET \
--url https://api.cal.com/v2/discover/listings/search \
--header 'Authorization: <authorization>'import requests
url = "https://api.cal.com/v2/discover/listings/search"
headers = {"Authorization": "<authorization>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: '<authorization>'}};
fetch('https://api.cal.com/v2/discover/listings/search', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.cal.com/v2/discover/listings/search",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: <authorization>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.cal.com/v2/discover/listings/search"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "<authorization>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api.cal.com/v2/discover/listings/search")
.header("Authorization", "<authorization>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.cal.com/v2/discover/listings/search")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = '<authorization>'
response = http.request(request)
puts response.read_body{
"status": "success",
"data": {
"areaNeeded": true,
"areaNotFound": true,
"totalMatches": 3,
"results": [
{
"listingId": 12,
"title": "Skin fade",
"description": "A sharp fade with a hot-towel finish.",
"provider": "Fade House",
"avatarUrl": "https://cal.com/api/avatar/fade-house.png",
"category": "BARBERS",
"subcategory": "haircuts",
"city": "Seattle",
"neighborhood": "Capitol Hill",
"timeZone": "America/Los_Angeles",
"latitude": 47.62,
"longitude": -122.32,
"distanceMiles": 1.4,
"durationMinutes": 30,
"priceInCents": 4500,
"currency": "usd",
"price": "45.00 USD",
"rating": 4.8,
"reviewCount": 132,
"openNow": true,
"verified": true,
"newClientDiscountPercent": 15,
"path": "/discover/fade-house/skin-fade",
"bookingUrl": "https://cal.com/fade-house/skin-fade?utm_source=discover&utm_medium=api&utm_campaign=barbers&utm_content=listing%3A12",
"nextAvailableSlot": "2026-11-10T17:00:00.000Z",
"nextAvailableSlotLocal": "Today, 9:00 AM PST",
"nextAvailableSlotTimeZone": "PST"
}
],
"searchedArea": "within 10 miles of Seattle, WA",
"areaSource": "saved",
"ignoredFilters": [
"neighborhood"
],
"widenedFrom": "Capitol Hill",
"broadenedFrom": {
"subcategory": "haircuts",
"query": "skin fade"
},
"webBusinesses": [
{
"name": "Example Barbers",
"url": "https://barbers.example/book",
"description": "Fades and beard trims",
"appointmentEmails": [
"bookings@barbers.example"
],
"appointmentPhones": [
"(415) 555-0101"
]
}
]
}
}Headers
value must be Bearer <token> where <token> is api key prefixed with cal_, managed user access token, or OAuth access token
Query Parameters
The Discover category the need maps to, such as medical for tooth pain.
medical, barbers, coaching, consulting, lessons, pets, sports, dance, social, online-services, saas, home-services, other "barbers"
A subcategory of category, such as dentistry under medical. Ignored without its category, and named in ignoredFilters when it is not one of the category's.
dentistry, therapy, physiotherapy, nutrition, somatic-and-breathwork, doula, general-care, haircuts, beard-and-shave, coloring, kids-cuts, nails, waxing, lashes, skin-and-spa, massage, tattoo, life, career, executive, mindset, spiritual, business, tech-and-ai, finance-and-tax, legal, real-estate, marketing, hr, academic, language, music-and-voice, arts, chess, veterinary, grooming, training, sitting, fitness, yoga, tennis, swimming, skiing, salsa, ballet, hip-hop, ballroom, clubs, workshops, networking, tours, photoshoots-and-studios, pop-ups-and-showrooms, design, development, writing, virtual-assistance, legal-advice, product-demos, onboarding, customer-support, sales, cleaning, repairs, heating-and-cooling, inspections, auto "haircuts"
Free text matched against the title, description, provider and subcategory. Never a place.
100"skin fade"
A neighborhood of a launch region, such as Capitol Hill, for in-person listings there only. An unknown name comes back in ignoredFilters.
80"Capitol Hill"
Keep only online listings and search them everywhere, whatever area says. Online searches never fall back to the web.
false
For the web fallback, the need in the area's local language when it is not English, such as Zahnarzt for a dentist in Hamburg.
100"Zahnarzt"
The place to search around: a ZIP code, neighborhood or city. Without it, the search uses your saved ZIP code, and answers areaNeeded when there is none. Pass anywhere for results from anywhere.
80"98122"
The highest price, in whole currency units: 50 means $50, 0 means free only.
x >= 050
The longest session, in minutes.
x >= 160
Only listings with a real open slot in this window, in your saved time zone. this_week means the next 7 days.
today, tomorrow, this_week "today"
Was this page helpful?