Zammad to SurveySparrow Ticket Migration: Technical Guide
Technical guide for migrating tickets from Zammad to SurveySparrow. Covers API extraction, field mapping, attachment limits, rate limits, and comment threading.
Planning a migration?
Get a free 30-min call with our engineers. We'll review your setup and map out a custom migration plan — no obligation.
Schedule a free call- 1,500+ migrations completed
- Zero downtime guaranteed
- Transparent, fixed pricing
- Project success responsibility
- Post-migration support included
Zammad to SurveySparrow Ticket Migration: Technical Guide
Last verified against Zammad 6.x REST API and SurveySparrow API v3, July 2025.
Migrating from Zammad to SurveySparrow Ticket Management is an API reconstruction job, not a wizard-driven export/import. Zammad's article-based helpdesk architecture must be translated into SurveySparrow's flatter, feedback-first ticketing model — and no native migration path exists between the two. Every ticket, user, and conversation thread must be extracted from Zammad, transformed to match SurveySparrow's schema, and loaded through SurveySparrow's V3 API. (docs.zammad.org)
Zammad's migration tooling handles inbound moves from Freshdesk, Kayako, OTRS, and Zendesk into Zammad. SurveySparrow's migration app targets surveys, not support ticket history. Tools like n8n can wire the two systems together for live automation, but that is not historical migration with replay ordering, attachment handling, and delta cutover logic.
This guide covers the data model gap, API constraints on both sides, field mapping, extraction and loading strategies, contact deduplication, error handling, post-migration validation, and the failure modes that derail teams mid-migration.
No native import tool exists for this direction. Zammad has no export-to-SurveySparrow feature. SurveySparrow has no Zammad importer. SurveySparrow supports ticket export in Excel and JSON format via Settings → Ticket Management → Export Data — but these are export features, not import. Plan for custom API-based scripting from day one.
For related migrations from Zammad, see our Zammad to Zendesk Migration Guide, Zammad to Help Scout Migration Guide, and Zammad to Tidio Migration Guide. For inbound SurveySparrow migrations, see the HubSpot Service Hub to SurveySparrow Ticket Migration Guide. For outbound SurveySparrow migrations, see the SurveySparrow to Missive Migration Guide. For the broader cutover pattern, see our Zero-Downtime Help Desk Data Migration guide.
Zammad vs. SurveySparrow: Data Model Comparison
Understanding the structural gap between these two platforms is the most important step before writing any migration code.
Zammad uses a highly relational model. A Ticket is a container. The actual conversation lives in Articles — typed as email, phone, web, note, SMS, chat, or social messages — each with its own sender, recipient, CC, content type, internal/external visibility, timestamp, and attachments. Tickets belong to Users (customers) and Groups (agent teams), and Users can belong to Organizations. (docs.zammad.org)
SurveySparrow Ticket Management (also referred to as SparrowDesk in some docs and UI surfaces) uses a flatter model. A ticket has subject, description, priority, status, assignee, team, custom fields, and a flat comment thread. Tickets point to a requester Contact. (support.surveysparrow.com)
| Concept | Zammad | SurveySparrow Ticket Management |
|---|---|---|
| Core record | Ticket (with articles) | Ticket (with comments) |
| Messages/Replies | Articles — typed (email, note, phone, web, social), with from, to, cc, content_type, internal flag |
Comments — flat text thread on a ticket |
| Customers | Users (customer role) linked to Organizations | Contacts (requester linked by requester_id or email) |
| Organizations | First-class object; users belong to organizations | No native Organizations object |
| Agent assignment | owner_id (agent), Group-based routing |
assignee_id (agent), Team-based routing |
| Groups | Groups with granular permissions | Teams |
| Custom fields | Object attributes on tickets, users, organizations (booleans, dates, integers, selects, text, tree selects) | Custom ticket fields (dropdown, multiselect, date, text, dependent) |
| Tags | Tag system on tickets, users, organizations | No native tag system on tickets |
| Ticket hierarchy | Ticket linking (parent, child, normal) | Related Tickets (parent/child) |
| Attachments | Any file type, stored per article | PDF, PNG, JPEG, MP3, CSV, WAV only; 15 MB max per file |
| Priorities | Configurable (default: 1 low, 2 normal, 3 high) | Configurable (numeric IDs) |
| States | Configurable (new, open, pending reminder, pending close, closed, etc.) | Configurable statuses (numeric IDs) |
| SLAs | Built-in SLA engine with escalation | SLA tracking with first-response and resolution due dates |
| Timestamps | created_at, updated_at on tickets and articles |
created_at, updated_at on tickets (server-set on creation) |
The biggest structural loss: Zammad articles carry rich metadata (sender, recipient, CC, content type, channel type, internal visibility) that has no direct equivalent in SurveySparrow's flat comment model. You will need to serialize this metadata into comment text or accept the data loss.
Be honest about the trade-off. SurveySparrow Ticket Management is not a full helpdesk replacement for Zammad. It lacks Zammad's multi-channel article types, knowledge base, SLA engine depth, organization hierarchy, and the flexibility of a self-hosted open-source system. Its strength is connecting surveys, NPS, and CSAT feedback directly to ticketing workflows. Make sure this migration genuinely fits your operational needs before investing in the build.
Extraction: Getting Data Out of Zammad
Zammad's REST API is the primary extraction path. For self-hosted instances, direct database access is an option but introduces version-coupling risk.
API Endpoints
| Resource | Endpoint | Notes |
|---|---|---|
| Tickets | GET /api/v1/tickets?page={n}&per_page={n} |
Paginated; up to 100 per page by default |
| Ticket search | GET /api/v1/tickets/search?query=<filter>&expand=true&with_total_count=true |
Supports with_total_count for auditable pagination (docs.zammad.org) |
| Ticket articles | GET /api/v1/ticket_articles/by_ticket/{ticket_id} |
All articles for a ticket |
| Attachments | GET /api/v1/ticket_attachment/{ticket_id}/{article_id}/{attachment_id} |
Binary download per attachment |
| Users | GET /api/v1/users/{id} or GET /api/v1/users?expand=true |
Customers and agents |
| Organizations | GET /api/v1/organizations |
If you need org context |
| Groups | GET /api/v1/groups |
For mapping to SurveySparrow teams (docs.zammad.org) |
| Tags | GET /api/v1/tags?object=Ticket&o_id={ticket_id} |
Per-ticket tag retrieval |
| Priorities | GET /api/v1/ticket_priorities |
Reference data |
| States | GET /api/v1/ticket_states |
Reference data (docs.zammad.org) |
Authentication and Token Scope
Zammad supports HTTP Basic Auth, API token (Authorization: Token token={your_token}), and OAuth2. For migration scripts, API token auth is simplest. Use an admin-level token. Zammad API tokens inherit the permissions of the user they belong to — a non-admin token may silently skip tickets in groups the token owner cannot access, producing a subtly incomplete extraction with no error signal. There is no API response to distinguish "no tickets in this group" from "this token cannot see this group." Validate completeness by comparing total ticket counts from the Zammad admin panel against your extracted set.
Pagination and Limits
Zammad's list endpoints use page and per_page query parameters. The default page size is up to 100 records. A configurable server-side limit (commonly 500) caps per_page regardless of what you request — if you ask for per_page=1000 but the server limit is 500, you get 500. Zammad does not return a total count header on standard list endpoints. You must iterate until results are exhausted, or use the search endpoint with with_total_count=true for explicit counting.
Appending ?expand=true to ticket requests forces the API to return fully hydrated user and group objects alongside the ticket array, reducing follow-up calls.
# Fetch page 1, 100 tickets per page, with expanded relations
curl -H "Authorization: Token token=YOUR_TOKEN" \
"https://your-zammad.example.com/api/v1/tickets?page=1&per_page=100&expand=true"Paginate until you receive an empty array.
Extracting Articles and Attachments
For each ticket, fetch articles separately via GET /api/v1/ticket_articles/by_ticket/{ticket_id}?expand=true. This is where the bulk of your API calls live — 10,000 tickets averaging 5 articles each means 10,000 article-fetch calls on top of ticket pagination.
Each article object includes an attachments array with metadata (id, filename, size, content type). Downloading the actual file content requires an additional call per attachment. Attachment-heavy tickets can easily 3–5x your API call count. Plan your rate budget accordingly.
Zammad stores article attachment metadata in this structure:
{
"id": 42,
"article_id": 17,
"filename": "invoice.pdf",
"size": "204800",
"preferences": {
"Mime-Type": "application/pdf",
"Content-ID": "abc123@zammad"
}
}The Content-ID value in preferences is the CID reference used in inline images (see Inline Images section below). Capture it during extraction — you will need it during transform.
For self-hosted Zammad: If you control the server, consider direct database export (PostgreSQL/MySQL) plus filesystem access to /opt/zammad/storage/ for attachments. This bypasses API pagination limits and is dramatically faster for large datasets. Verify the storage backend your instance uses — Zammad supports local filesystem and S3.
Loading: Getting Data Into SurveySparrow
SurveySparrow's V3 API provides the loading endpoints.
| Operation | Endpoint | Method | Notes |
|---|---|---|---|
| Create contact | POST /v3/contacts |
JSON | Must exist before ticket creation if using requester_id (developers.surveysparrow.com) |
| Search contacts | GET /v3/contacts?email={email} |
JSON | Use for deduplication before creation |
| Create ticket | POST /v3/tickets |
multipart/form-data | Subject (≤200 chars), priority, status required (developers.surveysparrow.com) |
| Batch create tickets | POST /v3/tickets/batch |
JSON | Returns 202 + token; no attachment support (developers.surveysparrow.com) |
| Check batch status | GET /v3/tickets/batch/status/{token} |
JSON | Poll for completion |
| Create comment | POST /v3/tickets/{id}/comments |
JSON | Threaded comments on an existing ticket |
| List ticket fields | GET /v3/ticket-fields |
JSON | Returns field definitions including internal_name (developers.surveysparrow.com) |
| Create team | POST /v3/teams |
JSON | Map from Zammad groups |
Comment Creation Schema
The comment endpoint is the most frequently called operation in the entire migration. The request body for POST /v3/tickets/{id}/comments:
{
"body": "Comment text here",
"private": false
}Set "private": true to map Zammad's internal: true articles to agent-only notes. The body field accepts plain text; HTML rendering behavior varies by SurveySparrow plan and client — test this with your target account before assuming HTML is safe to pass through.
There is no author or created_at field in the comment create schema. Both are set server-side. Serialize original author identity and timestamp into the body field (see Article → Comment Mapping below).
Rate Limits
Rate limit numbers are not officially published by SurveySparrow for all plan tiers. Community reports indicate a baseline of approximately 120 calls per hour and 1,000 calls per day on lower-tier plans, but these numbers are unverified by official documentation and may be wrong for your account. Do not use them for migration planning without confirmation. Before starting any migration, contact SurveySparrow support with your estimated call volume and request written confirmation of your account's actual limits and any available temporary increases.
For a migration of any meaningful size, implement exponential backoff in your loading scripts to handle 429 Too Many Requests responses. See the Error Handling section for retry logic.
The Batch Endpoint Trade-Off
The POST /v3/tickets/batch endpoint accepts an array of ticket objects and returns a 202 Accepted response with a status token. This is significantly more efficient than individual creates. The catch: the batch endpoint does not support attachments, and comment history must still be posted through the comment endpoint ticket-by-ticket. (developers.surveysparrow.com)
Most teams use batch creation for ticket shells with clean metadata, then replay comments and upload files individually.
The batch payload structure:
{
"tickets": [
{
"subject": "Ticket subject here",
"description": "First message body",
"email": "customer@example.com",
"priority": 2,
"status": 1,
"assignee_id": 456,
"team_id": 12,
"custom_fields": {
"zammad_ticket_id": "12345",
"zammad_ticket_number": "67890"
}
}
]
}Contact Deduplication
What happens when you POST a contact with an email that already exists? SurveySparrow's POST /v3/contacts creates a duplicate rather than returning the existing record. To avoid duplicate contacts, search before you create:
GET /v3/contacts?email=customer@example.com
If the response contains results, extract the existing contact's id. If the response is empty, create the contact and capture the returned id. Store the mapping of source email → SurveySparrow contact ID in a local lookup table. This makes reruns safe: on a second run, the search returns the contact you created on the first run, and you skip the create call.
def get_or_create_contact(email, name):
"""Returns SurveySparrow contact ID, creating if absent."""
response = get(f"/v3/contacts?email={email}")
contacts = response.get("data", [])
if contacts:
return contacts[0]["id"]
create_response = post("/v3/contacts", json={"email": email, "name": name})
return create_response["data"]["id"]Attachment Restrictions
SurveySparrow only accepts these file types: PDF, PNG, JPEG, MP3, CSV, WAV. Maximum file size is 15 MB per file. Zammad places no such restriction — any file type, any size. (developers.surveysparrow.com)
If your Zammad tickets contain DOC, XLSX, ZIP, EML, or files larger than 15 MB, they will not import. Run a pre-migration audit to identify affected attachments.
Attachment handling decision tree:
| Attachment type | Action |
|---|---|
| PDF, PNG, JPEG, MP3, CSV, WAV and ≤15 MB | Upload directly via ticket create or comment |
| XLSX | Convert to CSV, then upload |
| DOC/DOCX | Convert to PDF, then upload |
| ZIP, EML, and other unsupported types | Upload to external storage (S3, Google Drive); append signed download URL to comment body |
| Any file >15 MB (regardless of type) | Upload to external storage; append signed download URL to comment body |
| Files where conversion is not feasible | Log in structured exception file; append note to ticket description |
Do not leave this decision until cutover weekend. Run the audit in Phase 1 and resolve the strategy before writing loading code.
Field Mapping: Zammad → SurveySparrow
Ticket Core Fields
| Zammad Field | SurveySparrow Field | Transform Notes |
|---|---|---|
title |
subject |
Truncate to 200 characters; store full title in description if needed |
First article body |
description |
Convert HTML to plain text or sanitized HTML as needed |
customer_id → User email |
email or requester_id |
Create/deduplicate contact first, then reference |
owner_id → Agent |
assignee_id |
Map Zammad agent IDs to SurveySparrow user IDs |
group_id → Group name |
team_id |
Map Zammad groups to SurveySparrow teams; fetch target teams with type=TICKET filter |
priority_id |
priority |
Map numeric IDs; Zammad default: 1=low, 2=normal, 3=high |
state_id |
status |
Map states; Zammad has more states than SurveySparrow |
tags |
Custom field | Serialize into a custom dropdown/multiselect field or append to description |
organization_id |
Custom field | Flatten into contact properties and/or a custom ticket field (admin-docs.zammad.org) |
created_at |
Server-set — cannot override | Prepend to description or store in custom field |
Use hidden source fields. SurveySparrow ticket fields expose a stable internal_name that does not change even if the field label is renamed. Use it to store zammad_ticket_id, zammad_ticket_number, and other reconciliation keys for idempotent reruns and post-migration validation. Retrieve field definitions including internal_name via GET /v3/ticket-fields. (developers.surveysparrow.com)
Article → Comment Mapping
Each Zammad article after the first becomes a SurveySparrow comment. You need to preserve:
- Who said it — Zammad articles have
from,sender_id(Customer, Agent, System), andcreated_by_id. SurveySparrow's comment create schema has no author field. Serialize the original author into the comment body. - When — Article
created_attimestamps. SurveySparrow setscreated_atserver-side on comments; you cannot backdate them. Prepend the original timestamp. - Visibility — Zammad's
internal: truemarks agent-only notes. Map to"private": truein the SurveySparrow comment body. If you cannot verify private comment support on your plan, prefix with[INTERNAL NOTE]to prevent accidental customer exposure. - Content type — Zammad articles can be
text/htmlortext/plain. Strip or convert HTML usinghtml2text(Python) orcheerio(Node.js) rather than regex. - Channel type — A
twitter direct-messageortelegram personal-messagearticle becomes a plain comment unless you preserve the channel type in serialized metadata. (docs.zammad.org)
A practical comment body template:
[2023-10-14 09:32 UTC] [Customer: jane.doe@example.com] [Channel: email]
Original message text goes here...For internal notes:
[2023-10-14 10:15 UTC] [Agent: john.smith@company.com] [INTERNAL NOTE]
Internal note text goes here...State Mapping
Zammad's default states are richer than SurveySparrow's. Both systems use configurable, instance-specific numeric IDs — do not hardcode mappings across systems. Fetch both sides first, then build mappings by business label and workflow meaning. (docs.zammad.org)
| Zammad State | Suggested SurveySparrow Status |
|---|---|
| new | Open |
| open | Open |
| pending reminder | Open (note in description) |
| pending close | Open (note in description) |
| closed | Closed |
| merged | Closed (note in description) |
SurveySparrow's configurable statuses let you create custom values. If you need Pending as a distinct status, create it in SurveySparrow before migration.
Migration Architecture
Decouple extraction, transformation, and loading into separate phases. Do not attempt all three in a single synchronous script.
Phase 1: Audit and Prepare the Target
Before moving data, inventory both systems:
- Count total tickets, articles per ticket, attachment count and types, custom fields in use, active tags in Zammad
- Run a query against your Zammad attachment store to identify file types and sizes that exceed SurveySparrow's restrictions
- Create teams (mapped from Zammad groups), custom ticket fields, priority values, status values, and agent accounts in SurveySparrow
- Identify tickets with subjects over 200 characters
- Map every Zammad agent who owns tickets to a corresponding SurveySparrow user
- Contact SurveySparrow support with your estimated total API call volume and request written confirmation of your rate limits and any available increase
Phase 2: Extract from Zammad
- Enumerate all tickets — Paginate through
GET /api/v1/tickets?page={n}&per_page=100&expand=true - Validate completeness — Compare extracted ticket count against the Zammad admin panel's total ticket count. A mismatch indicates a token scope gap or pagination error.
- Fetch articles per ticket —
GET /api/v1/ticket_articles/by_ticket/{ticket_id}?expand=true - Capture CID metadata — For each article's attachments, record the
Content-IDfrompreferencesalongside the attachment ID. You will need this to rewrite inline image references during transform. - Download attachments — For each article with attachments, fetch binary content via the attachment endpoint
- Extract reference data — Groups, priorities, states, tags, users, organizations
- Store locally — Write to structured JSON files or a staging database. Never transform in-memory during extraction.
Phase 3: Transform and Sanitize
This is where most migrations fail. Your middleware must:
- Build ID mapping tables — Zammad user ID → SurveySparrow contact ID, Zammad group → SurveySparrow team, priority and state mappings
- Create and deduplicate contacts — Use the
GET /v3/contacts?email={email}search before eachPOST /v3/contactsto avoid duplicates. Store the resulting SurveySparrow Contact IDs mapped against original Zammad User IDs. - Sanitize HTML — Zammad articles often contain complex HTML from inbound emails. Strip tracking pixels, malformed tags, and unsupported markup.
- Rewrite CID inline images — See the Inline Images section below for the full implementation path.
- Handle unsupported attachments — Follow the attachment decision tree above.
- Transform ticket fields — Map fields per the mapping tables. First article body becomes
description. Subsequent articles become comments with serialized metadata.
Phase 4: Load into SurveySparrow
- Batch-create ticket shells — Use
POST /v3/tickets/batchfor throughput on tickets without attachments - Poll batch status —
GET /v3/tickets/batch/status/{token}until complete - Map batch results — Record which Zammad ticket ID maps to which SurveySparrow ticket ID
- Add comments — For each ticket with multiple articles,
POST /v3/tickets/{id}/commentsfor articles 2+, in strict chronological order - Add attachments — For tickets with supported attachments, upload via individual API calls with
multipart/form-data
If a comment fails to post due to an API timeout, your script must pause, retry, and log the failure without skipping. Skipping breaks the conversation timeline.
# Pseudocode: Core migration loop
for zammad_ticket in extracted_tickets:
ss_ticket = {
"subject": zammad_ticket["title"][:200],
"description": build_description(zammad_ticket, articles[0]),
"email": get_customer_email(zammad_ticket["customer_id"]),
"priority": map_priority(zammad_ticket["priority_id"]),
"status": map_state(zammad_ticket["state_id"]),
"assignee_id": agent_map.get(zammad_ticket["owner_id"]),
"team_id": group_map.get(zammad_ticket["group_id"]),
"custom_fields": {
"zammad_ticket_id": str(zammad_ticket["id"]),
"zammad_ticket_number": str(zammad_ticket["number"])
}
}
batch_queue.append(ss_ticket)
# Batch create
response = post("/v3/tickets/batch", json={"tickets": batch_queue})
status_token = response["token"]
# Poll until complete
while not batch_complete(status_token):
sleep(5)
# Load batch results: map Zammad IDs → SurveySparrow IDs
for result in get_batch_results(status_token):
id_map[result["custom_fields"]["zammad_ticket_id"]] = result["id"]
# Add comments for multi-article tickets
for ticket_id, articles in ticket_articles_map.items():
ss_ticket_id = id_map[str(ticket_id)]
for article in sorted(articles[1:], key=lambda a: a["created_at"]):
comment_body = format_comment(article)
is_internal = article.get("internal", False)
post(
f"/v3/tickets/{ss_ticket_id}/comments",
json={"body": comment_body, "private": is_internal}
)Phase 5: Delta Migration and Cutover
When to use webhooks vs. polling for the delta phase:
- Use Zammad webhooks when your team can deploy and maintain a receiving endpoint and your migration window is short (days, not weeks). Webhooks fire on triggers and deliver ticket, article, user, organization, and group context in near real-time. Attachments are delivered as authenticated download links, not binary content — your webhook receiver must follow the link to download the file before the link expires. (admin-docs.zammad.org)
- Use polling when you cannot run a persistent endpoint or when the migration runs over weeks. Poll
GET /api/v1/tickets/search?query=updated_at:>{last_run_timestamp}&expand=trueon a schedule. Polling is simpler to implement, easier to pause and resume, and does not require infrastructure beyond your migration script.
For most migrations, polling is the right default. Use webhooks only if near-real-time delta capture is a firm requirement.
Cutover sequence:
- Run the main historical import while Zammad stays live
- Capture late updates via scheduled polling or webhook queue
- Freeze admin changes before the last delta so mappings do not shift underneath you
- Replay the final delta, switch inbound email, forms, and routing
- Keep Zammad read-only for a short validation window
- Verify — see the Validation section below
Error Handling
SurveySparrow API responses use standard HTTP status codes. Treatment by code:
| HTTP Status | Meaning | Action |
|---|---|---|
200 OK / 201 Created / 202 Accepted |
Success | Record ID mapping; continue |
400 Bad Request |
Malformed request payload | Do not retry. Log the payload, fix the data problem (e.g., subject over 200 chars, missing required field), reprocess. |
401 Unauthorized |
Invalid or expired API token | Stop migration. Reauthorize. |
403 Forbidden |
Token lacks permission for this operation | Stop migration. Check plan tier and token scopes. |
404 Not Found |
Referenced resource (ticket, contact, team) does not exist | Do not retry. Check ID mapping table; the resource may not have been created yet. Fix ordering. |
422 Unprocessable Entity |
Payload is valid JSON but fails business validation | Do not retry. Log the full response body — SurveySparrow returns validation error details. Fix the data. |
429 Too Many Requests |
Rate limit exceeded | Retry with exponential backoff. Respect Retry-After header if present; otherwise wait 60 seconds, then 120, then 240. |
500 Internal Server Error |
Server-side error | Retry up to 3 times with a 30-second delay. If it persists, log and skip; investigate after the batch completes. |
502 Bad Gateway / 504 Gateway Timeout |
Infrastructure timeout | Retry with exponential backoff, same as 429. |
The critical distinction: 400 and 422 errors will fail on every retry. Do not build a retry loop that keeps hammering a malformed payload — it burns your rate limit budget without progress. Route these to a separate error queue for human review.
Implement a structured error log with at minimum: timestamp, operation type, Zammad source ID, HTTP status, response body, and retry count.
Post-Migration Validation
Do not rely on "it finished without errors" as your validation signal. Run explicit checks after each phase.
Ticket Count Reconciliation
Compare record counts at the top level first:
# What you should validate
zammad_total = fetch_zammad_ticket_count() # From admin panel or search endpoint
ss_imported = fetch_ss_tickets_with_source_field() # GET /v3/tickets filtered by zammad_ticket_id present
if zammad_total != ss_imported:
missing = identify_missing_tickets(zammad_ids, ss_ids)
# missing is your re-run listArticle/Comment Count Per Ticket
For a random sample of 50–100 tickets, compare article count in Zammad against comment count in SurveySparrow:
for sample_ticket_id in random_sample:
zammad_article_count = len(fetch_zammad_articles(sample_ticket_id))
ss_comment_count = len(fetch_ss_comments(ss_id_map[sample_ticket_id]))
expected_ss_comments = zammad_article_count - 1 # first article → description
assert ss_comment_count == expected_ss_comments, f"Ticket {sample_ticket_id}: expected {expected_ss_comments}, got {ss_comment_count}"Field-Level Spot Checks
For each migration-critical field, verify at least 20 tickets:
subjectmatches (or is correctly truncated with full text in description)statusmaps to the correct SurveySparrow statusprioritymaps correctlyassignee_idpoints to the right agentteam_idpoints to the correct teamzammad_ticket_idcustom field contains the correct source IDrequester_idpoints to the correct contact
Attachment Audit
From your exception log, verify every unsupported attachment has either a download URL in the comment body or a documented exception entry. Do not consider the migration complete if attachment exceptions are unresolved.
Export-Based Final Check
SurveySparrow supports ticket export in XLSX and JSON format via Settings → Ticket Management → Export Data. Export all tickets after migration and run a row count against your Zammad extraction count. This provides an independent validation path outside the API. (support.surveysparrow.com)
Edge Cases and Failure Modes
Timestamp Preservation
SurveySparrow sets created_at and updated_at server-side when tickets and comments are created. You cannot import historical records with their original timestamps through the documented public API. This is a hard limitation. Reporting and SLA calculations in SurveySparrow will reflect migration-time dates, not historical ones. The workaround — prepending original timestamps into the description or storing them in a custom field — preserves the metadata for human readers but does not fix system-level reporting. (developers.surveysparrow.com)
Subject Length
SurveySparrow enforces a 200-character limit on ticket subjects. Zammad has no such limit. Long titles will be rejected with a 400 or 422 error. Truncate proactively to 200 characters and store the full original title in the description field.
Inline Images (CID)
Zammad handles inline images in emails using Content-ID (CID) tags. Article bodies contain HTML like <img src="cid:abc123@zammad">. The mapping between CID and the actual attachment lives in the article's attachments array, specifically in preferences ["Content-ID"].
Full implementation path:
- During extraction, build a lookup:
{cid_value: attachment_id}for each article - During transform, parse the article HTML and find all
<img src="cid:...">tags - For each CID match, fetch the corresponding binary attachment from Zammad
- Upload the attachment to SurveySparrow (if the file type is supported) or to external storage
- Rewrite the
srcattribute to the new URL - If no match is found for a CID reference, replace the tag with
[Image: attachment not migrated]
If you push raw CID-referenced HTML to SurveySparrow without rewriting, images appear as broken references. This is silent data corruption — no API error will tell you it happened.
Internal Notes
Zammad articles marked internal: true are agent-only. Map these to SurveySparrow comments with "private": true. If your plan or API version does not support the private flag on comments, you have three options:
- Skip internal notes entirely — Safest from a customer-visibility standpoint; loses historical agent context
- Import with
[INTERNAL NOTE]prefix — Preserves content; risk that the note becomes customer-visible if private comments are not supported - Store in a separate custom text field on the ticket — Works only if internal notes are short and infrequent
Most teams choose option 2 and verify comment visibility settings before going live.
Merged Tickets
Zammad supports ticket merging. Merged tickets have a merged state and a link to the target ticket via ticket_article type ticket merge. SurveySparrow has no merge concept. Import the merged ticket as a standalone closed ticket with a note in the description referencing the target ticket's Zammad ID, or skip it entirely. If you skip, ensure the target ticket's article history includes the merged content by fetching the merged ticket's articles and appending them to the target.
HTML Content
Zammad articles frequently contain HTML (content_type: text/html). Test how SurveySparrow renders HTML in ticket descriptions and comments on your specific account and plan — rendering support is not uniform across plans and clients. Use a purpose-built library (html2text in Python, cheerio in Node.js) rather than regex to strip or convert HTML.
Tags
Zammad's tag system has no SurveySparrow equivalent on tickets. If tags carry operational meaning (product categories, escalation flags), create a custom dropdown or multiselect field in SurveySparrow before migration and map tag values there. If tags are informal, append them to the description. Do not silently drop tags without documenting which tickets had them.
API Payload Size and Timeouts
If a Zammad ticket has 150+ replies, posting all comments in a tight sequence can cause 502 or 504 errors. Process comments in batches of 20–30, respect rate limits, and use the error handling table above to distinguish retriable timeouts from data errors.
Time and Cost Estimation
Based on patterns across similar migrations:
| Dataset Size | Estimated Timeline | Primary Bottleneck |
|---|---|---|
| < 1,000 tickets, few attachments | 2–4 days | Script development and testing |
| 1,000–10,000 tickets | 1–2 weeks | SurveySparrow rate limits; comment loading |
| 10,000–50,000 tickets | 2–4 weeks | Rate limits; attachment handling; QA |
| 50,000+ tickets | 4–8 weeks | Rate limit negotiation; phased loading; validation |
The biggest time sink is comment loading. If your average ticket has 6 articles, a 10,000-ticket migration means ~50,000 comment API calls. At a hypothetical baseline of 120 calls/hour (unverified — confirm your actual limit before planning), that is over 17 days of comment loading alone. At 1,000 calls/hour (a reasonable enterprise limit), the same volume takes ~2 days. The difference between these two scenarios is entirely determined by your negotiated rate limit — confirm it before scoping.
Pre-Migration Checklist
- Audit Zammad data — Total tickets, articles per ticket, attachment count and types, custom fields in use, active tags
- Identify token scope gaps — Confirm your extraction token is admin-level; validate extracted count against admin panel total
- Confirm SurveySparrow plan — Verify API access is enabled; confirm actual rate limits in writing from SurveySparrow support
- Create SurveySparrow structure — Teams, custom fields (including
zammad_ticket_idandzammad_ticket_number), status values, priority values before migration - Map agents — Every Zammad agent who owns tickets needs a corresponding SurveySparrow user
- Build contact deduplication logic — Implement
GET /v3/contacts?email={email}search before every contact create - Decide on attachment strategy — Audit unsupported formats using the decision tree; resolve convert, external-store, or skip before writing loading code
- Decide on internal note handling — Skip, prefix
[INTERNAL NOTE], or private comment; verify private comment support on your plan - Decide on CID inline image handling — Build the CID lookup and rewrite pipeline, or document which tickets will have broken images
- Request rate limit increase — Contact SurveySparrow support with your estimated total call volume
- Run a pilot — Migrate 50–100 tickets first, validate every field against the validation methodology above, then proceed
- Plan cutover — Choose webhook or polling for delta; decide on freeze window; keep Zammad read-only for validation period
When to Skip This Migration
Not every migration should happen. Reconsider if:
- You need timestamp-accurate historical reporting. SurveySparrow cannot preserve original
created_atdates. If compliance or audit requirements mandate accurate historical timestamps on records, this platform will not satisfy them. - You rely on multi-channel article types. Zammad's email, phone, Twitter, Facebook, and Telegram article types carry channel-specific metadata that SurveySparrow's flat comment model cannot represent.
- Your attachment ecosystem is diverse. If tickets frequently contain DOC, XLSX, ZIP, EML, or files over 15 MB, you will lose meaningful attachment coverage without significant conversion work.
- You have 100,000+ tickets and need them all in SurveySparrow. Without a substantial rate limit increase, very large migrations become multi-month projects. Evaluate whether historical data truly needs to live in SurveySparrow or if an archive-and-start-fresh approach is more practical.
- Your team depends on Zammad's SLA engine for reporting. SurveySparrow's SLA tracking calculates from the migration-time
created_at, not the original ticket date. Historical SLA compliance reports will be wrong.
What ClonePartner Handles
For this migration path, we handle the full pipeline: extraction scripting against your Zammad instance (API or direct DB for self-hosted), field mapping and transform logic, SurveySparrow rate limit negotiation, attachment conversion and external hosting, CID inline image rewriting, comment threading with metadata preservation, delta cutover, and post-migration validation with ticket-level diff reports.
Frequently Asked Questions
- Is there a native migration tool from Zammad to SurveySparrow?
- No. Zammad's migration tools handle inbound moves from other platforms into Zammad. SurveySparrow's migration app targets surveys, not ticket history. You must extract data via Zammad's REST API and load it through SurveySparrow's V3 Tickets API using custom scripts.
- Can I preserve original ticket timestamps when migrating to SurveySparrow?
- No. SurveySparrow sets created_at and updated_at server-side when tickets and comments are created via API. You cannot backdate records through the documented public API. The workaround is to prepend original Zammad timestamps into the ticket description or store them in a custom field.
- What file types does SurveySparrow accept for ticket attachments?
- SurveySparrow only accepts PDF, PNG, JPEG, MP3, CSV, and WAV files, with a maximum size of 15 MB per file. Zammad has no such restriction. Unsupported files must be converted, stored externally with a download link, or documented as exceptions.
- How do Zammad ticket articles map to SurveySparrow?
- The first Zammad article typically becomes the SurveySparrow ticket description. Subsequent articles are loaded as comments. Rich metadata like sender, CC, channel type, and internal visibility must be serialized into the comment body since SurveySparrow's flat comment model has no direct equivalent for these fields.
- What are SurveySparrow's API rate limits for ticket migration?
- Community reports indicate a baseline of 120 API calls per hour and 1,000 per day on lower-tier plans. Business and Enterprise plans have higher limits. For any migration over a few hundred tickets, contact SurveySparrow support to request a temporary rate limit increase before starting.


