This documentation always describes the latest published version.
This page describes all 12 endpoints of the plugin: paths, methods, parameters, required API key permissions, responses and error codes. Installation and web server setup are covered on Installation, the permissions on Configuration.
| # | Method | Path | Permission |
|---|---|---|---|
| 1 | POST | /api/tickets.json |
see POST Create Ticket |
| 2 | GET | /api/tickets-get.php/{number}.{format} |
can_read_tickets |
| 3 | PATCH, PUT | /api/tickets-update.php/{number}.{format} or /api/tickets/{number}.{format} |
can_update_tickets |
| 4 | GET | /api/tickets-search.php |
can_search_tickets or can_read_tickets |
| 5 | DELETE | /api/tickets-delete.php/{number}.{format} |
can_delete_tickets |
| 6 | GET | /api/tickets-stats.php |
can_read_stats or can_read_tickets |
| 7 | GET | /api/tickets-statuses.php |
can_read_stats or can_read_tickets |
| 8 | GET | /api/tickets-subtickets-parent.php/{number}.{format} |
can_manage_subtickets |
| 9 | GET | /api/tickets-subtickets-list.php/{number}.{format} |
can_manage_subtickets |
| 10 | POST | /api/tickets-subtickets-create.php/{parentNumber}.{format} |
can_manage_subtickets |
| 11 | DELETE | /api/tickets-subtickets-unlink.php/{number}.{format} |
can_manage_subtickets |
| 12 | GET | /api/tickets-attachment-download.php/{file_id}.{format} |
can_read_tickets |
{format} is json or xml. The permission names are the columns of the API key table, see Configuration.
Every request carries the API key in the X-API-Key header.
X-API-Key: YOUR_API_KEY
The standalone endpoints (2 to 12) reject a missing or invalid key with 401. An API key without the permission required by an endpoint receives 401 with a message such as API key not authorized to read tickets. Only the subticket endpoints and the attachment download answer with 403 in specific cases, see the status codes. Endpoint 1 is handled differently, see its section.
can_read_ticketsalso grants search, statistics and the status list. A key that only hascan_search_ticketsorcan_read_statscan use just that one endpoint. All other permissions have no fallback: a missing or disabled permission means access is denied.
https://your-domain.com/api
| Endpoint type | Success response |
|---|---|
| Create (1), update (3), delete (5) | Ticket number as plain text (text/plain) |
Endpoints with {number}.{format} (2, 8, 9, 10, 11, 12) |
JSON, or XML when the path ends in .xml |
| Search, statistics, status list (4, 6, 7) | JSON. The path info after the script name is optional and may be .json or .xml, with .xml the response is XML |
XML responses use response as root element, list entries are rendered as item. Errors raised by the plugin itself are JSON, even for .xml requests:
{
"error": true,
"message": "Description of what went wrong"
}
Errors of the API key check (missing or invalid key, IP restriction) come from osTicket core or from the API Key Wildcard plugin and use their body format.
Errors from the two routes registered through the signal (create and PATCH /api/tickets/{number}.{format}) are returned as plain text containing only the message.
| Code | Meaning | When |
|---|---|---|
200 |
OK | Request successful |
201 |
Created | Ticket created (endpoint 1) |
400 |
Bad Request | Invalid URL format, invalid or missing parameters, failed validation |
401 |
Unauthorized | Standalone endpoints and the update route: missing or invalid API key, or the key lacks the required permission |
403 |
Forbidden | Subticket endpoints: missing can_manage_subtickets, or the API key object provides department restrictions that exclude the ticket's department. Attachment download: no attachment row for the file |
404 |
Not Found | Ticket, department, status, topic, SLA, staff member or file not found |
405 |
Method Not Allowed | Wrong HTTP method for a standalone endpoint (Method Not Allowed. Use GET) |
422 |
Unprocessable Entity | Subticket link already exists, or the child already has a different parent |
500 |
Internal Server Error | Operation failed on the server |
501 |
Not Implemented | Subticket plugin not available |
departmentId, statusId, topicId and slaId accept a name or an ID. A value that is not numeric is looked up as a name (case-insensitive) and returns 404 if nothing matches. A numeric value, also when sent as a string, is always used as an ID. staffId accepts the staff ID, the username or the email address, not the display name.
Creates a ticket. The plugin adds format, departmentId and parentTicketNumber to osTicket's own creation parameters.
POST /api/tickets.json
Permission: For this signal route the plugin itself checks neither the API key nor the permission can_create_tickets. Whether osTicket's core checks the key before dispatching the signal cannot be derived from this plugin's code.
Send the body as JSON with Content-Type: application/json.
| Parameter | Type | Description | Example |
|---|---|---|---|
format |
string | Message format: text, html or markdown. Aliases: md for markdown, plain and txt for text |
"markdown" |
departmentId |
string or int | Department name or ID. Active departments only, sub-departments are found by their name | "Support" or 5 |
parentTicketNumber |
string or int | Number of the parent ticket (an internal ID is accepted as fallback). The parent must exist and must not be a child itself | "181752" |
All other fields are processed by osTicket's own ticket creation, for example name, email, subject, message, topicId and attachments. Which of them are required depends on your osTicket ticket form.
Attachments are passed as an array of objects, each with the file name as key and a Base64 data URI as value:
{
"attachments": [
{"screenshot.png": "data:image/png;base64,iVBORw0KGgo..."},
{"report.pdf": "data:application/pdf;base64,JVBERi0xLjQ..."}
]
}
201 Created: the ticket number as plain text.
781258
# Basic ticket
curl -X POST "https://osticket.local/api/tickets.json" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "John Doe",
"email": "john@example.com",
"subject": "Login Issue",
"message": "I cannot access my account"
}'
# Markdown message, routed to a department
curl -X POST "https://osticket.local/api/tickets.json" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Jane Smith",
"email": "jane@example.com",
"subject": "Bug Report: Payment Failed",
"message": "## Problem\n\nPayment fails with error CARD_DECLINED.",
"format": "markdown",
"departmentId": "Billing"
}'
# Create as a subticket of an existing ticket
curl -X POST "https://osticket.local/api/tickets.json" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Developer",
"email": "dev@example.com",
"subject": "Subtask: Database Migration",
"message": "Migrate the production database",
"parentTicketNumber": "181752",
"departmentId": "Development"
}'
format, unknown or inactive department, unknown parent ticket) are returned as 400 with a plain text message, including cases that would be 404 elsewhere.201 is returned.format=markdown returns 400 (Markdown Support plugin is required but not active) while the Markdown Support plugin is not active. With the setting disabled, the value is passed through without that check.parentTicketNumber only sets the parent reference. The dedicated subticket endpoints additionally check the permission can_manage_subtickets.Returns one ticket including its thread.
GET /api/tickets-get.php/{number}.{format}
Permission: can_read_tickets
| Parameter | Type | Description |
|---|---|---|
number |
string or int | Ticket number. If no ticket has that number, the value is tried as an internal ticket ID |
format |
string | json or xml |
{
"id": 123,
"number": "781258",
"subject": "Login Issue",
"statusId": 1,
"status": "Open",
"priorityId": 2,
"priority": "Normal",
"departmentId": 5,
"department": "IT Support",
"topicId": 10,
"topic": "Technical Support",
"userId": 45,
"user": {
"name": "John Doe",
"email": "john@example.com"
},
"staffId": 3,
"staff": "Jane Smith",
"teamId": 2,
"team": "Level 1 Support",
"slaId": 1,
"sla": "Standard SLA",
"created": "2025-11-08 10:30:00",
"updated": "2025-11-08 14:20:00",
"duedate": "2025-11-15 10:30:00",
"closed": null,
"isOverdue": false,
"isAnswered": true,
"source": "API",
"ip": "192.168.1.100",
"children": [124, 125],
"thread": [
{
"id": 456,
"type": "M",
"poster": "John Doe",
"timestamp": "2025-11-08 10:30:00",
"body": "I cannot access my account",
"userId": 45,
"attachments": [
{
"id": 12,
"file_id": 29,
"filename": "screenshot.png",
"size": 45320,
"mime_type": "image/png",
"inline": false
}
],
"attachment_count": 1
}
]
}
| Field | Description |
|---|---|
thread[].type |
Thread entry type as stored by osTicket: M message, R response, N internal note |
thread[].staffId, thread[].staff |
Only present for entries written by a staff member |
thread[].userId |
Only present for entries written by a user |
thread[].attachments, thread[].attachment_count |
Only present when the entry has attachments. file_id is the value for the attachment download |
children |
Internal IDs (not numbers) of the child tickets |
closed |
Close date, only when the ticket is closed, otherwise null |
curl "https://osticket.local/api/tickets-get.php/781258.json" \
-H "X-API-Key: YOUR_API_KEY"
Updates properties of a ticket and optionally adds an internal note with attachments.
PATCH /api/tickets-update.php/{number}.{format}
PUT /api/tickets-update.php/{number}.{format}
The same update is available through the route PATCH /api/tickets/{number}.json (also PUT, .xml accepted). That route answers errors as plain text instead of JSON.
Permission: can_update_tickets
Send the body as JSON with Content-Type: application/json. Only include the fields you want to change.
| Field | Type | Description |
|---|---|---|
statusId |
string or int | Status name or ID |
departmentId |
string or int | Department name or ID |
topicId |
string or int | Help topic name or ID |
slaId |
string or int | SLA plan name or ID |
staffId |
string or int | Staff ID, username or email address |
dueDate |
string or null | Due date in a format PHP's DateTime understands, for example 2025-12-31 or 2025-12-31T17:30:00. null or an empty string removes the due date |
parentTicketNumber |
string | Ticket number of the parent (makes the ticket a subticket) |
note |
string | Internal note. Posted as API, shown to staff only |
noteTitle |
string | Title of the note, default API Update |
noteFormat |
string | Format of the note, default markdown |
attachments |
array | Attachments of the note, same format as on creation. Only used together with note, otherwise silently ignored |
Other fields, priority included, are ignored without an error.
200 OK: the ticket number as plain text.
# Change status
curl -X PATCH "https://osticket.local/api/tickets-update.php/781258.json" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"statusId": "Resolved"}'
# Assign to a staff member with a note
curl -X PATCH "https://osticket.local/api/tickets-update.php/781258.json" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"staffId": "john.doe", "note": "Assigned to John for follow-up"}'
# Remove the due date
curl -X PATCH "https://osticket.local/api/tickets-update.php/781258.json" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"dueDate": null}'
400. A parent ticket that is itself a child returns 400 (Parent ticket cannot be a child of another ticket).GET /api/tickets-search.php
Permission: can_search_tickets or can_read_tickets
| Parameter | Type | Default | Description |
|---|---|---|---|
query |
string | Case-insensitive search in the ticket subject | |
status |
string or int | Status name or ID | |
department |
string or int | Department name, ID or path such as Development / osTicket |
|
limit |
int | 20 | Maximum results. Values below 1 become 20, values above 100 become 100 |
offset |
int | 0 | Pagination offset |
sort |
string | created |
created (newest first), updated (most recently updated first) or number (ascending). Other values fall back to created |
A status or department name that matches nothing is ignored and does not filter.
A JSON array, not wrapped in an object. Entries carry no thread:
[
{
"id": 123,
"number": "781258",
"subject": "Login Issue",
"statusId": 1,
"status": "Open",
"priorityId": 2,
"priority": "Normal",
"departmentId": 5,
"department": "IT Support",
"topicId": 10,
"topic": "Technical Support",
"created": "2025-11-08 10:00:00",
"updated": "2025-11-08 14:30:00",
"dueDate": "2025-11-15 10:00:00",
"staffId": 3,
"staff": "Jane Smith",
"teamId": 2,
"team": "Level 1 Support",
"slaId": 1,
"sla": "Standard SLA",
"isOverdue": false,
"isAnswered": true
}
]
curl "https://osticket.local/api/tickets-search.php?query=login" \
-H "X-API-Key: YOUR_API_KEY"
curl "https://osticket.local/api/tickets-search.php?query=payment&status=Open&department=Support&limit=50&offset=50" \
-H "X-API-Key: YOUR_API_KEY"
Permanently deletes a ticket.
This action cannot be undone. The ticket and its thread are removed from the database.
DELETE /api/tickets-delete.php/{number}.{format}
Permission: can_delete_tickets, disabled by default for every API key.
200 OK: the deleted ticket number as plain text.
curl -X DELETE "https://osticket.local/api/tickets-delete.php/781258.json" \
-H "X-API-Key: YOUR_API_KEY"
Consider a
PATCHto a closed status instead of deleting.
GET /api/tickets-stats.php
Permission: can_read_stats or can_read_tickets
{
"total": 150,
"open": 45,
"closed": 105,
"overdue": 3,
"by_department": {
"Billing": {"total": 70, "open": 25, "closed": 45, "overdue": 2},
"Support": {"total": 80, "open": 20, "closed": 60, "overdue": 1}
},
"by_staff": [
{
"staff_id": 1,
"staff_name": "John Smith",
"total": 35,
"departments": {
"Support": {"open": 10, "closed": 20, "overdue": 0}
}
}
]
}
by_department is an object keyed by department name, sorted alphabetically.by_staff is an array sorted by staff name. Only assigned tickets are counted.GET /api/tickets-statuses.php
Permission: can_read_stats or can_read_tickets
[
{"id": 1, "name": "Open", "state": "open"},
{"id": 3, "name": "Closed", "state": "closed"}
]
The list is sorted by the status sort order and includes custom statuses. state is the osTicket state of the status.
The endpoints 8 to 11 require the Subticket Manager plugin (class SubticketPlugin) and the permission can_manage_subtickets. They check in this order: input validation first (an invalid ticket number answers 400, endpoint 10 additionally answers 400 for a missing childId and for linking a ticket to itself), then the permission (403, API key not authorized for subticket operations), then the plugin (501, Subticket plugin not available), then the ticket lookups. Endpoints 10 and 11 additionally check the ticket's department, but only if the API key object provides department restrictions. Standard osTicket API keys have none.
GET /api/tickets-subtickets-parent.php/{number}.{format}
number is the child ticket number.
{
"parent": {
"ticket_id": 100,
"number": "781200",
"subject": "Parent Ticket",
"status": "Open"
}
}
Returns {"parent": null} if the ticket has no parent. 404 (Child ticket not found) if the ticket does not exist.
GET /api/tickets-subtickets-list.php/{number}.{format}
number is the parent ticket number.
{
"children": [
{"ticket_id": 124, "number": "781259", "subject": "Child Ticket 1", "status": "Open"},
{"ticket_id": 125, "number": "781260", "subject": "Child Ticket 2", "status": "Closed"}
]
}
Returns {"children": []} if there are none. 404 (Parent ticket not found) if the ticket does not exist.
Links two existing tickets.
POST /api/tickets-subtickets-create.php/{parentNumber}.{format}
| Body field | Type | Required | Description |
|---|---|---|---|
childId |
string | Yes | Ticket number of the child ticket |
curl -X POST "https://osticket.local/api/tickets-subtickets-create.php/781200.json" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"childId": "781259"}'
{
"success": true,
"message": "Subticket relationship created successfully",
"parent": {"ticket_id": 100, "number": "781200", "subject": "Parent Ticket", "status": "Open"},
"child": {"ticket_id": 124, "number": "781259", "subject": "Child Ticket", "status": "Open"}
}
| Code | Message | Cause |
|---|---|---|
400 |
Missing childId in request body |
childId missing |
400 |
Cannot link ticket to itself |
Parent and child are the same ticket |
403 |
Access denied to parent ticket department or ... child ticket department |
Only if the API key object provides department restrictions that exclude the department |
404 |
Parent ticket not found or Child ticket not found |
Ticket does not exist |
422 |
Subticket relationship already exists |
The link is already there |
422 |
Child ticket already has a different parent |
The child has another parent |
Removes the parent link of a child ticket.
DELETE /api/tickets-subtickets-unlink.php/{number}.{format}
number is the child ticket number.
{
"success": true,
"message": "Subticket relationship removed successfully",
"child": {"ticket_id": 124, "number": "781259", "subject": "Child Ticket", "status": "Open"}
}
| Code | Message | Cause |
|---|---|---|
403 |
Access denied to child ticket department |
Only if the API key object provides department restrictions that exclude the department |
404 |
Child ticket not found |
Ticket does not exist |
404 |
Child has no parent to unlink |
The ticket has no parent |
Returns a file as Base64 together with its metadata.
GET /api/tickets-attachment-download.php/{file_id}.{format}
Permission: can_read_tickets
| Parameter | Type | Description |
|---|---|---|
file_id |
integer | file_id from the attachment metadata of a thread entry, see GET Retrieve Ticket |
format |
string | json or xml |
{
"file_id": 29,
"filename": "screenshot.png",
"mime_type": "image/png",
"size": 45320,
"content": "iVBORw0KGgoAAAANSUhEUgAA..."
}
| Code | Message | Cause |
|---|---|---|
400 |
Invalid file ID |
file_id smaller than 1 |
403 |
Attachment not accessible |
No row with this file_id exists in the attachment table |
404 |
File not found |
File does not exist |
500 |
Failed to read file content |
File content could not be read |
Before serving a file, the endpoint checks that a row with this
file_idexists in the attachment table. The check does not filter by the type of the object the attachment belongs to.
curl -s "https://osticket.local/api/tickets-attachment-download.php/29.json" \
-H "X-API-Key: YOUR_API_KEY" \
| jq -r '.content' \
| base64 -d > screenshot.png
The whole file is read into memory and Base64 encoded, which makes the response about a third larger than the file.
The plugin implements no rate limiting. If you need it, configure it on the web server.