Documentation Index
Fetch the complete documentation index at: https://docs.telli.com/llms.txt
Use this file to discover all available pages before exploring further.
The telli V2 API is a new RESTful API for managing contacts and contact properties. It coexists alongside the V1 API — both remain fully operational. This guide walks you through the differences and how to migrate your integration.
Prerequisites
- A telli account with API access
- An API key from your telli dashboard (Settings > API & Webhooks)
What’s changed
The V2 API introduces several improvements over V1:
- Typed contact properties — Structured, validated properties replace untyped dynamic variables (see Contact Properties for background)
- Cursor-based pagination — Efficient pagination for listing contacts
- Structured errors — Consistent error responses with HTTP status codes and error codes
Authentication
Authentication is unchanged. Use the same API key with the same Authorization header:
Authorization: Bearer <your-api-key>
The base URL is the same — V2 endpoints are available under the /v2/ prefix.
Endpoint mapping
| V1 Endpoint | V2 Endpoint | Notes |
|---|
POST /v1/add-contact | POST /v2/contacts | Returns 201 with full contact object |
GET /v1/get-contact/:contactId | GET /v2/contacts/{id} | Returns typed response with properties |
GET /v1/get-contact-by-external-id/:externalId | GET /v2/external/contacts/{externalId} | Same behavior, new path |
PATCH /v1/update-contact | PATCH /v2/contacts/{id} | Contact ID moves from body to URL path |
DELETE /v1/delete-contact/:contact_id | DELETE /v2/contacts/{id} | Returns 204 (no body) |
| — | GET /v2/contacts | New. Cursor-paginated contact list |
POST /v1/add-contacts-batch | — | Not yet available in V2 |
PATCH /v1/update-contacts-batch | — | Not yet available in V2 |
POST /v1/get-contacts-batch | — | Not yet available in V2 |
Field mapping
Request and response fields have moved from snake_case to camelCase:
| V1 Field | V2 Field | Notes |
|---|
external_contact_id | externalId | |
external_url | externalUrl | |
first_name | firstName | |
last_name | lastName | |
phone_number | phoneNumber | |
email | email | Unchanged |
salutation | salutation | Unchanged |
timezone | timezoneIana | Renamed for clarity |
dynamic_variables | properties | See Migrating dynamic variables |
contact_details | properties | See Migrating dynamic variables |
contact_id (response) | id (response) | |
created_at (response) | createdAt (response) | ISO 8601 datetime string |
V2 responses include a type field and return enriched contact property data.
{
"contact_id": "b3f1a2c4-5d6e-7f8a-9b0c-1d2e3f4a5b6c",
"external_contact_id": "crm-123",
"external_url": null,
"first_name": "Max",
"last_name": "Mustermann",
"phone_number": "+4915112345678",
"email": "[email protected]",
"salutation": null,
"timezone": "Europe/Berlin",
"contact_details": {
"appointment_date": "2026-03-15",
"interest_level": "high"
},
"created_at": "2026-02-06T10:00:00.000Z",
"status": "new",
"call_attempts": 0,
"next_call_at": null,
"in_call_since": null,
"reached_at": null
}
Key differences in the response:
contact_id is now id
contact_details (flat key-value object) is now properties (array of typed, enriched objects)
- Each property in the response includes its
dataType, label, and options (for select types)
- The
type field identifies the resource type ("Contact")
- Call-related fields (
status, call_attempts, next_call_at, in_call_since, reached_at) are not part of the V2 contacts response
This is the most significant change between V1 and V2. In V1, you could attach arbitrary key-value data to contacts via dynamic_variables or contact_details without any prior setup. In V2, you first define a property schema with a human-readable key and then use that key when setting values.
For a full overview of what contact properties are and how to manage them in the telli UI, see Contact Properties.
How it works
- Define a property — Create a property definition with a key, data type, label, and optional constraints via the API (or through the telli UI)
- Choose a property key — You define a human-readable, URL-safe key (e.g.,
appointment_date) when creating the property
- Use the key on contacts — When creating or updating contacts, pass properties as
[{key, value}] pairs using your chosen keys
Example: before and after
Suppose you were storing these dynamic variables on contacts in V1:
{
"dynamic_variables": {
"appointment_date": "2026-03-15",
"interest_level": "high",
"notes": "Interested in premium plan"
}
}
To migrate to V2, first define each as a typed property:
# Create a date property for appointment dates
curl -X POST https://api.telli.com/v2/properties/contacts \
-H "Authorization: Bearer <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"key": "appointment_date",
"dataType": "date",
"label": "Appointment Date"
}'
# Response: { "key": "appointment_date", "dataType": "date", ... }
# Create a select property for interest level
curl -X POST https://api.telli.com/v2/properties/contacts \
-H "Authorization: Bearer <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"key": "interest_level",
"dataType": "select",
"label": "Interest Level",
"options": [
{ "value": "low", "label": "Low" },
{ "value": "medium", "label": "Medium" },
{ "value": "high", "label": "High" }
]
}'
# Response: { "key": "interest_level", "dataType": "select", ... }
# Create a text property for notes
curl -X POST https://api.telli.com/v2/properties/contacts \
-H "Authorization: Bearer <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"key": "notes",
"dataType": "string",
"label": "Notes"
}'
# Response: { "key": "notes", "dataType": "string", ... }
Then use those keys when creating or updating contacts:
{
"properties": [
{ "key": "appointment_date", "value": "2026-03-15" },
{ "key": "interest_level", "value": "high" },
{ "key": "notes", "value": "Interested in premium plan" }
]
}
Available property types
| API Data Type | Description | Example Value |
|---|
string | Free-text string | "Enterprise" |
number | Numeric value | 42 |
boolean | True or false | true |
date | Calendar date (YYYY-MM-DD) | "2026-03-15" |
datetime | Date with time (ISO 8601) | "2026-03-15T14:30:00Z" |
select | Single choice from predefined options | "gold" |
multi_select | Multiple choices from predefined options | ["german", "english"] |
phone_number | Phone number in E.164 format | "+4915112345678" |
email | Email address | "[email protected]" |
System properties
Some contact fields are represented as system properties in V2. These are always present and cannot be modified or deleted through the properties API:
| Key | Data Type | Label |
|---|
externalId | string | External ID |
firstName | string | First Name |
lastName | string | Last Name |
phoneNumber | phone_number | Phone Number |
email | email | Email |
timezoneIana | string | Timezone |
externalUrl | string | External URL |
System properties are set directly as top-level fields on the contact (e.g., firstName, email), not through the properties array.
Operation-by-operation migration
curl -X POST https://api.telli.com/v1/add-contact \
-H "Authorization: Bearer <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"external_contact_id": "crm-123",
"first_name": "Max",
"last_name": "Mustermann",
"phone_number": "+4915112345678",
"email": "[email protected]",
"timezone": "Europe/Berlin",
"dynamic_variables": {
"appointment_date": "2026-03-15",
"interest_level": "high"
}
}'
V1 returns { "contact_id": "..." }. V2 returns 201 with the full contact object including enriched properties.
curl https://api.telli.com/v1/get-contact/<contact-id> \
-H "Authorization: Bearer <your-api-key>"
curl https://api.telli.com/v1/get-contact-by-external-id/crm-123 \
-H "Authorization: Bearer <your-api-key>"
This endpoint is new in V2 — there is no V1 equivalent. It returns a paginated list of contacts using cursor-based pagination.
curl "https://api.telli.com/v2/contacts?limit=10" \
-H "Authorization: Bearer <your-api-key>"
Response:
{
"type": "ContactCollection",
"data": [
{ "id": "...", "type": "Contact", "firstName": "Max", ... }
],
"pageInfo": {
"hasNextPage": true,
"endCursor": "eyJjcmVhdGVkQXQiOi..."
},
"meta": {
"limit": 10,
"count": 10,
"total": 142
}
}
To fetch the next page, pass the endCursor value as the cursor query parameter:
curl "https://api.telli.com/v2/contacts?limit=10&cursor=eyJjcmVhdGVkQXQiOi..." \
-H "Authorization: Bearer <your-api-key>"
In V1, you pass the contact_id in the request body. In V2, the contact ID is part of the URL path.
curl -X PATCH https://api.telli.com/v1/update-contact \
-H "Authorization: Bearer <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"contact_id": "<contact-id>",
"first_name": "Maximilian",
"dynamic_variables": {
"interest_level": "medium"
}
}'
Important difference in update behavior:
- V1 replaces the entire
dynamic_variables object. If you only send {"interest_level": "medium"}, all other dynamic variables are lost.
- V2 merges properties. Only the keys you include are updated — all other existing properties are preserved. To clear a property, set its value to
null.
curl -X DELETE https://api.telli.com/v1/delete-contact/<contact-id> \
-H "Authorization: Bearer <your-api-key>"
V1 returns { "message": "Contact deleted successfully", "contact_id": "..." }. V2 returns 204 No Content with an empty response body.
V2 introduces a dedicated API for managing property definitions. You only need to create property definitions once per account — they then apply to all contacts.
For managing properties through the telli UI instead, see Contact Properties.
List all property definitions
curl https://api.telli.com/v2/properties/contacts \
-H "Authorization: Bearer <your-api-key>"
Returns both system properties and your custom properties:
{
"type": "ContactPropertyList",
"data": [
{
"type": "ContactProperty",
"key": "firstName",
"dataType": "string",
"source": "system",
"label": "First Name",
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z"
},
{
"type": "ContactProperty",
"key": "appointment_date",
"dataType": "date",
"source": "user",
"label": "Appointment Date",
"createdAt": "2026-02-06T10:00:00.000Z",
"updatedAt": "2026-02-06T10:00:00.000Z"
}
]
}
Get a single property definition
curl https://api.telli.com/v2/properties/contacts/appointment_date \
-H "Authorization: Bearer <your-api-key>"
Create a property definition
curl -X POST https://api.telli.com/v2/properties/contacts \
-H "Authorization: Bearer <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"key": "customer_tier",
"dataType": "select",
"label": "Customer Tier",
"options": [
{ "value": "free", "label": "Free" },
{ "value": "pro", "label": "Pro" },
{ "value": "enterprise", "label": "Enterprise" }
]
}'
Update a property definition
You can update the label, description, and add new options (for select types). You cannot change the data type or remove existing options.
curl -X PATCH https://api.telli.com/v2/properties/contacts/appointment_date \
-H "Authorization: Bearer <your-api-key>" \
-H "Content-Type: application/json" \
-d '{
"label": "Appointment Date (Updated)",
"description": "The next scheduled appointment date"
}'
Key differences to be aware of
-
Properties require prior definition — You must create a property definition before you can use it on contacts. Unknown property keys are rejected with a
422 validation error.
-
Property keys are user-defined — You choose a human-readable, URL-safe key (e.g.,
appointment_date) when creating a property. Keys must be unique within your account.
-
Update merges properties — In V2, updating a contact’s properties only affects the keys you include. Existing properties are preserved. Set a value to
null to clear a specific property. In V1, sending dynamic_variables replaced the entire object.
-
Values are validated — V2 validates property values against their data type and constraints (e.g., a
date property rejects "not-a-date", a select property rejects values not in the options list). V1 accepted any value.
-
Batch endpoints are not yet available in V2 — If you rely on batch operations (
add-contacts-batch, update-contacts-batch, get-contacts-batch), continue using the V1 endpoints for now.
-
V2 returns enriched properties — GET responses include the
dataType, label, and options for each property value, so you don’t need a separate lookup to interpret property data.