
Is concern rising about losing ticket history, timestamps or attachments during a platform move? Migrating customer support platforms (Zendesk) and preserve tickets requires planning, API-level imports, exact mapping and fail-safe verification. This guide provides the technical steps, payload examples, CSV/JSON templates, attachment strategies and QA checklist needed to migrate tickets intact with minimal downtime.
Key takeaways: what to know in 1 minute
- Plan mapping first: map users before tickets and map custom fields, tags and macros to avoid orphaned records.
- Use Ticket Import API or official bulk import tools: import preserves timestamps and author metadata when done correctly (see payload examples below).
- Preserve attachments and URLs: migrate file storage or proxy URLs, then re-link attachments during import to keep references intact.
- Rate limits and batch sizes matter: throttle and checkpoint every batch to prevent data loss and enable rollback.
- Validate and QA before cutover: run end-to-end checks, sampling, and a rollback plan to guarantee ticket history and searchability.
Planning: scope, objectives and success criteria
Start by defining measurable success criteria for the migration. Typical objectives include preserve all ticket IDs where possible, retain original timestamps and authors, migrate attachments with intact URLs, and keep internal notes and public replies unchanged. Include a legal and compliance review for data retention and PII handling.
Key inventory items:
- User accounts and identities (external IDs, emails)
- Ticket count and age distribution
- Attachments size and file types
- Custom ticket fields, tags, macros, triggers
- Integrations that write to tickets (CRMs, phone systems)
Deliver a migration plan with milestones, rollback windows and a communication schedule for agents and customers.
Discovery and data mapping: fields, IDs and relationships
Create a canonical mapping spreadsheet (CSV/JSON) that maps source fields to Zendesk fields. Essential mapping columns:
- source_ticket_id → target_ticket.external_id or metadata
- requester_email → requester.email (or user.external_id)
- assignee_id → assignee.user_id
- subject, description → subject, comment.body
- comments (chronological) → ticket.comments[]
- attachments → attachment.url or upload token
- created_at, updated_at → created_at (import allows overriding)
- status mapping: closed/resolved → status
- custom fields → custom_fields[ id ]
Provide a template mapping CSV (first rows):
source_ticket_id,requester_email,assignee_email,subject,created_at,status,tag_list,custom_priority
12345,user@example.com,agent@example.com,"Login issue","2024-11-02T12:34:56Z",closed,"billing,urgent",high
Map users first, then tickets. If users are not present, ticket imports may create placeholder requesters with mismatched IDs.
User migration: preserve identities and external ids
Best practice: import users before tickets, using a stable external_id (source system ID). When importing users, include name, email, external_id, role and organization. That ensures comments and ticket authors link to the correct accounts during ticket import.
Sample user import JSON (curl):
{
"users": [
{
"name": "Jane Customer",
"email": "jane@example.com",
"external_id": "src-user-987",
"role": "end-user"
}
]
}
Use the Zendesk Users API or bulk user CSV import where available. Anchor documentation: Zendesk Users API.
Ticket import strategy: APIs, batch sizes and checkpoints
For reliable preservation of metadata, use the Ticket Import API or official bulk import endpoints. The Ticket Import API accepts tickets with explicit created_at timestamps and preserves the chronology when imported properly.
Key parameters:
- Batch size: recommended 50–200 tickets per request for large imports; adjust for attachment sizes.
- Checkpointing: log last imported source_ticket_id and timestamp to resume safely.
- Idempotency: add a unique import token or external_id to detect duplicates.
- Throttling: respect rate limits; implement exponential backoff on 429 responses.
Sample ticket import payload (JSON):
{
"ticket": {
"subject": "Customer cannot log in",
"comment": { "body": "First contact message", "public": true },
"created_at": "2024-11-02T12:34:56Z",
"requester": { "name": "Jane Customer", "email": "jane@example.com", "external_id": "src-user-987" },
"assignee": { "email": "agent@example.com" },
"tags": ["billing","urgent"],
"status": "closed",
"external_id": "src-ticket-12345",
"custom_fields": [{ "id": 360012345678, "value": "high" }]
}
}
Refer to the Ticket Import docs: Ticket import API.
Attachments: migrating files without breaking links
Attachments are commonly the highest-friction item. Two reliable patterns:
- Host attachments centrally (S3/Cloud storage) and import tickets with attachment URLs or upload tokens that point to the new hosting. During import, ensure the comment includes the public or authenticated URL expected by the target platform.
- Use the platform's attachment upload endpoint to upload files, then reference the upload token inside ticket comments in the import payload.
Attachment strategy checklist:
- Export files from source and verify checksums (MD5/SHA256).
- Normalize filenames and avoid collisions by prefixing with source ticket ID.
- Preserve original URLs in a custom field or metadata for traceability.
- If privacy requires, host attachments behind signed URLs and refresh tokens after migration.
Sample flow (upload then import):
- POST file to uploads endpoint → receive upload_token.
- Use upload_token in ticket.comments as "uploads": ["token"] or attach references.
Important: large attachments should be migrated in a separate batch and validated before ticket import.
To preserve created_at, updated_at and author attribution, include those fields explicitly in the ticket import payload. Use the source system's timestamp in ISO 8601 UTC. For agent comments and internal notes, set the comment's author to the appropriate user.external_id.
If the target platform restricts explicit created_at overrides, plan to store original timestamps in a dedicated custom field and expose them to search and reports.
Handling IDs: maintain referential integrity
Two approaches to preserve IDs:
- Use external_id fields to store original ticket IDs and user IDs. This is the safest approach because most platforms allow an external_id and will not conflict with native generated IDs.
- Where supported, import with the original numeric ID (rare and often restricted). Unless the platform explicitly supports replacing native IDs, rely on external_id.
Always maintain a crosswalk file (CSV/JSON) that maps source_ticket_id → target_ticket_id for traceability and rollback.
Rate limits, retry logic and error handling
Design the importer with robust retry logic:
- Handle 429 responses with
Retry-After header and exponential backoff.
- On server errors (5xx), retry with backoff up to a configurable limit.
- Record failed items with error messages and skip to next batch; do not stop entire import on single-ticket failures.
- Maintain an errors log with source_ticket_id, HTTP status, API response and retry_count.
QA checklist: pre-cutover, cutover and post-cutover tests
Pre-cutover QA:
- User mapping validated: sample of 100 users matched and authenticated.
- Attachment integrity: 20 random attachments checksum-verified.
- Ticket chronology: sample of 200 tickets retains created_at order.
- Search and filters: verify that saved views and macros still work or are re-created.
Cutover QA:
- Import low volume of new tickets into target while continuing to ingest into source (dual-write test) for 24 hours.
- Confirm inbound channels (email, webforms) are redirected in a controlled maintenance window.
Post-cutover QA:
- Verify counts: total tickets imported vs source total.
- Validate top 50 critical tickets (by SLA, severity).
- Confirm integrations and webhooks operate as expected.
Rollback plan: how to revert if things go wrong
A robust rollback plan should include:
- A frozen snapshot of the source data before migration.
- A documented stop condition (error threshold or missing attachments percentage) that triggers rollback.
- A reversible change window: redirect inbound channels back to the original platform.
- Delete-only markers for partially imported records (use import tokens and external_id to identify imported tickets for safe deletion).
Important: never delete source records. Rollback typically means switching channels back and removing imported tickets in the target using the recorded external_id list.
Automation and scripts: reusable patterns and examples
Recommended repository structure for automation (example):
- /scripts
- import_users.py
- upload_attachments.sh
- import_tickets_batch.js
- checkpoint_manager.py
- /mappings
- user_map.csv
- ticket_map.csv
- /logs
- import_errors.log
Example pseudocode for batching with checkpoint:
- read last_checkpoint
- fetch next batch of source tickets
- for each ticket: transform payload, upload attachments, collect upload_tokens
- send import request
- if success: write mapping and update checkpoint
- if failure: log and continue
Open-source examples and community tools can speed migration but verify maintenance and compatibility with 2026 API versions.
Comparative table: migration options, cost, time and limitations
| Option |
Best for |
Typical cost |
Limitations |
| Official Ticket Import API |
Full control, preserves metadata |
Low (engineering time) |
Requires engineering resources |
| Third-party migration tools |
Faster, managed service |
Medium to high (one-time fee) |
May not preserve all custom metadata |
| CSV export/import |
Small datasets, quick moves |
Low |
Not suitable for attachments or complex history |
| Hybrid (exports + API) |
Large datasets with attachments |
Medium |
Requires orchestration |
Example practical: how it works in a real case
📊 Case data:
- Total tickets: 24,500
- Attachments: 12,300 files (120 GB)
- Active agents: 85
🧮 Process: user import → attachments staged to S3 → batch upload to target uploads endpoint (tokens) → ticket import in 200-ticket batches with checkpointing
✅ Result: 24,480 tickets migrated on first pass, 20 tickets flagged for manual review due to malformed comments
This simulation shows that a staged attachments approach plus batching and checkpointing reduces failures and shortens cutover windows.
Migration process flow
Migration process: source to Zendesk
🟦
Step 1
→ discover and map fields
🟧
Step 2
→ import users and organizations
⚡
Step 3
→ stage attachments (S3) and upload tokens
🎯
Step 4
→ import tickets by batch with checkpoints
✅
Step 5
→ QA, reconcile counts and cutover
Technical appendix: API call examples and payload templates
1) upload attachment (curl example):
curl -v -u {email_address}:{api_token} /
-F "file=@/path/to/file.png" /
"https://{subdomain}.zendesk.com/api/v2/uploads.json?filename=file.png"
Expected response contains an upload token. Save token to use in ticket import.
2) import ticket using upload token (JSON payload):
{
"ticket": {
"subject": "Support request with attachment",
"comment": {
"body": "See attached screenshot",
"uploads": ["token_string"],
"public": true
},
"created_at": "2024-11-02T12:34:56Z",
"external_id": "src-ticket-12345"
}
}
3) error-handling pattern (pseudo-response handler):
- if HTTP 201 → success: store mapping
- if HTTP 429 → wait Retry-After header seconds, retry batch
- if 4xx client error → log and mark record for manual review
- if 5xx server error → retry with backoff up to 5 times
Security, compliance and legal considerations
- Verify that exporting and importing tickets complies with data retention policies and GDPR/CCPA where applicable.
- Encrypt attachments in transit and at rest; use signed URLs when exposing attachments temporarily.
- Anonymize or redact PII prior to migration when legal hold prohibits transfer.
- Maintain an audit trail: keep export logs, checksum lists and timestamps for legal defensibility.
Relevant compliance resource: Zendesk API documentation.
Common mistakes and how to avoid them
- Importing tickets before users: results in orphaned requesters. Always import users first.
- Not testing attachments at scale: causes timeouts and failed ticket imports. Stage attachments and test sample batches.
- Ignoring rate limits: throttling failures and partial imports. Implement backoff and checkpointing.
- Losing original timestamps: breaks SLA audits. Include created_at in payload or store original timestamp in custom fields.
Cases and metrics: expected duration and sample results
Typical migration benchmarks (2026):
- Small (1–10k tickets, few attachments): 1–3 days with a small engineering team.
- Medium (10–100k tickets, attachments 50–200 GB): 1–3 weeks including testing and staging.
- Large (100k+ tickets, multi-terabyte attachments): multiple weeks to months; recommend phased cutover and parallelisation.
Success metrics to track:
- Tickets migrated / total tickets (target 99.5%+)
- Attachments migrated / total attachments
- Percentage of tickets requiring manual remediation
- Time to recovery for rollback scenarios
- Ticket Import API: full control, preserves most metadata, requires development.
- Managed migration vendors: faster time-to-value, useful for limited engineering bandwidth; evaluate for metadata fidelity.
- CSV-based importers: fast for small datasets; avoid for attachments and complex comments.
Frequently asked questions
Frequently asked questions
How to preserve timestamps during migration?
Include the original ISO 8601 timestamps in the ticket import payload's created_at and updated_at fields where the API supports it; otherwise store original timestamps in a custom field for forensic access.
Can ticket IDs be kept exactly the same?
Most platforms do not allow overriding native numeric IDs. Use external_id fields to store source IDs and maintain a crosswalk for referential integrity.
What is the best way to migrate attachments?
Stage attachments on a cloud object store (S3) or upload them to the target's upload endpoint, then reference upload tokens in ticket comments to ensure attachments are linked during import.
How to avoid duplicate tickets after migration?
Use an idempotency key or external_id per ticket and check for existing external_id before creating new tickets; maintain a mapping file to detect duplicates.
Are there privacy concerns when migrating tickets?
Yes. Run a legal review for PII, use encryption in transit and at rest, and consider anonymization if data transfer is restricted by law.
How to handle rate limits during bulk import?
Implement exponential backoff on 429 responses, respect Retry-After headers, and break large datasets into smaller batches with checkpointing.
What checks should be done post-migration?
Compare ticket counts, verify attachment availability, sample ticket histories, test search and views, and validate integrations.
When to hire a migration specialist?
Hire specialists for large-volume migrations, complex custom fields, heavy attachment volumes, or when SLA and compliance risk is high.
Your next step:
- Export a small sample (100–500 tickets) and perform a full end-to-end import test including attachments and users.
- Build the mapping spreadsheet and a checkpointing plan; schedule the cutover window with stakeholders.
- Run a parallel dual-write test (if possible) before switching inbound channels to the new platform.