Extends osTicket's REST API with powerful endpoints for advanced ticket management. Enables ticket creation with Markdown formatting, department routing, subticket support, and comprehensive ticket management (update, retrieve, search, delete, statistics).
Perfect for integrations that need granular control beyond osTicket's standard API capabilities - ideal for support portals, bug trackers, automation platforms (Zapier/Make.com), and custom workflows.
| Requirement | Version | Notes |
|---|---|---|
| osTicket | 1.18.x | Plugin extends API via Signal system |
| PHP | 7.4+ | Recommended: PHP 8.1+ for best performance |
| Web Server | Apache 2.4+ or NGINX 1.18+ | Apache: .htaccess support required |
| Parsedown | Automatic | Included in Composer lock |
Optional Dependencies:
| Plugin | Purpose | Link |
|---|---|---|
| Markdown Support | Markdown rendering in tickets | GitHub |
| Subticket Manager | UI for subticket management | GitHub |
| API Key Wildcard | Wildcard IP validation for API keys | GitHub |
api-endpoints folder to /include/plugins/ on your osTicket serverFinal path: /path/to/osticket/include/plugins/api-endpoints/
cd /path/to/osticket/include/plugins
git clone https://github.com/markus-michalski/osticket-api-endpoints.git
The plugin automatically configures:
On activation, the plugin automatically deploys /api/.htaccess:
# osTicket API Endpoints Plugin - Apache Configuration
<IfModule mod_rewrite.c>
RewriteEngine On
# Enable PATH_INFO for all API endpoints
AcceptPathInfo On
# Rewrite rules for endpoints with path info
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(tickets-update|tickets-get|tickets-delete)\.php/(.+)$ $1.php [L,QSA]
</IfModule>
# Allow PATH_INFO for endpoints
<Files "tickets-update.php">
AcceptPathInfo On
</Files>
<Files "tickets-get.php">
AcceptPathInfo On
</Files>
<Files "tickets-delete.php">
AcceptPathInfo On
</Files>
Verify .htaccess is working:
curl -I "https://your-domain.com/api/tickets-get.php/123456.json" \
-H "X-API-Key: YOUR_API_KEY"
# Should return 200 OK (not 404)
Add the following configuration to your NGINX server block:
# osTicket API Endpoints Plugin - NGINX Configuration
# Endpoints with PATH_INFO (e.g., /api/tickets-update.php/123456.json)
location ~ ^/api/tickets-(update|get|delete)\.php/ {
fastcgi_split_path_info ^(/api/tickets-(update|get|delete)\.php)(/.+)$;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
include fastcgi_params;
fastcgi_pass unix:/var/run/php/php-fpm.sock; # Or 127.0.0.1:9000
}
# Endpoints without PATH_INFO (e.g., /api/tickets-search.php?query=test)
location ~ ^/api/tickets-(search|stats|statuses)\.php$ {
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_pass unix:/var/run/php/php-fpm.sock;
}
# Subticket endpoints with PATH_INFO
location ~ ^/api/tickets-subtickets-(parent|list)\.php/ {
fastcgi_split_path_info ^(/api/tickets-subtickets-(parent|list)\.php)(/.+)$;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
include fastcgi_params;
fastcgi_pass unix:/var/run/php/php-fpm.sock;
}
# Subticket endpoints without PATH_INFO
location ~ ^/api/tickets-subtickets-(create|unlink)\.php$ {
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_pass unix:/var/run/php/php-fpm.sock;
}
After adding configuration:
# Test configuration
sudo nginx -t
# Reload NGINX
sudo systemctl reload nginx
# Test endpoint
curl "https://your-domain.com/api/tickets-stats.php" \
-H "X-API-Key: YOUR_API_KEY"
After plugin installation, you need to configure API Key permissions to grant access to the new endpoints.
Configure API Key Permissions:
POST /tickets)PATCH /tickets/:id)GET /tickets/:number)GET /tickets/search)DELETE /tickets/:number) - Optional, disabled by default for securityGET /tickets-stats)Screenshot Example:
The API Key configuration shows permissions like:
β Create Tickets (can_create_tickets)
β Read Tickets (can_read_tickets)
β Update Tickets (can_update_tickets)
β Search Tickets (can_search_tickets)
β Delete Tickets (can_delete_tickets) [Disabled for security]
β Read Statistics (can_read_stats)
β Manage Subtickets (can_manage_subtickets)
After plugin installation, a new configuration page appears under Admin Panel β Plugins β API Endpoints β Configure, displaying all available endpoints with their associated permissions:
Available API Endpoints:
can_create_tickets permissioncan_read_tickets permissioncan_update_tickets permissioncan_search_tickets permissioncan_delete_tickets permissioncan_read_stats permissioncan_read_stats permissioncan_manage_subtickets permissionImportant: The plugin configuration page is informational only and shows which endpoints are available. Actual access to each endpoint is controlled individually per API Key via the API Key permissions system (see API Key Permissions).
The plugin provides 11 REST endpoints:
| Endpoint | Method | Description |
|---|---|---|
/api/tickets.json |
POST | Extended ticket creation (Markdown, Department, Subticket) |
/api/tickets-get.php/{number}.json |
GET | Retrieve ticket details |
/api/tickets-update.php/{number}.json |
PATCH | Update ticket (Status, Department, SLA, etc.) |
/api/tickets-search.php |
GET | Search tickets |
/api/tickets-delete.php/{number}.json |
DELETE | Permanently delete ticket |
/api/tickets-stats.php |
GET | Ticket statistics (global, Department, Staff) |
/api/tickets-statuses.php |
GET | List available ticket statuses |
/api/tickets-subtickets-parent.php/{id}.json |
GET | Get parent ticket of a subticket |
/api/tickets-subtickets-list.php/{id}.json |
GET | Get all child tickets of a parent |
/api/tickets-subtickets-create.php |
POST | Create parent-child relationship |
/api/tickets-subtickets-unlink.php |
DELETE | Remove parent-child relationship |
All GET endpoints support both JSON and XML:
.json for JSON response.xml for XML responseCreate tickets with extended parameters like Markdown formatting, department routing, and subticket linking.
URL: POST /api/tickets.json
Extended Parameters:
| Parameter | Type | Description | Example |
|---|---|---|---|
format |
string | Message format: text, html, markdown |
"markdown" |
departmentId |
string/int | Department name or ID | "Support" or 5 |
parentTicketId |
string/int | Parent ticket number for subticket | "181752" |
autorespond |
bool | Send auto-response | true |
source |
string | Ticket source | "API", "Web" |
topicId |
int | Help Topic ID | 10 |
priority |
string/int | Priority name or ID | "High" or 2 |
duedate |
string | Due date | "2025-12-31" |
Example: Create ticket with Markdown
curl -X POST "https://your-domain.com/api/tickets.json" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "John Doe",
"email": "john@example.com",
"subject": "Bug Report: Login failed",
"message": "## Problem\n\nLogin fails with the following error:\n\n```bash\nERROR: Invalid credentials\n```\n\n**Browser:** Chrome 120\n**OS:** Windows 11",
"format": "markdown",
"departmentId": "IT Support",
"priority": "High"
}'
Response:
{
"ticket_id": 123456,
"number": "ABC123",
"subject": "Bug Report: Login failed",
"message": "Ticket created successfully"
}
Example: Create subticket
curl -X POST "https://your-domain.com/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 production database to new server",
"parentTicketId": "181752",
"departmentId": "Development"
}'
Error Codes:
400 - Invalid parameters (missing required fields, invalid format)403 - API key not authorized (missing can_create_tickets permission)404 - Department/Parent ticket not found501 - Markdown plugin not available (if format=markdown and plugin inactive)Retrieve details of a single ticket.
URL: GET /api/tickets-get.php/{number}.{format}
Path Parameters:
{number} - Ticket number (e.g., ABC123) or ticket ID (e.g., 123456){format} - Response format: json or xmlExample:
curl "https://your-domain.com/api/tickets-get.php/191215.json" \
-H "X-API-Key: YOUR_API_KEY"
Response:
{
"ticket_id": 191215,
"number": "ABC191",
"subject": "Login Problem",
"name": "John Doe",
"email": "john@example.com",
"phone": "+1 123 456789",
"status": "Open",
"priority": "Normal",
"department": "IT Support",
"created": "2025-11-08 10:30:00",
"updated": "2025-11-08 14:20:00",
"duedate": "2025-11-15",
"isoverdue": false,
"thread": [
{
"id": 1,
"type": "message",
"poster": "John Doe",
"body": "I can't log in...",
"format": "text",
"created": "2025-11-08 10:30:00"
},
{
"id": 2,
"type": "response",
"poster": "Support Agent",
"body": "## Solution\n\nPlease try...",
"format": "markdown",
"created": "2025-11-08 11:00:00"
}
]
}
XML Response:
curl "https://your-domain.com/api/tickets-get.php/191215.xml" \
-H "X-API-Key: YOUR_API_KEY"
<?xml version="1.0"?>
<ticket>
<ticket_id>191215</ticket_id>
<number>ABC191</number>
<subject>Login Problem</subject>
<status>Open</status>
<thread>
<entry>
<id>1</id>
<poster>John Doe</poster>
<body>I can't log in...</body>
</entry>
</thread>
</ticket>
Error Codes:
400 - Invalid ticket number/ID403 - API key not authorized404 - Ticket not foundUpdate ticket properties like status, department, priority, SLA, staff assignment.
URL: PATCH /api/tickets-update.php/{number}.{format}
Updatable Fields:
| Field | Type | Description | Example |
|---|---|---|---|
statusId |
string/int | Status name or ID | "Resolved" or 3 |
departmentId |
string/int | Department name or ID | "Sales" or 7 |
topicId |
int | Help Topic ID | 15 |
slaId |
string/int | SLA Plan name or ID | "Premium Support" or 2 |
staffId |
string/int | Staff username or ID | "admin" or 5 |
duedate |
string | Due date | "2025-12-31" |
priority |
string/int | Priority | "Urgent" or 4 |
Example: Set status to "Resolved"
curl -X PATCH "https://your-domain.com/api/tickets-update.php/191215.json" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"statusId": "Resolved"
}'
Example: Assign ticket and increase priority
curl -X PATCH "https://your-domain.com/api/tickets-update.php/191215.json" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"staffId": "john.doe",
"priority": "High",
"duedate": "2025-11-15"
}'
Example: Change department
curl -X PATCH "https://your-domain.com/api/tickets-update.php/191215.json" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"departmentId": "Billing"
}'
Response:
{
"success": true,
"message": "Ticket updated successfully",
"ticket_id": 191215,
"number": "ABC191"
}
Error Codes:
400 - Invalid parameters (unknown status, department, etc.)403 - API key not authorized404 - Ticket not foundSearch tickets by query string, status, department, and other filters.
URL: GET /api/tickets-search.php?query={query}&status={status}
Query Parameters:
| Parameter | Type | Description | Example |
|---|---|---|---|
query |
string | Search term (ticket number, subject, message) | "login" |
status |
string | Status filter | "Open", "Closed" |
departmentId |
int | Department ID filter | 5 |
limit |
int | Max number of results | 50 (default: 20) |
offset |
int | Offset for pagination | 0 |
Example: Search for "login" in open tickets
curl "https://your-domain.com/api/tickets-search.php?query=login&status=Open" \
-H "X-API-Key: YOUR_API_KEY"
Example: Filter by department
curl "https://your-domain.com/api/tickets-search.php?query=refund&departmentId=5&limit=10" \
-H "X-API-Key: YOUR_API_KEY"
Response:
{
"count": 3,
"tickets": [
{
"ticket_id": 123,
"number": "ABC123",
"subject": "Login Issue",
"status": "Open",
"department": "IT Support",
"created": "2025-11-08 10:00:00"
},
{
"ticket_id": 124,
"number": "ABC124",
"subject": "Password Reset",
"status": "Open",
"department": "IT Support",
"created": "2025-11-08 11:30:00"
}
]
}
Error Codes:
400 - Invalid query parameters403 - API key not authorizedPermanently delete a ticket from the database.
β οΈ WARNING: This action is irreversible! Ticket and all thread entries will be permanently deleted.
URL: DELETE /api/tickets-delete.php/{number}.{format}
Example:
curl -X DELETE "https://your-domain.com/api/tickets-delete.php/191215.json" \
-H "X-API-Key: YOUR_API_KEY"
Response:
{
"success": true,
"message": "Ticket deleted successfully",
"ticket_id": 191215,
"number": "ABC191"
}
Error Codes:
400 - Invalid ticket number/ID403 - API key not authorized (missing can_delete_tickets permission)404 - Ticket not foundSecurity Notes:
Retrieve comprehensive ticket statistics: Global, per department, per staff, per status.
URL: GET /api/tickets-stats.php
Query Parameters:
| Parameter | Type | Description | Example |
|---|---|---|---|
departmentId |
int | Filter by department | 5 |
staffId |
int | Filter by staff member | 3 |
dateRange |
string | Time period filter | "today", "week", "month", "year" |
Example: Global statistics
curl "https://your-domain.com/api/tickets-stats.php" \
-H "X-API-Key: YOUR_API_KEY"
Response:
{
"global": {
"total": 1523,
"open": 342,
"closed": 1181,
"overdue": 15,
"answered": 305,
"assigned": 280
},
"by_department": [
{
"department_id": 1,
"department_name": "IT Support",
"total": 856,
"open": 198,
"closed": 658
},
{
"department_id": 2,
"department_name": "Billing",
"total": 445,
"open": 89,
"closed": 356
}
],
"by_staff": [
{
"staff_id": 1,
"staff_name": "John Doe",
"assigned": 45,
"closed": 120
},
{
"staff_id": 2,
"staff_name": "Jane Smith",
"assigned": 38,
"closed": 95
}
],
"by_status": [
{
"status": "Open",
"count": 342
},
{
"status": "Resolved",
"count": 890
},
{
"status": "Closed",
"count": 291
}
]
}
Example: Department-specific stats
curl "https://your-domain.com/api/tickets-stats.php?departmentId=5" \
-H "X-API-Key: YOUR_API_KEY"
Error Codes:
403 - API key not authorizedRetrieve all available ticket statuses with IDs (for status name β ID resolution).
URL: GET /api/tickets-statuses.php
Example:
curl "https://your-domain.com/api/tickets-statuses.php" \
-H "X-API-Key: YOUR_API_KEY"
Response:
{
"statuses": [
{
"id": 1,
"name": "Open",
"state": "open"
},
{
"id": 2,
"name": "Resolved",
"state": "closed"
},
{
"id": 3,
"name": "Closed",
"state": "closed"
},
{
"id": 4,
"name": "Archived",
"state": "archived"
}
]
}
Usage:
Use this endpoint to resolve status names to IDs if you want to use status IDs instead of names in PATCH /tickets-update.
The plugin provides 4 dedicated endpoints for subticket management. These endpoints require the Subticket Manager Plugin.
Requirements:
can_manage_subtickets permissionSubticket Endpoints:
| Endpoint | Method | Description |
|---|---|---|
/api/tickets-subtickets-parent.php/{id}.json |
GET | Get parent ticket of a subticket |
/api/tickets-subtickets-list.php/{id}.json |
GET | Get all child tickets |
/api/tickets-subtickets-create.php |
POST | Create parent-child relationship |
/api/tickets-subtickets-unlink.php |
DELETE | Remove relationship |
Returns the parent ticket of a child ticket.
URL: GET /api/tickets-subtickets-parent.php/{child_id}.{format}
Example:
curl "https://your-domain.com/api/tickets-subtickets-parent.php/200.json" \
-H "X-API-Key: YOUR_API_KEY"
Response (Parent exists):
{
"parent": {
"ticket_id": 100,
"number": "ABC100",
"subject": "Server Migration Project",
"status": "Open"
}
}
Response (No parent):
{
"parent": null
}
Error Codes:
400 - Invalid ticket ID403 - API key not authorized (missing can_manage_subtickets permission)404 - Child ticket not found501 - Subticket Manager Plugin not availableReturns all child tickets of a parent ticket.
URL: GET /api/tickets-subtickets-list.php/{parent_id}.{format}
Example:
curl "https://your-domain.com/api/tickets-subtickets-list.php/100.json" \
-H "X-API-Key: YOUR_API_KEY"
Response (With children):
{
"children": [
{
"ticket_id": 123,
"number": "ABC123",
"subject": "Subtask: Database Migration",
"status": "Open"
},
{
"ticket_id": 124,
"number": "ABC124",
"subject": "Subtask: DNS Configuration",
"status": "Resolved"
},
{
"ticket_id": 125,
"number": "ABC125",
"subject": "Subtask: Testing",
"status": "Closed"
}
]
}
Response (No children):
{
"children": []
}
Error Codes:
400 - Invalid ticket ID403 - API key not authorized404 - Parent ticket not found501 - Subticket Manager Plugin not availableCreates a parent-child relationship between two existing tickets.
URL: POST /api/tickets-subtickets-create.php
Request Body:
{
"parent_id": 100,
"child_id": 200
}
Example:
curl -X POST "https://your-domain.com/api/tickets-subtickets-create.php" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"parent_id": 100,
"child_id": 200
}'
Response:
{
"success": true,
"message": "Subticket relationship created successfully",
"parent": {
"ticket_id": 100,
"number": "ABC100",
"subject": "Server Migration Project",
"status": "Open"
},
"child": {
"ticket_id": 200,
"number": "ABC200",
"subject": "Database Backup",
"status": "Open"
}
}
Validations:
Error Codes:
400 - Invalid ticket IDs or self-link attempt403 - API key not authorized (missing permission or department access)404 - Parent or child ticket not found409 - Child already has a different parent501 - Subticket Manager Plugin not availableRemoves the parent-child relationship of a child ticket.
URL: DELETE /api/tickets-subtickets-unlink.php
Request Body:
{
"child_id": 200
}
Example:
curl -X DELETE "https://your-domain.com/api/tickets-subtickets-unlink.php" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"child_id": 200
}'
Response:
{
"success": true,
"message": "Subticket relationship removed successfully",
"child": {
"ticket_id": 200,
"number": "ABC200",
"subject": "Database Backup",
"status": "Open"
}
}
Error Codes:
400 - Invalid child ticket ID403 - API key not authorized404 - Child ticket not found or has no parent501 - Subticket Manager Plugin not availableScenario: Server migration with 3 subtasks
API_URL="https://osticket.local/api"
API_KEY="YOUR_API_KEY"
# 1. Create parent ticket
parent_id=$(curl -X POST "$API_URL/tickets.json" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Project Manager",
"email": "pm@example.com",
"subject": "Server Migration Project",
"message": "# Migration Plan\n\n- Database backup\n- DNS update\n- Testing"
}' | jq -r '.ticket_id')
echo "Parent ticket created: ID $parent_id"
# 2. Subtask 1: Database Backup
child1_id=$(curl -X POST "$API_URL/tickets.json" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "DBA",
"email": "dba@example.com",
"subject": "Subtask: Database Backup",
"message": "Create full database backup"
}' | jq -r '.ticket_id')
# 3. Subtask 2: DNS Configuration
child2_id=$(curl -X POST "$API_URL/tickets.json" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Network Admin",
"email": "net@example.com",
"subject": "Subtask: DNS Update",
"message": "Update DNS records"
}' | jq -r '.ticket_id')
# 4. Subtask 3: Testing
child3_id=$(curl -X POST "$API_URL/tickets.json" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "QA Engineer",
"email": "qa@example.com",
"subject": "Subtask: Migration Testing",
"message": "Test all services after migration"
}' | jq -r '.ticket_id')
# 5. Link all subtasks
curl -X POST "$API_URL/tickets-subtickets-create.php" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d "{\"parent_id\": $parent_id, \"child_id\": $child1_id}"
curl -X POST "$API_URL/tickets-subtickets-create.php" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d "{\"parent_id\": $parent_id, \"child_id\": $child2_id}"
curl -X POST "$API_URL/tickets-subtickets-create.php" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d "{\"parent_id\": $parent_id, \"child_id\": $child3_id}"
# 6. Check child tickets
curl "$API_URL/tickets-subtickets-list.php/$parent_id.json" \
-H "X-API-Key: $API_KEY" | jq
# 7. Check parent ticket
curl "$API_URL/tickets-subtickets-parent.php/$child1_id.json" \
-H "X-API-Key: $API_KEY" | jq
The plugin extends osTicket's API key permission system with granular permissions.
| Permission | Database Field | Grants Access To |
|---|---|---|
| Create Tickets | can_create_tickets |
POST /tickets |
| Read Tickets | can_read_tickets |
GET /tickets/:number |
| Update Tickets | can_update_tickets |
PATCH /tickets/:number |
| Search Tickets | can_search_tickets |
GET /tickets/search |
| Delete Tickets | can_delete_tickets |
DELETE /tickets/:number |
| Read Statistics | can_read_stats |
GET /tickets-stats, GET /tickets-statuses |
| Manage Subtickets | can_manage_subtickets |
All subticket endpoints |
Screenshot:
The plugin config adds a new tab "API Endpoints Permissions" with the following options:
β Create Tickets (can_create_tickets)
β Read Tickets (can_read_tickets)
β Update Tickets (can_update_tickets)
β Search Tickets (can_search_tickets)
β Delete Tickets (can_delete_tickets) [Disabled for security]
β Read Statistics (can_read_stats)
β Manage Subtickets (can_manage_subtickets)
The plugin supports legacy API keys (without new permissions):
can_create_tickets missing β fallback to canCreateTickets() (osTicket standard)can_read_tickets missing β fallback to canCreateTickets() (READ = CREATE allowed)can_update_tickets missing β fallback to canCreateTickets()Recommendation: Migrate to new permissions for fine-grained control.
Principle of Least Privilege:
can_read_statscan_create_ticketsRotation & Revocation:
Audit Logging:
Symptoms:
curl "https://osticket.local/api/tickets-get.php/123456.json"
# HTTP 404 Not Found
Cause: Apache/NGINX not configured for PATH_INFO.
Solution (Apache):
Check if /api/.htaccess exists:
ls -la /path/to/osticket/api/.htaccess
If missing, re-enable plugin (deploys .htaccess automatically)
Check Apache configuration:
# In VirtualHost or .conf
<Directory /path/to/osticket/api>
AllowOverride All
</Directory>
Reload Apache:
sudo systemctl reload apache2
Solution (NGINX):
Add PATH_INFO configuration (see Installation β NGINX)
Test configuration:
sudo nginx -t
Reload NGINX:
sudo systemctl reload nginx
Symptoms:
**bold** instead of bold)Check:
Markdown Support Plugin installed?
ls /path/to/osticket/include/plugins/markdown-support
Plugin enabled?
API request correct?
curl -X POST "https://osticket.local/api/tickets.json" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Test",
"email": "test@example.com",
"subject": "Markdown Test",
"message": "## Heading\n\n**Bold text**",
"format": "markdown" # β Important!
}'
Plugin configuration:
Workaround: If Markdown Support Plugin unavailable:
format: "html" instead of format: "markdown"Symptoms:
{
"error": "API key not authorized for this operation"
}
Cause: API key has missing permissions.
Solution:
Check API key permissions:
can_create_ticketscan_read_ticketscan_update_ticketscan_delete_ticketscan_search_ticketscan_read_statscan_manage_subticketsRe-enable plugin after update:
Check fallback permission:
canCreateTickets() as fallbackSymptoms:
{
"error": "Department 'Support' not found"
}
Cause: Department name misspelled or department inactive.
Check:
Department exists?
SELECT dept_id, dept_name, ispublic
FROM ost_department
WHERE dept_name = 'Support';
Department active?
Case-sensitivity:
"Support", "support", "SUPPORT" are all validWorkaround: Use department ID instead of name:
{
"departmentId": 5
}
Get department IDs:
SELECT dept_id, dept_name FROM ost_department;
Symptoms:
{
"error": "Subticket plugin not available. Please install and enable the Subticket Manager Plugin."
}
Cause: Subticket Manager Plugin missing or inactive.
Solution:
Install plugin:
cd /path/to/osticket/include/plugins
git clone https://github.com/markus-michalski/osticket-subticket-manager.git
Enable plugin:
Test API endpoint again:
curl "https://osticket.local/api/tickets-subtickets-parent.php/123.json" \
-H "X-API-Key: $API_KEY"
Symptoms:
{
"error": "Circular dependency detected"
}
Cause: Attempt to make ticket A a child of ticket B, while B is already a child of A (or deeper circular relationship).
Example:
Solution:
Check structure:
# Check parent of ticket C
curl "$API_URL/tickets-subtickets-parent.php/C.json" -H "X-API-Key: $API_KEY"
# Check children of ticket A
curl "$API_URL/tickets-subtickets-list.php/A.json" -H "X-API-Key: $API_KEY"
Remove relationship first:
curl -X DELETE "$API_URL/tickets-subtickets-unlink.php" \
-H "X-API-Key: $API_KEY" \
-d '{"child_id": C}'
Organize tickets in tree structure (no circles)
Plugin Structure:
api-endpoints/
βββ plugin.php # Plugin metadata
βββ class.ApiEndpointsPlugin.php # Main plugin class
βββ config.php # Plugin configuration
βββ controllers/
β βββ ExtendedTicketApiController.php # Extended ticket API logic
β βββ SubticketApiController.php # Subticket API logic
βββ lib/
β βββ XmlHelper.php # XML response formatting
βββ api/ # API endpoint files (deployed to /api/)
β βββ tickets-get.php
β βββ tickets-update.php
β βββ tickets-delete.php
β βββ tickets-search.php
β βββ tickets-stats.php
β βββ tickets-statuses.php
β βββ tickets-subtickets-parent.php
β βββ tickets-subtickets-list.php
β βββ tickets-subtickets-create.php
β βββ tickets-subtickets-unlink.php
βββ assets/
β βββ admin-apikey-extension.js # API key config UI extension
βββ htaccess.template # Apache .htaccess template
βββ tests/
β βββ Unit/ # PHPUnit unit tests
β βββ Integration/ # PHPUnit integration tests
βββ vendor/ # Composer dependencies
βββ Parsedown.php
Signal-based Routing:
The plugin uses osTicket's Signal system for API endpoint registration:
Signal::connect('api', function($dispatcher) {
// POST /tickets with extended parameters
$dispatcher->append(
url_post('^/tickets\.(?P<format>json|xml)$',
['controller' => 'ExtendedTicketApiController', 'action' => 'createTicket'])
);
// GET /tickets/:number
$dispatcher->append(
url_get('^/tickets-get\.php/(?P<number>\d+)\.(?P<format>json|xml)$',
['controller' => 'ExtendedTicketApiController', 'action' => 'getTicket'])
);
// PATCH /tickets/:number
$dispatcher->append(
url('^/tickets-update\.php/(?P<number>\d+)\.(?P<format>json|xml)$',
['controller' => 'ExtendedTicketApiController', 'action' => 'updateTicket'])
);
});
Benefits:
ExtendedTicketApiController (extends TicketApiController):
createTicket() for extended parametersSubticketApiController (standalone):
Dependency Injection:
Both controllers use constructor injection for testability:
class ExtendedTicketApiController extends TicketApiController {
private $ticketService;
private $validator;
public function __construct(
TicketService $ticketService,
TicketValidator $validator
) {
parent::__construct();
$this->ticketService = $ticketService;
$this->validator = $validator;
}
}
CRITICAL: Plugin uses two-step lookup logic:
// First: Lookup by NUMBER (e.g., "ABC123")
$ticket = Ticket::lookupByNumber($number);
// Fallback: Lookup by ID (e.g., 123456)
if (!$ticket) {
$ticket = Ticket::lookup($number);
}
Reason: Ticket numbers like "123456" could be misinterpreted as IDs.
Best Practice: Always lookupByNumber() first, then lookup() as fallback.
All endpoints use:
json_encode($result, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
Benefits:
JSON_THROW_ON_ERROR β Throws exception on encoding errors (no false return)JSON_UNESCAPED_UNICODE β Umlauts stay readable ("MΓΌller" not "M\u00fcller")All error responses validate HTTP codes:
$code = $e->getCode();
if ($code < 100 || $code > 599) {
$code = 400; // Fallback for invalid codes
}
http_response_code($code);
Prevents: Invalid HTTP codes like 0, -1, 999
Input Validation:
db_input() for SQL escaping (osTicket standard)XSS Prevention:
Format::sanitize() for all contenthtmlspecialchars()API Key Validation:
CSRF Protection:
Single-Query Ticket Lookup:
// Instead of N+1 queries
$ticket = Ticket::lookupByNumber($number);
$thread = $ticket->getThread(); // Single JOIN
Batch Operations:
tickets-search uses optimized SQL queriesLIMIT/OFFSETCaching Headers:
header('Cache-Control: no-cache, must-revalidate');
header('Expires: Thu, 01 Jan 1970 00:00:00 GMT');
Lazy Loading:
format=markdown77 Tests (64 Unit + 13 Integration):
Unit Tests:
Integration Tests:
CI/CD:
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
php: ['7.4', '8.0', '8.1', '8.2', '8.3']
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
- name: Install Dependencies
run: composer install
- name: Run Tests
run: composer test
Q: Do I need to modify osTicket core files?
A: No! The plugin uses osTicket's Signal system and is completely update-safe.
Q: Does it work with custom osTicket themes?
A: Yes, the plugin doesn't change any frontend elements (API backend only).
Q: Can I upgrade osTicket without breaking the plugin?
A: Yes, as long as osTicket maintains the Signal system (1.18.x β 1.19.x should be fine).
Q: How do I uninstall the plugin?
A: Admin Panel β Plugins β API Endpoints β Delete. The plugin removes:
/api/tickets-*.php)Q: Can I use API keys with IP restrictions?
A: Yes! Install API Key Wildcard Plugin for flexible IP validation (including wildcard 0.0.0.0).
Q: How do I create tickets with attachments?
A: Use multipart/form-data instead of application/json:
curl -X POST "https://osticket.local/api/tickets.json" \
-H "X-API-Key: $API_KEY" \
-F "name=Test" \
-F "email=test@example.com" \
-F "subject=Ticket with Attachment" \
-F "message=See attachment" \
-F "attachments[]=@/path/to/file.pdf"
Q: Can I create tickets on behalf of another user?
A: Yes, set name and email in the request body. The ticket will be created under that user.
Q: Does the API support custom fields?
A: Yes! osTicket's standard API supports custom fields via dynamic keys:
{
"subject": "Test",
"message": "Test",
"custom_field_1": "Value",
"custom_field_2": "Other value"
}
Q: How do I get all open tickets?
A: Use search without query:
curl "https://osticket.local/api/tickets-search.php?status=Open&limit=100" \
-H "X-API-Key: $API_KEY"
Q: Can a ticket have multiple parents?
A: No. osTicket's ticket_pid field only supports one parent (tree structure, not graph).
Q: What happens if I delete a parent ticket?
A: Child tickets remain, but their ticket_pid is set to NULL (unlinking).
Q: Is there a maximum depth for subticket hierarchies?
A: No, but recommended for performance: max. 3-4 levels.
Q: Can I convert existing tickets to subtickets retroactively?
A: Yes! Use POST /api/tickets-subtickets-create.php to link existing tickets.
Q: How many requests per second can the API handle?
A: Depends on web server/hardware. Recommendations:
Q: Are API responses cached?
A: No, every request hits the database. For caching:
Q: How do I optimize search with 100,000+ tickets?
A:
SHOW INDEX FROM ost_ticket;limit + offset)Q: Which Markdown syntax is supported?
A: Standard Markdown (CommonMark) via Parsedown:
# H1, ## H2)**bold**, *italic*)```)-, 1.)[text](url))Q: Can I use HTML in Markdown?
A: No, for security reasons. Parsedown runs in SafeMode (blocks inline HTML).
Q: How do I add syntax highlighting to code blocks?
A: Parsedown marks code blocks with class="language-php", but doesn't highlight automatically. Integrate Prism.js or Highlight.js in osTicket's frontend.
Q: Why do I get 500 Internal Server Error?
A: Check Apache/NGINX error log:
# Apache
tail -f /var/log/apache2/error.log
# NGINX
tail -f /var/log/nginx/error.log
# PHP-FPM
tail -f /var/log/php8.1-fpm.log
Common causes:
memory_limit = 256M in php.inicomposer installost-config.phpQ: API responds with "Page not found"?
A:
ls /path/to/osticket/api/tickets-*.phpThis Plugin is released under the GNU General Public License v2, compatible with osTicket core.
See LICENSE for details.
For questions or issues, please create an issue on GitHub:
Issue Tracker: https://github.com/markus-michalski/osticket-api-endpoints/issues
When reporting issues, please include:
php -v)Developed by Markus Michalski
Contributions welcome!
Development Setup:
# Clone repository
git clone https://github.com/markus-michalski/osticket-api-endpoints.git
cd osticket-api-endpoints
# Install dependencies
composer install
# Run tests
composer test
# Check code style
composer cs-check
See CHANGELOG.md for version history.