curl --request POST \
--url https://api.chatsailer.com/v1/contacts/search \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"cursor": "<string>",
"expand": "<string>",
"fields": "<string>",
"filter": {
"conditions": [
{
"field": "status",
"operator": "equals",
"value": "<unknown>"
}
],
"join": "and"
},
"limit": 50,
"q": "<string>",
"sort": "<string>"
}
'import requests
url = "https://api.chatsailer.com/v1/contacts/search"
payload = {
"cursor": "<string>",
"expand": "<string>",
"fields": "<string>",
"filter": {
"conditions": [
{
"field": "status",
"operator": "equals",
"value": "<unknown>"
}
],
"join": "and"
},
"limit": 50,
"q": "<string>",
"sort": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
cursor: '<string>',
expand: '<string>',
fields: '<string>',
filter: {
conditions: [{field: 'status', operator: 'equals', value: '<unknown>'}],
join: 'and'
},
limit: 50,
q: '<string>',
sort: '<string>'
})
};
fetch('https://api.chatsailer.com/v1/contacts/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.chatsailer.com/v1/contacts/search",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'cursor' => '<string>',
'expand' => '<string>',
'fields' => '<string>',
'filter' => [
'conditions' => [
[
'field' => 'status',
'operator' => 'equals',
'value' => '<unknown>'
]
],
'join' => 'and'
],
'limit' => 50,
'q' => '<string>',
'sort' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.chatsailer.com/v1/contacts/search"
payload := strings.NewReader("{\n \"cursor\": \"<string>\",\n \"expand\": \"<string>\",\n \"fields\": \"<string>\",\n \"filter\": {\n \"conditions\": [\n {\n \"field\": \"status\",\n \"operator\": \"equals\",\n \"value\": \"<unknown>\"\n }\n ],\n \"join\": \"and\"\n },\n \"limit\": 50,\n \"q\": \"<string>\",\n \"sort\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.chatsailer.com/v1/contacts/search")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"cursor\": \"<string>\",\n \"expand\": \"<string>\",\n \"fields\": \"<string>\",\n \"filter\": {\n \"conditions\": [\n {\n \"field\": \"status\",\n \"operator\": \"equals\",\n \"value\": \"<unknown>\"\n }\n ],\n \"join\": \"and\"\n },\n \"limit\": 50,\n \"q\": \"<string>\",\n \"sort\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.chatsailer.com/v1/contacts/search")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"cursor\": \"<string>\",\n \"expand\": \"<string>\",\n \"fields\": \"<string>\",\n \"filter\": {\n \"conditions\": [\n {\n \"field\": \"status\",\n \"operator\": \"equals\",\n \"value\": \"<unknown>\"\n }\n ],\n \"join\": \"and\"\n },\n \"limit\": 50,\n \"q\": \"<string>\",\n \"sort\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"data": [
{
"created_at": "2023-11-07T05:31:56Z",
"id": "con_9f2ac41d8b7e4a51b0d3e6f7a1c2d3e4",
"status": "open",
"updated_at": "2023-11-07T05:31:56Z",
"visibility": "private",
"address": "<string>",
"avatar_url": "<string>",
"channels": [
{
"identifier": "<string>",
"platform": "<string>",
"status": "<string>",
"username": "<string>"
}
],
"city": "<string>",
"company_name": "<string>",
"custom_fields": {},
"email": "<string>",
"first_name": "<string>",
"job_title": "<string>",
"last_name": "<string>",
"lost_reason": "<string>",
"name": "<string>",
"object": "contact",
"organization": {
"created_at": "2023-11-07T05:31:56Z",
"id": "org_9f2ac41d8b7e4a51b0d3e6f7a1c2d3e4",
"name": "<string>",
"updated_at": "2023-11-07T05:31:56Z",
"visibility": "private",
"object": "organization",
"owner": {
"id": "usr_9f2ac41d8b7e4a51b0d3e6f7a1c2d3e4",
"email": "<string>",
"name": "<string>",
"object": "user"
},
"owner_id": "usr_9f2ac41d8b7e4a51b0d3e6f7a1c2d3e4",
"tags": [
{
"id": "tag_9f2ac41d8b7e4a51b0d3e6f7a1c2d3e4",
"name": "<string>",
"color": "<string>",
"object": "tag"
}
],
"website": "<string>"
},
"organization_id": "org_9f2ac41d8b7e4a51b0d3e6f7a1c2d3e4",
"owner": {
"id": "usr_9f2ac41d8b7e4a51b0d3e6f7a1c2d3e4",
"email": "<string>",
"name": "<string>",
"object": "user"
},
"owner_id": "usr_9f2ac41d8b7e4a51b0d3e6f7a1c2d3e4",
"phone": "<string>",
"score": "cold",
"state": "<string>",
"tags": [
{
"id": "tag_9f2ac41d8b7e4a51b0d3e6f7a1c2d3e4",
"name": "<string>",
"color": "<string>",
"object": "tag"
}
]
}
],
"links": {
"next": "<string>"
},
"meta": {
"has_more": true,
"limit": 123
}
}{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>",
"ctx": {},
"input": "<unknown>"
}
]
}Search contacts
For queries GET /v1/contacts cannot express: nested and/or, ranges, and conditions across relationships.
Returns the same contact objects in the same envelope, and accepts the same expand and fields. Both endpoints compile to one query engine, so their results cannot disagree.
curl --request POST \
--url https://api.chatsailer.com/v1/contacts/search \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"cursor": "<string>",
"expand": "<string>",
"fields": "<string>",
"filter": {
"conditions": [
{
"field": "status",
"operator": "equals",
"value": "<unknown>"
}
],
"join": "and"
},
"limit": 50,
"q": "<string>",
"sort": "<string>"
}
'import requests
url = "https://api.chatsailer.com/v1/contacts/search"
payload = {
"cursor": "<string>",
"expand": "<string>",
"fields": "<string>",
"filter": {
"conditions": [
{
"field": "status",
"operator": "equals",
"value": "<unknown>"
}
],
"join": "and"
},
"limit": 50,
"q": "<string>",
"sort": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
cursor: '<string>',
expand: '<string>',
fields: '<string>',
filter: {
conditions: [{field: 'status', operator: 'equals', value: '<unknown>'}],
join: 'and'
},
limit: 50,
q: '<string>',
sort: '<string>'
})
};
fetch('https://api.chatsailer.com/v1/contacts/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.chatsailer.com/v1/contacts/search",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'cursor' => '<string>',
'expand' => '<string>',
'fields' => '<string>',
'filter' => [
'conditions' => [
[
'field' => 'status',
'operator' => 'equals',
'value' => '<unknown>'
]
],
'join' => 'and'
],
'limit' => 50,
'q' => '<string>',
'sort' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.chatsailer.com/v1/contacts/search"
payload := strings.NewReader("{\n \"cursor\": \"<string>\",\n \"expand\": \"<string>\",\n \"fields\": \"<string>\",\n \"filter\": {\n \"conditions\": [\n {\n \"field\": \"status\",\n \"operator\": \"equals\",\n \"value\": \"<unknown>\"\n }\n ],\n \"join\": \"and\"\n },\n \"limit\": 50,\n \"q\": \"<string>\",\n \"sort\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.chatsailer.com/v1/contacts/search")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"cursor\": \"<string>\",\n \"expand\": \"<string>\",\n \"fields\": \"<string>\",\n \"filter\": {\n \"conditions\": [\n {\n \"field\": \"status\",\n \"operator\": \"equals\",\n \"value\": \"<unknown>\"\n }\n ],\n \"join\": \"and\"\n },\n \"limit\": 50,\n \"q\": \"<string>\",\n \"sort\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.chatsailer.com/v1/contacts/search")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"cursor\": \"<string>\",\n \"expand\": \"<string>\",\n \"fields\": \"<string>\",\n \"filter\": {\n \"conditions\": [\n {\n \"field\": \"status\",\n \"operator\": \"equals\",\n \"value\": \"<unknown>\"\n }\n ],\n \"join\": \"and\"\n },\n \"limit\": 50,\n \"q\": \"<string>\",\n \"sort\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"data": [
{
"created_at": "2023-11-07T05:31:56Z",
"id": "con_9f2ac41d8b7e4a51b0d3e6f7a1c2d3e4",
"status": "open",
"updated_at": "2023-11-07T05:31:56Z",
"visibility": "private",
"address": "<string>",
"avatar_url": "<string>",
"channels": [
{
"identifier": "<string>",
"platform": "<string>",
"status": "<string>",
"username": "<string>"
}
],
"city": "<string>",
"company_name": "<string>",
"custom_fields": {},
"email": "<string>",
"first_name": "<string>",
"job_title": "<string>",
"last_name": "<string>",
"lost_reason": "<string>",
"name": "<string>",
"object": "contact",
"organization": {
"created_at": "2023-11-07T05:31:56Z",
"id": "org_9f2ac41d8b7e4a51b0d3e6f7a1c2d3e4",
"name": "<string>",
"updated_at": "2023-11-07T05:31:56Z",
"visibility": "private",
"object": "organization",
"owner": {
"id": "usr_9f2ac41d8b7e4a51b0d3e6f7a1c2d3e4",
"email": "<string>",
"name": "<string>",
"object": "user"
},
"owner_id": "usr_9f2ac41d8b7e4a51b0d3e6f7a1c2d3e4",
"tags": [
{
"id": "tag_9f2ac41d8b7e4a51b0d3e6f7a1c2d3e4",
"name": "<string>",
"color": "<string>",
"object": "tag"
}
],
"website": "<string>"
},
"organization_id": "org_9f2ac41d8b7e4a51b0d3e6f7a1c2d3e4",
"owner": {
"id": "usr_9f2ac41d8b7e4a51b0d3e6f7a1c2d3e4",
"email": "<string>",
"name": "<string>",
"object": "user"
},
"owner_id": "usr_9f2ac41d8b7e4a51b0d3e6f7a1c2d3e4",
"phone": "<string>",
"score": "cold",
"state": "<string>",
"tags": [
{
"id": "tag_9f2ac41d8b7e4a51b0d3e6f7a1c2d3e4",
"name": "<string>",
"color": "<string>",
"object": "tag"
}
]
}
],
"links": {
"next": "<string>"
},
"meta": {
"has_more": true,
"limit": 123
}
}{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>",
"ctx": {},
"input": "<unknown>"
}
]
}Authorizations
A workspace API token. Create one in Settings → API. Send it as Authorization: Bearer sk_live_....
Body
Body for POST /v1/contacts/search, and arguments for the MCP
search_records tool.
Defined here rather than beside the route because two surfaces share it. The MCP tool derives its JSON Schema from this class, so a filter added for REST reaches the tool in the same commit — there is no second copy to forget.
Extras are forbidden for the same reason they are on FilterCondition: every
field here is optional, so an ignored filtr typo would silently mean "no
filter" and return the whole collection with 200 OK.
A set of conditions combined with and / or. Groups may nest.
- FilterGroup
- FilterCondition
Show child attributes
Show child attributes
1 <= x <= 200Free text across name, email and phone.
Same syntax as the sort query parameter.
Response
Successful Response
A page of contacts.
A named subclass purely so the generated SDK type is ContactList rather
than the Page_Contact_ FastAPI derives from the generic.