API
0.2.2 — 2026-08-24
Section titled “0.2.2 — 2026-08-24”- Replying to a ticket that carries no requester e-mail address no longer reports the mail server as unreachable. Sending was attempted with an empty recipient list, and the resulting rejection was reported as “The configured mail server could not be reached” — a misleading message that sent operators checking a healthy SMTP host, while a test send from the mail settings page succeeded every time. Such a send now fails immediately with its own message, “The e-mail could not be sent, it has no recipient address”, and the log line names the ticket’s subject instead of the transport. The check covers every outbound path (ticket reply, processing a ticket with “send e-mail” ticked, workflow e-mail actions, welcome mail), and blank address entries are now skipped rather than rejected by the mail library. Tickets created by hand or through a form with no e-mail field are the ones affected; a ticket opened from an incoming e-mail always has a requester address.
0.2.1 — 2026-08-18
Section titled “0.2.1 — 2026-08-18”- The knowledge base integration takes a second address: Knowledge base site address
(
siteUrl), alongside the existing API address. They are different hosts on a real deployment — the API answers JSON and is not meant to be opened in a browser — so the dashboard can now link operators to the help centre people actually read. The field is optional: an instance connected before this version keeps working untouched, and one that publishes no help centre can leave it empty. A value that IS supplied is validated like any other address on this form.
0.2.0 — 2026-08-17
Section titled “0.2.0 — 2026-08-17”-
Attachment records on the dashboard conversation endpoint (
GET v1/dashboard/tickets/{id}/messages) now carry anobjectKeyfield as well. Files are downloaded withGET v1/attachments/{objectKey}; because the response returned onlyid,originalFileName,contentTypeandfileSizeuntil now, a frontend could list an attachment by name but could not download it. The new field isrequired— frontends must regenerate their clients. -
The integration conversation endpoints (
GET v1/integration/tickets/{id}/messagesand its tracking-key equivalent) now check the pool of the parent tickets against the token’s pool scope, not just the pool of the ticket addressed. Child tickets are opened in the parent’s pool, so the chain started out in a single pool; but once a ticket is moved to another pool from the dashboard the chain can span two pools, and a token scoped to pool A could read the message contents of a parent ticket in pool B. Parent tickets outside the scope are now dropped from the chain and their messages are never queried. The same walk on the dashboard is deliberately unchanged — authority there comes from a permission, not from a pool set. -
When the same token uses the same attachment id in two concurrent message requests, the attachment is now attached to only one of them. Consuming the attachment claim was made atomic; previously both requests were told “this attachment is yours” and the same file could be attached to two different tickets, even though the endpoint states an attachment id may be used once. The losing request now gets the same response (
403) as it would for an attachment id that was never issued. -
An
Idempotency-Keyfirst written against an open ticket and then repeated against a different ticket id no longer produces a self-contradictory response. It used to returncontinuedInNewTicket: truetogether withparentTicketId: null— a claim of continuing into a parent that does not exist.continuedInNewTicketis nowtrueonly when there really is a parent; in this scenario it isfalsewithparentTicketIdnull, andticketIdpoints at the ticket the message actually landed on. -
On upload,
originalFileNamenow stores only the file name rather than the path the client sent (fixtures/sample.txt->sample.txt,C:\dir\report.pdf->report.pdf). Since this value is written into theContent-Dispositionheader on download, storing a client-supplied path verbatim was wrong. Applies to bothPOST v1/attachmentsandPOST v1/integration/attachments; existing records are unchanged.
Changed
Section titled “Changed”- Breaking (database): the
TicketEmailLogstable was renamed toTicketMessages, and theTicketEmailLogIdcolumn onTicketAttachmentstoTicketMessageId. The migration renames in place without moving data — no record is lost or copied — but this upgrade is not zero downtime: during a rolling update a pod still running the old version queries the old table name and returns500. Upgrade this version with a short maintenance window, or by setting the deployment strategy toRecreatefor this version. The same change renamed two localization keys (in both en-US and tr-TR) —TicketAttachment:Create:TicketEmailLogIsRequired->TicketAttachment:Create:TicketMessageIsRequired,TicketEmailLog:RecordInbound:SourceMessageIdIsRequired->TicketMessage:RecordInbound:SourceMessageIdIsRequired— and the controller action behind the dashboard ticket-history endpoint (and therefore its OpenAPI operation id) becameGetEmailLogsAsync->GetMessagesAsync; the route ({id}/email-logs) did not change in this step, see the route change below. Two DTO/OpenAPI schema names were also renamed:TicketEmailLogForDashboardResponseDto->TicketMessageForDashboardResponseDtoandTicketEmailLogAttachmentForDashboardResponseDto->TicketMessageAttachmentForDashboardResponseDto— regenerate your client types. - The dashboard ticket conversation endpoint was renamed:
GET v1/dashboard/tickets/{id}/email-logs->GET v1/dashboard/tickets/{id}/messages. The old address keeps returning the same body for one version, announcing that it is time to move with theDeprecation: trueandLink: <.../messages>; rel="successor-version"headers.
- Ticket conversation entries now have a
channelfield (Email/Api) showing how the message arrived. Every existing record, and every message that arrives by e-mail today, is markedEmail; added to theGET v1/dashboard/tickets/{id}/messagesresponse. - The opening text of a ticket created through the integration API (the description, or the subject
when there is none) is now recorded as the ticket’s first conversation message too — as it
already was for tickets opened by e-mail. The conversation now starts from the first message and
appears in the
GET v1/dashboard/tickets/{id}/messagesresponse. - New endpoints:
POST v1/integration/tickets/{id}/messagesandPOST v1/integration/tickets/by-tracking-key/{trackingKey}/messages— they add a reply coming from the customer’s own system to an existing ticket’s conversation. When anIdempotency-Keyheader is sent and the same key is repeated with the same content, nothing is written and the original message is returned with200(isReplay: true); repeated with different content it returns409. There are two distinct409s, and because the client’s correct reaction is the opposite in each case, tell them apart by theerror.codefield — the message text varies withAccept-Language:FD:3001means this key was already used with different content (do not retry, fix the key or the body), whileFD:3002means a request with the same key is still being processed (retry shortly and you will get the result with200). When a new message is written the response is201.attachmentIdsaccepts only attachment ids uploaded by the same token throughPOST v1/integration/attachmentsand not yet used; someone else’s attachment, or an expired one, returns403. A ticket outside the token’s pool scope returns404even if it exists. - When a message is added through the integration to a closed ticket (completed or cancelled), the
message is not written to that ticket; following the same rule as the e-mail flow, a new child
ticket is opened and the message is written there — so that the closed ticket’s
CompletedAt, its SL measurement and its already-reported periods do not change retroactively. In this caseticketIdandtrackingKeyin the response point at the child ticket,continuedInNewTicketistrue, andparentTicketIdgives the ticket you addressed: update the id mapping in your integration from this response. Opening the child ticket depends on the pool being fully configured for intake; when it is not, the response is not a500but a422that says what needs to be done. If two requests sent with the sameIdempotency-Keyare processed at the same time, only one opens the child ticket; the other returns the winner’s result with200, or, if the winner has not finished writing yet, a409with codeFD:3002saying “retry” — two child tickets are never opened. - New endpoint:
POST v1/integration/attachments— for integration tokens to upload their own attachments. The id of an uploaded file is reserved (claimed) so that it can be used only by the token that uploaded it, only once, and only within 24 hours; the existingPOST v1/attachmentsendpoint is unchanged and was not opened to integration tokens. - New endpoints:
GET v1/integration/tickets/{id}/messagesandGET v1/integration/tickets/by-tracking-key/{trackingKey}/messages— they read a ticket’s conversation (every message, yours and ours), merged across the parent chain even when the ticket was closed and continued in a child, ordered oldest to newest. Each record carries which ticket it belongs to viaticketIdandtrackingKey, and how it arrived viadirection(Inbound/Outbound) andchannel(Email/Api); who wrote an outgoing message (the agent’s identity) is not exposed —directionis already enough to say “this came from us”. Send the last seen id asafterIdto poll incrementally. For older tickets opened through the integration API before Task 4, whose opening text was not yet recorded as a separate message row, the opening text is synthesised from the ticket’s own fields and returned withid: 0(this synthetic record is not listed whenafterIdis sent, so it does not reappear). A ticket outside the token’s pool scope returns404even if it exists.
0.1.35 — 2026-08-13
Section titled “0.1.35 — 2026-08-13”- The ticket status create and update endpoints now accept an
orderfield (POST/PUT v1/dashboard/ticket-statuses). The order can therefore be set from the individual record form as well; the bulkPUT v1/dashboard/ticket-statuses/orderendpoint is unchanged. On update the field is optional: leave it out and the existing order is kept, not reset.
See GitHub Releases for older versions.