Ticket Endpoint

v2.118.1 · Full CRUD · Tickets, Attachments & Claims

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

MethodEndpointScopeDescription
GET/Export/JSON/ticketstickets:readRetrieve all tickets (paginated)
GET/Export/JSON/ticket/{id}tickets:readSingle ticket with status history and associated appointments
GET/Export/JSON/ticket_attachments/{id}tickets:readList file attachments for a ticket
GET/Export/JSON/ticket_attachment/{id}?file=nametickets:readDownload a single file as a binary
POST/Import/JSON/ticketstickets:writeCreate a new ticket (INSERT) — optionally with a comment and file attachments
PUT/Import/JSON/ticketstickets:writeUpdate an existing ticket (UPDATE) — optionally with a comment, public reply, and file attachments
PUT/Import/JSON/ticket_claimstickets:writeSet, extend, take over from another administrator, or end an editing claim NEW v2.118.0
DELETE/Delete/JSON/ticket/{id}tickets:deleteDelete a ticket using cascade delete

Export: Query Parameters

ParameterDescription
?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-DDLast modified on
?language=deLanguage filter
?limit=50&offset=0Pagination (Default: 50, Max: 250)

Export: Response Fields

FieldTypeDescription
ticket_idintegerTicket ID
ticket_link_idstring14-character alphanumeric link ID
ticket_subjectstringTicket subject
ticket_statusstringCurrent status
ticket_prioritystringPriority
department_idintegerDepartment ID
department_namestringDepartment Name
status_history[]arrayComplete conversation with comments
assigned_admins[]arrayAssigned administrators
followers[]arrayFollowers of the ticket
linked_appointments[]arrayNEW v2.11.0: Linked CRM appointments (ID, Start, End, Title, Location, Status)
claimobject|nullNEW 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_lockobject|nullNEW 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 — Subject
  • ticket_customers_email — Customer email
  • ticket_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 ID
  • ticket_language_id — Language ID
  • ticket_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); requires ticket_comments
  • assigned_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.

Important: Comments are internal by default (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.
FieldTypeDescription
ticket_idintegerRequired. ID of the ticket to be updated
ticket_subjectstringChange subject
ticket_typestringChange Type
ticket_status_idintegerChange status (validated)
ticket_priority_idintegerChange priority (validated)
ticket_department_idintegerChange department (validated)
ticket_date_hide_untildatetimeHide ticket until
ticket_login_required0/1Login required
ticket_allow_learning0/1Allow AI learning
ticket_commentsstringComment text — creates a new history entry
admin_idintegerAuthor of the comment (0 = system; must be > 0 for a public reply)
is_public_replybooleanv2.25.0: true = reply visible to the customer instead of an internal note
notify_customerbooleanv2.25.0: true = Email to the customer (only when used with is_public_reply)
append_signaturebooleanv2.27.0: Append the admin's ticket signature to a public reply (default is on; opt-out only by setting to false)
attachments[]arrayNEW v2.60.0: File attachments (name + content_base64); requires ticket_comments
assigned_admins[]arrayReplace 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

  1. ticket_status_history — Comments/History
  2. ticket_to_admins — Admin assignments
  3. ticket_to_followers — Followers
  4. xocrm_appointments_relationships — Appointment links (appointments remain intact)
  5. xocrm_followers — CRM followers
  6. Files on disk — upload/ticket/{link_id}/
  7. 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…"
  } 
]
Attachments are linked to a history entry, not to the ticket. Without `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.
FieldTypeDescription
namestringRequired. File name including extension; the extension must be on the allowlist (same rule as for backend uploads)
content_base64stringRequired. 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" }
  ]
}
FieldTypeRequiredDescription
ticket_idintegerYesID of an existing ticket
admin_idintegeryesExisting administrator on whose behalf the work is being performed. A claim always refers to one person.
statestringyesclaimed Sets or renews the claim; ` released ` terminates it. There is no default value: The endpoint sets a state and never toggles.
ttl_minutesintegernoDuration in minutes; default is 60; limited to 5 through 240. Extending a claim never shortens a running claim.
labelstringnoSource label, e.g., KI-Agent; maximum 32 characters; HTML is removed. The backend displays “Name (Label).”
forcebooleannoJSON 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:

statusMeaning
claimedNew claim created
refreshedOwn claim extended; claimed_at remains unchanged
taken_overClaim from another administrator taken over with force: true; previous_claim contains the previous one
releasedClaim terminated; previous_claim contains the previous
unchangedstate: released Sent, but no active claim
conflictAnother 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.

Check before processing: If the export specifies a 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

ScopeDescription
tickets:readExport tickets and attachments
tickets:writeCreate and update tickets, write comments and file attachments, set and close editing claims
tickets:deleteDelete tickets (cascading)

Manage tickets via the API

Create an OAuth2 client with the tickets:read, tickets:write, and tickets:delete scopes.

Create an OAuth2 client