Skip to main content
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 EndpointV2 EndpointNotes
POST /v1/add-contactPOST /v2/contactsReturns 201 with full contact object
GET /v1/get-contact/:contactIdGET /v2/contacts/{id}Returns typed response with properties
GET /v1/get-contact-by-external-id/:externalIdGET /v2/external/contacts/{externalId}Same behavior, new path
PATCH /v1/update-contactPATCH /v2/contacts/{id}Contact ID moves from body to URL path
DELETE /v1/delete-contact/:contact_idDELETE /v2/contacts/{id}Returns 204 (no body)
GET /v2/contactsNew. Cursor-paginated contact list
POST /v1/add-contacts-batchNot yet available in V2
PATCH /v1/update-contacts-batchNot yet available in V2
POST /v1/get-contacts-batchNot yet available in V2

Field mapping

Request and response fields have moved from snake_case to camelCase:
V1 FieldV2 FieldNotes
external_contact_idexternalId
external_urlexternalUrl
first_namefirstName
last_namelastName
phone_numberphoneNumber
emailemailUnchanged
salutationsalutationUnchanged
timezonetimezoneIanaRenamed for clarity
dynamic_variablespropertiesSee Migrating dynamic variables
contact_detailspropertiesSee Migrating dynamic variables
contact_id (response)id (response)
created_at (response)createdAt (response)ISO 8601 datetime string

Response format

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

Migrating dynamic variables to contact properties

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

  1. Define a property — Create a property definition with a key, data type, label, and optional constraints via the API (or through the telli UI)
  2. Choose a property key — You define a human-readable, URL-safe key (e.g., appointment_date) when creating the property
  3. 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 TypeDescriptionExample Value
stringFree-text string"Enterprise"
numberNumeric value42
booleanTrue or falsetrue
dateCalendar date (YYYY-MM-DD)"2026-03-15"
datetimeDate with time (ISO 8601)"2026-03-15T14:30:00Z"
selectSingle choice from predefined options"gold"
multi_selectMultiple choices from predefined options["german", "english"]
phone_numberPhone number in E.164 format"+4915112345678"
emailEmail 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:
KeyData TypeLabel
externalIdstringExternal ID
firstNamestringFirst Name
lastNamestringLast Name
phoneNumberphone_numberPhone Number
emailemailEmail
timezoneIanastringTimezone
externalUrlstringExternal 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

Create a contact

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.

Get a contact by ID

curl https://api.telli.com/v1/get-contact/<contact-id> \
  -H "Authorization: Bearer <your-api-key>"

Get a contact by external ID

curl https://api.telli.com/v1/get-contact-by-external-id/crm-123 \
  -H "Authorization: Bearer <your-api-key>"

List contacts

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": {
    "totalCount": 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>"

Update a contact

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.

Delete a contact

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.

Contact Properties API

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.