Ticket Endpoint
Tickets
Create, update, export, and delete support tickets. Includes status history, admin assignments, linked CRM appointments, file attachments, and editing claims.
GET /Export/JSON/tickets
GET /Export/JSON/ticket/{id}
POST /Import/JSON/tickets
PUT /Import/JSON/tickets
PUT /Import/JSON/ticket_claims
DELETE /Delete/JSON/ticket/{id}Endpoints
| Method | Endpoint | Scope | Description |
|---|---|---|---|
| GET | /Export/JSON/tickets | tickets:read | Retrieve all tickets (paginated) |
| GET | /Export/JSON/ticket/{id} | tickets:read | Single ticket with status history and associated appointments |
| GET | /Export/JSON/ticket_attachments/{id} | tickets:read | List file attachments for a ticket |
| GET | /Export/JSON/ticket_attachment/{id}?file=name | tickets:read | Download a single file as a binary |
| POST | /Import/JSON/tickets | tickets:write | Create a new ticket (INSERT) — optionally with a comment and file attachments |
| PUT | /Import/JSON/tickets | tickets:write | Update an existing ticket (UPDATE) — optionally with a comment, public reply, and file attachments |
| PUT | /Import/JSON/ticket_claims | tickets:write | Set, extend, take over from another administrator, or end an editing claim NEW v2.118.0 |
| DELETE | /Delete/JSON/ticket/{id} | tickets:delete | Delete a ticket using cascade delete |
Export: Query Parameters
| Parameter | Description |
|---|---|
?status={id} | Filter by status ID |
?priority={id} | Filter by priority ID |
?department={id} | Filter by department ID |
?customer_id={id} | Tickets for a specific customer |
?admin_id={id} | Tickets assigned to an admin |
?type={type} | Filter by ticket type |
?date_from=&date_to= | Date range filter |
?last_modified=YYYY-MM-DD | Last modified on |
?language=de | Language filter |
?limit=50&offset=0 | Pagination (Default: 50, Max: 250) |
Export: Response Fields
| Field | Type | Description |
|---|---|---|
ticket_id | integer | Ticket ID |
ticket_link_id | string | 14-character alphanumeric link ID |
ticket_subject | string | Ticket subject |
ticket_status | string | Current status |
ticket_priority | string | Priority |
department_id | integer | Department ID |
department_name | string | Department Name |
status_history[] | array | Complete conversation with comments |
assigned_admins[] | array | Assigned administrators |
followers[] | array | Followers of the ticket |
linked_appointments[] | array | NEW v2.11.0: Linked CRM appointments (ID, Start, End, Title, Location, Status) |
claim | object|null | NEW v2.118.0: Active handling claim with admin_id, admin_name, label, claimed_at, expires_at, and expires_in_seconds; null if no one has claimed the ticket |
edit_lock | object|null | NEW v2.118.0: The ticket is currently open in the backend: admin_id, admin_name, locked_at (valid for 300 seconds from the last activity on the form); otherwise null |
Import POST: Create Ticket
Required and optional fields when creating a new ticket.
Required fields
ticket_subject— Subjectticket_customers_email— Customer emailticket_customers_name— Customer Name
Optional fields
ticket_type— Ticket type (default: customer)ticket_status_id— Status ID (validated)ticket_priority_id— Priority ID (validated)ticket_department_id— Department ID (validated)ticket_customers_id— Customer IDticket_language_id— Language IDticket_comments— First comment as text (always internal)admin_id— Admin ID for the comment (0 = System)attachments[]— NEW v2.60.0: File attachments (name+content_base64); requiresticket_commentsassigned_admins[]— Array of admin IDs
Example: Create a ticket
POST /Import/JSON/tickets
Authorization: Bearer {token}
Content-Type: application/json
{
"type": "tickets",
"data": [{
"ticket_subject": "API-Test-Ticket",
"ticket_customers_email": "kunde@example.com",
"ticket_customers_name": "Max Mustermann",
"ticket_priority_id": 2,
"ticket_department_id": 1,
"ticket_comments": "Erstellt via xoPort API",
"admin_id": 0,
"attachments": [
{ "name": "bestellung.pdf", "content_base64": "JVBERi0xLjQKJcfs…" } ],
"assigned_admins": [1, 3]
}]
}Import PUT: Update Ticket
Editable fields and comment system.
ticket_internal_comment=1). Since v2.25.0, a reply visible to the customer requires the explicit flag is_public_reply: true and a valid admin_id > 0; and, for email delivery, additionally `notify_customer: true`. Both flags are strictly checked for Boolean values — "true" or 1 do not count.| Field | Type | Description |
|---|---|---|
ticket_id | integer | Required. ID of the ticket to be updated |
ticket_subject | string | Change subject |
ticket_type | string | Change Type |
ticket_status_id | integer | Change status (validated) |
ticket_priority_id | integer | Change priority (validated) |
ticket_department_id | integer | Change department (validated) |
ticket_date_hide_until | datetime | Hide ticket until |
ticket_login_required | 0/1 | Login required |
ticket_allow_learning | 0/1 | Allow AI learning |
ticket_comments | string | Comment text — creates a new history entry |
admin_id | integer | Author of the comment (0 = system; must be > 0 for a public reply) |
is_public_reply | boolean | v2.25.0: true = reply visible to the customer instead of an internal note |
notify_customer | boolean | v2.25.0: true = Email to the customer (only when used with is_public_reply) |
append_signature | boolean | v2.27.0: Append the admin's ticket signature to a public reply (default is on; opt-out only by setting to false) |
attachments[] | array | NEW v2.60.0: File attachments (name + content_base64); requires ticket_comments |
assigned_admins[] | array | Replace admin assignments |
Comment format
"ticket_comments": "Internal note via API",
"admin_id": 1`ticket_comments ` is the comment text; `admin_id ` is its author (0 = system, >0 = admin ID; this is validated).
Example: Update a ticket with a comment
PUT /Import/JSON/tickets
Authorization: Bearer {token}
Content-Type: application/json
{
"type": "tickets",
"data": [{
"ticket_id": 42,
"ticket_status_id": 3,
"ticket_priority_id": 1,
"ticket_comments": "Status auf Erledigt gesetzt",
"admin_id": 1,
"assigned_admins": [1, 5]
}]
}Delete: Cascading Deletion
When a ticket is deleted, all associated data is automatically removed:
Cascade order
ticket_status_history— Comments/Historyticket_to_admins— Admin assignmentsticket_to_followers— Followersxocrm_appointments_relationships— Appointment links (appointments remain intact)xocrm_followers— CRM followers- Files on disk —
upload/ticket/{link_id}/ ticket_ticket— The ticket itself
Example
DELETE /Delete/JSON/ticket/42
Authorization: Bearer {token}Response
{
"success": true,
"deleted_id": 42,
"details": {
"ticket_id": 42,
"ticket_subject": "Gelöschtes Ticket",
"ticket_link_id": "8i1c5O8g7w8b5c"
}
}Linked CRM Appointments v2.11.0
In the export, each ticket contains a ` linked_appointments[] ` array with linked CRM appointments:
"linked_appointments": [
{
"appointment_id": 15,
"startdate": "2026-04-14 10:00:00",
"enddate": "2026-04-14 11:00:00",
"allday": 0,
"done": 0,
"status_id": 1,
"title": "Rückruf Kunde",
"description": "Bezüglich offener Rückfrage",
"location": ""
}
]Appointments are linked via ` xocrm_appointments_relationships ` (target_type = ticket_id). Deleting a ticket only removes the link, not the appointment itself.
Fixed in v2.118.1: linked_appointments to POST/PUT /Import/JSON/tickets also triggered the warning " UNKNOWN_FIELD," even though the appointments were created. The warning has been removed; processing remains unchanged.
Attachment Endpoints
File attachments are addressed via the ticket ID:
Listing: GET /ticket_attachments/{id}
{
"success": true,
"ticket_id": 42,
"ticket_link_id": "8i1c5O8g7w8b5c",
"files": [
{
"name": "screenshot.png",
"size": 245760,
"mime_type": "image/png",
"last_modified": "2026-03-15 14:30:22"
}
]
}Download: GET /ticket_attachment/{id}?file=screenshot.png
Binary download with correct Content-Type and Content-Disposition: attachment headers. Path traversal protection via basename() + realpath() validation.
Upload: attachments[] via POST/PUT NEW v2.60.0
Up to v2.59.0, attachments could only be read —they were added exclusively via the backend form. A ticket created via the API would therefore have no supporting documents (PDF, .eml, screenshot). Starting with v2.60.0,POST/PUT /Import/JSON/tickets accepts the optional attachments[] field:
"ticket_comments": "Documentation for the complaint",
"admin_id": 1,
"attachments": [
{
"name": "rechnung.pdf",
"content_base64": "JVBERi0xLjQKJcfs…"
}
]`ticket_comments` in the same request, no entry is created to which the file could be attached—it is then rejected with an error message instead of being silently discarded.| Field | Type | Description |
|---|---|---|
name | string | Required. File name including extension; the extension must be on the allowlist (same rule as for backend uploads) |
content_base64 | string | Required. File content encoded in Base64; invalid Base64 is rejected and not saved |
Storage uses the same mechanism as the backend upload—same extension allowlist, naming convention, and hash-based deduplication to prevent duplicate storage. The actual file name assigned for each record is listed in the response under `attachments`. The upper limit per file corresponds to the ` /media` endpoint(XOPORT_MEDIA_MAX_FILESIZE, default 10 MB).
Invalid entries (unallowed file extension, invalid Base64, size exceeded) generate an error for each file; the ticket update itself remains successful. If ` is_public_reply: true ` is set along with ` notify_customer: true `, the files are sent with the customer email—they are written before the email is sent.
Processing Claim NEW v2.118.0
A claim indicates that someone is currently working on a ticket, for example, an AI agent acting on behalf of an employee. Other agents see this claim in the export and in the backend (ticket list and ticket view) and do not start working on the ticket simultaneously. The claim is a notification, not a lock: it is not enforced by write access checks, and the ticket remains editable for everyone.
PUT /Import/JSON/ticket_claims
{
"type": "ticket_claims",
"data": [
{ "ticket_id": 33999, "admin_id": 12, "state": "claimed", "ttl_minutes": 60, "label": "KI-Agent" }
]
}| Field | Type | Required | Description |
|---|---|---|---|
ticket_id | integer | Yes | ID of an existing ticket |
admin_id | integer | yes | Existing administrator on whose behalf the work is being performed. A claim always refers to one person. |
state | string | yes | claimed Sets or renews the claim; ` released ` terminates it. There is no default value: The endpoint sets a state and never toggles. |
ttl_minutes | integer | no | Duration in minutes; default is 60; limited to 5 through 240. Extending a claim never shortens a running claim. |
label | string | no | Source label, e.g., KI-Agent; maximum 32 characters; HTML is removed. The backend displays “Name (Label).” |
force | boolean | no | JSON only—true/false. true takes over or releases another administrator’s claim. |
Result per record
records[].status refers to the result; records[].claim refers to the status after the call:
| status | Meaning |
|---|---|
claimed | New claim created |
refreshed | Own claim extended; claimed_at remains unchanged |
taken_over | Claim from another administrator taken over with force: true; previous_claim contains the previous one |
released | Claim terminated; previous_claim contains the previous |
unchanged | state: released Sent, but no active claim |
conflict | Another administrator holds the claim. Nothing is written; claim specifies the holder, and errors[] contains the explanation. The data record is classified as failed. |
Response (HTTP 200) when another administrator makes a request while Max Mustermann holds the claim:
{
"success": false,
"outcome": "failed",
"api_version": "2.118.1",
"type": "ticket_claims",
"stats": { "total": 1, "claimed": 0, "refreshed": 0, "taken_over": 0, "released": 0, "unchanged": 0, "failed": 1 },
"errors": [
{ "record": 1, "error": "Ticket 33999 is claimed by Max Mustermann (admin_id 12) until 2026-09-17 14:05:10. Send force=true to take it over." }
],
"timestamp": "2026-09-17 13:20:02",
"records": [
{
"index": 1,
"ticket_id": 33999,
"status": "conflict",
"claim": {
"admin_id": 12,
"admin_name": "Max Mustermann",
"label": "KI-Agent",
"claimed_at": "2026-09-17 13:05:10",
"expires_at": "2026-09-17 14:05:10",
"expires_in_seconds": 2708
}
}
]
}Automatic Release
- Public response to the customer, via API (
PUT /Import/JSON/tickets) or in the backend. It doesn’t matter who responds. - Change to a “Closed” status. Saving a ticket that is already closed does not release a claim.
- Expiration of the validity period.
An internal note does not release the claim. Setting and releasing do not change ticket_date_last_modified or the history and do not trigger any events; sorting and delta export (?last_modified=) remain unaffected. There is at most one claim per ticket: Of two simultaneous requests, exactly one receives conflict.
claim belonging to another administrator or a edit_lock, someone else is currently working on the ticket. Coordinate with them before sending force: true.Starting with Shop 4.9.73: If the ticket_claims table is still missing, the endpoint responds with an HTTP 503 TICKET_CLAIMS_UNAVAILABLE; the Shop creates it during the next schema synchronization (when opening an xoCRM page or the HealthCheck).
Scopes
| Scope | Description |
|---|---|
tickets:read | Export tickets and attachments |
tickets:write | Create and update tickets, write comments and file attachments, set and close editing claims |
tickets:delete | Delete tickets (cascading) |
Manage tickets via the API
Create an OAuth2 client with the tickets:read, tickets:write, and tickets:delete scopes.