curl --request POST \
--url https://app.thinnest.ai/api/v1/contacts \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"phone": "+91 98765 43210",
"name": "Asha Rao",
"email": "Asha.Rao@gmail.com",
"externalId": "LSQ-42",
"tags": [
"Hot Lead",
"Sky Towers"
],
"language": "Hindi",
"consent": {
"status": "granted",
"source": "99acres enquiry form, 17 Sep"
}
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
phone: '+91 98765 43210',
name: 'Asha Rao',
email: 'Asha.Rao@gmail.com',
externalId: 'LSQ-42',
tags: ['Hot Lead', 'Sky Towers'],
language: 'Hindi',
consent: {status: 'granted', source: '99acres enquiry form, 17 Sep'}
})
};
fetch('https://app.thinnest.ai/api/v1/contacts', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://app.thinnest.ai/api/v1/contacts"
payload = {
"phone": "+91 98765 43210",
"name": "Asha Rao",
"email": "Asha.Rao@gmail.com",
"externalId": "LSQ-42",
"tags": ["Hot Lead", "Sky Towers"],
"language": "Hindi",
"consent": {
"status": "granted",
"source": "99acres enquiry form, 17 Sep"
}
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text){
"id": "cust_4e7b1c9d-2a6f-4385-b0d2-9f6a3e8c1b74",
"phone": "919876543210",
"name": "Asha Rao",
"email": "asha.rao@gmail.com",
"externalId": "LSQ-42",
"tags": [
"hot-lead",
"sky-towers"
],
"language": "Hindi",
"note": null,
"consent": {
"status": "granted",
"source": "99acres enquiry form, 17 Sep",
"recordedAt": "2026-10-06T09:40:02.311Z"
},
"source": "agent",
"acquiredFrom": null,
"createdAt": "2026-08-30T14:12:45.008Z"
}{
"id": "cust_4e7b1c9d-2a6f-4385-b0d2-9f6a3e8c1b74",
"phone": "919876543210",
"name": "Asha Rao",
"email": "asha.rao@gmail.com",
"externalId": "LSQ-42",
"tags": [
"hot-lead",
"sky-towers"
],
"language": "Hindi",
"note": null,
"consent": {
"status": "granted",
"source": "99acres enquiry form, 17 Sep",
"recordedAt": "2026-10-06T09:40:02.311Z"
},
"source": "api",
"acquiredFrom": null,
"createdAt": "2026-10-06T09:40:02.311Z"
}{
"error": "`consent.source` is required when recording consent — where did they agree, or refuse?"
}{
"error": "Send a valid API key as `Authorization: Bearer <key>`."
}{
"error": "This API key is read-only: it can read everything but change nothing."
}{
"error": "Another contact already has that `externalId`."
}{
"error": "Over 240 requests a minute. Slow down and retry."
}Create Contact
Creates a contact — or, when someone is already on that number, updates that person instead, so a CRM that syncs the same lead twice never doubles your list. Answers 201 for a new contact and 200 for one it updated. Creating a contact is not consent: only consent.status: "granted" with a source is. A build key may do this.
curl --request POST \
--url https://app.thinnest.ai/api/v1/contacts \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"phone": "+91 98765 43210",
"name": "Asha Rao",
"email": "Asha.Rao@gmail.com",
"externalId": "LSQ-42",
"tags": [
"Hot Lead",
"Sky Towers"
],
"language": "Hindi",
"consent": {
"status": "granted",
"source": "99acres enquiry form, 17 Sep"
}
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
phone: '+91 98765 43210',
name: 'Asha Rao',
email: 'Asha.Rao@gmail.com',
externalId: 'LSQ-42',
tags: ['Hot Lead', 'Sky Towers'],
language: 'Hindi',
consent: {status: 'granted', source: '99acres enquiry form, 17 Sep'}
})
};
fetch('https://app.thinnest.ai/api/v1/contacts', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://app.thinnest.ai/api/v1/contacts"
payload = {
"phone": "+91 98765 43210",
"name": "Asha Rao",
"email": "Asha.Rao@gmail.com",
"externalId": "LSQ-42",
"tags": ["Hot Lead", "Sky Towers"],
"language": "Hindi",
"consent": {
"status": "granted",
"source": "99acres enquiry form, 17 Sep"
}
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text){
"id": "cust_4e7b1c9d-2a6f-4385-b0d2-9f6a3e8c1b74",
"phone": "919876543210",
"name": "Asha Rao",
"email": "asha.rao@gmail.com",
"externalId": "LSQ-42",
"tags": [
"hot-lead",
"sky-towers"
],
"language": "Hindi",
"note": null,
"consent": {
"status": "granted",
"source": "99acres enquiry form, 17 Sep",
"recordedAt": "2026-10-06T09:40:02.311Z"
},
"source": "agent",
"acquiredFrom": null,
"createdAt": "2026-08-30T14:12:45.008Z"
}{
"id": "cust_4e7b1c9d-2a6f-4385-b0d2-9f6a3e8c1b74",
"phone": "919876543210",
"name": "Asha Rao",
"email": "asha.rao@gmail.com",
"externalId": "LSQ-42",
"tags": [
"hot-lead",
"sky-towers"
],
"language": "Hindi",
"note": null,
"consent": {
"status": "granted",
"source": "99acres enquiry form, 17 Sep",
"recordedAt": "2026-10-06T09:40:02.311Z"
},
"source": "api",
"acquiredFrom": null,
"createdAt": "2026-10-06T09:40:02.311Z"
}{
"error": "`consent.source` is required when recording consent — where did they agree, or refuse?"
}{
"error": "Send a valid API key as `Authorization: Bearer <key>`."
}{
"error": "This API key is read-only: it can read everything but change nothing."
}{
"error": "Another contact already has that `externalId`."
}{
"error": "Over 240 requests a minute. Slow down and retry."
}Authorizations
Your API key (ta_live_…) from Settings → API keys, sent as Authorization: Bearer <key>. Keep it on a server: it can message every customer you have. A key is full, build or read-only; a request its level does not allow is refused with 403.
Headers
Developers only: the customer workspace this request acts in — its org_… id from POST /customers. Leave it out to act in your own workspace.
"org_3fKq9TzQ1mN8vB2xR7cLpA"
Body
Their number, with or without the +; spaces are fine. Without a +, ten digits or fewer — or a leading 0 — is read as an Indian number. The number is the contact's identity: a second create on it updates the first.
"+91 98765 43210"
Their name.
120"Asha Rao"
Their email. Stored lower-cased.
200"asha.rao@gmail.com"
Your CRM's id for them, unique in your workspace. GET /contacts?externalId= finds them by it.
200"LSQ-42"
Tags, normalised as the console stores them: lower case, spaces to hyphens, duplicates dropped, 40 characters and 20 tags at most. Replaces the list.
20["hot-lead", "sky-towers"]
The language to write to them in, by its English name as the console offers it, auto to match theirs, or null.
auto, English, Hindi, Assamese, Bengali, Bodo, Dogri, French, German, Gujarati, Indonesian, Italian, Japanese, Kannada, Kashmiri, Konkani, Korean, Maithili, Malayalam, Manipuri, Marathi, Nepali, Odia, Polish, Portuguese, Punjabi, Russian, Sanskrit, Santali, Sindhi, Spanish, Swahili, Tamil, Telugu, Thai, Turkish, Urdu, Vietnamese, null "Hindi"
Free-text notes about them.
2000Their word on marketing messages, recorded with the time and where it came from — what a regulator asks.
Show child attributes
Show child attributes
Response
Someone was already on that number; they were updated.
The contact's id (cust_…).
"cust_4e7b1c9d-2a6f-4385-b0d2-9f6a3e8c1b74"
Digits only, with the country code — 919876543210.
Their name.
Their email, lower-cased.
Your CRM's id for them, unique in your workspace.
Their tags, lower-case and hyphenated.
The language to write to them in, e.g. Hindi, or auto to match theirs.
Free-text notes.
Show child attributes
Show child attributes
How you came to have them: agent (they spoke to an agent), import, api, whatsapp_app.
Where the lead came from, when recorded.
When the contact was created.