Skip to main content
Prefix: /api/merchant/v1. Send:
The key must be allowed to access {storeId}. When its store scope is SELECTED, {storeId} must be in that list. RESTRICTED keys must satisfy the scope on the method. ticket:read is satisfied by ticket:read or ticket:write. ticket:write is satisfied only by ticket:write. FULL keys skip that scope list. Either way the user must still have dashboard permission manageTickets. Success JSON is the payload below. It is not wrapped in data. Errors are { "success": false, "error", "code" }. A 500 also includes errorId. A storeId property in a JSON body is ignored. The path store id is the one that is used. Unknown body keys are dropped. GET {ticketId} may be the internal id or the public id (TKT-XXXX-XXXX-XXXX-XXXX-XXXX). Reply, close, and reopen write using the path value as the internal id, so send id on those methods. A ticket that belongs to another shop returns 404 with error Ticket not found.

List tickets

Scope: ticket:read (ticket:write also satisfies it) If support tickets are turned off for the store, the response is 403 and error Support tickets are not available for this store. If the store row is missing, the response is 404 and error Store not found. Get, reply, close, and reopen do not check whether tickets are enabled. When tickets are enabled and autoCloseInactiveDays is greater than 0, this call first closes up to 50 tickets in open, in_progress, pending, or on_hold whose updatedAt is earlier than now minus that many 24-hour periods. Each of those gets status closed and a system reply (isAdmin true, seenAt set): This ticket was automatically closed after N day of inactivity. when N is 1, and days when N is not 1. The handler then loads at most 200 tickets for the store, newest createdAt first, and paginates that array. total is the size of that loaded set (200 at most), not a count of every ticket when the store has more. Other query parameters are ignored.
Ticket fields on the list:

Get a ticket

Scope: ticket:read (ticket:write also satisfies it) Status 200. Returns the ticket row plus the full reply thread and a small store object. Ticket fields are the list fields except _count, unreadCount, and the one-reply preview. replies is every reply, createdAt ascending. store is { "subdomain", "name" }. Each reply: This method does not check whether tickets are enabled on the store. Reply, close, and reopen do not either.

Reply

Scope: ticket:write Status 201. Returns the created reply (the fields in the reply table). The reply is stored with isAdmin: true. A body isAdmin value is ignored. audioDuration is not accepted and is stored as null. After a successful reply the ticket status is set to pending. Body must be a JSON object: imageUrl and audioUrl may be omitted, null, or empty. A non-empty value must be a path starting with /uploads/, or an http/https URL that matches one of these: the origin (scheme, host, and port) equals R2_PUBLIC_URL and the path contains /uploads/ or /stores/; the host ends with .r2.dev and the path contains /uploads/ or /stores/; or the host is static plus optional digits plus .klipy.com, compared case-insensitively (static.klipy.com, static1.klipy.com), with any path. Otherwise 400 and error Invalid image URL or Invalid audio URL. A ticket with status closed returns 403 and error This ticket is closed. resolved and the other non-closed statuses still accept a reply. When replyToId is set, replyToMessage is a preview of that reply. A leading Automatic reply\n is removed, then **bold** and _italic_ markers are stripped. If no text remains, the preview is Voice note when audioUrl is set, otherwise Photo when imageUrl is set, otherwise Attachment. replyToIsAdmin is copied from the quoted reply.

Close

Scope: ticket:write Status 200. Sets status to closed and appends a system reply (isAdmin true, seenAt set) whose message is:
Returns { "replies" }: every reply on the ticket, createdAt ascending, with the reply fields above. The current status is not checked, so closing an already closed ticket appends another system reply.

Reopen

Scope: ticket:write No body. Status 200. Sets status to open and appends a system reply with message Ticket reopened by admin. Returns { "replies" } in the same shape as close. The current status is not checked.

Errors