Cross-ticket responses query 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.
This endpoint returns responses drawn from across every ticket the token can see — the bulk counterpart to List responses, which works one ticket at a time. Filter by a created-at window, queue, author, the internal/staff flags, attachments, a set of tickets or response ids, and page through the matches with a keyset cursor — or ask only for the number of matches, or tallies grouped by a column. Use it to pull all activity in a date range without walking tickets one by one: to feed an external analytics store, build a digest, size a population before reading it, or reconcile an audit log.
Endpoint
POST /api/v1/tickets/responses/queryHeaders
Authorization: Bearer YOUR_TOKEN— required. Token must have thetickets:readability. Atickets:writetoken also works (write implies read).Content-Type: application/json— required. NoIdempotency-Keyis needed (this is a read).
Body
Every field is optional. With an empty body the endpoint returns the first page of responses across all tickets you can see, oldest first.
Filters (all combine with AND):
created_from— lower bound on a response’s creation time. A bareYYYY-MM-DDmeans that day from00:00:00; a fullYYYY-MM-DD HH:MM:SSis used as given. Compared againstcreated_atas stored (UTC wall-clock).created_to— upper bound. A bareYYYY-MM-DDmeans that day to23:59:59.is_internal—truefor internal staff notes only,falsefor public responses only. Omit to include both.staff_response—truefor staff-authored responses only,falsefor customer responses only. Omit for either.queue— array of queue names; restrict to responses on tickets in those queues.queue_id— array of queue ids; the same restriction by id.ticket_number— array of 6-digit ticket numbers (as strings); restrict to responses on those tickets.response_id— array of response ids; return just those responses.created_by_email— only responses written by this address (exact match).has_attachments—truefor responses with at least one attachment,falsefor responses with none.is_empty—truefor responses with no message body (audit entries, attachment-only replies),falsefor responses that have one.include_deleted—trueto include soft-deleted responses. Defaults tofalse.
Shaping the result:
fields— array naming the fields to return; see Choosing fields.metadata_only—trueto drophtml,plain_textandattachmentsfrom every row.count_only—trueto return only the number of matching responses, as{ "count": n }.group_by— one ofticket_number,created_by_email,queue,is_internal,staff_responseorday, to return tallies instead of rows; see Counting and grouping.limit— page size. Defaults to200, maximum1000.cursor— a{ "created_at": ..., "id": ... }object copied verbatim from a previous page’snext_cursor. Omit it for the first page. See Paging below.
What you can see
Results obey the same visibility rule as the staff interface: a response is included only if its ticket sits in a queue the acting user can view, that user is a participant on the ticket, or the ticket is assigned to a team the user belongs to. A non-staff token owner is refused with 403. The filters narrow within that visible set — they can never widen it, so a token cannot read responses its owner could not read on screen.
Choosing fields
Each row can carry html, plain_text, attachments, is_internal, is_pinned, staff_response, created_by_email, created_at, updated_at, is_email_sourced, audit and is_audit_only, plus response_id and ticket_number, which are always present. fields may name any of these:
- Omitted — every field.
- Only body fields (
html,plain_text,attachments) — all the metadata plus the body fields named."fields": []is a metadata-only pull. - Any metadata field named — exactly the named fields, plus
response_idandticket_number.["created_by_email", "created_at"]returns four keys per row.
A field that is not returned is absent from each row rather than null. Body fields that are not requested are skipped server-side, which makes large pulls much cheaper. metadata_only removes the three body fields from whatever fields selects (from every field when fields is omitted).
Example
Pull the metadata and attachment lists for every response created in the Sales queue over a two-day window, dropping the bodies:
curl -X POST https://support.sole-provider.example/api/v1/tickets/responses/query \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"created_from": "2026-05-01",
"created_to": "2026-05-02",
"queue": ["Sales"],
"fields": ["attachments"],
"limit": 200
}'Successful response
A responses array, ordered oldest to newest by created_at (ties broken by id), plus the next_cursor to fetch the following page and the count on this page. Each row carries the ticket it belongs to, so you can group responses back onto their tickets. audit and is_audit_only have the same meaning as in Show ticket.
{
"ok": true,
"data": {
"responses": [
{
"response_id": "41",
"ticket_number": "220009",
"is_internal": true,
"is_pinned": false,
"staff_response": true,
"created_by_email": "[email protected]",
"created_at": "2026-05-02T10:05:11+00:00",
"updated_at": "2026-05-02T10:05:11+00:00",
"is_email_sourced": false,
"audit": null,
"is_audit_only": false,
"html": "<p>Internal note: replacement authorised.</p>",
"plain_text": "Internal note: replacement authorised.",
"attachments": []
}
],
"next_cursor": { "created_at": "2026-05-02 10:05:11", "id": 41 },
"count": 1
}
}Counting and grouping
To size a result before reading it, send the same filters with "count_only": true:
{ "ok": true, "data": { "count": 19 } }With group_by the endpoint tallies the matching responses by that column instead of returning rows. day groups by the UTC date of created_at; queue by queue name; is_internal and staff_response by their value (1 or 0). Groups are ordered largest first, up to 1000 groups, and total is the sum of their counts:
{
"ok": true,
"data": {
"group_by": "created_by_email",
"groups": [
{ "value": "[email protected]", "count": 12 },
{ "value": "[email protected]", "count": 7 }
],
"total": 19
}
}Both use the filters and ignore limit, cursor and fields. When both are sent, count_only applies.
Paging
Paging is keyset-based, not offset-based, so it stays correct even as new responses arrive mid-walk. To read the whole result set: call the endpoint, process data.responses, then if data.next_cursor is not null call again with that object as cursor. Stop when next_cursor comes back null. Keep the other filters identical across pages.
Error responses
422 validation_failed— a field has the wrong type,fieldscontains a name not listed in Choosing fields (the message lists the valid names),group_byis not one of its values,limitis out of range, or a suppliedcursoris missing itscreated_atorid.401 unauthenticated— bad or missing token.403 forbidden— token lacks thetickets:read(ortickets:write) ability, or the token owner is not a staff account.
Where to go next
- List responses API — the single-ticket version, when you already know the ticket.
- Download attachment API — fetch the bytes of an attachment listed on a response.
- Ticket operations API — read a single response by id, move responses between tickets, or edit and delete them.
- MCP server — reach this endpoint from an AI assistant via the
query_responsesandexport_responsestools.