curl --request POST \
--url https://api.nuvia.ai/v1/contacts \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"name": "João Silva",
"country_code": "55",
"phone_number": "11999998888",
"email": "joao.silva@exemplo.com.br",
"job_title": "Gerente de Vendas",
"city": "São Paulo",
"additional_attributes": {
"cpf": "12345678900"
}
}
'import requests
url = "https://api.nuvia.ai/v1/contacts"
payload = {
"name": "João Silva",
"country_code": "55",
"phone_number": "11999998888",
"email": "joao.silva@exemplo.com.br",
"job_title": "Gerente de Vendas",
"city": "São Paulo",
"additional_attributes": { "cpf": "12345678900" }
}
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({
name: 'João Silva',
country_code: '55',
phone_number: '11999998888',
email: 'joao.silva@exemplo.com.br',
job_title: 'Gerente de Vendas',
city: 'São Paulo',
additional_attributes: {cpf: '12345678900'}
})
};
fetch('https://api.nuvia.ai/v1/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.nuvia.ai/v1/contacts",
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([
'name' => 'João Silva',
'country_code' => '55',
'phone_number' => '11999998888',
'email' => 'joao.silva@exemplo.com.br',
'job_title' => 'Gerente de Vendas',
'city' => 'São Paulo',
'additional_attributes' => [
'cpf' => '12345678900'
]
]),
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.nuvia.ai/v1/contacts"
payload := strings.NewReader("{\n \"name\": \"João Silva\",\n \"country_code\": \"55\",\n \"phone_number\": \"11999998888\",\n \"email\": \"joao.silva@exemplo.com.br\",\n \"job_title\": \"Gerente de Vendas\",\n \"city\": \"São Paulo\",\n \"additional_attributes\": {\n \"cpf\": \"12345678900\"\n }\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.nuvia.ai/v1/contacts")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"João Silva\",\n \"country_code\": \"55\",\n \"phone_number\": \"11999998888\",\n \"email\": \"joao.silva@exemplo.com.br\",\n \"job_title\": \"Gerente de Vendas\",\n \"city\": \"São Paulo\",\n \"additional_attributes\": {\n \"cpf\": \"12345678900\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.nuvia.ai/v1/contacts")
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 \"name\": \"João Silva\",\n \"country_code\": \"55\",\n \"phone_number\": \"11999998888\",\n \"email\": \"joao.silva@exemplo.com.br\",\n \"job_title\": \"Gerente de Vendas\",\n \"city\": \"São Paulo\",\n \"additional_attributes\": {\n \"cpf\": \"12345678900\"\n }\n}"
response = http.request(request)
puts response.read_body{
"name": "<string>",
"company": "<unknown>",
"created": true,
"matched_by": "email",
"matches_count": 123,
"ignored_fields": [
"<string>"
],
"_id": "<string>",
"id": "<string>",
"country_code": "<string>",
"email": "<string>",
"last_activity_at": "<unknown>",
"phone_number": "<string>",
"linkedin_identifier": "<string>",
"job_title": "<string>",
"seniority": "<string>",
"department": "<string>",
"country": "<string>",
"region": "<string>",
"city": "<string>",
"experience": "<string>",
"skills": "<string>",
"additional_attributes": {},
"business": "<string>",
"notes": [
{
"_id": "<string>",
"content": "<string>",
"user": "<string>",
"createdAt": "<unknown>",
"updatedAt": "<unknown>"
}
],
"marketing_opt_out": {},
"whatsapp_user_id": "<string>",
"whatsapp_username": "<string>",
"whatsapp_parent_user_id": "<string>",
"createdAt": "<unknown>",
"updatedAt": "<unknown>"
}{
"name": "<string>",
"company": "<unknown>",
"created": true,
"matched_by": "email",
"matches_count": 123,
"ignored_fields": [
"<string>"
],
"_id": "<string>",
"id": "<string>",
"country_code": "<string>",
"email": "<string>",
"last_activity_at": "<unknown>",
"phone_number": "<string>",
"linkedin_identifier": "<string>",
"job_title": "<string>",
"seniority": "<string>",
"department": "<string>",
"country": "<string>",
"region": "<string>",
"city": "<string>",
"experience": "<string>",
"skills": "<string>",
"additional_attributes": {},
"business": "<string>",
"notes": [
{
"_id": "<string>",
"content": "<string>",
"user": "<string>",
"createdAt": "<unknown>",
"updatedAt": "<unknown>"
}
],
"marketing_opt_out": {},
"whatsapp_user_id": "<string>",
"whatsapp_username": "<string>",
"whatsapp_parent_user_id": "<string>",
"createdAt": "<unknown>",
"updatedAt": "<unknown>"
}{
"statusCode": 123,
"code": "CONTACT_IDENTITY_CONFLICT",
"message": "<string>",
"conflicts": [
{
"field": "email",
"contact": {
"_id": "<string>",
"name": "<string>",
"email": "<string>",
"phone_number": "<string>",
"country_code": "<string>",
"linkedin_identifier": "<string>"
}
}
]
}Create contact (or reuse the existing one)
Creates a contact or, if the company already has a contact with any of the identifiers sent, reuses that contact instead of creating another.
- Identifiers:
phone_number(withcountry_code),emailandlinkedin_identifier. Besidesname, at least one of them is required; with none, 400"Informe pelo menos um identificador: telefone, e-mail ou LinkedIn.". The contact can be created without a phone. - Lookup order: email, LinkedIn and phone. Email only counts with
@and is recognized in its normalized form (no leading or trailing spaces, lowercase), which is how it’s saved from now on; old emails saved with uppercase letters are only recognized with the same spelling, until the database is normalized. LinkedIn accepts the profile URL or just the name. Phone is normalized (area code, 9th digit). - 201: contact created. The body is the contact with
created: true,matched_by: null,matches_count: 0andignored_fields: []. - 200: existing contact reused. The body is the contact with
created: false,matched_by(email,linkedinorphone) andmatches_count(how many contacts in the company matched by that key; above 1, there are duplicates and the oldest one was used). Found by phone, the fields sent are updated, as the POST always did; found by email or LinkedIn, only the fields that were empty are filled in, andignored_fieldslists the ones sent that weren’t saved because the contact already had another value (e.g.email,phone_number,job_title,additional_attributes.<slug>).nameand the owner never change. To change any field of an existing contact, usePUT /v1/contacts/{id}. - 409
CONTACT_IDENTITY_CONFLICT: the identifiers sent belong to different contacts (e.g. the phone belongs to one contact and the email to another). Nothing is saved;conflictsreturns the contact for each identifier. - Profile fields:
job_title,seniority,department,country,region,cityandexperienceare saved on creation; on an existing contact, they follow the 200 rule (updated if found by phone, only filled in if empty when found by email or LinkedIn). - Phone: digits only, area code + number, without the country code (the country code goes in
country_code, required together with the phone). An invalid phone returns 400. - additional_attributes:
{ "slug": value }, with a primitive value (string, number, boolean or null). A slug that doesn’t exist, or that belongs to a custom field whose context isn’t CONTACT, is silently ignored. On an existing contact, merge by slug: found by phone, the slugs sent are updated; found by email or LinkedIn, only empty ones are filled in. - Check first:
POST /v1/contacts/identity-checktells you what this route would do, without saving.
curl --request POST \
--url https://api.nuvia.ai/v1/contacts \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"name": "João Silva",
"country_code": "55",
"phone_number": "11999998888",
"email": "joao.silva@exemplo.com.br",
"job_title": "Gerente de Vendas",
"city": "São Paulo",
"additional_attributes": {
"cpf": "12345678900"
}
}
'import requests
url = "https://api.nuvia.ai/v1/contacts"
payload = {
"name": "João Silva",
"country_code": "55",
"phone_number": "11999998888",
"email": "joao.silva@exemplo.com.br",
"job_title": "Gerente de Vendas",
"city": "São Paulo",
"additional_attributes": { "cpf": "12345678900" }
}
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({
name: 'João Silva',
country_code: '55',
phone_number: '11999998888',
email: 'joao.silva@exemplo.com.br',
job_title: 'Gerente de Vendas',
city: 'São Paulo',
additional_attributes: {cpf: '12345678900'}
})
};
fetch('https://api.nuvia.ai/v1/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.nuvia.ai/v1/contacts",
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([
'name' => 'João Silva',
'country_code' => '55',
'phone_number' => '11999998888',
'email' => 'joao.silva@exemplo.com.br',
'job_title' => 'Gerente de Vendas',
'city' => 'São Paulo',
'additional_attributes' => [
'cpf' => '12345678900'
]
]),
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.nuvia.ai/v1/contacts"
payload := strings.NewReader("{\n \"name\": \"João Silva\",\n \"country_code\": \"55\",\n \"phone_number\": \"11999998888\",\n \"email\": \"joao.silva@exemplo.com.br\",\n \"job_title\": \"Gerente de Vendas\",\n \"city\": \"São Paulo\",\n \"additional_attributes\": {\n \"cpf\": \"12345678900\"\n }\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.nuvia.ai/v1/contacts")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"name\": \"João Silva\",\n \"country_code\": \"55\",\n \"phone_number\": \"11999998888\",\n \"email\": \"joao.silva@exemplo.com.br\",\n \"job_title\": \"Gerente de Vendas\",\n \"city\": \"São Paulo\",\n \"additional_attributes\": {\n \"cpf\": \"12345678900\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.nuvia.ai/v1/contacts")
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 \"name\": \"João Silva\",\n \"country_code\": \"55\",\n \"phone_number\": \"11999998888\",\n \"email\": \"joao.silva@exemplo.com.br\",\n \"job_title\": \"Gerente de Vendas\",\n \"city\": \"São Paulo\",\n \"additional_attributes\": {\n \"cpf\": \"12345678900\"\n }\n}"
response = http.request(request)
puts response.read_body{
"name": "<string>",
"company": "<unknown>",
"created": true,
"matched_by": "email",
"matches_count": 123,
"ignored_fields": [
"<string>"
],
"_id": "<string>",
"id": "<string>",
"country_code": "<string>",
"email": "<string>",
"last_activity_at": "<unknown>",
"phone_number": "<string>",
"linkedin_identifier": "<string>",
"job_title": "<string>",
"seniority": "<string>",
"department": "<string>",
"country": "<string>",
"region": "<string>",
"city": "<string>",
"experience": "<string>",
"skills": "<string>",
"additional_attributes": {},
"business": "<string>",
"notes": [
{
"_id": "<string>",
"content": "<string>",
"user": "<string>",
"createdAt": "<unknown>",
"updatedAt": "<unknown>"
}
],
"marketing_opt_out": {},
"whatsapp_user_id": "<string>",
"whatsapp_username": "<string>",
"whatsapp_parent_user_id": "<string>",
"createdAt": "<unknown>",
"updatedAt": "<unknown>"
}{
"name": "<string>",
"company": "<unknown>",
"created": true,
"matched_by": "email",
"matches_count": 123,
"ignored_fields": [
"<string>"
],
"_id": "<string>",
"id": "<string>",
"country_code": "<string>",
"email": "<string>",
"last_activity_at": "<unknown>",
"phone_number": "<string>",
"linkedin_identifier": "<string>",
"job_title": "<string>",
"seniority": "<string>",
"department": "<string>",
"country": "<string>",
"region": "<string>",
"city": "<string>",
"experience": "<string>",
"skills": "<string>",
"additional_attributes": {},
"business": "<string>",
"notes": [
{
"_id": "<string>",
"content": "<string>",
"user": "<string>",
"createdAt": "<unknown>",
"updatedAt": "<unknown>"
}
],
"marketing_opt_out": {},
"whatsapp_user_id": "<string>",
"whatsapp_username": "<string>",
"whatsapp_parent_user_id": "<string>",
"createdAt": "<unknown>",
"updatedAt": "<unknown>"
}{
"statusCode": 123,
"code": "CONTACT_IDENTITY_CONFLICT",
"message": "<string>",
"conflicts": [
{
"field": "email",
"contact": {
"_id": "<string>",
"name": "<string>",
"email": "<string>",
"phone_number": "<string>",
"country_code": "<string>",
"linkedin_identifier": "<string>"
}
}
]
}Authorizations
JWT authentication token
Headers
Target company identifier. Required only for global API keys (type=global). Ignored for company API keys and human users.
Body
The contact's full name. Required. If the contact already exists, its name is preserved.
1Country calling code (1-3 digits). Examples: 55 (Brazil), 1 (USA), 351 (Portugal). Required when phone_number is sent; without a phone, don't send it.
1 - 3^\d{1,3}$The contact's email address. Identifies the contact by its normalized form (no leading or trailing spaces, lowercase), which is how it's saved. Old emails saved with uppercase letters are only recognized with the same spelling, until the database is normalized. Only counts as an identifier if it contains "@". On an existing contact, it's updated when the contact is found by phone; found by email or LinkedIn, it's only filled in if empty.
The contact's LinkedIn identifier: profile URL or just the profile name. Example: joao-silva-123. Identifies the contact. On an existing contact, it's updated when the contact is found by phone; found by email or LinkedIn, it's only filled in if empty.
Phone number without formatting, digits only (area code + number, without the country code). Example: 11999998888. Identifies the contact. A phone that doesn't form a valid number with country_code is rejected with 400. Optional: the contact needs at least one identifier among phone, email, and LinkedIn.
^\d*$The contact's job title. Example: Sales Manager. Saved on creation; on an existing contact, it's updated when the contact is found by phone; found by email or LinkedIn, it's only filled in if empty.
The contact's seniority level. Example: Senior, Junior, Mid-level. Saved on creation; on an existing contact, it's updated when the contact is found by phone; found by email or LinkedIn, it's only filled in if empty.
The contact's department. Example: Sales, IT, Marketing. Saved on creation; on an existing contact, it's updated when the contact is found by phone; found by email or LinkedIn, it's only filled in if empty.
The contact's country. Example: Brazil. Saved on creation; on an existing contact, it's updated when the contact is found by phone; found by email or LinkedIn, it's only filled in if empty.
The contact's state or region. Example: São Paulo. Saved on creation; on an existing contact, it's updated when the contact is found by phone; found by email or LinkedIn, it's only filled in if empty.
The contact's city. Example: São Paulo. Saved on creation; on an existing contact, it's updated when the contact is found by phone; found by email or LinkedIn, it's only filled in if empty.
The contact's professional experience. Saved on creation; on an existing contact, it's updated when the contact is found by phone; found by email or LinkedIn, it's only filled in if empty.
The contact's custom fields. Format: { "field_slug": value }. Slugs must match custom fields registered with the CONTACT context. Invalid or nonexistent slugs are silently ignored. Values must be primitives (string, number, boolean or null). On an existing contact found by phone, the slugs sent are updated (merge); found by email or LinkedIn, only empty slugs are filled in. Slugs not sent stay as they are. Example: { "cpf": "12345678900", "data_nascimento": "1990-01-15", "valor_total": 1500.50 }
Show child attributes
Show child attributes
Response
Existing contact reused (created: false). Found by phone, the fields sent were updated; by email or LinkedIn, only empty ones were filled in and ignored_fields lists what wasn't saved.
Contact name
Contact's company
true when the contact was created (201); false when it was reused (200).
Identifier that matched
email, linkedin, phone How many contacts in the company matched by the matched_by key (0 on creation). Above 1, the company has duplicate contacts and the oldest one was reused.
Fields sent that were NOT saved because the existing contact already had another value (only when reused by email or LinkedIn). E.g. email, phone_number, job_title, additional_attributes.<slug>. Empty on creation and when found by phone.
Contact ID
Contact ID (same value as _id)
Country code of the number. Absent, along with the phone, when the contact only has a WhatsApp identifier.
Contact's email
Date of the contact's last activity
The contact's phone number. Absent when the contact only has a WhatsApp identifier.
LinkedIn profile identifier
Contact's job title
Contact's seniority level
Contact's department
Contact's country
Contact's state or region
Contact's city
Contact's professional experience
Contact's skills
Values of the contact's custom fields, by slug. When saved via additional_attributes, the value comes in the envelope { id, value, type, updatedAt }. When saved via custom_fields (PUT), it comes raw: the option_id (select), the list of option_id (multi_select) or the text (text).
Show child attributes
Show child attributes
ID of the Business the contact belongs to
Notes recorded on the contact
Show child attributes
Show child attributes
Marketing opt-out per communication channel (whatsapp, email, linkedin). Absent or empty when the contact hasn't opted out of any channel.
Show child attributes
Show child attributes
The contact's WhatsApp identifier (BSUID), assigned by Meta. Opaque string.
The contact's WhatsApp @username, as Meta sends it
The contact's WhatsApp parent_user_id, as Meta sends it
Contact creation date
Contact update date