curl --request GET \
--url https://api.example.com/contacts \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.example.com/contacts"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.example.com/contacts', 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.example.com/contacts",
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: Bearer <token>"
],
]);
$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.example.com/contacts"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
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.example.com/contacts")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/contacts")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body[
{}
]{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>"
}
]
}List Contacts
List contacts (one page) with the full match count in X-Total-Count.
stage accepts a single value (contact) or a comma list
(target,lead) so the /leads view and /contacts view share one endpoint.
curl --request GET \
--url https://api.example.com/contacts \
--header 'Authorization: Bearer <token>'import requests
url = "https://api.example.com/contacts"
headers = {"Authorization": "Bearer <token>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
fetch('https://api.example.com/contacts', 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.example.com/contacts",
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: Bearer <token>"
],
]);
$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.example.com/contacts"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("Authorization", "Bearer <token>")
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.example.com/contacts")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.example.com/contacts")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body[
{}
]{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "<string>"
}
]
}limit/offset) in the X-Total-Count response header — see Errors & Pagination. stage accepts a comma-list precisely so one endpoint can serve both a “leads” listing (target,lead) and a “contacts” listing (contact) without two near-duplicate endpoints.
Auth
Requires a CRM read scope and an active organization on the token. Any ofcontacts:read, deals:read, companies:read, or activities:read qualifies, as does any *:manage scope — manage implies read.
Response
An array of contact objects, each also carrying an embeddedaccounts object (this endpoint always requests select=*,accounts(id,name)):
| Field | Type | Description |
|---|---|---|
id | string (uuid) | Primary key. |
org_id | string (uuid) | Owning organization. |
account_id | string (uuid) | null | Linked account, if any. |
accounts | object | null | PostgREST embed: { id, name } of the linked account, or null when unlinked. Only present on this endpoint — other endpoints in this reference return the flat row without it. |
first_name / last_name | string | null | Name. At least one identity field (name, email, or LinkedIn URL) is guaranteed by the create validator. |
email | string | null | Email address. |
phone | string | null | Phone number, stored as entered. |
title | string | null | Job title. |
department | string | null | Department/team. Not accepted on create — set via PATCH (see Update a Contact) or by mapping a CSV column to it during an import. |
linkedin_url | string | null | LinkedIn profile URL. |
avatar_url | string | null | Profile photo URL, set by the enrichment pipeline’s photo-resolution step. |
stage | string | target, lead, or contact. |
target_at / lead_at / contact_at | string (ISO 8601) | null | Timestamp of each forward transition; only stamped, never cleared. |
disqualified_at | string (ISO 8601) | null | Set when disqualified; cleared by ?disqualify=false. Independent of stage. |
company | string | null | Free-text company name. Populated for unlinked prospects (imports, card capture); once account_id is set, use accounts.name for display instead. |
company_domain | string | null | Company’s email domain. |
source | string | Origin tag. Values written by the backend: manual (created through the API/app), import (CSV import), customer_intel, scan_badge, scan_card (AnyCard capture), share_back. Defaults to manual. The source filter is an exact match, so pass one of these verbatim. |
notes | string | null | Free-text notes. |
owner_id | string | null | Logto user ID of the owning rep. |
tags | array of string | Defaults to []. |
custom_fields | object | Defaults to {}. |
email_opt_out | boolean | Defaults to false. |
last_contacted_at | string (ISO 8601) | null | |
enrichment_status | string | null | pending, running, complete, or failed. See Start Enrichment. |
enrichment_data | object | Provider-namespaced blob (apollo, scrapin, hunter, meta) written by the enrichment workflow. {} before enrichment runs. |
enriched_at | string (ISO 8601) | null | |
enrichment_job_id | string | null | Temporal workflow ID of the most recent enrichment run. |
email_verified | boolean | null | Set by the Hunter verification step. |
email_verification_score | number | null | |
email_verification_status | string | null | |
captured_by_card_id | string (uuid) | null | Set when this contact originated from an AnyCard capture. |
agent_demo_id / agent_demo_workflow_id / agent_demo_url / agent_demo_phone | string | null | AnyCard voice-agent demo metadata. |
source_run_id / source_result_id | string (uuid) | null | Set when this contact originated from a customer-intel run. |
capture_payload | object | Raw capture/enrichment payload from AnyCard or import. Defaults to {}. Flattened into columns in the CSV export. |
converted_to_account_id / converted_to_deal_id | string (uuid) | null | Set by Convert. |
converted_at | string (ISO 8601) | null | Set by Convert. |
created_by | string | null | |
created_at / updated_at | string (ISO 8601) |
Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Query Parameters
Funnel stage filter: a single value ('contact') or a comma list ('target,lead').
Filter to contacts owned by this Logto user id.
Filter by origin tag; exact match. Values written by the backend: 'manual', 'import' (CSV import), 'customer_intel', 'scan_badge', 'scan_card', 'share_back'.
Filter to contacts linked to this account.
Filter by disqualification: true for disqualified only, false for active only.
Free-text search across first/last name, email, title, company, and company domain.
Max rows per page; capped at 500 server-side.
Row offset for pagination.
Response
Successful Response