Cookbook: Opening a Ticket from a Monitoring System
Step-by-step guide for automatically creating a support ticket from a monitoring alert: identify the customer from their 3CX PBX host, look up the right technical contact, and open the ticket — all via the BlueRockTEL API.
This cookbook covers the end-to-end flow for a monitoring agent (such as LibreNMS) that detects an issue on a customer's 3CX PBX instance and needs to automatically open a support ticket in BlueRockTEL. The same pattern applies to any alerting system that monitors customer infrastructure.
Prerequisites: You must have a valid API bearer token. See Authentication for details.
export BASE_URL="https://your-instance.bluerocktel.net/api"
export TOKEN="your-bearer-token"
Step 1: Discover Ticket Metadata
Before creating tickets, your monitoring system needs three IDs: a priority, a category, and a sub-category. Fetch them once on startup and store them in your configuration — they rarely change.
Endpoints:
| Endpoint | Purpose |
|---|---|
GET /v1/ticketing/priorities |
List available priorities |
GET /v1/ticketing/categories |
List ticket categories |
GET /v1/ticketing/sub-categories |
List sub-categories with their parent category |
# Fetch priorities
curl -s -X GET "$BASE_URL/v1/ticketing/priorities" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json"
# Fetch categories
curl -s -X GET "$BASE_URL/v1/ticketing/categories" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json"
# Fetch sub-categories
curl -s -X GET "$BASE_URL/v1/ticketing/sub-categories" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json"
import requests
BASE_URL = "https://your-instance.bluerocktel.net/api"
TOKEN = "your-bearer-token"
HEADERS = {"Authorization": f"Bearer {TOKEN}", "Accept": "application/json"}
priorities = requests.get(f"{BASE_URL}/v1/ticketing/priorities", headers=HEADERS).json()
categories = requests.get(f"{BASE_URL}/v1/ticketing/categories", headers=HEADERS).json()
sub_categories = requests.get(f"{BASE_URL}/v1/ticketing/sub-categories", headers=HEADERS).json()
# Pick the IDs that match your monitoring workflow and hard-code them:
PRIORITY_ID = 2 # e.g. "High"
CATEGORY_ID = 5 # e.g. "Infrastructure"
SUB_CATEGORY_ID = 12 # e.g. "PBX / 3CX"
use Illuminate\Support\Facades\Http;
$priorities = Http::withToken($token)->get("{$baseUrl}/v1/ticketing/priorities")->json();
$categories = Http::withToken($token)->get("{$baseUrl}/v1/ticketing/categories")->json();
$subCategories = Http::withToken($token)->get("{$baseUrl}/v1/ticketing/sub-categories")->json();
// Store the matching IDs in your config:
// $priorityId = 2; $categoryId = 5; $subCategoryId = 12;
const headers = { Authorization: `Bearer ${token}`, Accept: "application/json" };
const [priorities, categories, subCategories] = await Promise.all([
fetch(`${BASE_URL}/v1/ticketing/priorities`, { headers }).then(r => r.json()),
fetch(`${BASE_URL}/v1/ticketing/categories`, { headers }).then(r => r.json()),
fetch(`${BASE_URL}/v1/ticketing/sub-categories`, { headers }).then(r => r.json()),
]);
// Store the matching IDs in your config:
// const PRIORITY_ID = 2; const CATEGORY_ID = 5; const SUB_CATEGORY_ID = 12;
Step 2: Identify the Customer
Choose the lookup strategy that matches what your monitoring system knows about the affected host.
| Available identifier | Use |
|---|---|
| Customer name or partial text | Option A — Search by customer name |
Customer account code (e.g. CL2385) |
Option B — Look up by customer account number |
PBX hostname (e.g. acme.3cx.fr) |
Option C — Resolve from PBX hostname |
Option A — Search by customer name
Endpoint: GET /v1/customers/search
The search matches against the customer's full-text index (name, account, contacts, phone numbers, IP addresses, and licence keys). Returns up to 15 results.
Query parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
term |
string | Yes | Search term (spaces become wildcards) |
curl -s -X GET "$BASE_URL/v1/customers/search?term=acme" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json"
response = requests.get(
f"{BASE_URL}/v1/customers/search",
params={"term": "acme"},
headers=HEADERS,
)
results = response.json()
if len(results) == 1:
customer_id = results[0]["id"]
elif len(results) == 0:
raise Exception("No customer found")
else:
# Multiple matches — narrow down by exact name or account
customer_id = next(c["id"] for c in results if c["name"] == "Acme Telecom SAS")
$response = Http::withToken($token)
->get("{$baseUrl}/v1/customers/search", ['term' => 'acme']);
$results = $response->json();
$customerId = $results[0]['id']; // refine if multiple results
const response = await fetch(
`${BASE_URL}/v1/customers/search?term=acme`,
{ headers }
);
const results = await response.json();
const customerId = results[0]?.id;
Response 200:
[
{
"id": 42,
"name": "Acme Telecom SAS",
"customer_account": "CL000042",
"quick_search_label": "Acme Telecom SAS CL000042"
}
]
Option B — Look up by customer account number
Endpoint: GET /v1/customers/lookup
Exact lookup by customer account code. Recommended when your monitoring inventory already stores the customer_account alongside each host.
Query parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
customer_account |
string | Yes | Customer account code (e.g. CL2385) |
curl -s -X GET "$BASE_URL/v1/customers/lookup?customer_account=CL2385" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json"
response = requests.get(
f"{BASE_URL}/v1/customers/lookup",
params={"customer_account": "CL2385"},
headers=HEADERS,
)
data = response.json()
if not data.get("found"):
raise Exception("Customer not found")
customer_id = data["data"]["id"]
$response = Http::withToken($token)
->get("{$baseUrl}/v1/customers/lookup", ['customer_account' => 'CL2385']);
$data = $response->json();
$customerId = $data['data']['id'];
const response = await fetch(
`${BASE_URL}/v1/customers/lookup?customer_account=CL2385`,
{ headers }
);
const data = await response.json();
const customerId = data.found ? data.data.id : null;
Response 200:
{
"found": true,
"data": {
"id": 42,
"name": "Acme Telecom SAS",
"customer_account": "CL2385",
"vip": false,
"on_call_duty": false,
"setup_follow_up": false,
"setup_follow_up_user": null
}
}
Option C — Resolve from PBX hostname
Endpoint: GET /v1/pbx3cx-hosts/lookup
Look up a 3CX host by its hostname. Returns the host record with all associated customers — the most direct path when your monitoring alert carries the PBX FQDN.
Query parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
host_name |
string | Yes | Hostname or FQDN of the 3CX instance (e.g. acme.3cx.fr) |
curl -s -X GET "$BASE_URL/v1/pbx3cx-hosts/lookup?host_name=acme.3cx.fr" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json"
response = requests.get(
f"{BASE_URL}/v1/pbx3cx-hosts/lookup",
params={"host_name": "acme.3cx.fr"},
headers=HEADERS,
)
if response.status_code == 404:
raise Exception("3CX host not found in BlueRockTEL")
host = response.json()
customers = host.get("customers", [])
if not customers:
raise Exception("Host found but no customer is linked to it yet")
customer_id = customers[0]["id"]
print(f"Customer resolved: id={customer_id}, name={customers[0]['name']}")
$response = Http::withToken($token)
->get("{$baseUrl}/v1/pbx3cx-hosts/lookup", ['host_name' => 'acme.3cx.fr']);
if ($response->status() === 404) {
throw new \Exception('3CX host not found');
}
$host = $response->json();
$customerId = $host['customers'][0]['id'] ?? null;
const response = await fetch(
`${BASE_URL}/v1/pbx3cx-hosts/lookup?host_name=acme.3cx.fr`,
{ headers }
);
if (response.status === 404) throw new Error("3CX host not found");
const host = await response.json();
const customerId = host.customers?.[0]?.id;
Response 200:
{
"id": 7,
"host_name": "acme.3cx.fr",
"code": "XK93PL",
"active": true,
"ip_address": "203.0.113.10",
"licence_type": "Enterprise",
"customers": [
{
"id": 42,
"name": "Acme Telecom SAS",
"customer_account": "CL2385"
}
]
}
Option D — List a customer's PBX hosts (optional confirmation)
Endpoint: GET /v1/pbx3cx-hosts/lookup-by-customer
List all 3CX hosts registered for a customer, identified by their account code. Useful to confirm which PBX instances are on record before filing a ticket, or to map a customer account to its hosts.
Query parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
customer_account |
string | Yes | Customer account code (e.g. CL2385) |
curl -s -X GET "$BASE_URL/v1/pbx3cx-hosts/lookup-by-customer?customer_account=CL2385" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json"
response = requests.get(
f"{BASE_URL}/v1/pbx3cx-hosts/lookup-by-customer",
params={"customer_account": "CL2385"},
headers=HEADERS,
)
hosts = response.json().get("data", [])
print(f"{len(hosts)} host(s) registered for this customer")
$response = Http::withToken($token)
->get("{$baseUrl}/v1/pbx3cx-hosts/lookup-by-customer", [
'customer_account' => 'CL2385',
]);
$hosts = $response->json()['data'] ?? [];
const response = await fetch(
`${BASE_URL}/v1/pbx3cx-hosts/lookup-by-customer?customer_account=CL2385`,
{ headers }
);
const { data: hosts } = await response.json();
Response 200:
{
"data": [
{
"id": 7,
"host_name": "acme.3cx.fr",
"code": "XK93PL",
"active": true
}
]
}
Step 3: Create the Ticket
Endpoint: POST /v1/tickets
The customer is passed as query parameters (ticketable_type and ticketable_id). The ticket body accepts HTML; only the tags br, img, ul, ol, li, strong, i, p, and a are preserved — all others are stripped.
Required query parameters:
| Parameter | Type | Description |
|---|---|---|
ticketable_type |
string | Always App\Customer |
ticketable_id |
integer | Customer ID from Step 2 |
Body (JSON):
| Field | Type | Required | Description |
|---|---|---|---|
subject |
string | Yes | Ticket subject (HTML stripped) |
body |
string | Yes | Ticket body (HTML) |
priority_id |
integer | Yes | Priority ID from Step 1 |
ticket_category_id |
integer | Yes | Category ID from Step 1 |
ticket_sub_category_id |
integer | Yes | Sub-category ID from Step 1 |
estimated_resolution_date |
datetime | No | Format Y-m-d H:i:s. Defaults to now + 4 hours |
tags |
array | No | Array of tag strings |
team_id |
integer | No | Route to a specific support team |
close |
boolean | No | Set true to close the ticket immediately after creation |
curl -s -X POST "$BASE_URL/v1/tickets?ticketable_type=App%5CCustomer&ticketable_id=42" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"subject": "3CX offline: acme.3cx.fr",
"body": "<p>Monitoring alert: host <strong>acme.3cx.fr</strong> unreachable since 2026-07-02T08:15:00Z.</p>",
"priority_id": 2,
"ticket_category_id": 5,
"ticket_sub_category_id": 12,
"tags": ["monitoring", "3cx", "auto"]
}'
import datetime
alert_time = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
response = requests.post(
f"{BASE_URL}/v1/tickets",
params={
"ticketable_type": "App\\Customer",
"ticketable_id": customer_id,
},
json={
"subject": f"3CX offline: acme.3cx.fr",
"body": f"<p>Monitoring alert: host <strong>acme.3cx.fr</strong> unreachable since {alert_time}.</p>",
"priority_id": PRIORITY_ID,
"ticket_category_id": CATEGORY_ID,
"ticket_sub_category_id": SUB_CATEGORY_ID,
"tags": ["monitoring", "3cx", "auto"],
},
headers=HEADERS,
)
response.raise_for_status()
ticket = response.json()
print(f"Ticket created: #{ticket['number']} (id={ticket['id']})")
$response = Http::withToken($token)
->post("{$baseUrl}/v1/tickets", [
'ticketable_type' => 'App\\Customer',
'ticketable_id' => $customerId,
'subject' => '3CX offline: acme.3cx.fr',
'body' => '<p>Monitoring alert: host <strong>acme.3cx.fr</strong> unreachable.</p>',
'priority_id' => $priorityId,
'ticket_category_id' => $categoryId,
'ticket_sub_category_id' => $subCategoryId,
'tags' => ['monitoring', '3cx', 'auto'],
]);
$ticket = $response->json();
$ticketId = $ticket['id'];
const ticketResponse = await fetch(
`${BASE_URL}/v1/tickets?ticketable_type=App%5CCustomer&ticketable_id=${customerId}`,
{
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({
subject: "3CX offline: acme.3cx.fr",
body: "<p>Monitoring alert: host <strong>acme.3cx.fr</strong> unreachable.</p>",
priority_id: PRIORITY_ID,
ticket_category_id: CATEGORY_ID,
ticket_sub_category_id: SUB_CATEGORY_ID,
tags: ["monitoring", "3cx", "auto"],
}),
}
);
const ticket = await ticketResponse.json();
const ticketId = ticket.id;
console.log(`Ticket created: #${ticket.number}`);
Response 201:
{
"id": 1024,
"number": "A7KZ2F",
"subject": "3CX offline: acme.3cx.fr",
"open": true,
"origin": "admin",
"priority_id": 2,
"ticket_category_id": 5,
"ticket_sub_category_id": 12,
"tags": "[\"monitoring\",\"3cx\",\"auto\"]",
"estimated_resolution_date": "2026-07-02 12:15:00",
"opened_at": "2026-07-02 08:15:00",
"created_at": "2026-07-02T08:15:00.000000Z"
}
Step 4: Attach a Technical Contact (Optional)
Linking the customer's technical contact to the ticket ensures notifications reach the right person. This step requires two calls: first fetch the customer's contacts to find the technical one, then attach it to the ticket.
Step 4a — List contacts for the customer
Endpoint: GET /v1/contacts
Query parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
contactable_type |
string | Yes | App\Customer (URL-encode the backslash as %5C) |
contactable_id |
integer | Yes | Customer ID from Step 2 |
Filter the results client-side by the role field to find the technical contact. The role field is a free-text string (e.g. "Technical", "IT Manager").
curl -s -X GET "$BASE_URL/v1/contacts?contactable_type=App%5CCustomer&contactable_id=42" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json"
response = requests.get(
f"{BASE_URL}/v1/contacts",
params={
"contactable_type": "App\\Customer",
"contactable_id": customer_id,
},
headers=HEADERS,
)
contacts = response.json()
# Filter for the technical contact (role is a free-text field)
technical_contact = next(
(c for c in contacts if "technical" in (c.get("role") or "").lower()),
None,
)
if technical_contact:
contact_id = technical_contact["id"]
print(f"Technical contact: {technical_contact['first_name']} {technical_contact['last_name']}")
$response = Http::withToken($token)
->get("{$baseUrl}/v1/contacts", [
'contactable_type' => 'App\\Customer',
'contactable_id' => $customerId,
]);
$contacts = $response->json();
$technicalContact = collect($contacts)->first(
fn ($c) => str_contains(strtolower($c['role'] ?? ''), 'technical')
);
$contactId = $technicalContact['id'] ?? null;
const contactsResponse = await fetch(
`${BASE_URL}/v1/contacts?contactable_type=App%5CCustomer&contactable_id=${customerId}`,
{ headers }
);
const contacts = await contactsResponse.json();
const technicalContact = contacts.find(
c => (c.role ?? "").toLowerCase().includes("technical")
);
const contactId = technicalContact?.id;
Response 200:
[
{
"id": 88,
"first_name": "Marie",
"last_name": "Dupont",
"email_address": "m.dupont@acme-telecom.fr",
"role": "Technical Manager",
"mobile_phone": "+33612345678"
}
]
Step 4b — Attach the contact to the ticket
Endpoint: PUT /v1/tickets/{id}/associates
URL parameters:
| Parameter | Type | Description |
|---|---|---|
id |
integer | Ticket ID from Step 3 |
Body (JSON):
| Field | Type | Required | Description |
|---|---|---|---|
associated |
array | Yes | Array of contact IDs to attach |
unassociated |
array | Yes | Array of contact IDs to detach (pass [] if none) |
curl -s -X PUT "$BASE_URL/v1/tickets/1024/associates" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"associated": [88], "unassociated": []}'
if contact_id:
requests.put(
f"{BASE_URL}/v1/tickets/{ticket['id']}/associates",
json={"associated": [contact_id], "unassociated": []},
headers=HEADERS,
).raise_for_status()
print(f"Contact {contact_id} attached to ticket")
if ($contactId) {
Http::withToken($token)
->put("{$baseUrl}/v1/tickets/{$ticketId}/associates", [
'associated' => [$contactId],
'unassociated' => [],
]);
}
if (contactId) {
await fetch(`${BASE_URL}/v1/tickets/${ticketId}/associates`, {
method: "PUT",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({ associated: [contactId], unassociated: [] }),
});
}
Response 201:
{ "response": "1 associated and 0 unassociated" }
Step 5: Add a Reply with Alert Details (Optional)
Attach structured alert data (metrics, log excerpt, timestamps) as the first reply. This makes the ticket immediately actionable for the support team.
Endpoint: POST /v1/replies
Required query parameters:
| Parameter | Type | Description |
|---|---|---|
ticket_id |
integer | Ticket ID from Step 3 |
Body (JSON):
| Field | Type | Required | Description |
|---|---|---|---|
body |
string | Yes | Reply body (HTML) |
close |
boolean | Yes | Set false to keep the ticket open |
send_to_assignee |
boolean | Yes | Notify the assigned technician |
send_to_contacts |
boolean | Yes | Send the reply to the ticket's linked contacts |
customer_visible |
boolean | No | Set false to create an internal note visible only to your team (default: true) |
curl -s -X POST "$BASE_URL/v1/replies?ticket_id=1024" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"body": "<p><strong>Alert details</strong></p><ul><li>Host: acme.3cx.fr</li><li>Time: 2026-07-02 08:14:57 UTC</li><li>Check: ICMP ping — timeout after 30s</li></ul>",
"close": false,
"send_to_assignee": true,
"send_to_contacts": false,
"customer_visible": false
}'
requests.post(
f"{BASE_URL}/v1/replies",
params={"ticket_id": ticket["id"]},
json={
"body": (
"<p><strong>Alert details</strong></p>"
"<ul>"
f"<li>Host: acme.3cx.fr</li>"
f"<li>Time: {alert_time} UTC</li>"
"<li>Check: ICMP ping — timeout after 30s</li>"
"</ul>"
),
"close": False,
"send_to_assignee": True,
"send_to_contacts": False,
"customer_visible": False,
},
headers=HEADERS,
).raise_for_status()
Http::withToken($token)
->post("{$baseUrl}/v1/replies", [
'ticket_id' => $ticketId,
'body' => '<p><strong>Alert details</strong></p><ul><li>Host: acme.3cx.fr</li></ul>',
'close' => false,
'send_to_assignee' => true,
'send_to_contacts' => false,
'customer_visible' => false,
]);
await fetch(`${BASE_URL}/v1/replies?ticket_id=${ticketId}`, {
method: "POST",
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify({
body: "<p><strong>Alert details</strong></p><ul><li>Host: acme.3cx.fr</li></ul>",
close: false,
send_to_assignee: true,
send_to_contacts: false,
customer_visible: false,
}),
});
Response 201:
{
"id": 4096,
"ticket_id": 1024,
"body": "<p><strong>Alert details</strong></p>...",
"origin": "admin",
"created_at": "2026-07-02T08:15:01.000000Z"
}
Complete Workflow (Python)
The following script combines all steps into a single function. It resolves the customer from the PBX hostname, finds the technical contact, opens the ticket, attaches the contact, and adds an alert-detail reply.
import datetime
import requests
BASE_URL = "https://your-instance.bluerocktel.net/api"
TOKEN = "your-bearer-token"
HEADERS = {"Authorization": f"Bearer {TOKEN}", "Accept": "application/json"}
# Hard-coded from one-time Step 1 discovery:
PRIORITY_ID = 2 # High
CATEGORY_ID = 5 # Infrastructure
SUB_CATEGORY_ID = 12 # PBX / 3CX
def open_monitoring_ticket(host_name: str, alert_message: str) -> dict:
alert_time = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
# Step 2C — resolve customer from PBX hostname
r = requests.get(
f"{BASE_URL}/v1/pbx3cx-hosts/lookup",
params={"host_name": host_name},
headers=HEADERS,
)
if r.status_code == 404:
raise Exception(f"Host '{host_name}' is not registered in BlueRockTEL")
host = r.json()
customers = host.get("customers", [])
if not customers:
raise Exception(f"Host '{host_name}' found but has no linked customer")
customer_id = customers[0]["id"]
customer_name = customers[0]["name"]
print(f"Customer: {customer_name} (id={customer_id})")
# Step 4a — find the technical contact
contacts = requests.get(
f"{BASE_URL}/v1/contacts",
params={"contactable_type": "App\\Customer", "contactable_id": customer_id},
headers=HEADERS,
).json()
technical_contact = next(
(c for c in contacts if "technical" in (c.get("role") or "").lower()),
None,
)
# Step 3 — create the ticket
ticket = requests.post(
f"{BASE_URL}/v1/tickets",
params={"ticketable_type": "App\\Customer", "ticketable_id": customer_id},
json={
"subject": f"Monitoring alert: {host_name}",
"body": f"<p>{alert_message}</p><p>Detected at: {alert_time} UTC</p>",
"priority_id": PRIORITY_ID,
"ticket_category_id": CATEGORY_ID,
"ticket_sub_category_id": SUB_CATEGORY_ID,
"tags": ["monitoring", "3cx", "auto"],
},
headers=HEADERS,
)
ticket.raise_for_status()
ticket = ticket.json()
ticket_id = ticket["id"]
print(f"Ticket created: #{ticket['number']} (id={ticket_id})")
# Step 4b — attach technical contact
if technical_contact:
requests.put(
f"{BASE_URL}/v1/tickets/{ticket_id}/associates",
json={"associated": [technical_contact["id"]], "unassociated": []},
headers=HEADERS,
).raise_for_status()
print(f"Contact attached: {technical_contact['first_name']} {technical_contact['last_name']}")
# Step 5 — add alert detail reply
requests.post(
f"{BASE_URL}/v1/replies",
params={"ticket_id": ticket_id},
json={
"body": (
f"<p><strong>Alert details</strong></p>"
f"<ul>"
f"<li>Host: {host_name}</li>"
f"<li>Time: {alert_time} UTC</li>"
f"<li>Message: {alert_message}</li>"
f"</ul>"
),
"close": False,
"send_to_assignee": True,
"send_to_contacts": False,
"customer_visible": False,
},
headers=HEADERS,
).raise_for_status()
return ticket
# Example usage:
# ticket = open_monitoring_ticket("acme.3cx.fr", "ICMP ping timeout after 30s")
What's Next?
-
Avoid duplicate tickets: Before opening a new ticket, check for an existing open one:
GET /v1/tickets/search?filter[ticketable_id]={customer_id}&filter[ticketable_type]=App\Customer&filter[open]=trueSkip ticket creation if one already exists. -
Close on resolution: When the monitoring system clears the alert:
PUT /v1/tickets/close/{ticket_id} -
Log time if a technician intervened:
PUT /v1/tickets/amend/{ticket_id}with{ "time": 30 }(minutes) -
Retrieve the full ticket at any time:
GET /v1/tickets/{ticket_id}
On this page