Reply to ticket 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 reply endpoint posts a response onto an existing ticket and, in the same call, can change the ticket itself — its status, hold date, assignee, priority, resolution, queue, subject and participants. Use it when an external system has new information to add to a conversation Tickiti is already tracking — for example an order system posting a shipping update, or a CRM adding a note when a customer record changes — or when it needs to move the ticket along.

Endpoint

POST /api/v1/tickets/respond

Two shortcuts go through the same endpoint and return the same response: close a ticket and add or remove participants.

Headers

  1. Authorization: Bearer YOUR_TOKEN — required. Token must have the tickets:write ability.
  2. Idempotency-Key: <uuid> — required. See Idempotency.
  3. Content-Type: application/json — required.

Body

Required fields:

  1. ticket_number — the 6-digit number of the ticket to respond to, as a string or a JSON number. Alternatively send ticket_id, the ticket’s internal id. Send exactly one of the two.
  2. from_email — the email address the response is attributed to. If this address is not already a participant on the ticket it is added as one.
  3. content — the response body (string, HTML allowed). Required unless the request carries a ticket change (any of the attribute fields below), in which case the change is recorded as a response with no body.

The body is always sent as content. When content is empty and the request carries html, body, message, text, reply, response, note or content_html, the request is refused with 422 validation_failed naming that parameter, so a body sent under the wrong name is never lost. Any other parameter the endpoint does not recognise is returned in data.ignored_params, so a misspelt field is visible in the result.

Optional fields:

  1. is_internal — boolean, default false. When true the response is a staff-only internal note and is not shown to the customer or included in customer notifications.
  2. attachments — an array of { "sha256": ..., "name": ..., "file_size": ... } objects returned by POST /api/v1/tickets/attachment-upload, attached to the response as regular (non-inline) attachments. See Create ticket API — Attaching files.
  3. send_full_email — boolean, default false. Email customer participants the reply itself, in the full conversation format, instead of the short notification with a link. Staff notifications are unchanged, and it has no effect on an internal note. data.full_email_sent confirms it.
  4. keep_status — boolean, default false. Leave the ticket’s status as it is even for a public reply. See Status and reopening.

Ticket changes, applied together with the response (each optional):

  1. status — open, on-hold or closed. open and closed clear any hold date.
  2. on_hold_until — the date the hold ends (YYYY-MM-DD). Required with status: on-hold; sent on its own it puts the ticket on hold until that date.
  3. assigned_to_email — a staff member’s email, a team’s key team:<id> (list the keys with POST /api/v1/tickets/teams) or Unassigned. The assignee — for a team, every member — becomes a participant.
  4. priority — a code "10", "20", "30", "40" or a name Low, Normal, High, Urgent (case-insensitive), sent as a string.
  5. resolved — a resolution category, by id or by name. List the categories with the resolutions lookup in the Ticket operations API.
  6. queue — a queue name; moves the ticket to that queue.
  7. subject — renames the ticket.
  8. add_participants — an array of email addresses to add as participants. Each person added is notified that they were added, even on an internal note.
  9. remove_participants — an array of email addresses to remove. The originator always stays on the ticket. People removed are not notified of the response that records their removal.

Every change is recorded on the response it arrived with, in the response’s audit (see Show ticket API), exactly as a change made on the ticket page is.

Status and reopening

  1. With status (or on_hold_until) supplied, Tickiti applies it as given.
  2. With no status supplied, a public reply with a body reopens a closed or on-hold ticket and clears its hold date — it is a reply the customer will answer. data.reopened is true when this happens. Add "keep_status": true to leave the status alone.
  3. An internal note, or a request that only changes attributes or participants, leaves the status and any hold date as they were.
  4. A response attributed to a non-staff from_email is handled as a reply from that customer: it reopens a closed or on-hold ticket (other than a task), and if the ticket has no staff participant Tickiti routes it to the inbox queue as incoming customer mail.

Closing and resolutions

On a queue that tracks resolutions, closing a ticket that has no resolution yet requires resolved in the same request; without it the request is refused with 422 validation_failed and an errors.resolved message. When the request also moves the ticket with queue, the destination queue’s setting applies. This holds for status: closed here and for /tickets/close.

@tags and #hashtags

In a response from a staff author, @tags and #hashtags work as described for Create ticket: a tag that resolves to nobody (or to several people) is kept as plain text and reported in data.tag_warnings, a hashtag resembling an existing one is reported in data.hashtag_warnings, and an @ or # inside a code block is not a tag.

Inline images

To embed an image in the response, include it in content as a base64 data URI — an <img src="data:image/png;base64,…"> element placed wherever the image should appear in the body. Tickiti decodes each such image, stores it as an inline attachment, and rewrites the body to reference it, so it renders within the response thread and in customer emails.

Examples

A public reply:

curl -X POST https://support.sole-provider.example/api/v1/tickets/respond \
  -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "ticket_number": "220009",
    "from_email": "[email protected]",
    "content": "<p>Your replacement Vapor Pro 3 has shipped. Tracking: TRACK-XYZ.</p>"
  }'

An internal note that hands the ticket to a team, raises its priority and parks it until a date:

{
  "ticket_number": 220009,
  "from_email": "[email protected]",
  "content": "<p>Waiting on the courier — warehouse team to follow up.</p>",
  "is_internal": true,
  "assigned_to_email": "team:3",
  "priority": "High",
  "on_hold_until": "2026-05-08"
}

A change with no body — move the ticket and rename it:

{
  "ticket_number": "220009",
  "from_email": "[email protected]",
  "queue": "Warranty",
  "subject": "Vapor Pro 3 plate failure — warranty claim"
}

Successful response

{
  "ok": true,
  "data": {
    "ticket_id": 1,
    "response_id": 42,
    "reopened": true,
    "status": "open",
    "on_hold_until": null,
    "priority": "20",
    "assigned_to_email": "[email protected]",
    "resolved": null,
    "queue_id": 4,
    "queue": "Returns",
    "subject": "Carbon plate cracked on first marathon",
    "is_internal": false,
    "full_email_sent": false,
    "tag_warnings": [],
    "hashtag_warnings": [],
    "ignored_params": []
  }
}
  1. response_id — the response that was written (for a body-less change, the response that records it).
  2. reopened — true when a public reply reopened a closed or on-hold ticket (see Status and reopening). An explicit status: open reports false; read status for the result.
  3. status, on_hold_until, priority, assigned_to_email, resolved, queue_id, queue, subject — the ticket’s values after the request.
  4. is_internal — whether the response is an internal note.
  5. full_email_sent — true when send_full_email applied.
  6. tag_warnings, hashtag_warnings — tags posted as text; see @tags and #hashtags.
  7. ignored_params — parameters in the request that the endpoint does not use.

Close a ticket

POST /api/v1/tickets/close

Closes a ticket, optionally with a closing response and a resolution. Same headers as /tickets/respond (tickets:write, Idempotency-Key). The body takes the ticket reference (ticket_number or ticket_id) and optionally from_email (defaults to the token’s owner), content, is_internal and resolved. The rules in Closing and resolutions apply, and the response is the same as for /tickets/respond.

{ "ticket_number": "220009", "resolved": "Replaced under warranty" }

Add or remove participants

POST /api/v1/tickets/participants/add
POST /api/v1/tickets/participants/remove

Changes a ticket’s participants without composing a message. Same headers as /tickets/respond. The body takes the ticket reference, participants — a non-empty array of email addresses — and optionally from_email (defaults to the token’s owner). The change is recorded in the thread as a response with no body, and the response is the same as for /tickets/respond. People added are notified that they were added; the originator cannot be removed.

{ "ticket_number": "220009", "participants": ["[email protected]"] }

Error responses

Every error has "ok": false and an error code; validation errors list the problems per field under errors.

  1. 422 validation_failed — missing from_email; no body and no change; an unknown ticket number; both or neither of ticket_number and ticket_id; a body sent under another name; on-hold without a date; an unknown priority, resolution, queue or team key; a close that needs a resolution; an empty participants list; or a missing Idempotency-Key.
  2. 404 not_found — no ticket has the given ticket_id.
  3. 409 idempotency_key_conflict — the idempotency key was already used with a different payload. See Idempotency.
  4. 401 unauthenticated — bad or missing token.
  5. 403 forbidden — token does not have the tickets:write ability, or the user it acts as cannot see the ticket (they must be its assignee, a participant, a member of its assigned team, or able to view its queue). A token owned by an API account, a system administrator or the system user may act on any ticket. This check runs before the idempotency key is recorded, so the same key can be retried once access is granted.

What happens server-side

  1. Tickiti locates the ticket by ticket_number or ticket_id and validates the request.
  2. It checks that the token’s owner may act on the ticket.
  3. It applies the requested status, hold date, priority, assignee, resolution, queue and subject, or reopens the ticket under the rules in Status and reopening.
  4. It adds from_email as a participant if it was not one already.
  5. It writes the response, public or internal per is_internal, attributed to from_email, adding any add_participants, resolving @tags and #hashtags, and extracting base64 data-URI images into inline attachments.
  6. It sends the notifications (the full reply instead of the short notification when send_full_email is set), then removes any remove_participants.
  7. It records every change in the response’s audit, attaches any uploaded attachments, and refreshes the affected ticket-list views.

To read the conversation before or after replying, use the Show ticket API. To edit or delete a response, read a single response by id, or look up staff and resolution categories, see the Ticket operations API. See the API overview for the shared conventions.