> ## Documentation Index
> Fetch the complete documentation index at: https://ekso.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrate from Zendesk

> Move Zendesk organisations, tickets, comments, and attachments into Ekso — in two commands.

`ekso migrate zendesk` pulls organisations, tickets, comments, attachments, and tags from Zendesk Support (REST v2) into your Ekso tenant. Two commands — `collect` and `apply`. Read [Migrate overview](/cli/migrate/overview) and [Before you start](/cli/migrate/before-you-start) first.

Zendesk is a support-ticket platform, not an issue tracker. The data shape is different and so is the customer's mental model. This page calls out the differences explicitly.

## What gets imported

| Zendesk concept             | Ekso shape                                                                                                         |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| Organisation                | [`DataContainer`](/data-model/data-container) (or a synthetic `zendesk-default` container if the instance is flat) |
| Ticket                      | [`DataItem`](/data-model/data-item)                                                                                |
| Comment (public or private) | [`DataAnnotation`](/data-model/data-annotation) (visibility flag preserved as `Tags=["public"]` or `["private"]`)  |
| Attachment (per-comment)    | [`DataFile`](/data-model/data-file)                                                                                |
| End-user (customer)         | [`DataUser`](/data-model/data-user) (tagged `migrated-from:zendesk-enduser`)                                       |
| Agent (staff)               | [`DataUser`](/data-model/data-user) (tagged `migrated-from:zendesk-agent`)                                         |
| Group (agent group)         | [`ConfigLabel`](/data-model/config-label) (prefix `group:`) + `DataItem.Tags`                                      |
| Tag                         | [`ConfigLabel`](/data-model/config-label)                                                                          |
| Custom ticket field         | [`DataItem.Field[]`](/data-model/data-item-field-value) (via `--field-map`) or `Meta`                              |
| Satisfaction rating (CSAT)  | `DataItem.Meta.zendesk_csat`                                                                                       |
| Brand                       | `DataItem.Meta.zendesk_brand_id`                                                                                   |

## What does NOT get imported

* **SLA policies & breach data.** Read-only fidelity in `Meta.zendesk_sla` only — Ekso v1 has no first-class SLA entity. Use Ekso [clocks](/cli/reference/clock) for SLA replacement going forward.
* **Macros & triggers.** Workflow, not data. Re-author as Ekso [rules](/cli/reference/rule).
* **Views & filters.** Re-author on the Ekso side.
* **Talk / Chat / Sell / Explore** — adjacent products, out of scope.
* **Side conversations.** v1 limitation; tracked.
* **Article / KB content.** Use Ekso [docs](/cli/reference/docs) and import separately.

## Before you start

Pre-flight checklist on top of [the general one](/cli/migrate/before-you-start):

* A Zendesk Support instance.
* A Zendesk admin account.
* A Zendesk API token — generate one in **Admin Centre → Channels → API → Token Access**.
* Your Zendesk subdomain — e.g. `acme.zendesk.com`.

Add a `zendesk` block to your `migration.config.json`:

```json theme={null}
{
  "ekso": { "url": "https://ekso.acme.com", "apiKey": "ek_live_..." },

  "source": "zendesk",

  "zendesk": {
    "url": "https://acme.zendesk.com",
    "email": "you@acme.com",
    "apiToken": "abc123..."
  }
}
```

Or use the env-var override `EKSO_MIGRATE_ZENDESK_TOKEN` for CI use cases.

Zendesk auth uses the `email/token:API_TOKEN` basic-auth format — the CLI assembles this from `email` and `apiToken`.

## Step 1 — list projects

```bash theme={null}
ekso migrate zendesk list-projects \
    --config migration.config.json \
    --url https://ekso.acme.com
```

The verb is `list-projects` for parity with every other source — for Zendesk this enumerates the **organisations** configured on the workspace, since Zendesk doesn't have a project concept of its own. Each one becomes a `DataContainer` (an Ekso "project") on apply.

```
NAME                    ID         DOMAIN              MEMBERS
ACME Internal           360...01   acme.com            18
ACME Customers          360...02   external            132
```

If your Zendesk instance is flat (no organisations configured), the list is empty — that's expected. Every ticket will land in a single synthetic container called `zendesk-default` on apply.

## Step 2 — collect

To collect every ticket in the workspace (recommended for first-run):

```bash theme={null}
ekso migrate zendesk collect \
    --config migration.config.json \
    --url https://ekso.acme.com
```

To restrict to a specific organisation (or several — `--project` repeats):

```bash theme={null}
ekso migrate zendesk collect \
    --config migration.config.json \
    --project 360001234567 \
    --url https://ekso.acme.com
```

The value is the Zendesk organisation ID (the numeric `360...` shown by `list-projects`).

Sample run:

```
fetching orgs... 2 fetched
fetching tickets... 8421/8421  ok
fetching comments... 26109/26109  ok
fetching attachments... 1247/1247  ok
saved cache: ~/.ekso/migrate/zendesk-2026-04-29T143205.sqlite
```

Useful flags:

* `--no-attachments` — skip downloads.
* `--no-comments` — skip comments. (Note: Zendesk has no separate "description" field — the first comment IS the description, so `--no-comments` will leave items with empty bodies.)
* `--exclude-closed` — filter out closed tickets at the API layer.
* `--resume` — pick up where a killed collect left off.

Zendesk's rate limits are aggressive (\~700 requests/minute on Enterprise, 200/minute on lower plans). A 50K-ticket migration on a Pro plan takes \~4 hours and benefits from `--resume`.

## Step 3 — dry-run apply

```bash theme={null}
ekso migrate zendesk apply \
    --config migration.config.json \
    --process proc_support \
    --url https://ekso.acme.com \
    --dry-run
```

## Step 4 — apply for real

```bash theme={null}
ekso migrate zendesk apply \
    --config migration.config.json \
    --process proc_support \
    --url https://ekso.acme.com
```

Sample run:

```
applying container ACME Internal...     ok
applying users (162)...                 ok (40 agents, 122 end-users)
applying items (8421)...                ok (8421 created)
applying annotations (26109)...         ok
applying files (1247)...                ok (1242 created, 5 skipped >20MB)
done in 18m12s — exit 0
```

If interrupted, re-run with `--resume`.

## Identity resolution for Zendesk

Zendesk has two identity classes:

* **Agents** are tenant-internal staff. They become Ekso `DataUser`s tagged `migrated-from:zendesk-agent`.
* **End-users** are customers — the people who opened tickets. They become Ekso `DataUser`s tagged `migrated-from:zendesk-enduser`.

Both classes are minted by default with `--user-strategy match-or-create`. The end-user vs agent distinction stays visible via the migrated-from tag so you can later filter, separate by permissions, or remove the end-user accounts entirely if you only need them for audit.

**Anonymous end-users** (Zendesk lets customers email in without registering) get a placeholder email like `<ticket-id>@anon.zendesk.acme.local` and are tagged `migrated-from:zendesk-anon`. Use `--user-strategy migration-bot --fallback-user user_zendesk_admin` if you'd rather collapse all anonymous end-users into one bot user.

Read [Identity resolution](/cli/migrate/identity-resolution) for the full mechanics.

## Custom fields for Zendesk

Zendesk ticket field IDs are numeric strings — quote them in YAML:

```yaml theme={null}
zendesk:
  "360001234567": { ekso: AffectedComponent, kind: text }
  "360001234568":
    ekso: CustomerTier
    kind: picker
    picker:
      Free: tier-free
      Pro:  tier-pro
      Ent:  tier-ent
```

Discover your ticket field IDs:

```bash theme={null}
curl -u you@acme.com/token:API_TOKEN \
    https://acme.zendesk.com/api/v2/ticket_fields.json \
    | jq '.ticket_fields[] | {id, title, type}'
```

See [Field mapping](/cli/migrate/field-mapping) for the full format.

## Iron rule — first comment as description

Zendesk has no separate "description" field on a ticket. The **first comment IS the description**. The migrator handles this:

1. The first comment's body is used as the item's description (`DataItem.Field[CoreFieldType.Description]`).
2. If the ticket has only one comment, no `DataAnnotation` is created — the comment is just the description.
3. If the ticket has multiple comments, all comments (including the first) become `DataAnnotation`s as well, so the audit log is preserved.

The Zendesk API natively returns this shape — you don't need any special flags. But if you're comparing the Ekso output against Zendesk's UI, expect items where the description and the first comment look identical. That's expected.

## Public vs private comments

Zendesk distinguishes public-facing comments (visible to the requester) from internal notes (visible to agents only). The migrator preserves this:

* Public comments → `DataAnnotation` with `Tags=["public"]`.
* Internal notes → `DataAnnotation` with `Tags=["private"]`.

Filter with `ekso annotation list --item <id> --tag private` after migration.

## Orgs vs flat instances

Zendesk has no project concept. The migrator uses **organisations** as the closest analog and maps each org to one Ekso `DataContainer`. If your instance is flat (no orgs configured — common for smaller Zendesk customers), the migrator creates a single synthetic container called `zendesk-default` and lands every ticket there.

You can split tickets into multiple Ekso containers post-migration via the admin UI if the flat-container approach doesn't fit.

## Multi-brand Zendesk

If your Zendesk instance has multiple brands, the brand ID is preserved in `DataItem.Meta.zendesk_brand_id`. Per-brand container creation is on the v2 roadmap; in v1, the org-as-container model is preserved across brands.

## Troubleshooting Zendesk-specific issues

| Symptom                                            | Fix                                                                                                                                    |
| -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `exit 3 — Zendesk credentials invalid`             | The `email/token:API_TOKEN` basic-auth format is unusual — check both fields. Re-issue the token in Admin Centre → API → Token Access. |
| `Synthetic container created`                      | Expected for flat instances. Every ticket lands in `zendesk-default`.                                                                  |
| `Anonymous end-user minted with placeholder email` | Source user has no email. Tagged `migrated-from:zendesk-anon`.                                                                         |
| `Attachment over 20MB skipped`                     | Ekso file-size limit. Skipped with warning; URL preserved in `Meta`.                                                                   |
| `exit 7 — Zendesk rate-limit exceeded`             | Wait — Zendesk limits are tight. Re-run with `--resume`.                                                                               |

See [Troubleshooting](/cli/migrate/troubleshooting) for the full per-source error table.

## Why migrate to Ekso

Zendesk specialises in support; Ekso unifies support, project management, and time tracking with financial intelligence. Tickets that turn into work flow naturally without leaving the platform. See [Ekso vs Zendesk](https://ekso.app/compare/ekso-vs-zendesk) for the broader comparison.

## Where to next

* **[Command reference](/cli/migrate/command-reference)** — full flag surface.
* **[Identity resolution](/cli/migrate/identity-resolution)** — agents vs end-users.
* **[Field mapping](/cli/migrate/field-mapping)** — ticket field shapes.
* **[Troubleshooting](/cli/migrate/troubleshooting)** — exit codes and recovery.
