Skip to main content
Getting Started

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]=true Skip 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}