Ticketing
Support queue configuration and staffing, plus the ticketing system routed through those queues.
Support queues
GET /queues
List support queues
User keyExample 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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.SpacesAdministration - Finance - PersonnelDigital Identity and AuthorizationsRepository and ArchivesSafety & SecurityDatabaseData and IT managementMaintenanceOther |
| ↳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.visiblehidden |
| ↳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.structuresgroups |
| ↳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 keyRequest body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| name | string | — | Required | Max 150 characters. |
| category | string | — | Required | Which broad category this queue falls under, for grouping in the queue picker.SpacesAdministration - Finance - PersonnelDigital Identity and AuthorizationsRepository and ArchivesSafety & SecurityDatabaseData and IT managementMaintenanceOther |
| 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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 keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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 keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The 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
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| name | string | — | Optional | Cannot be set to an empty string. |
| category | string | — | Optional | A new category for this queue.SpacesAdministration - Finance - PersonnelDigital Identity and AuthorizationsRepository and ArchivesSafety & SecurityDatabaseData and IT managementMaintenanceOther |
| description | string | — | Optional | A new description for this queue. |
| status | string | — | Optional | Show or hide this queue in the queue picker.visiblehidden |
| 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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 keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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 keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The queue's id — same as on GET /queues/{id}. |
Request body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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 keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The queue's id — same as on GET /queues/{id}. |
Query parameters (1)
Other
| Parameter | Type | Required | Notes |
|---|---|---|---|
| ?structure | string | Required | A 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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 keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The queue's id — same as on GET /queues/{id}. |
Request body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| 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.operatormanager |
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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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 keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The queue's id — same as on GET /queues/{id}. |
| {user_id} | integer | The 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
| Parameter | Type | Required | Notes |
|---|---|---|---|
| ?structure | string | Required | A structure acronym. |
| ?role | string | Required | Removes only this ONE role — a user with both operator and manager keeps the other one unless you call this twice.operatormanager |
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
| Field | Type | Flags | Notes |
|---|---|---|---|
| x-note-see | string | — | — |
Example response
{
"success": true,
"data": {
"team": []
},
"error": null
}Tickets Helpdesk
GET /tickets
List tickets
User key PaginatedQuery parameters (9)
Filtering
| Parameter | Type | Required | Notes |
|---|---|---|---|
| ?filter[status] | integer | Optional | Filter by exact status.-1 = Refused0 = Draft1 = Open2 = Solved3 = Paused |
| ?filter[queue_id] | integer | Optional | Filter by exact queue id. |
| ?filter[priority] | integer | Optional | Filter by exact priority.1 = Low2 = Medium3 = High4 = Critical |
| ?filter[search] | string | Optional | Case-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
| Parameter | Type | Required | Notes |
|---|---|---|---|
| ?sort | string | Optional | Comma-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
| Parameter | Type | Required | Notes |
|---|---|---|---|
| ?page | integer | Optional | Page number, starting at 1. Defaults to 1. |
| ?limit | integer | Optional | Rows per page, 1-200. Defaults to 50. |
Other
| Parameter | Type | Required | Notes |
|---|---|---|---|
| ?scope | string | Optional | Defaults 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.operatormine |
| ?expand | string | Optional | Set 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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 = Refused0 = Draft1 = Open2 = Solved3 = 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.refuseddraftopensolvedpaused |
| ↳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 = Low2 = Medium3 = High4 = 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
| Field | Type | Notes |
|---|---|---|
| page | integer | Current page number. |
| limit | integer | Rows per page. |
| total | integer | Total matching rows across all pages. |
| total_pages | integer | Total number of pages. |
| next_page_url | string | Ready-to-call URL for the next page, same filters applied. Null on the last page. |
| prev_page_url | string | Ready-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 keyRequest body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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.1234 |
| 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 keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The 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
| Field | Type | Flags | Access | Notes |
|---|---|---|---|---|
| id | integer | — | — | See GET /tickets' id field. |
| subject | string | — | Editable | See GET /tickets. |
| status | integer | — | Editable | See GET /tickets.-1 = Refused0 = Draft1 = Open2 = Solved3 = Paused |
| status_label | string | — | — | See GET /tickets.refuseddraftopensolvedpaused |
| priority | integer | — | Editable | See GET /tickets.1 = Low2 = Medium3 = High4 = 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.-10123 |
| ↳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.userqueue |
| ↳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.readedit |
| ↳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.useroperator |
| 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 keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The 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
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| 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 = Refused0 = Draft1 = Open2 = 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 = Low2 = Medium3 = High4 = 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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.-10123 |
| status_label | string | — | Lowercase name of status, above.refuseddraftopensolvedpaused |
| priority | integer | — | The ticket's priority after this update.1234 |
| 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 keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The ticket's id — same as on GET /tickets/{id}. |
Request body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| 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.testingmulti_app_changeother |
| 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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 keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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 keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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 keyExample 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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 keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The ticket's id — same as on GET /tickets/{id}. |
Request body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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 keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The ticket's id — same as on GET /tickets/{id}. |
| {operator_id} | string | The 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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 keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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 keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| watching | boolean | — | Always false on success. |
Example response
{
"success": true,
"data": {
"watching": false
},
"error": null
}POST /tickets/{id}/links
Link two tickets (generic, non-parent/child)
User keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The ticket's id — same as on GET /tickets/{id}. |
Request body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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
}DELETE /tickets/{id}/links/{target_id}
Remove a ticket link
User keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The ticket's id — same as on GET /tickets/{id}. |
| {target_id} | integer | The OTHER ticket's id, from that ticket's linked_ticket_ids array (see GET /tickets/{id}). |
Example request
curl -X DELETE "https://api.openstudio.one/v1/tickets/{id}/links/{target_id}" \
-H "X-Api-User-Key: YOUR_USER_KEY"<?php
$ch = curl_init('https://api.openstudio.one/v1/tickets/{id}/links/{target_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}/links/{target_id}", headers=headers)
data = response.json()fetch('https://api.openstudio.one/v1/tickets/{id}/links/{target_id}', {
method: 'DELETE',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY'
}
})
.then(res => res.json())
.then(data => console.log(data));Response fields
| Field | Type | Flags | Notes |
|---|---|---|---|
| deleted | boolean | — | Always true if {target_id} resolved to a real ticket on your account (404 if it doesn't) — removing a link that doesn't currently exist between the two is still a harmless no-op, not an error. |
Example response
{
"success": true,
"data": {
"deleted": true
},
"error": null
}POST /tickets/{id}/children
Create a new child ticket
User keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The PARENT ticket's id — same as on GET /tickets/{id}. |
Request body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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 keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The PARENT ticket's id — same as on GET /tickets/{id}. |
Request body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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 keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The PARENT ticket's id — same as on GET /tickets/{id}. |
| {child_id} | integer | The child ticket's id, from that ticket's children array (see GET /tickets/{id}). |
Request body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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 keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The PARENT ticket's id — same as on GET /tickets/{id}. |
| {child_id} | integer | The 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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 keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The ticket being merged AWAY — same as on GET /tickets/{id}. |
Request body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| 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.currenttarget |
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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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 keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The ticket being split FROM — same as on GET /tickets/{id}. |
Request body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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 keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The ticket's id — same as on GET /tickets/{id}. |
Request body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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 keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The ticket's id — same as on GET /tickets/{id}. |
| {comment_id} | integer | The comment's own id, from the id field of an item in that ticket's comments array (see GET /tickets/{id}). |
Request body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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 keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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 keyExample 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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 keyRequest body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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 keyExample 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| type | string | — | — |
| format | string | — | — |
Example response
{
"success": true,
"data": {},
"error": null
}POST /tickets/bulk
Apply one change to multiple tickets at once
User keyRequest body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| 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.statuspriorityappqueue_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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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 keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The ticket's id — same as on GET /tickets/{id}. |
Request body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| 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
| Field | Type | Flags | Notes |
|---|---|---|---|
| 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
}