Tickets

5 Aufrufe Markdown

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

get/api/v1/client/tickets/departments
Tickets/GetTicketDepartments the opening schema

Returns the departments a ticket can open in, each with its own field schema.

Response fields data[] — 4
idintThe department id.
namestringThe department name.
descriptionstringWhat it is for.
custom_fieldsobject[]The fields asked at opening.
idintThe field id. It is the key of the map in the body.
namestringThe field label.
typestringThe input type.
requiredboolWhether it may be left empty.
optionsobject[]The choices on a picker field. Each carries a value and a label, and the value is what goes.
Errors 2
ticket_system_disabled422The operator closed the support system.
insufficient_scope403The key lacks the required scope.
Request
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

get/api/v1/client/tickets/access-groups
Tickets/GetTicketAccessGroups encrypted fields

Returns the access detail groups a message can carry, with their schemas.

Response fields data[] — 3
idintThe group id. This is what goes into the group field of an access row.
namestringThe group name.
custom_fieldsobject[]The group's access fields.
idintThe field id. It is the key of the map in the body.
namestringThe field label.
typestringThe input type.
requiredboolWhether it may be left empty.
optionsobject[]The choices on a picker field. Each carries a value and a label, and the value is what goes.
Errors 2
ticket_system_disabled422The operator closed the support system.
insufficient_scope403The key lacks the required scope.
Request
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

get/api/v1/client/tickets
Tickets/GetTickets state order

Returns the account's support tickets in order of state.

Query 5
pageintWhich page.
limitintRows per page. 100 at the most.
statusstringThe state filter. It takes the customer-facing vocabulary.
departmentintThe department filter.
searchstringSearches the subject, the reference and the department name.
Response fields data[] — 11 + meta — 4
ticket_idintThe ticket id.
refstringThe reference the customer quotes.
subjectstringThe subject.
statusstringThe raw state.
statestringThe state shown to the customer.
priorityintThe priority. One to four: low, medium, high, urgent.
departmentobjectThe department. It carries an id and a name.
ratingintThe rating given to the ticket.
created_atstringWhen it was opened.
unreadboolWhether an unread staff reply waits.
last_replyobjectThe last visible message. It carries when and who wrote it.
totalintHow many tickets there are. It comes back under meta.
pageintThe page you are on.
limitintThe page size.
next_pageintThe next page.
Errors 3
ticket_system_disabled422The operator closed the support system.
status_invalid422The state is not one that is known.
insufficient_scope403The key lacks the required scope.
Request
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

post/api/v1/client/tickets
Tickets/CreateTicket it takes attachments

Opens a new support ticket.

Body 9
department_idintreqThe department id.
subjectstringreqThe subject.
messagestringreqThe opening message. Five characters at the least, in plain text with markup stripped.
priorityintThe priority. Medium stands in when left out.
service_idintTies the ticket to a service.
custom_fieldsobjectThe answers to the department fields. It maps a field id to a value, and a tick field takes a list.
credentialsobject[]The access details. Each row carries a group id and a field map, and it is written into the message encrypted.
attachmentsarrayThe attachments. They go as encoded content and an address is refused.
encrypt_messageboolWhether the message body is stored encrypted.
Response fields 201 — data — 13
dataobjectThe ticket opened, in full. Same shape as the read endpoint.
Errors 10
not_found404The service to tie it to is not this account's.
ticket_system_disabled422The operator closed the support system.
subject_required422The subject is empty.
department_invalid422The department is unknown or closed.
message_too_short422The message is under five characters.
custom_field_required422A required department field is empty. Which one comes in the answer's detail.
access_group_invalid422An access row carries an unknown group.
credential_field_required422A required access field is empty.
attachment_invalid422An attachment came as an address or failed validation.
ticket_rejected422A hook refused the ticket.
Request
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

get/api/v1/client/tickets/{id}
Tickets/GetTicket it marks as read

Returns a ticket's heading and the whole run of messages.

Response fields data — 13
ticket_idintThe ticket id.
refstringThe reference the customer quotes.
subjectstringThe subject.
statusstringThe raw state.
statestringThe state shown to the customer.
priorityintThe priority. One to four: low, medium, high, urgent.
departmentobjectThe department. It carries an id and a name.
ratingintThe rating given to the ticket.
created_atstringWhen it was opened.
lockedboolWhether the staff locked it. Replying and reopening stop while it is.
can_replyboolWhether a reply can be written.
related_serviceobjectThe service tied to it. It carries an id, a name, a type and a state.
last_reply_atstringWhen the last message came.
messagesobject[]The visible messages, oldest first. The staff's internal notes never appear.
message_idintThe message id. This is the number given when rating a staff reply.
authorobjectWho wrote it. It carries a kind and a name.
is_htmlboolWhether the body carries markup. It can be true on a staff message.
bodystringThe message itself.
created_atstringWhen it was written.
ratingintThe rating given to this reply. It fills on a staff message alone.
attachmentsobject[]The attachments. Each carries an id, a name and a size, and the content comes from the download endpoint.
credentialsobject[]The access details unlocked. It carries the group and its fields, and a password-type value is marked secret.
Errors 3
not_found404No such ticket, or it belongs to another customer.
ticket_system_disabled422The operator closed the support system.
insufficient_scope403The key lacks the required scope.
Request
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

post/api/v1/client/tickets/{id}/reply
Tickets/ReplyTicket it takes attachments

Adds a reply to an open ticket.

Body 4
messagestringreqThe reply text. Five characters at the least.
credentialsobject[]The access details.
attachmentsarrayThe attachments.
encrypt_messageboolWhether the body is stored encrypted.
Response fields data — 2
ticketobjectThe ticket's new state. A reply moves it back to waiting.
messageobjectThe reply saved. It comes in the shape used in the message run.
Errors 7
not_found404No such ticket, or it belongs to another customer.
ticket_system_disabled422The operator closed the support system.
ticket_not_open422The ticket is closed or locked.
message_too_short422The message is under five characters.
access_group_invalid422The access rows did not pass the schema.
attachment_invalid422An attachment was refused.
reply_rejected422A hook refused the reply.
Request
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

post/api/v1/client/tickets/{id}/close
Tickets/CloseTicket a rating may come with it

Closes a ticket as solved.

Body 1
ratingintA rating given while closing. It counts only while the ticket carries none.
Response fields data — 3
ticket_idintThe ticket id.
statusstringThe new raw state.
statestringThe new state shown.
Errors 3
not_found404No such ticket, or it belongs to another customer.
ticket_system_disabled422The operator closed the support system.
insufficient_scope403The key lacks the required scope.
Request
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

post/api/v1/client/tickets/{id}/reopen
Tickets/ReopenTicket not while locked

Reopens a closed ticket.

Body
No body is needed. The id in the path names the ticket; send an empty body.
Response fields data — 3
ticket_idintThe ticket id.
statusstringThe new raw state.
statestringThe new state shown.
Errors 4
not_found404No such ticket, or it belongs to another customer.
ticket_system_disabled422The operator closed the support system.
ticket_locked422The staff locked the ticket.
insufficient_scope403The key lacks the required scope.
Request
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

post/api/v1/client/tickets/{id}/rating
Tickets/RateTicket once only

Gives the ticket a rating from one to five.

Body 1
ratingintreqThe rating given. One to five.
Response fields data — 2
ticket_idintThe ticket id.
ratingintThe rating written.
Errors 5
not_found404No such ticket, or it belongs to another customer.
ticket_system_disabled422The operator closed the support system.
rating_invalid422The rating is not between one and five.
already_rated422The ticket already carries a rating. It cannot be changed.
insufficient_scope403The key lacks the required scope.
Request
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

post/api/v1/client/tickets/{id}/replies/{rid}/rating
Tickets/RateTicketReply once only

Rates one staff reply on its own.

Body 1
ratingintreqThe rating given. One to five.
Response fields data — 2
message_idintThe reply rated.
ratingintThe rating written.
Errors 5
not_found404No such ticket, or the id given is not a visible staff reply.
ticket_system_disabled422The operator closed the support system.
rating_invalid422The rating is not between one and five.
already_rated422The reply already carries a rating.
insufficient_scope403The key lacks the required scope.
Request
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

get/api/v1/client/tickets/{id}/attachments/{aid}
Tickets/GetTicketAttachment encoded content

Returns a message attachment together with its content.

Response fields data — 6
attachment_idintThe attachment id.
message_idintThe message it hangs from. It can come empty on an older opening record.
namestringThe original file name.
sizeintThe file size.
mimestringThe type worked out from the extension. It falls back to a general value on an unknown one.
contentstringThe file itself. It comes encoded.
Errors 3
not_found404No such ticket or attachment, it is not yours, or it hangs from a hidden note.
ticket_system_disabled422The operator closed the support system.
insufficient_scope403The key lacks the required scope.
Request
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 the detail clears the unread mark

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.

A locked ticket cannot be reopened

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.

A rating is final and cannot be taken back

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.

Secrets go in the access field and not the message body

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.

An attachment cannot be sent as an address

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.

Internal notes never appear

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.

War das hilfreich?

Vielen Dank für Ihre Rückmeldung!

Brauchen Sie weitere Hilfe?

Unser Support-Team ist rund um die Uhr für Sie da, wenn Sie oben nicht fündig werden.