Ticketing

Support queue configuration and staffing, plus the ticketing system routed through those queues.

Support queues

GET /queues

List support queues

User key

Example request

curl -X GET "https://api.openstudio.one/v1/queues" \
  -H "X-Api-User-Key: YOUR_USER_KEY"
<?php
$ch = curl_init('https://api.openstudio.one/v1/queues');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

response = requests.get("https://api.openstudio.one/v1/queues", headers=headers)
data = response.json()
fetch('https://api.openstudio.one/v1/queues', {
  method: 'GET',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY'
  }
})
  .then(res => res.json())
  .then(data => console.log(data));

ⓘRuns a real request from your browser directly to queues. Keys are only held in this page's memory while you're here — never stored, never sent anywhere else.

Response fields

FieldTypeFlagsNotes
queues array of objects — Filtered per caller, same rule as the "get support" queue picker in the dashboard: founders, manage_queues_admin holders, and anyone who operates at least one queue see every queue; everyone else sees only queues they are eligible to submit a ticket into (enabled_categories empty, or a matching structures/users/categories/groups rule).
↳id integer — Matches queue_id on GET /tickets's items.
↳category string — A fixed classification label, not a hierarchical grouping.
Spaces
Administration - Finance - Personnel
Digital Identity and Authorizations
Repository and Archives
Safety & Security
Database
Data and IT management
Maintenance
Other
↳name string — The queue's display name.
↳description string A longer description of what this queue is for.
↳status string — A hidden queue still functions normally (existing tickets, replies, etc. all work) — it just doesn't surface as a destination for new submissions.
visible
hidden
↳recipient_structures array of objects — Which organizational structures can OPERATE this queue's tickets — the same rule POST /tickets's queue_id eligibility check and this API's own operator-resolution logic both read.
↳type string — Only one type exists today.
structures
↳value string — A structure acronym — matches GET /departments.
↳enabled_categories array of objects — Who is allowed to SUBMIT a new ticket into this queue. Empty array means anyone on the account can.
↳type string — Whether this rule targets structures or authorization groups.
structures
groups
↳value string — A structure or group code, matching type.
↳recipient_apps array of objects — Which applications surface a "get support" link pointing at this queue.
↳type string — The rule's own kind — mirrors type above.
↳value string — An application identifier.
↳sla_hours integer — Target resolution time, in hours. Defaults to 72 if never explicitly set.
↳reopen_days_limit integer — How many days after being solved a ticket may still be reopened by its non-operator author (see POST /tickets/{id}/reopen). Defaults to 7 if never explicitly set.

Example response

{
    "success": true,
    "data": {
        "queues": [
            {
                "id": 5,
                "category": "Other",
                "name": "Billing Support",
                "description": null,
                "status": "visible",
                "recipient_structures": [
                    {
                        "type": "structures",
                        "value": "FIN"
                    }
                ],
                "enabled_categories": [],
                "recipient_apps": [],
                "sla_hours": 48,
                "reopen_days_limit": 7
            }
        ]
    },
    "error": null
}

POST /queues

Create a support queue

User key

Request body

FieldTypeFlagsRequiredNotes
name string — Required Max 150 characters.
category string — Required Which broad category this queue falls under, for grouping in the queue picker.
Spaces
Administration - Finance - Personnel
Digital Identity and Authorizations
Repository and Archives
Safety & Security
Database
Data and IT management
Maintenance
Other
description string — Optional Max 250 characters.
recipient_structures array of objects — Optional Same shape as the response field. Omit for none.
enabled_categories array of objects — Optional Same shape as the response field. Omit for "anyone can submit."
recipient_apps array of objects — Optional Same shape as the response field. Omit for none.

Example request

curl -X POST "https://api.openstudio.one/v1/queues" \
  -H "X-Api-User-Key: YOUR_USER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"...","category":"...","description":"...","recipient_structures":[{}],"enabled_categories":[{}],"recipient_apps":[{}]}'
<?php
$ch = curl_init('https://api.openstudio.one/v1/queues');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'name' => '...',
    'category' => '...',
    'description' => '...',
    'recipient_structures' => [
        '0' => [
        ],
    ],
    'enabled_categories' => [
        '0' => [
        ],
    ],
    'recipient_apps' => [
        '0' => [
        ],
    ],
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

body = {
    'name': '...',
    'category': '...',
    'description': '...',
    'recipient_structures': [
        {
        },
    ],
    'enabled_categories': [
        {
        },
    ],
    'recipient_apps': [
        {
        },
    ],
}
response = requests.post("https://api.openstudio.one/v1/queues", headers=headers, json=body)
data = response.json()
fetch('https://api.openstudio.one/v1/queues', {
  method: 'POST',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    'name': '...',
    'category': '...',
    'description': '...',
    'recipient_structures': [
      {
      },
    ],
    'enabled_categories': [
      {
      },
    ],
    'recipient_apps': [
      {
      },
    ],
  })
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
id integer — The new queue's id.
category string — Echoes category.
name string — Echoes name.
description string Echoes description, or null if omitted.
status string — Always visible on creation.
recipient_structures array of objects — Echoes what you sent, or an empty array.
enabled_categories array of objects — Echoes what you sent, or an empty array.
recipient_apps array of objects — Echoes what you sent, or an empty array.
sla_hours integer — Always 72 on creation — this endpoint has no field to set an initial SLA; use PATCH /queues/{id}/settings afterward.
reopen_days_limit integer — Always 7 on creation — use PATCH /queues/{id}/settings to change it.

Example response

{
    "success": true,
    "data": {
        "id": 12,
        "category": "Other",
        "name": "IT Support",
        "description": null,
        "status": "visible",
        "recipient_structures": [],
        "enabled_categories": [],
        "recipient_apps": [],
        "sla_hours": 72,
        "reopen_days_limit": 7
    },
    "error": null
}

GET /queues/{id}

Get one support queue

User key

Path parameters

ParameterTypeNotes
{id}integerThe queue's id, from the id field of an item in GET /queues's queues array (or from the response of POST /queues, right after creating it) — the same id as queue_id on GET /tickets' items.

Example request

curl -X GET "https://api.openstudio.one/v1/queues/{id}" \
  -H "X-Api-User-Key: YOUR_USER_KEY"
<?php
$ch = curl_init('https://api.openstudio.one/v1/queues/{id}');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

response = requests.get("https://api.openstudio.one/v1/queues/{id}", headers=headers)
data = response.json()
fetch('https://api.openstudio.one/v1/queues/{id}', {
  method: 'GET',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY'
  }
})
  .then(res => res.json())
  .then(data => console.log(data));

ⓘRuns a real request from your browser directly to queues/{id}. Keys are only held in this page's memory while you're here — never stored, never sent anywhere else.

Response fields

FieldTypeFlagsNotes
x-note-see string — —

Example response

{
    "success": true,
    "data": {
        "id": 5,
        "category": "Other",
        "name": "Billing Support",
        "description": null,
        "status": "visible",
        "recipient_structures": [],
        "enabled_categories": [],
        "recipient_apps": [],
        "sla_hours": 48,
        "reopen_days_limit": 7
    },
    "error": null
}

PATCH /queues/{id}

Edit a support queue

User key

Path parameters

ParameterTypeNotes
{id}integerThe queue's id, from the id field of an item in GET /queues's queues array (or from the response of POST /queues, right after creating it) — the same id as queue_id on GET /tickets' items.

Request body

FieldTypeFlagsRequiredNotes
name string — Optional Cannot be set to an empty string.
category string — Optional A new category for this queue.
Spaces
Administration - Finance - Personnel
Digital Identity and Authorizations
Repository and Archives
Safety & Security
Database
Data and IT management
Maintenance
Other
description string — Optional A new description for this queue.
status string — Optional Show or hide this queue in the queue picker.
visible
hidden
recipient_structures array of objects — Optional Full replace, not a merge.
enabled_categories array of objects — Optional Full replace, not a merge.
recipient_apps array of objects — Optional Full replace, not a merge.

Example request

curl -X PATCH "https://api.openstudio.one/v1/queues/{id}" \
  -H "X-Api-User-Key: YOUR_USER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"...","category":"...","description":"...","status":"...","recipient_structures":[{}],"enabled_categories":[{}],"recipient_apps":[{}]}'
<?php
$ch = curl_init('https://api.openstudio.one/v1/queues/{id}');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'name' => '...',
    'category' => '...',
    'description' => '...',
    'status' => '...',
    'recipient_structures' => [
        '0' => [
        ],
    ],
    'enabled_categories' => [
        '0' => [
        ],
    ],
    'recipient_apps' => [
        '0' => [
        ],
    ],
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

body = {
    'name': '...',
    'category': '...',
    'description': '...',
    'status': '...',
    'recipient_structures': [
        {
        },
    ],
    'enabled_categories': [
        {
        },
    ],
    'recipient_apps': [
        {
        },
    ],
}
response = requests.patch("https://api.openstudio.one/v1/queues/{id}", headers=headers, json=body)
data = response.json()
fetch('https://api.openstudio.one/v1/queues/{id}', {
  method: 'PATCH',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    'name': '...',
    'category': '...',
    'description': '...',
    'status': '...',
    'recipient_structures': [
      {
      },
    ],
    'enabled_categories': [
      {
      },
    ],
    'recipient_apps': [
      {
      },
    ],
  })
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
x-note-see string — —

Example response

{
    "success": true,
    "data": {
        "id": 5,
        "category": "Other",
        "name": "Billing & Invoicing Support",
        "description": null,
        "status": "visible",
        "recipient_structures": [],
        "enabled_categories": [],
        "recipient_apps": [],
        "sla_hours": 48,
        "reopen_days_limit": 7
    },
    "error": null
}

DELETE /queues/{id}

Delete a support queue

User key

Path parameters

ParameterTypeNotes
{id}integerThe queue's id, from the id field of an item in GET /queues's queues array (or from the response of POST /queues, right after creating it) — the same id as queue_id on GET /tickets' items.

Example request

curl -X DELETE "https://api.openstudio.one/v1/queues/{id}" \
  -H "X-Api-User-Key: YOUR_USER_KEY"
<?php
$ch = curl_init('https://api.openstudio.one/v1/queues/{id}');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

response = requests.delete("https://api.openstudio.one/v1/queues/{id}", headers=headers)
data = response.json()
fetch('https://api.openstudio.one/v1/queues/{id}', {
  method: 'DELETE',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY'
  }
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
deleted boolean — Always true on success. A soft delete — tickets already in this queue are untouched and remain reachable individually; the queue just stops appearing in GET /queues.

Example response

{
    "success": true,
    "data": {
        "deleted": true
    },
    "error": null
}

PATCH /queues/{id}/settings

Edit a queue's SLA, reopen window, and notification rules

User key

Path parameters

ParameterTypeNotes
{id}integerThe queue's id — same as on GET /queues/{id}.

Request body

FieldTypeFlagsRequiredNotes
sla_hours integer — Optional Must be a positive integer.
reopen_days_limit integer — Optional Must be zero or a positive integer.
notifications object — Optional Keyed by trigger type: new_ticket, new_reply, status_change, reopened, assignment, queue_transfer, paused, resumed. Each value is an object with notify_author/notify_operators/notify_assigned, each one of none/web/email/both. Only the trigger types you include are changed — omitted ones keep their existing rule.

Example request

curl -X PATCH "https://api.openstudio.one/v1/queues/{id}/settings" \
  -H "X-Api-User-Key: YOUR_USER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"sla_hours":123,"reopen_days_limit":123,"notifications":{}}'
<?php
$ch = curl_init('https://api.openstudio.one/v1/queues/{id}/settings');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'sla_hours' => 123,
    'reopen_days_limit' => 123,
    'notifications' => [
    ],
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

body = {
    'sla_hours': 123,
    'reopen_days_limit': 123,
    'notifications': {
    },
}
response = requests.patch("https://api.openstudio.one/v1/queues/{id}/settings", headers=headers, json=body)
data = response.json()
fetch('https://api.openstudio.one/v1/queues/{id}/settings', {
  method: 'PATCH',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    'sla_hours': 123,
    'reopen_days_limit': 123,
    'notifications': {
    },
  })
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
id integer — See GET /queues.
sla_hours integer — Reflects sla_hours if it was part of this PATCH.
reopen_days_limit integer — Reflects reopen_days_limit if it was part of this PATCH.
notifications object — Every trigger type this queue has an explicit rule for (not just the ones in this request) — same shape as the request field.

Example response

{
    "success": true,
    "data": {
        "id": 5,
        "sla_hours": 24,
        "reopen_days_limit": 7,
        "notifications": {
            "new_ticket": {
                "notify_author": "both",
                "notify_operators": "web",
                "notify_assigned": "both"
            }
        }
    },
    "error": null
}

GET /queues/{id}/team

Get one structure's team roster for a queue

User key

Path parameters

ParameterTypeNotes
{id}integerThe queue's id — same as on GET /queues/{id}.
Query parameters (1)

Other

ParameterTypeRequiredNotes
?structurestringRequiredA structure acronym — matches GET /departments.

Example request

curl -X GET "https://api.openstudio.one/v1/queues/{id}/team?structure=..." \
  -H "X-Api-User-Key: YOUR_USER_KEY"
<?php
$ch = curl_init('https://api.openstudio.one/v1/queues/{id}/team?structure=...');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

response = requests.get("https://api.openstudio.one/v1/queues/{id}/team?structure=...", headers=headers)
data = response.json()
fetch('https://api.openstudio.one/v1/queues/{id}/team?structure=...', {
  method: 'GET',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY'
  }
})
  .then(res => res.json())
  .then(data => console.log(data));

ⓘRuns a real request from your browser directly to queues/{id}/team. Keys are only held in this page's memory while you're here — never stored, never sent anywhere else.

Response fields

FieldTypeFlagsNotes
team array of objects — Includes the structure's leader automatically, even if they have no explicit role granted — see implicit below.
↳user_id integer — This team member's own user id.
↳first_name string This team member's first name.
↳last_name string This team member's last name.
↳roles array of strings — One or both of operator (handles tickets in this queue) and manager (can manage this structure's team and this queue's settings — a DIFFERENT capability from operator, not a superset).
↳implicit boolean — true for this structure's leader when they hold no explicit POST /queues/{id}/team row — they still always have full operator+manager rights. Cannot be removed via DELETE /queues/{id}/team/{user_id}; change the structure's leader instead.

Example response

{
    "success": true,
    "data": {
        "team": [
            {
                "user_id": 7,
                "first_name": "Jane",
                "last_name": "Doe",
                "roles": [
                    "operator",
                    "manager"
                ],
                "implicit": true
            },
            {
                "user_id": 12,
                "first_name": "John",
                "last_name": "Smith",
                "roles": [
                    "operator"
                ],
                "implicit": false
            }
        ]
    },
    "error": null
}

POST /queues/{id}/team

Add one operator or manager to a structure's team for a queue

User key

Path parameters

ParameterTypeNotes
{id}integerThe queue's id — same as on GET /queues/{id}.

Request body

FieldTypeFlagsRequiredNotes
structure string — Required A structure acronym.
user_id integer — Required Must currently belong to the given structure (same account, same primary department) — rejected with a 422 otherwise.
role string — Required A user can hold both roles at once — call this twice with each role if you want that.
operator
manager

Example request

curl -X POST "https://api.openstudio.one/v1/queues/{id}/team" \
  -H "X-Api-User-Key: YOUR_USER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"structure":"...","user_id":123,"role":"..."}'
<?php
$ch = curl_init('https://api.openstudio.one/v1/queues/{id}/team');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'structure' => '...',
    'user_id' => 123,
    'role' => '...',
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

body = {
    'structure': '...',
    'user_id': 123,
    'role': '...',
}
response = requests.post("https://api.openstudio.one/v1/queues/{id}/team", headers=headers, json=body)
data = response.json()
fetch('https://api.openstudio.one/v1/queues/{id}/team', {
  method: 'POST',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    'structure': '...',
    'user_id': 123,
    'role': '...',
  })
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
x-note-see string — —

Example response

{
    "success": true,
    "data": {
        "team": [
            {
                "user_id": 12,
                "first_name": "John",
                "last_name": "Smith",
                "roles": [
                    "operator"
                ],
                "implicit": false
            }
        ]
    },
    "error": null
}

DELETE /queues/{id}/team/{user_id}

Remove one operator or manager from a structure's team for a queue

User key

Path parameters

ParameterTypeNotes
{id}integerThe queue's id — same as on GET /queues/{id}.
{user_id}integerThe team member's id, from the user_id field of an item in GET /queues/{id}/team's team array. Must not be implicit — see that field's description.
Query parameters (2)

Other

ParameterTypeRequiredNotes
?structurestringRequiredA structure acronym.
?rolestringRequiredRemoves only this ONE role — a user with both operator and manager keeps the other one unless you call this twice.
operator
manager

Example request

curl -X DELETE "https://api.openstudio.one/v1/queues/{id}/team/{user_id}?structure=...&role=..." \
  -H "X-Api-User-Key: YOUR_USER_KEY"
<?php
$ch = curl_init('https://api.openstudio.one/v1/queues/{id}/team/{user_id}?structure=...&role=...');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

response = requests.delete("https://api.openstudio.one/v1/queues/{id}/team/{user_id}?structure=...&role=...", headers=headers)
data = response.json()
fetch('https://api.openstudio.one/v1/queues/{id}/team/{user_id}?structure=...&role=...', {
  method: 'DELETE',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY'
  }
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
x-note-see string — —

Example response

{
    "success": true,
    "data": {
        "team": []
    },
    "error": null
}

Tickets Helpdesk

GET /tickets

List tickets

User key Paginated
Query parameters (9)

Filtering

ParameterTypeRequiredNotes
?filter[status]integerOptionalFilter by exact status.
-1 = Refused
0 = Draft
1 = Open
2 = Solved
3 = Paused
?filter[queue_id]integerOptionalFilter by exact queue id.
?filter[priority]integerOptionalFilter by exact priority.
1 = Low
2 = Medium
3 = High
4 = Critical
?filter[search]stringOptionalCase-insensitive substring match against subject, app, and reply/comment content — combined with OR, then AND'd with every other filter on this list, same as any other filter. An internal note is only matched under scope=operator (every ticket in that scope is one you already have full operator-level access to); under scope=mine, only public replies are searched, matching the same visibility rule GET /tickets/{id}'s own comments array already applies.

Sorting

ParameterTypeRequiredNotes
?sortstringOptionalComma-separated sort fields: id, created_at, modified_at, priority, status. Prefix a field with - for descending, e.g. sort=-created_at. Defaults to -modified_at (most recently active first).

Pagination

ParameterTypeRequiredNotes
?pageintegerOptionalPage number, starting at 1. Defaults to 1.
?limitintegerOptionalRows per page, 1-200. Defaults to 50.

Other

ParameterTypeRequiredNotes
?scopestringOptionalDefaults to "operator" if you operate at least one queue, otherwise "mine". "operator" returns every ticket in a queue you operate, plus any assigned or shared with you. "mine" returns only tickets you personally filed, regardless of queue role. Requesting scope=operator when you don't operate any queue returns a 403.
operator
mine
?expandstringOptionalSet to comments to inline each ticket's comments array directly in this response (one extra query for the whole page, not one per ticket) — avoids a follow-up GET /tickets/{id} per row just to read replies. Same internal-note visibility rule as filter[search] above: included under scope=operator, public-only under scope=mine. Omit for the lighter default shape (no comments key at all).

Example request

curl -X GET "https://api.openstudio.one/v1/tickets?scope=...&filter%5Bstatus%5D=123&filter%5Bqueue_id%5D=123&filter%5Bpriority%5D=123&filter%5Bsearch%5D=...&expand=...&sort=...&page=123&limit=123" \
  -H "X-Api-User-Key: YOUR_USER_KEY"
<?php
$ch = curl_init('https://api.openstudio.one/v1/tickets?scope=...&filter%5Bstatus%5D=123&filter%5Bqueue_id%5D=123&filter%5Bpriority%5D=123&filter%5Bsearch%5D=...&expand=...&sort=...&page=123&limit=123');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

response = requests.get("https://api.openstudio.one/v1/tickets?scope=...&filter%5Bstatus%5D=123&filter%5Bqueue_id%5D=123&filter%5Bpriority%5D=123&filter%5Bsearch%5D=...&expand=...&sort=...&page=123&limit=123", headers=headers)
data = response.json()
fetch('https://api.openstudio.one/v1/tickets?scope=...&filter%5Bstatus%5D=123&filter%5Bqueue_id%5D=123&filter%5Bpriority%5D=123&filter%5Bsearch%5D=...&expand=...&sort=...&page=123&limit=123', {
  method: 'GET',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY'
  }
})
  .then(res => res.json())
  .then(data => console.log(data));

ⓘRuns a real request from your browser directly to tickets. Keys are only held in this page's memory while you're here — never stored, never sent anywhere else.

Response fields

FieldTypeFlagsNotes
tickets array of objects — Ordered by modified_at descending by default (see the sort query param).
↳id integer — Human-facing, per-tenant sequential ticket number — the {id} used in every other ticket route, and what the dashboard itself shows as "Ticket #123". This is the ONLY ticket identifier this API ever exposes; no internal/database identifier is ever returned.
↳subject string — Ticket title, set at creation and changeable via PATCH.
↳status integer — A brand-new ticket starts at 0 (Draft) — the dashboard's own create flow immediately follows up with a status change to 1 (Open) in practice, but a ticket created via POST /tickets starts at 0 too.
-1 = Refused
0 = Draft
1 = Open
2 = Solved
3 = Paused
↳status_label string — Lowercase name of status, e.g. open — a display convenience so a caller doesn't have to hardcode the status-code-to-name mapping themselves.
refused
draft
open
solved
paused
↳priority integer — Defaults to 1 (Low) on a new ticket. No automatic escalation — only changes when a PATCH or bulk update explicitly sets it.
1 = Low
2 = Medium
3 = High
4 = Critical
↳queue_id integer null for an "orphan" ticket — one not (or no longer) assigned to any queue.
↳queue_name string — Nullable, mirrors queue_id — the queue's display name at the time of the request (not cached; always current).
↳app string The application this ticket was filed from/about, if any (e.g. helpdesk, manage_tickets) — an internal app identifier, not a human-readable name. Not validated against a fixed list; any string you send on create/PATCH is stored as-is.
↳author_id string — The ticket author's identifier — a DIFFERENT id space from user_id elsewhere in this API (e.g. author_user_id on PATCH, operator_user_id on assignments). Returned as an opaque identifier; this API has no endpoint to resolve it to a display name yet.
↳assigned_operator_ids array of strings — Same id space as author_id (not user_id) — the operator(s) currently assigned to work this ticket. Empty array if unassigned.
↳created_at string — When the ticket was created, format YYYY-MM-DD HH:MM:SS (server time). Set once at creation — there is no way to set this yourself on create, and no endpoint ever lets you change it afterward.
↳modified_at string — When the ticket (or any of its comments, via a reply) last changed, format YYYY-MM-DD HH:MM:SS (server time). Updated automatically by the server on every write action in this API — there is no field on any request body that sets this directly.
↳comments array of objects — Only present when expand=comments was requested. Same shape as GET /tickets/{id}'s own comments array.

Pagination

FieldTypeNotes
pageintegerCurrent page number.
limitintegerRows per page.
totalintegerTotal matching rows across all pages.
total_pagesintegerTotal number of pages.
next_page_urlstringReady-to-call URL for the next page, same filters applied. Null on the last page.
prev_page_urlstringReady-to-call URL for the previous page, same filters applied. Null on the first page.

Example response

{
    "success": true,
    "data": {
        "tickets": [
            {
                "id": 123,
                "subject": "Cannot access invoices",
                "status": 1,
                "status_label": "open",
                "priority": 2,
                "queue_id": 5,
                "queue_name": "Billing Support",
                "app": null,
                "author_id": "10000042",
                "assigned_operator_ids": [
                    "10000001"
                ],
                "created_at": "2026-08-10 09:12:00",
                "modified_at": "2026-08-11 14:03:00"
            }
        ]
    },
    "error": null,
    "pagination": {
        "page": 1,
        "limit": 50,
        "total": 134,
        "total_pages": 3,
        "next_page_url": "https://api.openstudio.one/v1/departments?page=2&limit=50",
        "prev_page_url": null
    }
}

POST /tickets

Create a ticket

User key

Request body

FieldTypeFlagsRequiredNotes
subject string — Required Ticket title.
content string — Required The first message/description of the issue — becomes the ticket's first comment (public, not internal). Stored and returned exactly as sent — plain text or HTML both work, this API doesn't require or convert either way.
queue_id integer — Required Must be an active (non-deleted, status=1), on-account queue, AND one you're eligible to file into per that queue's recipient rules (same structures:/users:/categories:/groups: rules the dashboard's own "New Ticket" form uses to build its dropdown) — UNLESS you operate at least one queue yourself, in which case eligibility isn't checked (an operator may file into any active queue on the account, matching the dashboard's own staff "create on behalf" flow).
app string — Optional Optional related application code. Free text, not validated against a fixed list.
author_user_id integer — Optional Create the ticket on behalf of ANOTHER user — that user's user_id. Only honored when you're an operator on at least one queue; silently ignored (the ticket is created as yourself) otherwise, same as a non-operator never seeing this option in the dashboard at all. A nonexistent or cross-account id is rejected with a 422, never silently accepted. Note: the ticket's author becomes this user, but the first comment (your content) is still recorded as written by YOU, the caller — matching the dashboard's own "create on behalf" behavior exactly.

Example request

curl -X POST "https://api.openstudio.one/v1/tickets" \
  -H "X-Api-User-Key: YOUR_USER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"subject":"...","content":"...","queue_id":123,"app":"...","author_user_id":123}'
<?php
$ch = curl_init('https://api.openstudio.one/v1/tickets');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'subject' => '...',
    'content' => '...',
    'queue_id' => 123,
    'app' => '...',
    'author_user_id' => 123,
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

body = {
    'subject': '...',
    'content': '...',
    'queue_id': 123,
    'app': '...',
    'author_user_id': 123,
}
response = requests.post("https://api.openstudio.one/v1/tickets", headers=headers, json=body)
data = response.json()
fetch('https://api.openstudio.one/v1/tickets', {
  method: 'POST',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    'subject': '...',
    'content': '...',
    'queue_id': 123,
    'app': '...',
    'author_user_id': 123,
  })
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
id integer — The new ticket's id (see GET /tickets' id field).
subject string — Echoes the subject you sent.
status integer — Always 0 (Draft) on creation — see the note on GET /tickets' status field.
status_label string — Always draft on creation.
priority integer — Always 1 (Low) on creation — this endpoint has no field to set an initial priority; use PATCH afterward if needed.
1
2
3
4
queue_id integer — Echoes the queue_id you sent.
queue_name string — The queue's current display name.
app string — Echoes the app you sent, or null if omitted.
author_id string — Your own identifier — UNLESS you sent a valid author_user_id as an operator, in which case this identifies that user instead.
assigned_operator_ids array of strings — Always an empty array on creation — a brand-new ticket has no operator assigned yet.
created_at string — Set by the server at creation time. Not settable by the caller.
modified_at string — Equal to created_at on a freshly-created ticket. Not settable by the caller.

Example response

{
    "success": true,
    "data": {
        "id": 124,
        "subject": "Cannot access invoices",
        "status": 0,
        "status_label": "draft",
        "priority": 1,
        "queue_id": 5,
        "queue_name": "Billing Support",
        "app": null,
        "author_id": "10000042",
        "assigned_operator_ids": [],
        "created_at": "2026-08-20 10:00:00",
        "modified_at": "2026-08-20 10:00:00"
    },
    "error": null
}

GET /tickets/{id}

Get one ticket, with its comments

User key

Path parameters

ParameterTypeNotes
{id}integerThe ticket's id, from the id field of an item in GET /tickets' tickets array (or from the response of POST /tickets, right after creating it).

Example request

curl -X GET "https://api.openstudio.one/v1/tickets/{id}" \
  -H "X-Api-User-Key: YOUR_USER_KEY"
<?php
$ch = curl_init('https://api.openstudio.one/v1/tickets/{id}');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

response = requests.get("https://api.openstudio.one/v1/tickets/{id}", headers=headers)
data = response.json()
fetch('https://api.openstudio.one/v1/tickets/{id}', {
  method: 'GET',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY'
  }
})
  .then(res => res.json())
  .then(data => console.log(data));

ⓘRuns a real request from your browser directly to tickets/{id}. Keys are only held in this page's memory while you're here — never stored, never sent anywhere else.

Response fields

FieldTypeFlagsAccessNotes
id integer — — See GET /tickets' id field.
subject string — Editable See GET /tickets.
status integer — Editable See GET /tickets.
-1 = Refused
0 = Draft
1 = Open
2 = Solved
3 = Paused
status_label string — — See GET /tickets.
refused
draft
open
solved
paused
priority integer — Editable See GET /tickets.
1 = Low
2 = Medium
3 = High
4 = Critical
queue_id integer Editable See GET /tickets.
queue_name string — See GET /tickets.
app string Editable See GET /tickets.
author_id string — — See GET /tickets. Settable indirectly via PATCH's author_user_id field (a different field name — see that section).
assigned_operator_ids array of strings — — See GET /tickets. Not directly settable here — use the assignments endpoints instead.
created_at string — — Set once at creation, never caller-settable. See GET /tickets.
modified_at string — — Set by the server on every write, never caller-settable. See GET /tickets.
comments array of objects — — Ordered oldest first. Internal notes are only included when you're an operator on this ticket — omitted entirely for the author or a read-only share, same as the dashboard's own comment list.
↳id integer — — The comment's own identifier — unlike ticket ids, comments have no separate "display id" concept anywhere in the product, so this IS the identifier to use, e.g. in PATCH /tickets/{id}/comments/{comment_id} or divide's comment_ids.
↳author_id string — — The commenter's identifier — a DIFFERENT id space from user_id elsewhere in this API.
↳author_user_id integer — The commenter's user_id, in the SAME id space as GET /me's own users.user_id. null only if author_id no longer resolves to a real person.
↳author_name string — The commenter's resolved display name. null if resolution failed.
↳content string — — The comment body, exactly as originally submitted (or last edited via PATCH /tickets/{id}/comments/{comment_id}) — returned as-is, not sanitized or reformatted by this API. Depending on how it was posted, this can be plain text with literal newlines, or real HTML (<p>, <strong>, <a href="...">, etc.) from the dashboard's rich-text reply editor. Render or sanitize on your end according to what you're building — don't assume either shape.
↳is_internal boolean — — true for an operator-only note, never visible to the ticket's author/a read-only share (see this array's own top-level description).
↳is_staff boolean — — true if the commenter was acting as an operator on this ticket at the moment they posted — independent of is_internal (an operator can post a normal, public reply too).
↳created_at string — — When posted — updated to the edit time if the comment was later changed via PATCH /tickets/{id}/comments/{comment_id} (matches the dashboard's own behavior of re-stamping the date on edit).
parent object — Present only if THIS ticket is itself a child of another one. null otherwise.
↳id integer — — The parent ticket's id.
↳subject string — — The parent ticket's subject.
children array of objects — — Tickets created as children OF this one. A non-operator (the author, or a read-only share) only sees entries where parent_visible is true — an operator always sees every child regardless, same as the dashboard.
↳id integer — — The child ticket's id.
↳subject string — — The child ticket's title.
↳status integer — — The child ticket's own status.
-1
0
1
2
3
↳parent_visible boolean — — Whether this ticket's non-operator author is allowed to know this child exists — set when the child was created/linked, changeable via PATCH /tickets/{id}/children/{child_id}.
linked_ticket_ids array of integers — — Ids of tickets linked to this one via POST /tickets/{id}/links (generic, non-parent/child relations only — parent/child relations show up in parent/children instead, not here).
shares array of objects — — Present ONLY when you're an operator on this ticket — omitted from the response entirely for a non-operator, same as the dashboard's share panel not being shown to them at all.
↳id integer — — The share's own identifier — use this in DELETE /tickets/{id}/shares/{share_id}.
↳target_type string — — Whether this share was granted to one specific user or to everyone in a queue.
user
queue
↳target_id integer — — The target user's or target queue's id, matching target_type.
↳permission string — — The access level this share grants — note that edit alone (with role = user) still does not make the target an operator; see role below.
read
edit
↳role string — — role = operator is what actually grants moderator-level access on this ticket via the share — role = user (even with permission = edit) does not.
user
operator
watching boolean — — Whether YOU (the authenticated caller) are currently watching this ticket — your own watch state only, not a list of every watcher.
assigned_operators array of objects — — assigned_operator_ids resolved to display names, in the same order — one entry per id, always the same length as assigned_operator_ids.
↳id string — — Same HR id as the matching assigned_operator_ids entry.
↳user_id integer — The same operator's user_id, the SAME id space as GET /me's own users.user_id. null if the id no longer resolves to a real person.
↳name string — null if the id no longer resolves to a real person (e.g. a deleted account) — never omitted, so this array always lines up 1:1 with assigned_operator_ids.
assigned_to_me boolean — — Whether the CALLER themselves is among assigned_operator_ids — resolved server-side so callers never need to bridge this against GET /me's personal_code by hand.

Example response

{
    "success": true,
    "data": {
        "id": 123,
        "subject": "Cannot access invoices",
        "status": 1,
        "status_label": "open",
        "priority": 2,
        "queue_id": 5,
        "queue_name": "Billing Support",
        "app": null,
        "author_id": "10000042",
        "assigned_operator_ids": [
            "10000001"
        ],
        "created_at": "2026-08-10 09:12:00",
        "modified_at": "2026-08-11 14:03:00",
        "comments": [
            {
                "id": 501,
                "author_id": "10000042",
                "content": "I get a 403 opening any invoice.\r\n\r\nHappens every time, not just once.",
                "is_internal": false,
                "is_staff": false,
                "created_at": "2026-08-10 09:12:00"
            },
            {
                "id": 502,
                "author_id": "10000001",
                "content": "<p>Looking into it now.</p><p>Can you confirm which <strong>invoice number</strong> this happens on?</p>",
                "is_internal": false,
                "is_staff": true,
                "created_at": "2026-08-11 14:03:00"
            }
        ],
        "parent": null,
        "children": [],
        "linked_ticket_ids": [],
        "shares": [],
        "watching": false,
        "assigned_operators": [
            {
                "id": "10000001",
                "name": "Jane Doe"
            }
        ],
        "assigned_to_me": false
    },
    "error": null
}

PATCH /tickets/{id}

Update a ticket (status, priority, subject, app, queue, author)

User key

Path parameters

ParameterTypeNotes
{id}integerThe ticket's id, from the id field of an item in GET /tickets' tickets array (or from the response of POST /tickets, right after creating it).

Request body

FieldTypeFlagsRequiredNotes
status integer — Optional Requires operator access. Any value outside -1/0/1/2 is rejected with a 422, never silently coerced. 3 (Paused) is deliberately NOT settable here — use the dedicated POST /tickets/{id}/pause endpoint instead, which also records paused_reason/paused_at; setting status directly would produce a "paused" ticket with none of that bookkeeping. For resume/reopen, similarly prefer POST /tickets/{id}/resume/POST /tickets/{id}/reopen over a raw status write — those apply extra rules this field bypasses (e.g. reopen's time-window check for a non-operator author).
-1 = Refused
0 = Draft
1 = Open
2 = Solved
priority integer — Optional Requires operator access. Any value outside 1-4 is rejected with a 422, never silently coerced (e.g. a non-numeric string casts to 0, which then fails this same check).
1 = Low
2 = Medium
3 = High
4 = Critical
subject string — Optional Requires operator access. Cannot be set to an empty string.
app string — Optional Requires operator access. Free text, not validated against a fixed list; send an empty string to clear it.
queue_id integer — Optional Requires operator access on the ticket's CURRENT queue — except for an orphan ticket (queue_id was already 0/null), which requires founder access instead, mirroring the dashboard's separate founder-only "assign orphan" action (an orphan ticket has no queue for anyone to be an "operator" of yet). Must refer to an active, on-account queue — validated with a 422, not silently ignored. Moving queues resets assigned_operator_ids to empty and forces status back to Open (1), same side effect as the dashboard's own change-queue action.
author_user_id integer — Optional Requires operator access. Must be the id of a user belonging to your own account — validated with a 422 if not, never silently accepted. Reflected back as author_id in the response.

Example request

curl -X PATCH "https://api.openstudio.one/v1/tickets/{id}" \
  -H "X-Api-User-Key: YOUR_USER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status":123,"priority":123,"subject":"...","app":"...","queue_id":123,"author_user_id":123}'
<?php
$ch = curl_init('https://api.openstudio.one/v1/tickets/{id}');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'status' => 123,
    'priority' => 123,
    'subject' => '...',
    'app' => '...',
    'queue_id' => 123,
    'author_user_id' => 123,
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

body = {
    'status': 123,
    'priority': 123,
    'subject': '...',
    'app': '...',
    'queue_id': 123,
    'author_user_id': 123,
}
response = requests.patch("https://api.openstudio.one/v1/tickets/{id}", headers=headers, json=body)
data = response.json()
fetch('https://api.openstudio.one/v1/tickets/{id}', {
  method: 'PATCH',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    'status': 123,
    'priority': 123,
    'subject': '...',
    'app': '...',
    'queue_id': 123,
    'author_user_id': 123,
  })
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
id integer — See GET /tickets' id field.
subject string — The ticket's subject after this update (unchanged fields keep their prior value).
status integer — The ticket's status after this update.
-1
0
1
2
3
status_label string — Lowercase name of status, above.
refused
draft
open
solved
paused
priority integer — The ticket's priority after this update.
1
2
3
4
queue_id integer The ticket's queue after this update.
queue_name string — Nullable, mirrors queue_id.
app string The catalog app this ticket is linked to, if any — clear it by sending an empty string.
author_id string — Reflects author_user_id if that field was part of this PATCH, otherwise unchanged.
assigned_operator_ids array of strings — Reset to an empty array if this PATCH changed queue_id, otherwise unchanged.
created_at string — Unchanged by this (or any) endpoint — a PATCH never touches created_at.
modified_at string — Set by the server to the current time as part of this update. Not settable by the caller — there is no field named modified_at (or similar) accepted anywhere in this request body; any such value in the JSON you send would simply be ignored, not applied.

Example response

{
    "success": true,
    "data": {
        "id": 123,
        "subject": "Cannot access invoices",
        "status": 2,
        "status_label": "solved",
        "priority": 2,
        "queue_id": 5,
        "queue_name": "Billing Support",
        "app": null,
        "author_id": "10000042",
        "assigned_operator_ids": [
            "10000001"
        ],
        "created_at": "2026-08-10 09:12:00",
        "modified_at": "2026-08-20 10:10:00"
    },
    "error": null
}

POST /tickets/{id}/pause

Pause an open ticket

User key

Path parameters

ParameterTypeNotes
{id}integerThe ticket's id — same as on GET /tickets/{id}.

Request body

FieldTypeFlagsRequiredNotes
reason string — Optional Any other value is silently treated as other, never rejected — matches the dashboard's own fallback for an unrecognized reason. Defaults to other if omitted.
testing
multi_app_change
other
reason_custom string — Optional Free text, used only when reason = other (ignored otherwise, even if sent). Truncated to 255 characters.

Example request

curl -X POST "https://api.openstudio.one/v1/tickets/{id}/pause" \
  -H "X-Api-User-Key: YOUR_USER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason":"...","reason_custom":"..."}'
<?php
$ch = curl_init('https://api.openstudio.one/v1/tickets/{id}/pause');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'reason' => '...',
    'reason_custom' => '...',
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

body = {
    'reason': '...',
    'reason_custom': '...',
}
response = requests.post("https://api.openstudio.one/v1/tickets/{id}/pause", headers=headers, json=body)
data = response.json()
fetch('https://api.openstudio.one/v1/tickets/{id}/pause', {
  method: 'POST',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    'reason': '...',
    'reason_custom': '...',
  })
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
id integer — Same shape as PATCH's response — status will be 3 (paused).

Example response

{
    "success": true,
    "data": {
        "id": 123,
        "subject": "Cannot access invoices",
        "status": 3,
        "status_label": "paused",
        "priority": 2,
        "queue_id": 5,
        "queue_name": "Billing Support",
        "app": null,
        "author_id": "10000042",
        "assigned_operator_ids": [
            "10000001"
        ],
        "created_at": "2026-08-10 09:12:00",
        "modified_at": "2026-08-20 10:11:00"
    },
    "error": null
}

POST /tickets/{id}/resume

Resume a paused ticket

User key

Path parameters

ParameterTypeNotes
{id}integerThe ticket's id — same as on GET /tickets/{id}.

Example request

curl -X POST "https://api.openstudio.one/v1/tickets/{id}/resume" \
  -H "X-Api-User-Key: YOUR_USER_KEY"
<?php
$ch = curl_init('https://api.openstudio.one/v1/tickets/{id}/resume');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

response = requests.post("https://api.openstudio.one/v1/tickets/{id}/resume", headers=headers)
data = response.json()
fetch('https://api.openstudio.one/v1/tickets/{id}/resume', {
  method: 'POST',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY'
  }
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
id integer — Same shape as PATCH's response — status will be 1 (open). 422 if the ticket wasn't Paused (status 3) to begin with.

Example response

{
    "success": true,
    "data": {
        "id": 123,
        "status": 1,
        "status_label": "open"
    },
    "error": null
}

POST /tickets/{id}/reopen

Reopen a solved ticket

User key

Path parameters

ParameterTypeNotes
{id}integerThe ticket's id — same as on GET /tickets/{id}.

Example request

curl -X POST "https://api.openstudio.one/v1/tickets/{id}/reopen" \
  -H "X-Api-User-Key: YOUR_USER_KEY"
<?php
$ch = curl_init('https://api.openstudio.one/v1/tickets/{id}/reopen');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

response = requests.post("https://api.openstudio.one/v1/tickets/{id}/reopen", headers=headers)
data = response.json()
fetch('https://api.openstudio.one/v1/tickets/{id}/reopen', {
  method: 'POST',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY'
  }
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
id integer — Same shape as PATCH's response — status will be 1 (open). 422 if the ticket wasn't Solved (status 2). 403 if you're not an operator on this ticket AND (you're not the author, OR you are but the queue's reopen time-window since the ticket was last modified has already passed).

Example response

{
    "success": true,
    "data": {
        "id": 123,
        "status": 1,
        "status_label": "open"
    },
    "error": null
}

GET /tickets/{id}/assignable-operators

List who can be assigned to a ticket

User key

Example request

curl -X GET "https://api.openstudio.one/v1/tickets/{id}/assignable-operators" \
  -H "X-Api-User-Key: YOUR_USER_KEY"
<?php
$ch = curl_init('https://api.openstudio.one/v1/tickets/{id}/assignable-operators');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

response = requests.get("https://api.openstudio.one/v1/tickets/{id}/assignable-operators", headers=headers)
data = response.json()
fetch('https://api.openstudio.one/v1/tickets/{id}/assignable-operators', {
  method: 'GET',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY'
  }
})
  .then(res => res.json())
  .then(data => console.log(data));

ⓘRuns a real request from your browser directly to tickets/{id}/assignable-operators. Keys are only held in this page's memory while you're here — never stored, never sent anywhere else.

Response fields

FieldTypeFlagsNotes
operators array of objects — Empty if the ticket is orphaned (no queue) or its queue has no structures/members configured.
↳user_id integer — The operator's own user id.
↳name string — The operator's display name.

Example response

{
    "success": true,
    "data": {
        "operators": [
            {
                "user_id": 42,
                "name": "Jane Doe"
            }
        ]
    },
    "error": null
}

POST /tickets/{id}/assignments

Assign an operator to a ticket

User key

Path parameters

ParameterTypeNotes
{id}integerThe ticket's id — same as on GET /tickets/{id}.

Request body

FieldTypeFlagsRequiredNotes
operator_user_id integer — Optional Omit entirely, or send the literal string me, to assign yourself — otherwise the id of a user belonging to your own account (validated with a 422 if not; a nonexistent or cross-account id is never silently accepted). Requires YOU to have operator access on this ticket, regardless of who's actually being assigned — assigning a colleague still requires you, the caller, to be an operator here.

Example request

curl -X POST "https://api.openstudio.one/v1/tickets/{id}/assignments" \
  -H "X-Api-User-Key: YOUR_USER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"operator_user_id":123}'
<?php
$ch = curl_init('https://api.openstudio.one/v1/tickets/{id}/assignments');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'operator_user_id' => 123,
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

body = {
    'operator_user_id': 123,
}
response = requests.post("https://api.openstudio.one/v1/tickets/{id}/assignments", headers=headers, json=body)
data = response.json()
fetch('https://api.openstudio.one/v1/tickets/{id}/assignments', {
  method: 'POST',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    'operator_user_id': 123,
  })
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
id integer — The updated ticket (same shape as PATCH's response) — check assigned_operator_ids for the new assignment. Already being assigned is a harmless no-op (200, unchanged), not an error.

Example response

{
    "success": true,
    "data": {
        "id": 123,
        "assigned_operator_ids": [
            "10000001"
        ]
    },
    "error": null
}

DELETE /tickets/{id}/assignments/{operator_id}

Unassign an operator from a ticket

User key

Path parameters

ParameterTypeNotes
{id}integerThe ticket's id — same as on GET /tickets/{id}.
{operator_id}stringThe operator to unassign — either the literal string me, or an id from that ticket's assigned_operator_ids array (see GET /tickets/{id}).

Example request

curl -X DELETE "https://api.openstudio.one/v1/tickets/{id}/assignments/{operator_id}" \
  -H "X-Api-User-Key: YOUR_USER_KEY"
<?php
$ch = curl_init('https://api.openstudio.one/v1/tickets/{id}/assignments/{operator_id}');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

response = requests.delete("https://api.openstudio.one/v1/tickets/{id}/assignments/{operator_id}", headers=headers)
data = response.json()
fetch('https://api.openstudio.one/v1/tickets/{id}/assignments/{operator_id}', {
  method: 'DELETE',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY'
  }
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
id integer — The updated ticket (same shape as PATCH's response). {operator_id} in the URL is the operator's id, or the literal me — NOT the same {id} as the ticket earlier in the path; the two are unrelated values. Unassigning someone not currently assigned is a harmless no-op (200, unchanged), not an error.

Example response

{
    "success": true,
    "data": {
        "id": 123,
        "assigned_operator_ids": []
    },
    "error": null
}

POST /tickets/{id}/watchers

Watch a ticket

User key

Path parameters

ParameterTypeNotes
{id}integerThe ticket's id — same as on GET /tickets/{id}.

Example request

curl -X POST "https://api.openstudio.one/v1/tickets/{id}/watchers" \
  -H "X-Api-User-Key: YOUR_USER_KEY"
<?php
$ch = curl_init('https://api.openstudio.one/v1/tickets/{id}/watchers');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

response = requests.post("https://api.openstudio.one/v1/tickets/{id}/watchers", headers=headers)
data = response.json()
fetch('https://api.openstudio.one/v1/tickets/{id}/watchers', {
  method: 'POST',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY'
  }
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
watching boolean — Always true on success — your own watch state after this call. No operator access required; any user with at least read access to this ticket can watch it (same as the dashboard, where the watch toggle isn't restricted to operators).

Example response

{
    "success": true,
    "data": {
        "watching": true
    },
    "error": null
}

DELETE /tickets/{id}/watchers

Unwatch a ticket

User key

Path parameters

ParameterTypeNotes
{id}integerThe ticket's id — same as on GET /tickets/{id}.

Example request

curl -X DELETE "https://api.openstudio.one/v1/tickets/{id}/watchers" \
  -H "X-Api-User-Key: YOUR_USER_KEY"
<?php
$ch = curl_init('https://api.openstudio.one/v1/tickets/{id}/watchers');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

response = requests.delete("https://api.openstudio.one/v1/tickets/{id}/watchers", headers=headers)
data = response.json()
fetch('https://api.openstudio.one/v1/tickets/{id}/watchers', {
  method: 'DELETE',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY'
  }
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
watching boolean — Always false on success.

Example response

{
    "success": true,
    "data": {
        "watching": false
    },
    "error": null
}

POST /tickets/{id}/shares

Share a ticket with a user or queue

User key

Path parameters

ParameterTypeNotes
{id}integerThe ticket's id — same as on GET /tickets/{id}.

Request body

FieldTypeFlagsRequiredNotes
target_type string — Required Any other value is rejected with a 422.
user
queue
target_id integer — Required The target user's or target queue's id, matching target_type — validated to actually exist on your account with a 422 if not.
permission string — Optional Defaults to read if omitted. Any other value is rejected with a 422.
read
edit
role string — Optional Defaults to user if omitted. Any other value is rejected with a 422. Only role = operator grants moderator-level access on this ticket via the share (permission = edit alone, with role = user, does NOT).
user
operator

Example request

curl -X POST "https://api.openstudio.one/v1/tickets/{id}/shares" \
  -H "X-Api-User-Key: YOUR_USER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"target_type":"...","target_id":123,"permission":"...","role":"..."}'
<?php
$ch = curl_init('https://api.openstudio.one/v1/tickets/{id}/shares');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'target_type' => '...',
    'target_id' => 123,
    'permission' => '...',
    'role' => '...',
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

body = {
    'target_type': '...',
    'target_id': 123,
    'permission': '...',
    'role': '...',
}
response = requests.post("https://api.openstudio.one/v1/tickets/{id}/shares", headers=headers, json=body)
data = response.json()
fetch('https://api.openstudio.one/v1/tickets/{id}/shares', {
  method: 'POST',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    'target_type': '...',
    'target_id': 123,
    'permission': '...',
    'role': '...',
  })
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
id integer — The share's own identifier — use this in DELETE /tickets/{id}/shares/{share_id}.
target_type string — Echoes what you sent.
user
queue
target_id integer — Echoes what you sent.
permission string — Echoes what you sent (or the read default).
read
edit
role string — Echoes what you sent (or the user default).
user
operator

Example response

{
    "success": true,
    "data": {
        "id": 9,
        "target_type": "user",
        "target_id": 10000025,
        "permission": "read",
        "role": "user"
    },
    "error": null
}

DELETE /tickets/{id}/shares/{share_id}

Remove a ticket share

User key

Path parameters

ParameterTypeNotes
{id}integerThe ticket's id — same as on GET /tickets/{id}.
{share_id}integerOne share's own id, from the id field of an item in that ticket's shares array (see GET /tickets/{id}), or from the response of POST /tickets/{id}/shares right after creating it.

Example request

curl -X DELETE "https://api.openstudio.one/v1/tickets/{id}/shares/{share_id}" \
  -H "X-Api-User-Key: YOUR_USER_KEY"
<?php
$ch = curl_init('https://api.openstudio.one/v1/tickets/{id}/shares/{share_id}');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

response = requests.delete("https://api.openstudio.one/v1/tickets/{id}/shares/{share_id}", headers=headers)
data = response.json()
fetch('https://api.openstudio.one/v1/tickets/{id}/shares/{share_id}', {
  method: 'DELETE',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY'
  }
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
deleted boolean — Always true — removing a share id that doesn't exist (already removed, or never existed) is a harmless no-op, not an error.

Example response

{
    "success": true,
    "data": {
        "deleted": true
    },
    "error": null
}

POST /tickets/{id}/links

Link two tickets (generic, non-parent/child)

User key

Path parameters

ParameterTypeNotes
{id}integerThe ticket's id — same as on GET /tickets/{id}.

Request body

FieldTypeFlagsRequiredNotes
target_ticket_id integer — Required The other ticket's id — must exist on your account and differ from this ticket's own {id}, both enforced with a 422/404.

Example request

curl -X POST "https://api.openstudio.one/v1/tickets/{id}/links" \
  -H "X-Api-User-Key: YOUR_USER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"target_ticket_id":123}'
<?php
$ch = curl_init('https://api.openstudio.one/v1/tickets/{id}/links');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'target_ticket_id' => 123,
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

body = {
    'target_ticket_id': 123,
}
response = requests.post("https://api.openstudio.one/v1/tickets/{id}/links", headers=headers, json=body)
data = response.json()
fetch('https://api.openstudio.one/v1/tickets/{id}/links', {
  method: 'POST',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    'target_ticket_id': 123,
  })
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
ticket_id integer — This ticket's id (echoes the {id} from the URL).
linked_ticket_id integer — Echoes target_ticket_id.

Example response

{
    "success": true,
    "data": {
        "ticket_id": 123,
        "linked_ticket_id": 145
    },
    "error": null
}

POST /tickets/{id}/children

Create a new child ticket

User key

Path parameters

ParameterTypeNotes
{id}integerThe PARENT ticket's id — same as on GET /tickets/{id}.

Request body

FieldTypeFlagsRequiredNotes
subject string — Required The new child ticket's title.
content string — Required The new child ticket's first comment. Plain text or HTML both accepted, stored/returned as-is — see content on the comments array under GET /tickets/{id}.
queue_id integer — Required Must be an active, on-account queue — validated with a 422 if not. Unlike POST /tickets (the top-level create endpoint), there is no eligibility check here at all: creating a child ticket already requires you to be an operator on the PARENT, so the same "not everyone can file anywhere" concern doesn't apply.
parent_visible boolean — Optional Whether the PARENT ticket's non-operator author is allowed to know this child exists (shows up in their view of the parent's children array). Defaults to false — hidden from them — if omitted.

Example request

curl -X POST "https://api.openstudio.one/v1/tickets/{id}/children" \
  -H "X-Api-User-Key: YOUR_USER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"subject":"...","content":"...","queue_id":123,"parent_visible":true}'
<?php
$ch = curl_init('https://api.openstudio.one/v1/tickets/{id}/children');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'subject' => '...',
    'content' => '...',
    'queue_id' => 123,
    'parent_visible' => true,
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

body = {
    'subject': '...',
    'content': '...',
    'queue_id': 123,
    'parent_visible': True,
}
response = requests.post("https://api.openstudio.one/v1/tickets/{id}/children", headers=headers, json=body)
data = response.json()
fetch('https://api.openstudio.one/v1/tickets/{id}/children', {
  method: 'POST',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    'subject': '...',
    'content': '...',
    'queue_id': 123,
    'parent_visible': true,
  })
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
id integer — The new child ticket's id (same response shape as GET /tickets/{id}, minus comments/parent/children/linked_ticket_ids/shares/watching, plus the parent_visible field below).

Example response

{
    "success": true,
    "data": {
        "id": 150,
        "subject": "Follow-up: check billing config",
        "status": 1,
        "parent_visible": false
    },
    "error": null
}

POST /tickets/{id}/children/link

Link an EXISTING ticket as a child

User key

Path parameters

ParameterTypeNotes
{id}integerThe PARENT ticket's id — same as on GET /tickets/{id}.

Request body

FieldTypeFlagsRequiredNotes
child_ticket_id integer — Required The other ticket's id — must exist on your account and differ from this ticket's own {id}, both enforced with a 422/404. That other ticket does NOT need to already be one you operate — only this (the parent) ticket does.
parent_visible boolean — Optional Same meaning as on the create-a-new-child endpoint. Defaults to false if omitted.

Example request

curl -X POST "https://api.openstudio.one/v1/tickets/{id}/children/link" \
  -H "X-Api-User-Key: YOUR_USER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"child_ticket_id":123,"parent_visible":true}'
<?php
$ch = curl_init('https://api.openstudio.one/v1/tickets/{id}/children/link');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'child_ticket_id' => 123,
    'parent_visible' => true,
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

body = {
    'child_ticket_id': 123,
    'parent_visible': True,
}
response = requests.post("https://api.openstudio.one/v1/tickets/{id}/children/link", headers=headers, json=body)
data = response.json()
fetch('https://api.openstudio.one/v1/tickets/{id}/children/link', {
  method: 'POST',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    'child_ticket_id': 123,
    'parent_visible': true,
  })
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
parent_ticket_id integer — This ticket's id.
child_ticket_id integer — Echoes child_ticket_id.
parent_visible boolean — Echoes what you sent (or the false default).

Example response

{
    "success": true,
    "data": {
        "parent_ticket_id": 123,
        "child_ticket_id": 150,
        "parent_visible": false
    },
    "error": null
}

PATCH /tickets/{id}/children/{child_id}

Toggle a child ticket's visibility to the parent's author

User key

Path parameters

ParameterTypeNotes
{id}integerThe PARENT ticket's id — same as on GET /tickets/{id}.
{child_id}integerThe child ticket's id, from that ticket's children array (see GET /tickets/{id}).

Request body

FieldTypeFlagsRequiredNotes
parent_visible boolean — Required The only field this endpoint accepts — there is no way to change which ticket is the parent/child here, only whether the relationship is visible to the parent's non-operator author.

Example request

curl -X PATCH "https://api.openstudio.one/v1/tickets/{id}/children/{child_id}" \
  -H "X-Api-User-Key: YOUR_USER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"parent_visible":true}'
<?php
$ch = curl_init('https://api.openstudio.one/v1/tickets/{id}/children/{child_id}');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'parent_visible' => true,
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

body = {
    'parent_visible': True,
}
response = requests.patch("https://api.openstudio.one/v1/tickets/{id}/children/{child_id}", headers=headers, json=body)
data = response.json()
fetch('https://api.openstudio.one/v1/tickets/{id}/children/{child_id}', {
  method: 'PATCH',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    'parent_visible': true,
  })
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
parent_ticket_id integer — This ticket's id.
child_ticket_id integer — The {child_id} from the URL.
parent_visible boolean — The new value, after this update.

Example response

{
    "success": true,
    "data": {
        "parent_ticket_id": 123,
        "child_ticket_id": 150,
        "parent_visible": true
    },
    "error": null
}

DELETE /tickets/{id}/children/{child_id}

Unlink a child ticket

User key

Path parameters

ParameterTypeNotes
{id}integerThe PARENT ticket's id — same as on GET /tickets/{id}.
{child_id}integerThe child ticket's id, from that ticket's children array (see GET /tickets/{id}).

Example request

curl -X DELETE "https://api.openstudio.one/v1/tickets/{id}/children/{child_id}" \
  -H "X-Api-User-Key: YOUR_USER_KEY"
<?php
$ch = curl_init('https://api.openstudio.one/v1/tickets/{id}/children/{child_id}');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

response = requests.delete("https://api.openstudio.one/v1/tickets/{id}/children/{child_id}", headers=headers)
data = response.json()
fetch('https://api.openstudio.one/v1/tickets/{id}/children/{child_id}', {
  method: 'DELETE',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY'
  }
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
deleted boolean — Always true if {child_id} resolved to a real ticket on your account (404 if it doesn't). Only removes the parent/child RELATIONSHIP — the child ticket itself is completely untouched and still exists as its own ticket.

Example response

{
    "success": true,
    "data": {
        "deleted": true
    },
    "error": null
}

POST /tickets/{id}/combine

Merge this ticket into another (this ticket becomes Solved)

User key

Path parameters

ParameterTypeNotes
{id}integerThe ticket being merged AWAY — same as on GET /tickets/{id}.

Request body

FieldTypeFlagsRequiredNotes
target_ticket_id integer — Required The SURVIVING ticket's id — must exist on your account and differ from this ticket's own {id}. This ticket's comments and time logs all move onto it; this ticket itself is not deleted, just marked Solved and linked to the target as its parent.
author_choice string — Optional current reassigns the TARGET ticket's author to match THIS ticket's author (i.e. this ticket's author "wins"). Defaults to target if omitted — leave the target ticket's existing author as-is.
current
target

Example request

curl -X POST "https://api.openstudio.one/v1/tickets/{id}/combine" \
  -H "X-Api-User-Key: YOUR_USER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"target_ticket_id":123,"author_choice":"..."}'
<?php
$ch = curl_init('https://api.openstudio.one/v1/tickets/{id}/combine');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'target_ticket_id' => 123,
    'author_choice' => '...',
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

body = {
    'target_ticket_id': 123,
    'author_choice': '...',
}
response = requests.post("https://api.openstudio.one/v1/tickets/{id}/combine", headers=headers, json=body)
data = response.json()
fetch('https://api.openstudio.one/v1/tickets/{id}/combine', {
  method: 'POST',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    'target_ticket_id': 123,
    'author_choice': '...',
  })
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
ticket_id integer — This (now-Solved) ticket's id.
merged_into_ticket_id integer — Echoes target_ticket_id — the surviving ticket.

Example response

{
    "success": true,
    "data": {
        "ticket_id": 123,
        "merged_into_ticket_id": 98
    },
    "error": null
}

POST /tickets/{id}/divide

Split part of this ticket into a new one

User key

Path parameters

ParameterTypeNotes
{id}integerThe ticket being split FROM — same as on GET /tickets/{id}.

Request body

FieldTypeFlagsRequiredNotes
subject string — Required The new, split-off ticket's subject.
content string — Required The new ticket's first comment.
comment_ids array of integers — Optional Existing comment ids from THIS ticket (the id field of an item in GET /tickets/{id}'s comments array) to MOVE onto the new ticket. An id that doesn't belong to this ticket is silently ignored, not an error. Omit for an empty array — none moved.

Example request

curl -X POST "https://api.openstudio.one/v1/tickets/{id}/divide" \
  -H "X-Api-User-Key: YOUR_USER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"subject":"...","content":"...","comment_ids":[123]}'
<?php
$ch = curl_init('https://api.openstudio.one/v1/tickets/{id}/divide');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'subject' => '...',
    'content' => '...',
    'comment_ids' => [
        '0' => 123,
    ],
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

body = {
    'subject': '...',
    'content': '...',
    'comment_ids': [
        123,
    ],
}
response = requests.post("https://api.openstudio.one/v1/tickets/{id}/divide", headers=headers, json=body)
data = response.json()
fetch('https://api.openstudio.one/v1/tickets/{id}/divide', {
  method: 'POST',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    'subject': '...',
    'content': '...',
    'comment_ids': [
      123,
    ],
  })
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
id integer — The new ticket's id (same response shape as GET /tickets/{id}, minus comments/parent/children/linked_ticket_ids/shares/watching). Starts at status 1 (Open), inherits this ticket's queue, app, and author.

Example response

{
    "success": true,
    "data": {
        "id": 151,
        "subject": "Split: separate billing question",
        "status": 1
    },
    "error": null
}

POST /tickets/{id}/time-logs

Log time spent on a ticket

User key

Path parameters

ParameterTypeNotes
{id}integerThe ticket's id — same as on GET /tickets/{id}.

Request body

FieldTypeFlagsRequiredNotes
minutes integer — Required Must be a positive integer greater than 0 — rejected with a 422 otherwise (including 0, negative values, or anything that doesn't cast to a positive int).
description string — Optional Free-text note on what the time was spent on. Optional — omit for an empty string.

Example request

curl -X POST "https://api.openstudio.one/v1/tickets/{id}/time-logs" \
  -H "X-Api-User-Key: YOUR_USER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"minutes":123,"description":"..."}'
<?php
$ch = curl_init('https://api.openstudio.one/v1/tickets/{id}/time-logs');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'minutes' => 123,
    'description' => '...',
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

body = {
    'minutes': 123,
    'description': '...',
}
response = requests.post("https://api.openstudio.one/v1/tickets/{id}/time-logs", headers=headers, json=body)
data = response.json()
fetch('https://api.openstudio.one/v1/tickets/{id}/time-logs', {
  method: 'POST',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    'minutes': 123,
    'description': '...',
  })
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
ticket_id integer — This ticket's id.
minutes integer — Echoes what you sent.

Example response

{
    "success": true,
    "data": {
        "ticket_id": 123,
        "minutes": 45,
        "description": "Investigated logs"
    },
    "error": null
}

PATCH /tickets/{id}/comments/{comment_id}

Edit a comment

User key

Path parameters

ParameterTypeNotes
{id}integerThe ticket's id — same as on GET /tickets/{id}.
{comment_id}integerThe comment's own id, from the id field of an item in that ticket's comments array (see GET /tickets/{id}).

Request body

FieldTypeFlagsRequiredNotes
content string — Required The comment's new content, replacing what was there before entirely (not appended). Cannot be empty.

Example request

curl -X PATCH "https://api.openstudio.one/v1/tickets/{id}/comments/{comment_id}" \
  -H "X-Api-User-Key: YOUR_USER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content":"..."}'
<?php
$ch = curl_init('https://api.openstudio.one/v1/tickets/{id}/comments/{comment_id}');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'content' => '...',
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

body = {
    'content': '...',
}
response = requests.patch("https://api.openstudio.one/v1/tickets/{id}/comments/{comment_id}", headers=headers, json=body)
data = response.json()
fetch('https://api.openstudio.one/v1/tickets/{id}/comments/{comment_id}', {
  method: 'PATCH',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    'content': '...',
  })
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
id integer — The comment's own id (echoes {comment_id} from the URL).
content string — The comment's new content, as saved.

Example response

{
    "success": true,
    "data": {
        "id": 501,
        "content": "Updated: I get a 403 opening invoice INV-2024-0031 specifically."
    },
    "error": null
}

GET /tickets/{id}/history

Get a ticket's audit log

User key

Path parameters

ParameterTypeNotes
{id}integerThe ticket's id — same as on GET /tickets/{id}.

Example request

curl -X GET "https://api.openstudio.one/v1/tickets/{id}/history" \
  -H "X-Api-User-Key: YOUR_USER_KEY"
<?php
$ch = curl_init('https://api.openstudio.one/v1/tickets/{id}/history');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

response = requests.get("https://api.openstudio.one/v1/tickets/{id}/history", headers=headers)
data = response.json()
fetch('https://api.openstudio.one/v1/tickets/{id}/history', {
  method: 'GET',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY'
  }
})
  .then(res => res.json())
  .then(data => console.log(data));

ⓘRuns a real request from your browser directly to tickets/{id}/history. Keys are only held in this page's memory while you're here — never stored, never sent anywhere else.

Response fields

FieldTypeFlagsNotes
ticket_id integer — This ticket's id.
history array of objects — Every recorded action on this ticket, newest first. Operator-only — 403 for anyone else, matching the dashboard's own History panel (which sits inside the same moderator-only sidebar as everything else operator-facing, not shown to the author or a shared user at all).
↳action_type string — e.g. STATUS_CHANGE, PRIORITY_CHANGE, QUEUE_CHANGE, ASSIGNED_ME, ASSIGNED_OPERATOR, UNASSIGNED, REOPENED, PAUSED, RESUMED, NEW_COMMENT, COMMENT_EDITED, TIME_LOGGED, SHARED, UNSHARED, TICKET_LINKED, TICKET_UNLINKED, TICKET_COMBINED, TICKET_DIVIDED, CHILD_CREATED, CHILD_LINKED, CHILD_UNLINKED, CHILD_VISIBILITY, APP_CHANGE, TITLE_CHANGE, AUTHOR_CHANGE, WATCHER_ADDED, WATCHER_REMOVED, or a bulk variant ending _BULK. Not a fixed enum here — every action this API (or the dashboard) performs writes its own type, and new ones can appear as this API grows.
↳actor_id string — The HR id of whoever performed the action.
↳actor_user_id integer The same actor's user_id, the SAME id space as GET /me's own users.user_id. null if resolution failed.
↳actor_name string The actor's resolved display name, if they still exist on this account. null if resolution failed (e.g. a since-removed account).
↳old_value string The RAW stored value — meaning depends on action_type (e.g. a numeric status code for STATUS_CHANGE, an HR id for ASSIGNED_ME/ASSIGNED_OPERATOR/AUTHOR_CHANGE, already a real name for QUEUE_CHANGE). Often null for actions with no meaningful "before" state (e.g. NEW_COMMENT, WATCHER_ADDED). See old_value_label for a human-readable version where one exists.
↳new_value string The RAW stored value, mirrors old_value — e.g. the new status code, or (for NEW_COMMENT specifically) the first 100 characters of the comment that was posted, INCLUDING for an internal note. Since this whole endpoint is already operator-only, that's not a cross-role leak here — but worth knowing if you ever build something that surfaces this data further downstream to a non-operator audience. See new_value_label for a human-readable version where one exists.
↳old_value_label string Human-readable version of old_value — a resolved status name for STATUS_CHANGE, a resolved person name for ASSIGNED_ME/ASSIGNED_OPERATOR/AUTHOR_CHANGE. null for every other action_type (nothing to resolve) or if resolution failed — prefer this over old_value when present.
↳new_value_label string Human-readable version of new_value, same rules as old_value_label.
↳timestamp string — YYYY-MM-DD HH:MM:SS, when the action was recorded.

Example response

{
    "success": true,
    "data": {
        "ticket_id": 123,
        "history": [
            {
                "action_type": "STATUS_CHANGE",
                "actor_id": "10000001",
                "actor_name": "Jane Doe",
                "old_value": "1",
                "new_value": "2",
                "old_value_label": "open",
                "new_value_label": "solved",
                "timestamp": "2026-08-20 10:10:00"
            },
            {
                "action_type": "ASSIGNED_ME",
                "actor_id": "10000001",
                "actor_name": "Jane Doe",
                "old_value": null,
                "new_value": "10000001",
                "old_value_label": null,
                "new_value_label": "Jane Doe",
                "timestamp": "2026-08-10 09:15:00"
            }
        ]
    },
    "error": null
}

GET /tickets/{id}/attachments

List a ticket's attachments

User key

Example request

curl -X GET "https://api.openstudio.one/v1/tickets/{id}/attachments" \
  -H "X-Api-User-Key: YOUR_USER_KEY"
<?php
$ch = curl_init('https://api.openstudio.one/v1/tickets/{id}/attachments');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

response = requests.get("https://api.openstudio.one/v1/tickets/{id}/attachments", headers=headers)
data = response.json()
fetch('https://api.openstudio.one/v1/tickets/{id}/attachments', {
  method: 'GET',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY'
  }
})
  .then(res => res.json())
  .then(data => console.log(data));

ⓘRuns a real request from your browser directly to tickets/{id}/attachments. Keys are only held in this page's memory while you're here — never stored, never sent anywhere else.

Response fields

FieldTypeFlagsNotes
attachments array of objects — —
↳id integer — —
↳comment_id integer null when attached to the ticket's opening message rather than a specific reply.
↳kind string — One of: file, screenshot, diagnostic.
↳filename string — —
↳mime_type string — —
↳uploaded_by integer HR id (personal_code) of the uploader.
↳created_at string — —

Example response

{
    "success": true,
    "data": {
        "attachments": [
            {
                "id": 5,
                "comment_id": null,
                "kind": "file",
                "filename": "screenshot.png",
                "mime_type": "image/png",
                "uploaded_by": 10000001,
                "created_at": "2026-08-25 10:00:00"
            }
        ]
    },
    "error": null
}

POST /tickets/{id}/attachments

Add a new attachment to a ticket

User key

Request body

FieldTypeFlagsRequiredNotes
file_base64 string — Required Raw base64, or a data:mime;base64,... URI — either form is accepted. 10 MB cap. Content is sniffed server-side; accepted types are pdf/jpg/png/doc/docx/gz regardless of what the filename or a claimed MIME type says.
kind string — Optional One of: file (default), screenshot, diagnostic — a display hint only, not enforced against the actual file content.
comment_id integer — Optional Attach to one specific reply instead of the ticket's opening message.

Example request

curl -X POST "https://api.openstudio.one/v1/tickets/{id}/attachments" \
  -H "X-Api-User-Key: YOUR_USER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"file_base64":"...","kind":"...","comment_id":123}'
<?php
$ch = curl_init('https://api.openstudio.one/v1/tickets/{id}/attachments');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'file_base64' => '...',
    'kind' => '...',
    'comment_id' => 123,
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

body = {
    'file_base64': '...',
    'kind': '...',
    'comment_id': 123,
}
response = requests.post("https://api.openstudio.one/v1/tickets/{id}/attachments", headers=headers, json=body)
data = response.json()
fetch('https://api.openstudio.one/v1/tickets/{id}/attachments', {
  method: 'POST',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    'file_base64': '...',
    'kind': '...',
    'comment_id': 123,
  })
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
attachment object — —

Example response

{
    "success": true,
    "data": {
        "attachment": {
            "id": 6,
            "comment_id": null,
            "kind": "file",
            "filename": "notes.pdf",
            "mime_type": "application/pdf",
            "uploaded_by": 10000001,
            "created_at": "2026-08-25 10:05:00"
        }
    },
    "error": null
}

GET /tickets/{id}/attachments/{attachment_id}

Download one attachment's raw bytes

User key

Example request

curl -X GET "https://api.openstudio.one/v1/tickets/{id}/attachments/{attachment_id}" \
  -H "X-Api-User-Key: YOUR_USER_KEY"
<?php
$ch = curl_init('https://api.openstudio.one/v1/tickets/{id}/attachments/{attachment_id}');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

response = requests.get("https://api.openstudio.one/v1/tickets/{id}/attachments/{attachment_id}", headers=headers)
data = response.json()
fetch('https://api.openstudio.one/v1/tickets/{id}/attachments/{attachment_id}', {
  method: 'GET',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY'
  }
})
  .then(res => res.json())
  .then(data => console.log(data));

ⓘRuns a real request from your browser directly to tickets/{id}/attachments/{attachment_id}. Keys are only held in this page's memory while you're here — never stored, never sent anywhere else.

Response fields

FieldTypeFlagsNotes
type string — —
format string — —

Example response

{
    "success": true,
    "data": {},
    "error": null
}

POST /tickets/bulk

Apply one change to multiple tickets at once

User key

Request body

FieldTypeFlagsRequiredNotes
ticket_ids array of integers — Required Ids to update. Every id is independently re-verified server-side to actually be in one of YOUR operator queues before anything is touched — an id outside that set is silently placed in the response's skipped_ticket_ids instead of erroring the whole batch or being applied anyway.
action string — Required Which single field to change on every ticket in ticket_ids. Any other value is rejected with a 422.
status
priority
app
queue_id
value string — Required The new value, validated according to action just like the equivalent single-ticket PATCH field: action = status needs one of -1/0/1/2 (3/Paused is deliberately excluded here too — use POST /tickets/{id}/pause per-ticket instead, since pausing records its own paused_reason/paused_at this bulk action has no way to set), action = priority needs one of 1/2/3/4 (both rejected with a 422 if outside those, including a non-numeric string — nothing is silently coerced to a "closest" valid value), action = queue_id needs an active on-account queue id (422 if not; also resets each ticket's assigned_operator_ids and forces status to Open, same as PATCH /tickets/{id}'s queue_id side effect), action = app accepts any free-text string.

Example request

curl -X POST "https://api.openstudio.one/v1/tickets/bulk" \
  -H "X-Api-User-Key: YOUR_USER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"ticket_ids":[123],"action":"...","value":"..."}'
<?php
$ch = curl_init('https://api.openstudio.one/v1/tickets/bulk');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'ticket_ids' => [
        '0' => 123,
    ],
    'action' => '...',
    'value' => '...',
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

body = {
    'ticket_ids': [
        123,
    ],
    'action': '...',
    'value': '...',
}
response = requests.post("https://api.openstudio.one/v1/tickets/bulk", headers=headers, json=body)
data = response.json()
fetch('https://api.openstudio.one/v1/tickets/bulk', {
  method: 'POST',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    'ticket_ids': [
      123,
    ],
    'action': '...',
    'value': '...',
  })
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
updated_ticket_ids array of integers — Ids that were actually changed.
skipped_ticket_ids array of integers — Requested ids that weren't in any queue you operate, so nothing was changed for them — not an error, just excluded. Compare against your original ticket_ids to see what didn't apply.

Example response

{
    "success": true,
    "data": {
        "updated_ticket_ids": [
            123,
            124
        ],
        "skipped_ticket_ids": [
            999
        ]
    },
    "error": null
}

POST /tickets/{id}/replies

Reply to a ticket

User key

Path parameters

ParameterTypeNotes
{id}integerThe ticket's id — same as on GET /tickets/{id}.

Request body

FieldTypeFlagsRequiredNotes
content string — Required The reply text. Cannot be empty or whitespace-only.
is_internal boolean — Optional Only honored when you're an operator on this ticket — silently treated as false otherwise (never rejected), same as a non-operator never seeing the internal-note option in the dashboard at all. Defaults to false if omitted.

Example request

curl -X POST "https://api.openstudio.one/v1/tickets/{id}/replies" \
  -H "X-Api-User-Key: YOUR_USER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content":"...","is_internal":true}'
<?php
$ch = curl_init('https://api.openstudio.one/v1/tickets/{id}/replies');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'content' => '...',
    'is_internal' => true,
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);

$response = curl_exec($ch);
$data = json_decode($response, true);
import requests

headers = {
    "X-Api-User-Key": "YOUR_USER_KEY",
}

body = {
    'content': '...',
    'is_internal': True,
}
response = requests.post("https://api.openstudio.one/v1/tickets/{id}/replies", headers=headers, json=body)
data = response.json()
fetch('https://api.openstudio.one/v1/tickets/{id}/replies', {
  method: 'POST',
  headers: {
    'X-Api-User-Key': 'YOUR_USER_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    'content': '...',
    'is_internal': true,
  })
})
  .then(res => res.json())
  .then(data => console.log(data));

Response fields

FieldTypeFlagsNotes
id integer — The new comment's own id (same id space as GET /tickets/{id}'s comments[].id).
ticket_id integer — The ticket's id (echoes the {id} from the URL).
author_id string — Your own id — you are always the commenter on a reply you post.
author_user_id integer Your own user_id, the SAME id space as GET /me's own users.user_id.
author_name string Your own resolved display name.
content string — Echoes what you sent.
is_internal boolean — The actual value stored — false if you sent true but weren't an operator (see the request field's own description).
is_staff boolean — true if you were acting as an operator on this ticket when you posted this reply.
created_at string — Set by the server to the current time at post time. Not settable by the caller.

Example response

{
    "success": true,
    "data": {
        "id": 503,
        "ticket_id": 123,
        "author_id": "10000001",
        "content": "This should be fixed now \u2014 can you confirm?",
        "is_internal": false,
        "is_staff": true,
        "created_at": "2026-08-20 10:05:00"
    },
    "error": null
}
ESC