Safravo Logosafravo.com

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

ParameterTypeDefaultDescription
pagenumber1Page number (1-indexed)
limitnumber25Results per page (max 100)
searchstringSearch 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

FieldTypeRequiredDescription
firstNamestringContact's first name
lastNamestringContact's last name
phonestring❌*Phone in E.164 format, e.g. +254712345678
emailstring❌*Email address
companystringCompany or organisation name
jobTitlestringJob title
websitestringWebsite URL
notesstringInternal 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 exists
req = 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" duplicate

Response

{
  "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.

On this page