> ## 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.

# Migration Guide

> Migrate from the deprecated contact endpoints to the current API for contacts and contact properties

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](/platform/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:

```bash theme={null}
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](#migrating-dynamic-variables-to-contact-properties) |
| `contact_details`       | `properties`           | See [Migrating dynamic variables](#migrating-dynamic-variables-to-contact-properties) |
| `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.

<CodeGroup>
  ```json V1 Response theme={null}
  {
    "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": "max@example.com",
    "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
  }
  ```

  ```json V2 Response theme={null}
  {
    "id": "b3f1a2c4-5d6e-7f8a-9b0c-1d2e3f4a5b6c",
    "type": "Contact",
    "externalId": "crm-123",
    "externalUrl": null,
    "firstName": "Max",
    "lastName": "Mustermann",
    "phoneNumber": "+4915112345678",
    "email": "max@example.com",
    "salutation": null,
    "timezoneIana": "Europe/Berlin",
    "properties": [
      {
        "key": "appointment_date",
        "value": "2026-03-15",
        "dataType": "date",
        "label": "Appointment Date"
      },
      {
        "key": "interest_level",
        "value": "high",
        "dataType": "select",
        "label": "Interest Level",
        "options": [
          { "value": "low", "label": "Low" },
          { "value": "medium", "label": "Medium" },
          { "value": "high", "label": "High" }
        ]
      }
    ],
    "createdAt": "2026-02-06T10:00:00.000Z",
    "updatedAt": "2026-02-06T10:00:00.000Z"
  }
  ```
</CodeGroup>

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](/platform/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:

```json theme={null}
{
  "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:

```bash theme={null}
# 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:

```json theme={null}
{
  "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                            | `"sarah@example.com"`    |

### 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

### Create a contact

<CodeGroup>
  ```bash V1 theme={null}
  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": "max@example.com",
      "timezone": "Europe/Berlin",
      "dynamic_variables": {
        "appointment_date": "2026-03-15",
        "interest_level": "high"
      }
    }'
  ```

  ```bash V2 theme={null}
  curl -X POST https://api.telli.com/v2/contacts \
    -H "Authorization: Bearer <your-api-key>" \
    -H "Content-Type: application/json" \
    -d '{
      "externalId": "crm-123",
      "firstName": "Max",
      "lastName": "Mustermann",
      "phoneNumber": "+4915112345678",
      "email": "max@example.com",
      "timezoneIana": "Europe/Berlin",
      "properties": [
        { "key": "appointment_date", "value": "2026-03-15" },
        { "key": "interest_level", "value": "high" }
      ]
    }'
  ```
</CodeGroup>

**V1** returns `{ "contact_id": "..." }`. **V2** returns `201` with the full contact object including enriched properties.

### Get a contact by ID

<CodeGroup>
  ```bash V1 theme={null}
  curl https://api.telli.com/v1/get-contact/<contact-id> \
    -H "Authorization: Bearer <your-api-key>"
  ```

  ```bash V2 theme={null}
  curl https://api.telli.com/v2/contacts/<contact-id> \
    -H "Authorization: Bearer <your-api-key>"
  ```
</CodeGroup>

### Get a contact by external ID

<CodeGroup>
  ```bash V1 theme={null}
  curl https://api.telli.com/v1/get-contact-by-external-id/crm-123 \
    -H "Authorization: Bearer <your-api-key>"
  ```

  ```bash V2 theme={null}
  curl https://api.telli.com/v2/external/contacts/crm-123 \
    -H "Authorization: Bearer <your-api-key>"
  ```
</CodeGroup>

### List contacts

This endpoint is **new in V2** — there is no V1 equivalent. It returns a paginated list of contacts using cursor-based pagination.

```bash theme={null}
curl "https://api.telli.com/v2/contacts?limit=10" \
  -H "Authorization: Bearer <your-api-key>"
```

Response:

```json theme={null}
{
  "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:

```bash theme={null}
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.

<CodeGroup>
  ```bash V1 theme={null}
  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"
      }
    }'
  ```

  ```bash V2 theme={null}
  curl -X PATCH https://api.telli.com/v2/contacts/<contact-id> \
    -H "Authorization: Bearer <your-api-key>" \
    -H "Content-Type: application/json" \
    -d '{
      "firstName": "Maximilian",
      "properties": [
        { "key": "interest_level", "value": "medium" }
      ]
    }'
  ```
</CodeGroup>

**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

<CodeGroup>
  ```bash V1 theme={null}
  curl -X DELETE https://api.telli.com/v1/delete-contact/<contact-id> \
    -H "Authorization: Bearer <your-api-key>"
  ```

  ```bash V2 theme={null}
  curl -X DELETE https://api.telli.com/v2/contacts/<contact-id> \
    -H "Authorization: Bearer <your-api-key>"
  ```
</CodeGroup>

**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](/platform/contact-properties).

### List all property definitions

```bash theme={null}
curl https://api.telli.com/v2/properties/contacts \
  -H "Authorization: Bearer <your-api-key>"
```

Returns both system properties and your custom properties:

```json theme={null}
{
  "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

```bash theme={null}
curl https://api.telli.com/v2/properties/contacts/appointment_date \
  -H "Authorization: Bearer <your-api-key>"
```

### Create a property definition

```bash theme={null}
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.

```bash theme={null}
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.
