Ticket operations API
This article and the rest of the API documentation in this section are written for a technical audience — integrators and developers connecting external systems to Tickiti. Familiarity with HTTP, REST, JSON and bearer-token authentication is assumed.
The ticket operations endpoints restructure tickets the way the ticket page does: delete and restore, merge, link, move or copy responses between tickets, split responses off into a new ticket, change the originator, and remove a single attachment. Alongside them sit the lookups a ticket-scoped token needs — staff addresses, resolution categories, the search vocabulary — and an endpoint that tells any token what it can do.
Every write runs through the same code as the ticket page, so the result is exactly what a staff member would get doing the same thing by hand, including the audit lines on the ticket thread. All endpoints are POST, take and return JSON, and use the response envelope described in the API overview.
Endpoints
| Method & path | Ability | Purpose |
|---|---|---|
| POST /api/v1/tickets/delete | tickets:write | Delete tickets by number or by sender |
| POST /api/v1/tickets/restore | tickets:write | Restore a deleted ticket |
| POST /api/v1/tickets/merge | tickets:write | Merge two tickets |
| POST /api/v1/tickets/link | tickets:write | Link tickets |
| POST /api/v1/tickets/unlink | tickets:write | Remove a link |
| POST /api/v1/tickets/links | tickets:read | List a ticket’s linked tickets |
| POST /api/v1/tickets/response | tickets:read | Read one response by id |
| POST /api/v1/tickets/response-move | tickets:write | Move responses to another ticket |
| POST /api/v1/tickets/response-copy | tickets:write | Copy responses to another ticket |
| POST /api/v1/tickets/split | tickets:write | Split responses off into a new linked ticket |
| POST /api/v1/tickets/originator-update | tickets:write | Change who raised the ticket |
| POST /api/v1/tickets/attachment-delete | tickets:write | Remove one attachment |
| POST /api/v1/tickets/response-update | tickets:write | Edit a response, change its visibility, or publish it |
| POST /api/v1/tickets/response-delete | tickets:write | Delete a response |
| POST /api/v1/tickets/staff | tickets:read | Staff who can be participants or assignees |
| POST /api/v1/tickets/resolutions | tickets:read | Resolution categories |
| POST /api/v1/tickets/search-modes | tickets:read | The ticket-query search vocabulary |
| POST /api/v1/token | any token | What the calling token can do |
Write implies read: a token holding tickets:write can call every endpoint above. None of these endpoints takes an Idempotency-Key.
Who may do what
A call runs as the token’s owner, and every ticket a request touches is checked against that owner with the same rules as the ticket page. There are two levels:
- Seeing a ticket — the owner is its assignee (directly, or as a member of the assigned team), one of its participants, or has access to its queue. Linking, listing links, reading a response and copying responses off a ticket need this.
- Managing a ticket — the owner can see the ticket and is an admin or holds Manage on its queue (see Queue permissions). Deleting, restoring, merging, moving responses, receiving moved or copied responses, changing the originator and removing an attachment need this.
Integration identities act on tickets in every queue without these per-ticket checks: the System user (the default owner on Administration → API keys), accounts marked API user, sysadmins and superusers. POST /api/v1/token reports which kind of owner a token has.
Every check runs before anything is written. If any ticket in a request fails, the whole request is refused with 403 forbidden and nothing changes.
Delete tickets
POST /api/v1/tickets/deleteDeletes tickets as Delete in the ticket list does: each ticket’s responses are deleted with it, the ticket leaves every ticket list, and it can be brought back with tickets/restore. Select tickets by number, or every ticket raised by one address — for example, clearing out a run of newsletters from one sender in a single call.
Body:
ticket_numbers— array of ticket numbers, up to 1,000.originator_email— select the tickets raised by this address. Giveticket_numbers,originator_email, or both (a ticket then has to match both).queue— optional queue name; only tickets in that queue are selected.dry_run— boolean. When true, Tickiti returns the tickets the call would delete and deletes nothing.
The operation is all-or-nothing. A selection of more than 1,000 tickets is refused (422, narrow it); every number in ticket_numbers must exist (404 names the missing ones); the owner must manage every selected ticket (403); and a ticket locked by another staff member stops the whole batch (422 refused). Permission checks apply to a dry run too, so a dry run that succeeds predicts the real call.
curl -X POST https://support.sole-provider.example/api/v1/tickets/delete \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "originator_email": "[email protected]", "queue": "Inbox", "dry_run": true }'{
"ok": true,
"data": {
"dry_run": true,
"count": 2,
"tickets": [
{ "ticket_number": "220141", "subject": "Weekend flash sale", "status": "open", "queue": "Inbox" },
{ "ticket_number": "220158", "subject": "Last chance: 40% off", "status": "open", "queue": "Inbox" }
]
}
}Without dry_run the response is { "deleted": 2, "tickets": [ ... ], "restorable": true }, listing the same fields for each ticket deleted.
Restore a ticket
POST /api/v1/tickets/restoreBody: ticket_number. Restores a deleted ticket together with the responses that were deleted with it; a response that was deleted on its own beforehand stays deleted. The thread records who restored the ticket. The owner must manage the ticket.
Response: { "ticket_number": "220141", "status": "open" }. A ticket that is not deleted is refused (422 refused), and so is a ticket that was merged into another — its responses live on the surviving ticket.
Merge two tickets
POST /api/v1/tickets/mergeBody: ticket_number and other_ticket_number (two different tickets, in either order). The owner must manage both.
- The older ticket (by creation time) survives, whichever order you give them in.
- The newer ticket’s participants are added to the survivor, and each of its responses is copied across with its attachments.
- The newer ticket is marked as merged into the survivor and deleted. The survivor’s thread records the merge.
- An on-hold survivor is reopened, its on-hold timer cleared, and its assignee notified (each member, when it is assigned to a team).
Response: { "surviving_ticket_number": "220009", "merged_ticket_number": "220077" }.
Link, unlink and list links
POST /api/v1/tickets/link
POST /api/v1/tickets/unlink
POST /api/v1/tickets/linksThese manage the ticket page’s Linked panel (see Ticket links). Each takes the ticket as ticket_number or ticket_id.
- link —
linked_ticket_numbers: array of 1 to 100 ticket numbers to link to the ticket. Links are two-way; linking a pair that is already linked changes nothing. The owner must be able to see the ticket and every ticket being linked to it. Response:newly_linked(how many links were added) andlinked_tickets. - unlink —
linked_ticket_number: the ticket to unlink. The owner must be able to see the ticket named inticket_number/ticket_id. Clone links — made by cloning or bytickets/split— are permanent, and unlinking two tickets that are not linked is refused (422 refusedin both cases). Response:linked_tickets. - links — reads the panel (
tickets:read, staff account). Response:ticket_numberandlinked_tickets.
Each entry in linked_tickets carries link_id, ticket_id, ticket_number, subject, status, link_type (manual or clone) and clone_direction — to when this ticket is the source of the clone, from when it is the clone, null for a manual link. Deleted tickets are left out, as in the panel.
Read one response
POST /api/v1/tickets/responseReads a single response by its id alone, together with the ticket it belongs to — useful when a webhook, report or search hands you a response id. Ability tickets:read; the owner must be a staff account (or an integration identity) that can see the ticket.
response_id— required, integer.max_body_chars— optional; truncateshtmlandplain_textto this many characters.
Deleted responses, and responses on deleted tickets, are returned too, with deleted set.
{
"ok": true,
"data": {
"response_id": "90412",
"ticket_id": "1187",
"ticket_number": "220009",
"ticket_subject": "Carbon plate cracked on first marathon",
"is_internal": false,
"staff_response": true,
"created_by_email": "[email protected]",
"created_at": "2026-09-14T09:12:44+00:00",
"updated_at": "2026-09-14T09:12:44+00:00",
"deleted": false,
"audit": null,
"other_changes": null,
"html": "<p>A replacement pair is on its way.</p>",
"plain_text": "A replacement pair is on its way.",
"attachments": []
}
}audit lists the ticket changes the response recorded (status, priority, assignee, queue, participants, subject and so on), or is null. Attachments have the same shape as in the List responses API.
Move and copy responses
POST /api/v1/tickets/response-move
POST /api/v1/tickets/response-copyBody: response_ids (array of 1 to 500 response ids, from one ticket or several) and target_ticket_number. Each response is re-created on the target with its author, internal/public flag, recorded changes, timestamps and attachments; a move then deletes the original. The target and each source ticket get a line on their thread recording how many responses were moved or copied, between which tickets, and by whom.
Permissions: the owner must manage the target. A move also needs management of every source ticket, since it takes responses away; a copy only needs sight of them.
Response: moved or copied (the count), target_ticket_number, and new_response_ids in the order the ids were given. An unknown response id returns 404; a response already on the target ticket is refused (422 refused).
Split responses into a new ticket
POST /api/v1/tickets/splitMoves (or copies) responses off one ticket onto a new ticket, linked to the original as its clone — the way to separate a second conversation that arrived on an existing ticket.
response_ids— array of 1 to 500 response ids, all from the same ticket.subject— the new ticket’s subject (up to 255 characters).queue— optional queue name for the new ticket; defaults to the source ticket’s queue.copy— boolean, default false. False moves the responses; true copies them and leaves the source untouched.
The new ticket opens with the source ticket’s originator, assignee, priority and participants, and both threads record the transfer as for response-move. Permissions follow move and copy: splitting with a move needs management of the source ticket, splitting with copy needs sight of it.
Response: { "new_ticket_number": "220212", "source_ticket_number": "220009", "queue": "Support", "moved": 3 } (copied in place of moved for a copy).
Change the originator
POST /api/v1/tickets/originator-updateBody: the ticket (ticket_number or ticket_id) and originator_email. Use it when a ticket was raised under the wrong one of a person’s addresses. The owner must manage the ticket.
The new address becomes the originator and a participant. The previous originator stays a participant — remove them with POST /api/v1/tickets/participants/remove if they should go. The thread records the change. Giving the address that is already the originator is refused (422 refused).
Response: ticket_number, previous, originator_email and audit_response_id (the response that records the change).
Delete an attachment
POST /api/v1/tickets/attachment-deleteRemoves one attachment from a response — above all, a malicious file that should not stay on the ticket. The owner must manage the ticket.
- The ticket, as
ticket_numberorticket_id. response_attachment_id— the attachment’sid, as returned by the Show ticket and List responses APIs. It must belong to a response on that ticket, or the call returns404.reason— optional, up to 500 characters.
Tickiti stores attachment content by its SHA-256 hash, so the same file can sit on several responses and drafts. The attachment is removed from the response, and the stored file is deleted as well when nothing else uses it. The response keeps a line recording the file name, its SHA-256, who removed it, when (UTC) and the reason, so the record of what arrived outlives the file.
Response: { "deleted": "invoice.pdf.exe", "sha256": "9f2c…", "file_purged": true }. file_purged is false when the stored file is still in use elsewhere.
Edit or delete a response
POST /api/v1/tickets/response-update
POST /api/v1/tickets/response-deleteresponse-update corrects an existing response in place. Body: response_id plus at least one of:
content— the whole new body (HTML; inline base64 images are stored as attachments, as on reply).replace— an array of{ "find": "…", "replace": "…", "all": false }edits applied in order to the stored body. Eachfindmust occur in the body, and must occur exactly once unlessallis true. Every edit is checked before anything is written. Givecontentorreplace, not both.is_internal— boolean; changes the response’s visibility between internal and public.notify— boolean, used withis_internal: falseon an internal response: the response is published, signed as it would have been had it been posted public, and every participant who is not staff is notified, as Change to public does on the ticket page.
A correction is silent: no notification is sent unless notify is true. Body edits apply to staff responses; a customer’s response is never rewritten, though its visibility can be changed with is_internal — for example, a customer reply that was filed internal. Making a public response internal takes it out of the customer’s view of the ticket; notifications already sent for it stay sent. The response records each edit and each visibility change with who made it and when. Response: response_id, is_internal, other_changes and notified.
response-delete takes response_id and deletes the response. Response: { "response_id": "90412" }.
Both endpoints are open to the response’s author, an admin, or a manager of the ticket’s queue, on a ticket they can see; integration identities are exempt as above.
Lookups
POST /api/v1/tickets/staff (tickets:read) — the staff who can be participants or assignees. Optional search matches part of a name or email address. Each entry carries id, name, email and teams (the team keys, such as team:4, that assigned_to_email accepts); the response also gives count. Participants and assignment take email addresses, so look them up here rather than guessing.
POST /api/v1/tickets/resolutions (tickets:read) — the resolution categories a ticket can be closed with, in display order: { "resolution_categories": [ { "id": "3", "name": "Fixed", "selectable": true } ] }.
POST /api/v1/tickets/search-modes (tickets:read) — the search vocabulary of the List tickets API: modes maps each mode to a description, and rules explains how criteria and tokens combine.
POST /api/v1/token (any token, no ability needed) — what the calling token can do, so a client can tell a missing ability from a missing role or plan feature. The response holds:
token—name,abilities,wildcard,expires_at,last_used_at,allowed_ip.owner—email,name, the role flagsis_staff,is_admin,is_sysadmin,is_superuser,is_api_user, andticket_access_exempt(true for an integration identity).families— one entry per ability family:readandwrite(whether the token holds them, write implying read),required_role, androle_ok(whether the owner holds that role).plan— the instance’s plan features, each true or false.
Error responses
422 validation_failed— a missing or malformed field;errorslists them by field.422 refused— the operation does not apply to this ticket as it stands (locked, not deleted, merged, a clone link, already the originator, already on the target);messagesays why.404 not_found— an unknown ticket, response or attachment;resourceandmessagesay which.403 forbidden— the token lacks the ability, or its owner cannot see or manage a ticket involved.401 unauthenticated— bad or missing token.
See Authentication and tokens for abilities, and the API overview for the shared conventions.