Tickets
The eleven endpoints that open, answer, close and rate a support ticket.
Overview
A support ticket is a conversation: the customer opens it, the staff reply, and it closes. Eleven endpoints cover every step of it.
Two dictionaries want reading before a ticket is opened: which fields each department asks for, and which access groups take encrypted details.
A password does not belong in the message body. A separate field exists for the access groups, and a password-type value there is stored encrypted.
Reference
Listing the Departments
Returns the departments a ticket can open in, each with its own field schema.
curl 'https://panel.example.com/api/v1/client/tickets/departments' \
-H "Authorization: Bearer $CLIENT_KEY"const res = await fetch('https://panel.example.com/api/v1/client/tickets/departments', {
headers: { Authorization: `Bearer ${clientKey}` },
});
const { data } = await res.json();
renderDepartmentForm(data[0].custom_fields);$ch = curl_init('https://panel.example.com/api/v1/client/tickets/departments');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $clientKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// The field schema differs PER DEPARTMENT: build the form from the chosen one's schema.
$deps = Kernel::internal('client:Tickets/GetTicketDepartments', ['owner_id' => $uid])['data'];
$schema = array_column($deps, 'custom_fields', 'id')[$did] ?? [];Listing the Access Groups
Returns the access detail groups a message can carry, with their schemas.
curl 'https://panel.example.com/api/v1/client/tickets/access-groups' \
-H "Authorization: Bearer $CLIENT_KEY"const res = await fetch('https://panel.example.com/api/v1/client/tickets/access-groups', {
headers: { Authorization: `Bearer ${clientKey}` },
});
const { data } = await res.json();
const server = data.find((g) => g.name.includes('Server'));$ch = curl_init('https://panel.example.com/api/v1/client/tickets/access-groups');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $clientKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// A password-type field is stored ENCRYPTED; use this rather than writing secrets into the message body.
$groups = Kernel::internal('client:Tickets/GetTicketAccessGroups', ['owner_id' => $uid])['data'];
$creds = [['group_id' => $groups[0]['id'], 'fields' => $values]];Listing the Tickets
Returns the account's support tickets in order of state.
curl 'https://panel.example.com/api/v1/client/tickets?status=answered' \
-H "Authorization: Bearer $CLIENT_KEY"const res = await fetch('https://panel.example.com/api/v1/client/tickets?status=answered', {
headers: { Authorization: `Bearer ${clientKey}` },
});
const { data } = await res.json();
const waiting = data.filter((t) => t.unread);$ch = curl_init('https://panel.example.com/api/v1/client/tickets?status=answered');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $clientKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// The unread mark shows in the LISTING alone: reading the detail clears it, so count from the list first.
$rows = Kernel::internal('client:Tickets/GetTickets', ['owner_id' => $uid])['data'];
$badge = count(array_filter($rows, fn ($t) => $t['unread']));Opening a Ticket
Opens a new support ticket.
curl -X POST 'https://panel.example.com/api/v1/client/tickets' \
-H "Authorization: Bearer $CLIENT_KEY" \
-H 'Content-Type: application/json' \
-d '{"department_id":4,"subject":"Cannot reach my server","message":"SSH times out since this morning.","priority":3}'const res = await fetch('https://panel.example.com/api/v1/client/tickets', {
method: 'POST',
headers: {
Authorization: `Bearer ${clientKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
department_id: 4,
subject,
message,
credentials: [{ group_id: 1, fields: { 30: ip, 32: password } }],
}),
});
const { data } = await res.json();$ch = curl_init('https://panel.example.com/api/v1/client/tickets');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $clientKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode($ticket),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// The attachments are checked BEFORE the ticket is saved: a broken file leaves no half ticket.
Kernel::internal('client:Tickets/CreateTicket', ['owner_id' => $uid] + $ticket);Reading a Ticket and Its Messages
Returns a ticket's heading and the whole run of messages.
curl 'https://panel.example.com/api/v1/client/tickets/429' \
-H "Authorization: Bearer $CLIENT_KEY"const res = await fetch(`https://panel.example.com/api/v1/client/tickets/${id}`, {
headers: { Authorization: `Bearer ${clientKey}` },
});
const { data } = await res.json();
renderThread(data.messages, data.can_reply);$ch = curl_init('https://panel.example.com/api/v1/client/tickets/' . $id);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $clientKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// This read MARKS AS READ: a job counting badges drops the badge the moment it calls.
$t = Kernel::internal('client:Tickets/GetTicket', ['owner_id' => $uid, 'id' => $id])['data'];
$html = array_filter($t['messages'], fn ($m) => $m['is_html']);Writing a Reply
Adds a reply to an open ticket.
curl -X POST 'https://panel.example.com/api/v1/client/tickets/429/reply' \
-H "Authorization: Bearer $CLIENT_KEY" \
-H 'Content-Type: application/json' \
-d '{"message":"The firewall rule is back, thank you."}'const res = await fetch(`https://panel.example.com/api/v1/client/tickets/${id}/reply`, {
method: 'POST',
headers: {
Authorization: `Bearer ${clientKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ message }),
});
const { data } = await res.json();
appendMessage(data.message);$ch = curl_init('https://panel.example.com/api/v1/client/tickets/' . $id . '/reply');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $clientKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['message' => $text]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// A reply moves the ticket back to WAITING: a closed one takes none, so reopen it first.
$t = Kernel::internal('client:Tickets/GetTicket', ['owner_id' => $uid, 'id' => $id])['data'];
if (! $t['can_reply']) Kernel::internal('client:Tickets/ReopenTicket', ['owner_id' => $uid, 'id' => $id]);Closing a Ticket
Closes a ticket as solved.
curl -X POST 'https://panel.example.com/api/v1/client/tickets/429/close' \
-H "Authorization: Bearer $CLIENT_KEY" \
-H 'Content-Type: application/json' \
-d '{"rating":5}'const res = await fetch(`https://panel.example.com/api/v1/client/tickets/${id}/close`, {
method: 'POST',
headers: {
Authorization: `Bearer ${clientKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ rating: 5 }),
});
const { data } = await res.json();$ch = curl_init('https://panel.example.com/api/v1/client/tickets/' . $id . '/close');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $clientKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['rating' => 5]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// Rating while closing is a ONE-TIME chance: a ticket that already carries one skips the value quietly.
Kernel::internal('client:Tickets/CloseTicket', ['owner_id' => $uid, 'id' => $id, 'rating' => 5]);Reopening a Ticket
Reopens a closed ticket.
curl -X POST 'https://panel.example.com/api/v1/client/tickets/429/reopen' \
-H "Authorization: Bearer $CLIENT_KEY"const res = await fetch(`https://panel.example.com/api/v1/client/tickets/${id}/reopen`, {
method: 'POST',
headers: { Authorization: `Bearer ${clientKey}` },
});
const { data } = await res.json();$ch = curl_init('https://panel.example.com/api/v1/client/tickets/' . $id . '/reopen');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $clientKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// A LOCKED ticket cannot be reopened: opening a new one is the only road there.
$t = Kernel::internal('client:Tickets/GetTicket', ['owner_id' => $uid, 'id' => $id])['data'];
if ($t['locked']) $openNewTicket();Rating a Ticket
Gives the ticket a rating from one to five.
curl -X POST 'https://panel.example.com/api/v1/client/tickets/429/rating' \
-H "Authorization: Bearer $CLIENT_KEY" \
-H 'Content-Type: application/json' \
-d '{"rating":5}'const res = await fetch(`https://panel.example.com/api/v1/client/tickets/${id}/rating`, {
method: 'POST',
headers: {
Authorization: `Bearer ${clientKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ rating }),
});
if (res.status === 422) hideRatingWidget();$ch = curl_init('https://panel.example.com/api/v1/client/tickets/' . $id . '/rating');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $clientKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['rating' => $n]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// A rating is FINAL: once given it cannot be changed, so ask for confirmation in your interface.
$t = Kernel::internal('client:Tickets/GetTicket', ['owner_id' => $uid, 'id' => $id])['data'];
if ($t['rating'] === null) Kernel::internal('client:Tickets/RateTicket',
['owner_id' => $uid, 'id' => $id, 'rating' => $n]);Rating a Staff Reply
Rates one staff reply on its own.
curl -X POST 'https://panel.example.com/api/v1/client/tickets/429/replies/878/rating' \
-H "Authorization: Bearer $CLIENT_KEY" \
-H 'Content-Type: application/json' \
-d '{"rating":4}'const res = await fetch(`https://panel.example.com/api/v1/client/tickets/${id}/replies/${rid}/rating`, {
method: 'POST',
headers: {
Authorization: `Bearer ${clientKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ rating }),
});
const { data } = await res.json();$ch = curl_init('https://panel.example.com/api/v1/client/tickets/' . $id . '/replies/' . $rid . '/rating');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $clientKey,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(['rating' => $n]),
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// STAFF replies alone can be rated: the id of your own message answers not found.
$t = Kernel::internal('client:Tickets/GetTicket', ['owner_id' => $uid, 'id' => $id])['data'];
$staff = array_filter($t['messages'], fn ($m) => $m['author']['kind'] === 'staff');Downloading an Attachment
Returns a message attachment together with its content.
curl 'https://panel.example.com/api/v1/client/tickets/429/attachments/55' \
-H "Authorization: Bearer $CLIENT_KEY"const res = await fetch(`https://panel.example.com/api/v1/client/tickets/${id}/attachments/${aid}`, {
headers: { Authorization: `Bearer ${clientKey}` },
});
const { data } = await res.json();
const blob = await (await fetch(`data:${data.mime};base64,${data.content}`)).blob();$ch = curl_init('https://panel.example.com/api/v1/client/tickets/' . $id . '/attachments/' . $aid);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $clientKey],
]);
$body = json_decode(curl_exec($ch), true);
curl_close($ch);// The content comes ENCODED inside the JSON: account for the memory on a large file.
$a = Kernel::internal('client:Tickets/GetTicketAttachment',
['owner_id' => $uid, 'id' => $id, 'aid' => $aid])['data'];
file_put_contents($a['name'], base64_decode($a['content']));Pitfalls
Reading a ticket's detail clears the unread mark on the account. A job building a notification badge drops it before the customer ever sees it. Count from the listing and call the detail only when the customer truly opens it.
Where the staff locked a ticket neither a reply nor a reopen works. That is different from being closed: a closed ticket reopens and a locked one does not. Read the lock field in the detail and offer opening a new ticket instead.
Both a ticket rating and a staff reply rating are given once, and a second attempt is refused. Sending a rating while closing spends the same right, and a ticket that already carries one skips the value quietly. Ask for confirmation in your interface.
A separate structure exists for a password or a key: the access group rows. A password-type value there is stored encrypted and unlocked for the owner and the staff alone. Writing the same detail into the message text leaves it in plain sight.
Attachments go as encoded content and giving an address is refused. They are checked before the ticket is saved, so a broken file leaves no half ticket. Downloading works the same way: the content comes encoded inside the JSON.
The notes the staff write among themselves never appear in the message run, and the attachments on them cannot be downloaded. A gap in the message ids on the customer side is the design rather than a fault.
Related Articles
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.