Contacts API
Create, list, search, and update contacts in your workspace programmatically.
Contacts are the central entity in Safravo — every conversation and message is linked to one. The API exposes the same contact data visible in the dashboard at /{workspace-slug}/contacts, filtered to fields relevant to external integrations.
List contacts
GET /v1/contacts
Required scope: read:contacts · Rate limit: 60 req/min
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | number | 1 | Page number (1-indexed) |
limit | number | 25 | Results per page (max 100) |
search | string | — | Search by name, phone, or email |
# First page, default limit
curl "https://api.safravo.com/v1/contacts?limit=25&page=1" \
-H "X-API-Key: saf_live_your_key"
# Search by name or phone
curl "https://api.safravo.com/v1/contacts?search=Jane&limit=10" \
-H "X-API-Key: saf_live_your_key"// Search
const { data } = await client.get('/v1/contacts', {
params: { search: 'Jane', limit: 10 },
});
// Paginate through all contacts
async function fetchAllContacts() {
const contacts = [];
let page = 1;
while (true) {
const { data } = await client.get('/v1/contacts', {
params: { page, limit: 100 },
});
contacts.push(...data.items);
if (page >= data.totalPages) break;
page++;
}
return contacts;
}# Search
resp = client.get("/v1/contacts", params={"search": "Jane", "limit": 10})
resp.raise_for_status()
contacts = resp.json()["items"]
# Paginate all contacts
def fetch_all_contacts():
page, results = 1, []
while True:
r = client.get("/v1/contacts", params={"page": page, "limit": 100})
r.raise_for_status()
body = r.json()
results.extend(body["items"])
if page >= body["totalPages"]:
break
page += 1
return results// Search
$contacts = Http::withHeaders(['X-API-Key' => env('SAFRAVO_API_KEY')])
->get('https://api.safravo.com/v1/contacts', [
'search' => 'Jane',
'limit' => 10,
])
->json('items');
// Paginate all contacts
$page = 1;
$all = [];
do {
$response = Http::withHeaders(['X-API-Key' => env('SAFRAVO_API_KEY')])
->get('https://api.safravo.com/v1/contacts', ['page' => $page, 'limit' => 100]);
$body = $response->json();
$all = array_merge($all, $body['items']);
$page++;
} while ($page <= $body['totalPages']);req, _ := http.NewRequest("GET", "https://api.safravo.com/v1/contacts", nil)
q := req.URL.Query()
q.Set("search", "Jane")
q.Set("limit", "10")
req.URL.RawQuery = q.Encode()
req.Header.Set("X-API-Key", os.Getenv("SAFRAVO_API_KEY"))
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()uri = URI('https://api.safravo.com/v1/contacts')
uri.query = URI.encode_www_form(search: 'Jane', limit: 10)
req = Net::HTTP::Get.new(uri)
req['X-API-Key'] = ENV['SAFRAVO_API_KEY']
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
resp = http.request(req)
contacts = JSON.parse(resp.body)['items']Response
{
"items": [
{
"id": "6634e2b1a7c3f500231a8b01",
"firstName": "Jane",
"lastName": "Doe",
"fullName": "Jane Doe",
"phone": "+254712345678",
"email": "[email protected]",
"company": "Acme Corp",
"jobTitle": "CTO",
"source": "api",
"tags": ["vip", "enterprise"],
"lifecycle": "Customer",
"createdAt": "2025-05-01T10:00:00.000Z",
"updatedAt": "2025-05-01T10:00:00.000Z"
}
],
"total": 1,
"page": 1,
"limit": 10,
"totalPages": 1
}Get a contact
GET /v1/contacts/:id
Required scope: read:contacts
curl https://api.safravo.com/v1/contacts/6634e2b1a7c3f500231a8b01 \
-H "X-API-Key: saf_live_your_key"const { data } = await client.get(`/v1/contacts/${contactId}`);
console.log(data.phone);resp = client.get(f"/v1/contacts/{contact_id}")
resp.raise_for_status()
contact = resp.json()$contact = Http::withHeaders(['X-API-Key' => env('SAFRAVO_API_KEY')])
->get("https://api.safravo.com/v1/contacts/{$contactId}")
->json();Create a contact
POST /v1/contacts
Required scope: write:contacts · Rate limit: 30 req/min
Safravo deduplicates contacts by phone number and email address.
Submitting a contact with a phone or email that already exists returns 409 Conflict. At least one of phone or email is required.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
firstName | string | ✅ | Contact's first name |
lastName | string | ❌ | Contact's last name |
phone | string | ❌* | Phone in E.164 format, e.g. +254712345678 |
email | string | ❌* | Email address |
company | string | ❌ | Company or organisation name |
jobTitle | string | ❌ | Job title |
website | string | ❌ | Website URL |
notes | string | ❌ | Internal notes (not visible to the contact) |
At least one of phone or email is required.
curl -X POST https://api.safravo.com/v1/contacts \
-H "X-API-Key: saf_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"firstName": "Jane",
"lastName": "Doe",
"phone": "+254712345678",
"email": "[email protected]",
"company": "Acme Corp"
}'const { data } = await client.post('/v1/contacts', {
firstName: 'Jane',
lastName: 'Doe',
phone: '+254712345678',
email: '[email protected]',
company: 'Acme Corp',
});
console.log('Created:', data.id);resp = client.post("/v1/contacts", json={
"firstName": "Jane",
"lastName": "Doe",
"phone": "+254712345678",
"email": "[email protected]",
"company": "Acme Corp",
})
if resp.status_code == 409:
print("Contact already exists")
else:
resp.raise_for_status()
print("Created:", resp.json()["id"])$response = Http::withHeaders(['X-API-Key' => env('SAFRAVO_API_KEY')])
->post('https://api.safravo.com/v1/contacts', [
'firstName' => 'Jane',
'lastName' => 'Doe',
'phone' => '+254712345678',
'email' => '[email protected]',
'company' => 'Acme Corp',
]);
if ($response->status() === 409) {
// Contact already exists — fetch by phone instead
} else {
$contactId = $response->json('id');
}payload, _ := json.Marshal(map[string]string{
"firstName": "Jane",
"lastName": "Doe",
"phone": "+254712345678",
"email": "[email protected]",
})
req, _ := http.NewRequest("POST",
"https://api.safravo.com/v1/contacts",
bytes.NewBuffer(payload))
req.Header.Set("X-API-Key", os.Getenv("SAFRAVO_API_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
// 201 → created, 409 → already existsreq = Net::HTTP::Post.new(URI('https://api.safravo.com/v1/contacts'))
req['X-API-Key'] = ENV['SAFRAVO_API_KEY']
req['Content-Type'] = 'application/json'
req.body = JSON.generate(
firstName: 'Jane', lastName: 'Doe',
phone: '+254712345678', email: '[email protected]'
)
resp = Net::HTTP.start('api.safravo.com', 443, use_ssl: true) { |h| h.request(req) }
puts resp.code # "201" created, "409" duplicateResponse
{
"id": "6634e2b1a7c3f500231a8b01",
"firstName": "Jane",
"lastName": "Doe",
"fullName": "Jane Doe",
"phone": "+254712345678",
"email": "[email protected]",
"company": "Acme Corp",
"source": "api",
"createdAt": "2025-05-01T10:00:00.000Z"
}Update a contact
PATCH /v1/contacts/:id
Required scope: write:contacts · Rate limit: 30 req/min
Only the fields you provide are updated. All other fields remain unchanged.
curl -X PATCH https://api.safravo.com/v1/contacts/6634e2b1a7c3f500231a8b01 \
-H "X-API-Key: saf_live_your_key" \
-H "Content-Type: application/json" \
-d '{ "company": "New Corp", "jobTitle": "CTO" }'await client.patch(`/v1/contacts/${contactId}`, {
company: 'New Corp',
jobTitle: 'CTO',
});client.patch(f"/v1/contacts/{contact_id}", json={
"company": "New Corp",
"jobTitle": "CTO",
}).raise_for_status()Http::withHeaders(['X-API-Key' => env('SAFRAVO_API_KEY')])
->patch("https://api.safravo.com/v1/contacts/{$contactId}", [
'company' => 'New Corp',
'jobTitle' => 'CTO',
]);To send a message to a contact you just created, use the contact's phone
number directly in POST /v1/messages. The id field is for referencing the
contact in your own systems.