Managing Tickets

9 vues Markdown

The nine endpoints that open, update, delete and reshape support tickets.

Overview

These nine endpoints deal with the ticket itself: opening one, reading it, changing its fields, deleting it and seeing what happened to it. Replies, notes and custom fields live at their own endpoints, and the detail call does not carry them.

Three reshaping operations sit alongside. The bulk call closes or deletes many at once. Merging gathers a scattered conversation into one ticket. Splitting lifts a subject that wandered in out into a ticket of its own.

This area reaches the client. Opening a ticket, marking one solved and notifying on a split each send an e-mail, and assigning staff tells the person assigned. Weigh that before writing a batch script.

Reference

Listing the Tickets

get/api/v1/admin/tickets
Tickets/GetTickets admin

Returns support tickets, filtered and paged.

Query 10
pageintWhich page.
limitintRecords per page. Clamped between one and a hundred.
searchstringSearches the subject, ticket number, work reference, client name or e-mail.
statusstringFilters by status: open, waiting, process, replied, solved.
department_idintFilters by department.
client_idintFilters by client.
assigned_idintFilters by the staff member assigned.
priorityintFilters by priority.
cdatestringFilters by the date opened.
cdate_opstringWhich way the date comparison runs. Goes together with the date field.
Response fields data[] — 15 + meta
idintThe ticket number.
referencestring | nullThe work reference.
subjectstringThe ticket subject.
statusstringThe ticket status: open, waiting, process, replied, solved.
custom_status_idintAn installation's own status. Zero means the standard one.
priorityintThe ticket priority.
pipeintWhether the ticket arrived by e-mail.
admin_unreadboolWhether staff have yet to read it.
user_unreadboolWhether the client has yet to read it.
created_atstring | nullWhen it was opened.
last_reply_atstring | nullWhen the last reply landed.
assigned_idintThe staff member assigned. Zero means nobody.
departmentobjectThe department: its id and name.
clientobjectThe client: id, name, company and e-mail.
last_replyobjectA preview of the last reply. On an encrypted reply the text arrives empty.
Errors 1
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/tickets?status=waiting&limit=25' \
  -H "Authorization: Bearer $API_KEY"
const url = new URL('https://panel.example.com/api/v1/admin/tickets');
url.searchParams.set('status', 'waiting');

const res  = await fetch(url, {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/tickets?' . http_build_query(['status' => 'waiting', 'limit' => 25]));
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// On an encrypted reply the preview text arrives EMPTY; ciphertext never enters a list.
$rows = Api::Tickets()->GetTickets([], ['status' => 'waiting'])['data'];
$prev = $rows[0]['last_reply']['message'] ?? null;

Opening a Ticket

post/api/v1/admin/tickets
Tickets/CreateTicket admin a notice goes out

Opens a ticket for a client on behalf of staff and writes the first message.

Body 12
client_idintreqThe client the ticket belongs to.
subjectstringreqThe ticket subject.
messagestringreqThe first message. It can carry placeholders that fill from the client record.
service_idintThe service it concerns. It has to be the client's own.
statusstringThe status it opens with: open, waiting, process, replied, solved.
custom_status_idintAn installation's own status.
department_idintThe department it goes to.
priorityintThe ticket priority.
assigned_idintThe staff member to assign. Assigning also tells that person.
lockedboolOpens the ticket locked. The client cannot reply.
encryptboolStores the first message encrypted.
attachmentsstring | object | arrayFiles to attach.
Response fields 201 — data
dataobjectThe ticket opened. Same shape as the detail endpoint.
Errors 10
invalid_client422The client is not valid.
subject_required422The subject is empty.
message_required422The message is empty.
invalid_custom_status422The custom status is not valid.
invalid_service422The service does not belong to this client.
invalid_department422The department is not valid.
invalid_assigned422The staff member is not valid.
blocked_by_gate422A hook refused to let it open.
create_failed422The ticket could not be opened.
insufficient_scope403The key lacks the required scope.
Request
curl -X POST 'https://panel.example.com/api/v1/admin/tickets' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"client_id":2,"subject":"Hos geldiniz","message":"Merhaba {FULL_NAME}, hizmetiniz hazir.","department_id":4}'
const res = await fetch('https://panel.example.com/api/v1/admin/tickets', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    client_id: 2,
    subject: 'Welcome aboard',
    message: 'Hi {FULL_NAME}, your service {SERVICE} is ready.',
    department_id: 4,
  }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/tickets');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'client_id'     => 2,
        'subject'       => 'Welcome aboard',
        'message'       => 'Hi {FULL_NAME}, your service {SERVICE} is ready.',
        'department_id' => 4,
    ]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// Opening a ticket MAILS THE CLIENT; in an import loop every record means one more message.
Api::Tickets()->CreateTicket([
    'client_id' => $uid,
    'subject'   => $subject,
    'message'   => 'Hi {FULL_NAME}, your service {SERVICE} is ready.',
]);

Reading One Ticket

get/api/v1/admin/tickets/{id}
Tickets/GetTicket admin

Returns one ticket with everything it points at resolved.

Response fields data — 20
idintThe ticket number.
referencestring | nullThe work reference.
subjectstringThe ticket subject.
statusstringThe ticket status: open, waiting, process, replied, solved.
custom_status_idintAn installation's own status.
priorityintThe ticket priority.
lockedboolWhether it is locked. A locked ticket takes no client reply.
langstringThe ticket language.
pipeintWhether it arrived by e-mail.
admin_unreadboolWhether staff have yet to read it.
user_unreadboolWhether the client has yet to read it.
created_atstring | nullWhen it was opened.
last_reply_atstring | nullWhen the last reply landed.
senderobjectThe raw sender: name, e-mail, phone and address. Filled in for senders with no account.
departmentobjectThe department: its id and name.
clientobjectThe client: id, name, company and e-mail.
assignedobject | nullThe staff member assigned: id, name and e-mail.
serviceobject | nullThe service it concerns: id, type, name, status and domain.
custom_fieldsarrayThe ticket's custom field values. Encrypted at rest and decoded on the way out.
statsobjectA summary of speed and rating: average response time, response count, average score and how many scored it.
Errors 2
not_found404No such ticket.
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/tickets/402' \
  -H "Authorization: Bearer $API_KEY"
const res  = await fetch(`https://panel.example.com/api/v1/admin/tickets/${id}`, {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/tickets/' . $id);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The detail CARRIES NO MESSAGES; replies, notes and history come from their own endpoints.
$ticket = Api::Tickets()->GetTicket(['id' => $id])['data'];

Updating a Ticket

patch/api/v1/admin/tickets/{id}
Tickets/UpdateTicket admin a notice per field

Applies the fields you send, and each changed field leaves its own trail.

Body 8
statusstringThe new status: open, waiting, process, replied, solved. Solved and in-process both tell the client.
custom_status_idintAn installation's own status.
department_idintMoves the ticket to another department.
priorityintThe new priority.
assigned_idintAssigns a staff member. Zero unassigns, and assigning tells that person.
service_idintThe service it concerns. Zero unlinks it.
client_idintHands the ticket to another client.
lockedboolSets the lock outright. It is a direct set rather than a toggle.
Response fields data + meta
dataobjectThe ticket as it now stands.
appliedstring[]The fields that actually changed. It comes back under meta, and sending the same value does nothing.
Errors 9
invalid_status422The status is not valid.
invalid_custom_status422The custom status is not valid.
invalid_department422The department is not valid.
invalid_priority422The priority is not valid.
invalid_assigned422The staff member is not valid.
invalid_service422The service is not valid.
invalid_client422The target client is not valid.
not_found404No such ticket.
insufficient_scope403The key lacks the required scope.
Request
curl -X PATCH 'https://panel.example.com/api/v1/admin/tickets/402' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"status":"solved","priority":3}'
const res = await fetch(`https://panel.example.com/api/v1/admin/tickets/${id}`, {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ status: 'solved', priority: 3 }),
});

const { meta } = await res.json();
console.log(meta.applied);
$ch = curl_init('https://panel.example.com/api/v1/admin/tickets/' . $id);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PATCH',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode(['status' => 'solved', 'priority' => 3]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// Sending the same value is a NO-OP; meta.applied tells you what really changed.
$r = Api::Tickets()->UpdateTicket(['id' => $id, 'status' => 'solved']);
$changed = $r['meta']['applied'];

Deleting a Ticket

delete/api/v1/admin/tickets/{id}
Tickets/DeleteTicket admin attachments go too

Removes a ticket along with its replies and files.

Response fields data — 2
deletedboolWhether the delete ran.
idintThe number of the ticket removed.
Errors 2
not_found404No such ticket.
insufficient_scope403The key lacks the required scope.
Request
curl -X DELETE 'https://panel.example.com/api/v1/admin/tickets/404' \
  -H "Authorization: Bearer $API_KEY"
const res = await fetch(`https://panel.example.com/api/v1/admin/tickets/${id}`, {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${apiKey}` },
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/tickets/' . $id);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'DELETE',
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// There is NO wastebasket: replies, attachments and custom field files all go with it.
Api::Tickets()->DeleteTicket(['id' => $id]);

Reading the History

get/api/v1/admin/tickets/{id}/history
Tickets/GetTicketHistory admin

Returns what happened on a ticket, in order.

Query 4
pageintWhich page.
limitintRecords per page.
searchstringSearches the events.
cdatestringFilters by date.
Response fields data[] — 5 + meta
idintThe event id.
eventstringWhat happened. A raw key, and turning it into readable text is up to you.
dataobjectDetail belonging to that event.
actorobjectWho did it: id, name and type.
created_atstring | nullWhen it happened.
Errors 2
not_found404No such ticket.
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/tickets/402/history' \
  -H "Authorization: Bearer $API_KEY"
const res  = await fetch(`https://panel.example.com/api/v1/admin/tickets/${id}/history`, {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/tickets/' . $id . '/history');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The event name is a RAW key; turning it into readable text is your screen's job.
$events = Api::Tickets()->GetTicketHistory(['id' => $id])['data'];

Acting on Many Tickets

post/api/v1/admin/tickets/bulk
Tickets/BulkTicketActions admin can block the client too

Closes or deletes several tickets in one call.

Body 2
idsint[]reqThe ticket numbers to work on.
actionstringreqWhat to do: close marks them solved, delete removes them, while block-close and block-delete also bar the client from opening tickets.
Response fields data — 3
actionstringWhat was done.
processedint[]The tickets worked on.
countintHow many were worked on.
Errors 3
ids_required422No ticket was given.
invalid_action422The action is not recognised.
insufficient_scope403The key lacks the required scope.
Request
curl -X POST 'https://panel.example.com/api/v1/admin/tickets/bulk' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"ids":[405,406],"action":"close"}'
const res = await fetch('https://panel.example.com/api/v1/admin/tickets/bulk', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ ids: [405, 406], action: 'close' }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/tickets/bulk');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode(['ids' => [405, 406], 'action' => 'close']),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The password step the panel asks for before deleting is NOT here; scope is the only gate.
Api::Tickets()->BulkTicketActions(['ids' => $ids, 'action' => 'close']);

Merging Tickets

post/api/v1/admin/tickets/merge
Tickets/MergeTickets admin lowest number wins

Gathers several tickets into one.

Body 1
idsint[]reqThe tickets to merge. At least two, and the lowest number becomes the main one.
Response fields data + meta — 2
dataobjectThe main ticket.
primary_idintThe main ticket number. It comes back under meta.
merged_idsint[]The tickets folded in and then removed.
Errors 4
min_two_required422At least two tickets are needed.
blocked_by_gate422A hook refused the merge.
not_found404The main ticket was not found.
insufficient_scope403The key lacks the required scope.
Request
curl -X POST 'https://panel.example.com/api/v1/admin/tickets/merge' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"ids":[419,418]}'
const res = await fetch('https://panel.example.com/api/v1/admin/tickets/merge', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ ids: [419, 418] }),
});

const { meta } = await res.json();
console.log(meta.primary_id);   // 418
$ch = curl_init('https://panel.example.com/api/v1/admin/tickets/merge');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode(['ids' => [419, 418]]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// YOU DO NOT PICK the main ticket: the lowest number wins and the rest are folded in.
$r = Api::Tickets()->MergeTickets(['ids' => [419, 418]]);
$primary = $r['meta']['primary_id'];   // 418

Splitting a Ticket

post/api/v1/admin/tickets/{id}/split
Tickets/SplitTicket admin

Moves the replies you pick into a new ticket.

Body 5
reply_idsint[]reqThe replies to move. All of them have to belong to the source.
subjectstringreqThe new ticket's subject.
department_idintThe new ticket's department.
priorityintThe new ticket's priority.
notify_clientboolTells the client.
Response fields 201 — data + meta — 2
dataobjectThe new ticket.
source_idintThe source ticket number. It comes back under meta.
moved_repliesintHow many replies moved.
Errors 7
replies_required422No reply was picked.
subject_required422The subject is empty.
invalid_department422The department is not valid.
invalid_replies422Some replies do not belong to this ticket.
blocked_by_gate422A hook refused the split.
create_failed422The new ticket could not be opened.
insufficient_scope403The key lacks the required scope.
Request
curl -X POST 'https://panel.example.com/api/v1/admin/tickets/402/split' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"reply_ids":[846,847],"subject":"Fatura sorusu","department_id":4}'
const res = await fetch(`https://panel.example.com/api/v1/admin/tickets/${id}/split`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    reply_ids: [846, 847],
    subject: 'Billing question split out',
    department_id: 4,
  }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/tickets/' . $id . '/split');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'reply_ids'     => [846, 847],
        'subject'       => 'Billing question split out',
        'department_id' => 4,
    ]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The replies MOVE rather than copy: they no longer appear on the source ticket.
$new = Api::Tickets()->SplitTicket([
    'id'        => $id,
    'reply_ids' => $ids,
    'subject'   => 'Billing question split out',
])['data'];

Pitfalls

You do not choose the main ticket

In a merge the ticket with the lowest number becomes the main one, and the order of your array changes nothing. The others have their replies moved across and are then removed. Assuming the number you put first wins gathers the conversation the opposite way round. Read the main number that comes back to see what happened.

An encrypted reply reads empty in a list

The last-reply preview in a ticket list arrives empty when that reply is encrypted. Ciphertext never enters a list, and that is a deliberate line. A client that prints the preview as it comes shows a blank row, so label the empty value as encrypted instead.

Changing a field sends mail

On an update each field runs its own path: marking a ticket solved notifies the client, and assigning staff notifies the person assigned. Sending the same value again does nothing, so mail goes out exactly when a record really changes. Read the applied-field list that comes back to see what went.

The blocking bulk actions reach past the ticket

The two blocking options do more than close and delete: they bar the client from opening tickets. That is a lasting decision which shuts that account out of support, and it is not the same as removing a ticket. When writing a spam clean-up, reach for the plain option rather than the blocking one.

A delete cannot be undone

When a ticket goes, its replies, attachments, note attachments and custom field files go with it. There is no wastebasket and no undo. A script that deletes where it meant to close takes the client's history along; marking the status solved is all closing needs.

Cet article vous a-t-il été utile ?

Merci pour votre retour !

Besoin d'aide supplémentaire ?

Notre équipe d'assistance est disponible 24h/24 pour tout ce que vous ne trouvez pas ci-dessus.