Introduction
The SupportDesk API is a RESTful interface for managing support tickets across 60+ connected external projects from a single hub. All requests and responses use JSON. The API has two distinct authentication layers depending on who is calling.
Content-Type: application/json and Accept:
application/json headers on every request. All timestamps are in ISO
8601 format (UTC).Authentication
The API uses two separate authentication strategies. Choose the one appropriate for your use case.
Header:
Authorization:
Bearer <token>
/api/external/*.Headers:
X-Api-Key:
sk_xxx
X-Api-Secret: ss_xxx
JWT_TTL).
Use POST /auth/refresh before expiry or handle 401 responses by re-authenticating.
The refresh window is 14 days (JWT_REFRESH_TTL).Error Handling
All errors return a JSON body with an error key. Validation errors
include a details object with per-field arrays.
| 200 | Success |
| 201 | Created |
| 401 | Unauthenticated |
| 403 | Forbidden / insufficient role |
| 404 | Resource not found |
| 422 | Validation failed |
| 500 | Server error |
// Simple error { "error": "Invalid credentials" } // Validation error (422) { "error": "Validation failed.", "details": { "email": ["The email field is required."], "password": ["Min 8 characters."] } }
JWT Authentication
Obtain and manage JWT tokens for portal users (admins, agents, customers).
| Field | Type | Req | Description |
|---|---|---|---|
| string | * | User email address | |
| password | string | * | Account password |
{
"email": "admin@support.local",
"password": "password"
}
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhb...",
"token_type": "bearer",
"expires_in": 3600,
"user": {
"id": 1,
"name": "Support Admin",
"email": "admin@support.local",
"role": "admin"
}
}
| Field | Type | Req | Description |
|---|---|---|---|
| name | string | * | Full name |
| string | * | Must be unique | |
| password | string | * | Min 8 characters |
| password_confirmation | string | * | Must match password |
| phone | string | Phone number |
customer role. Admins create agent/admin accounts via
POST /admin/users.Send the expired token in the
Authorization: Bearer header. The API returns a new token
within the refresh window (14 days). No request body needed.
{ "access_token": "eyJ...", "token_type": "bearer", "expires_in": 3600 }
Blacklists the current token. No body required.
{ "message": "Logged out successfully" }
Returns the current authenticated user including their assigned projects.
{ "user": { "id":1, "name":"Admin", "role":"admin", "projects":[...] } }
| Field | Type | Req | Description |
|---|---|---|---|
| name | string | Display name | |
| phone | string | Phone number | |
| password | string | New password (min 8) | |
| password_confirmation | string | Required if changing password |
{ "user": { /* updated user object */ } }
Tickets
Core ticket operations. Scoped by role: customers see only their own tickets, agents see assigned project tickets, admins see all.
| Param | Type | Req | Description |
|---|---|---|---|
| page | integer | Page number (default: 1) | |
| per_page | integer | Results per page (default: 20, max: 100) | |
| status | string | open |
in_progress | pending_customer |
resolved | closed |
|
| priority | string | low | medium |
high | critical |
|
| project_id | integer | Filter by project (admin only) | |
| assigned_to | integer | Filter by agent user ID | |
| unassigned | boolean | Only unassigned tickets | |
| overdue | boolean | Only SLA-breached tickets | |
| search | string | Search subject, ticket#, customer email |
{
"data": [
{
"id": 42,
"ticket_number": "NAIJ-2024-00042",
"subject": "Cannot upload photos",
"status": "open",
"priority": "high",
"reply_count": 3,
"project": { "id":1, "name":"NaijaHomes", "slug":"naijaHomes" },
"category": { "id":1, "name":"Technical Issue" },
"assignee": { "id":3, "name":"Alice Okafor" },
"creator": { "id":5, "name":"Tunde Adeyemi" },
"tags": [],
"last_activity_at": "2024-04-21T10:30:00Z",
"created_at": "2024-04-21T09:00:00Z"
}
],
"current_page": 1,
"last_page": 5,
"total": 94,
"per_page": 20
}
| Param | Type | Req | Description |
|---|---|---|---|
| id | integer | * | Ticket database ID |
Returns the full ticket with messages, attachments, activities, SLA data, watchers, and rating.
{
"ticket": {
"id": 42,
"ticket_number": "NAIJ-2024-00042",
"subject": "Cannot upload photos",
"description": "Full description...",
"status": "in_progress",
"priority": "high",
"source": "api",
"customer_name": "Tunde Adeyemi",
"customer_email": "tunde@gmail.com",
"first_response_due_at": "2024-04-21T10:00:00Z",
"resolution_due_at": "2024-04-21T17:00:00Z",
"first_response_breached": false,
"resolution_breached": false,
"project": { /* ... */ },
"category": { /* ... */ },
"creator": { /* ... */ },
"assignee": { /* ... */ },
"tags": [],
"messages": [ /* reply/note objects */ ],
"attachments": [],
"activities": [ /* audit trail */ ],
"rating": null,
"sla_policy": { /* ... */ }
}
}
| Field | Type | Req | Description |
|---|---|---|---|
| project_id | integer | * | Target project ID |
| subject | string | * | Max 255 chars |
| description | string | * | Detailed description |
| priority | string | low |
medium | high |
critical (default: medium) |
|
| category_id | integer | Category ID | |
| tags | integer[] | Array of tag IDs | |
| customer_name | string | Customer name override | |
| customer_email | string | Customer email override | |
| customer_phone | string | Customer phone | |
| metadata | object | Arbitrary JSON metadata | |
| attachments[] | file | File uploads (max 10MB each) |
{
"ticket": {
"id": 43,
"ticket_number": "NAIJ-2024-00043",
"status": "open",
"priority": "high",
"sla_policy": { /* auto-applied */ },
"first_response_due_at": "2024-04-21T10:00:00Z",
"created_at": "2024-04-21T09:00:00Z"
}
}
| Field | Type | Description |
|---|---|---|
| subject | string | Updated subject |
| description | string | Updated description |
| category_id | integer | New category |
| priority | string | New priority (logged in activity) |
| tags | integer[] | Replaces existing tags |
{ "ticket": { /* updated ticket */ } }
| Field | Type | Req | Description |
|---|---|---|---|
| status | string | * | open |
in_progress |
pending_customer |
resolved | closed |
{ "ticket": { "status": "resolved", "resolved_at": "2024-04-21T14:00:00Z" } }
| Field | Type | Description |
|---|---|---|
| user_id | integer|null | Agent user ID to assign to.
Send null to unassign. Auto-moves
status to in_progress on
assignment. |
{ "ticket": { "assigned_to": 3, "assignee": { "name": "Alice Okafor" } } }
| Field | Type | Req | Description |
|---|---|---|---|
| body | string | * | Message content |
| type | string | reply
(default, customer-visible) |
note (agents only) |
|
| attachments[] | file | File uploads, max 10MB each |
type=reply.
The note type is restricted to agents
and admins and is never visible to the customer.
{
"message": {
"id": 88,
"body": "Have you cleared your cache?",
"type": "reply",
"is_first_response": true,
"user": { "id":3, "name":"Alice Okafor" },
"attachments": [],
"created_at": "2024-04-21T09:45:00Z"
}
}
| Field | Type | Req | Description |
|---|---|---|---|
| score | integer | * | 1–5 star rating |
| comment | string | Optional feedback text |
resolved. Rating can be
submitted once only.{ "rating": { "score": 5, "comment": "Great support!" } }
Subscribe or unsubscribe to ticket activity notifications. No body required.
{ "watching": true }
Soft-deletes the ticket. Records are recoverable; hard purge runs monthly via the scheduler.
{ "message": "Ticket deleted" }
Projects
Manage the connected external projects. Each project gets its own API key pair for external integration.
| Param | Type | Description |
|---|---|---|
| search | string | Search name or slug |
| active | boolean | Filter by active status |
| per_page | integer | Default 20 |
{ "data": [{ "id":1, "name":"NaijaHomes", "slug":"naijaHomes", "tickets_count":42, "agents_count":3, "is_active":true }] }
| Field | Type | Req | Description |
|---|---|---|---|
| name | string | * | Project name. Slug is auto-generated. |
| description | string | Short description | |
| support_email | Project support email | ||
| website_url | url | Project website | |
| timezone | string | Valid timezone (default: UTC) | |
| settings | object | Arbitrary project-level JSON config |
{ "project": { "id":6, "name":"AgriMarket", "slug":"agrimarket" } }
Returns the project with agents,
sla_policies, and
api_keys (excluding secrets).
{ "project": { "agents":[...], "api_keys":[{ "id":1, "name":"Production Key", "last_used_at":"..." }] } }
Same fields as create. All optional (PATCH
semantics). Include "is_active":
false to disable the project.
{ "project": { /* updated */ } }
| Field | Type | Req | Description |
|---|---|---|---|
| user_id | integer | * | User ID to assign |
| is_lead | boolean | Mark as project lead agent |
{ "message": "Agent assigned to project" }
// DELETE returns:
{ "message": "Agent removed from project" }
| Field | Type | Req | Description |
|---|---|---|---|
| name | string | * | Key label e.g. "Production Key" |
| allowed_ips | string[] | IP whitelist (empty = any) | |
| expires_at | date | Expiry date (ISO format) |
api_secret is returned
only once at creation
and is not stored in readable form. Save
it immediately.{
"api_key": "sk_Abc123xyzAbc123xyz...",
"api_secret": "ss_XyzLongSecretValue...",
"key_id": 7,
"message": "Store the api_secret securely."
}
GET /admin/projects/{id}/api-keys // List all keys
DELETE /admin/projects/{id}/api-keys/{keyId} // Revoke key
User Management
Create and manage admins, agents, and customers. All routes require Admin or Super Admin role.
| Param | Type | Description |
|---|---|---|
| role | string |
super_admin |
admin |
agent |
customer |
| search | string | Search name or email |
| active | boolean | Filter active/inactive |
| per_page | integer | Default 20 |
{ "data": [{ "id":3, "name":"Alice Okafor", "role":"agent", "is_active":true, "last_login_at":"..." }] }
| Field | Type | Req | Description |
|---|---|---|---|
| name | string | * | Full name |
| * | Unique email | ||
| password | string | * | Min 8 chars |
| role | string | * |
admin |
agent |
customer |
| department | string | Department label | |
| phone | string | Phone number |
GET /admin/users/{id} // Get user with project list
PUT /admin/users/{id} // Update name/email/role/active
DELETE /admin/users/{id} // Soft-delete (can't self-delete)
Reports & Analytics
All report endpoints accept
?from=YYYY-MM-DD&to=YYYY-MM-DD date
range filters. Admins can also filter with
?project_id=N. Default range: last 30
days.
Returns total counts by status, unassigned count, SLA breached count, and average response/resolution times.
{
"total_tickets": 284,
"by_status": {
"open": 42, "inProgress": 18,
"pending": 7, "resolved": 198, "closed": 19
},
"unassigned": 11,
"sla_breached": 4,
"avg_first_response_mins": 38.5,
"avg_resolution_minutes": 312.0
}
Accepts from and
to query params.
[{ "project":"NaijaHomes", "total":84, "resolved":72, "breached":2, "resolve_rate":85.7 }]
Per-agent assigned/resolved counts, resolve rate, avg resolution time.
[{ "agent":"Alice Okafor", "total_assigned":34, "total_resolved":31, "resolve_rate":91.2, "avg_resolution_minutes":280.5 }]
| Param | Type | Description |
|---|---|---|
| group_by | string |
day
(default) |
week |
month |
[{ "period":"2024-04-21", "created":12, "resolved":8 }]
{ "total_tickets":284, "fr_breach_rate":3.2, "res_breach_rate":1.4, "by_priority":{ "critical":{"total":5,"fr_breached":1,"res_breached":0} } }
[{ "category":"Technical Issue", "color":"#EF4444", "total":98 }]
Configuration Endpoints
Categories, SLA policies, tags, and canned responses. Read access is available to all authenticated users; writes require admin or agent roles.
Returns root categories with
nested children.
Also: POST
/categories,
PUT
/categories/{id},
DELETE
/categories/{id}
(Admin only).
{ "categories": [{ "id":1, "name":"Technical Issue", "color":"#EF4444", "children":[] }] }
Filter with
?project_id=N for
project-specific policies.
Policies with project_id:
null are global
defaults.
| Field | Type | Description |
|---|---|---|
| project_id | integer|null | null = global policy |
| priority | string |
low |
medium
| high
|
critical
|
| first_response_minutes | integer | Max minutes before first reply |
| resolution_minutes | integer | Max minutes to full resolution |
| Priority | First Response | Resolution |
|---|---|---|
| critical | 15 min | 2 hours |
| high | 1 hour | 8 hours |
| medium | 4 hours | 24 hours |
| low | 8 hours | 80 hours |
POST /admin/sla // Create policy PUT /admin/sla/{id} // Update policy DELETE /admin/sla/{id} // Delete policy
Returns own responses plus
all shared responses. Filter
with
?project_id=N
or
?search=refund.
POST /canned-responses // Create { title, body, shortcut?, is_shared } DELETE /canned-responses/{id} // Delete own (admin can delete any) GET /tags // List all tags POST /tags // Create tag { name, color } DELETE /tags/{id} // Delete tag
External Project API
Routes under
/api/external/* are
authenticated via API key pair headers — no
JWT needed. Each project gets its own key
scoped to its data.
X-Api-Key:
sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxX-Api-Secret:
ss_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Use this to test that your API key pair works and to confirm which project it belongs to.
{ "status":"ok", "project":{ "id":1, "name":"NaijaHomes", "slug":"naijaHomes" }, "timestamp":"2024-04-21T09:00:00Z" }
| Field | Type | Req | Description |
|---|---|---|---|
| customer_name | string | * | End user's full name |
| customer_email | * | End user's email (auto-creates account) | |
| subject | string | * | Ticket subject (max 255) |
| description | string | * | Full issue description |
| customer_phone | string | End user phone | |
| customer_id | string | Your platform's user ID for reference | |
| priority | string |
low
|
medium
|
high
|
critical
|
|
| category_id | integer | Category from shared category list | |
| metadata | object | Arbitrary data (listing_id, browser, URL…) |
{
"customer_name": "Tunde Adeyemi",
"customer_email": "tunde@gmail.com",
"customer_id": "USR-98231",
"subject": "Cannot upload property photos",
"description": "Upload button does nothing on Chrome...",
"priority": "high",
"metadata": {
"listing_id": "LST-00421",
"browser": "Chrome 124",
"platform": "web"
}
}
// Headers:
X-Api-Key: sk_Abc123...
X-Api-Secret: ss_XyzLong...
{
"ticket_number": "NAIJ-2024-00043",
"ticket_id": 43,
"status": "open",
"priority": "high",
"created_at": "2024-04-21T09:00:00Z"
}
| Param | Type | Description |
|---|---|---|
| customer_email | Filter by customer email | |
| status | string | Filter by status |
| priority | string | Filter by priority |
| per_page | integer | Default 20 |
Path param is the
ticket
number
string
e.g.
NAIJ-2024-00043,
not the integer ID.
Internal notes are
hidden from external
responses.
{
"ticket": {
"ticket_number": "NAIJ-2024-00043",
"subject": "Cannot upload photos",
"description": "...",
"status": "in_progress",
"assigned_to": "Alice Okafor",
"messages": [
{ "body":"Hi, can you describe...", "from":"Alice Okafor", "is_agent":true }
],
"resolved_at": null
}
}
| Field | Type | Req | Description |
|---|---|---|---|
| body | string | * | Reply text |
| customer_email | * | Must match ticket's customer email |
{ "message_id":89, "ticket":"NAIJ-2024-00043", "status":"pending_customer" }
| Field | Type | Req | Description |
|---|---|---|---|
| customer_email | * | Must match ticket's customer email |
{ "message":"Ticket closed", "ticket":"NAIJ-2024-00043" }
PHP Integration Example
Drop
this class into any PHP
project to connect to the
SupportDesk API. Set
SUPPORT_API_KEY
and
SUPPORT_API_SECRET
in your .env.
// SupportClient.php — drop into any PHP/Laravel project class SupportClient { private string $baseUrl = 'http://api-support.test/api/external'; public function __construct( private string $apiKey, private string $apiSecret ) {} /** Submit a new ticket */ public function createTicket(array $data): array { return $this->post('/tickets', $data); } /** Get ticket by ticket number e.g. NAIJ-2024-00001 */ public function getTicket(string $ticketNumber): array { return $this->get("/tickets/{$ticketNumber}"); } /** Add a customer reply */ public function addReply(string $ticketNumber, string $body, string $email): array { return $this->post("/tickets/{$ticketNumber}/reply", [ 'body' => $body, 'customer_email' => $email, ]); } /** List tickets for a customer email */ public function listByCustomer(string $email): array { return $this->get('/tickets', ['customer_email' => $email]); } /** Validate API key */ public function ping(): array { return $this->get('/ping'); } // ── Internals ────────────────────────────────────── private function get(string $path, array $query = []): array { return $this->request('GET', $path, query: $query); } private function post(string $path, array $body = []): array { return $this->request('POST', $path, body: $body); } private function request(string $method, string $path, array $body = [], array $query = []): array { $url = $this->baseUrl . $path; if ($query) $url .= '?' . http_build_query($query); $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Accept: application/json', 'X-Api-Key: ' . $this->apiKey, 'X-Api-Secret: ' . $this->apiSecret, ], CURLOPT_POSTFIELDS => $method !== 'GET' ? json_encode($body) : null, ]); $response = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $decoded = json_decode($response, true) ?? []; if ($status >= 400) { throw new RuntimeException($decoded['error'] ?? 'API error', $status); } return $decoded; } } // ── Usage in your project ────────────────────────── $support = new SupportClient( apiKey: $_ENV['SUPPORT_API_KEY'], apiSecret: $_ENV['SUPPORT_API_SECRET'], ); // Submit ticket $ticket = $support->createTicket([ 'customer_name' => $user->name, 'customer_email' => $user->email, 'customer_id' => $user->id, 'subject' => 'Cannot upload property photos', 'description' => $request->message, 'priority' => 'high', 'metadata' => [ 'listing_id' => $listing->id, 'url' => url()->current(), ], ]); echo $ticket['ticket_number']; // "NAIJ-2024-00043"
Roles & Permissions Reference
| Capability | super_admin | admin | agent | customer |
|---|---|---|---|---|
| Create tickets | ✓ | ✓ | ✓ | ✓ |
| View own tickets | ✓ | ✓ | ✓ | ✓ |
| View all tickets | ✓ | ✓ | — project only | ✗ |
| Reply to tickets | ✓ | ✓ | ✓ | ✓ |
| Add internal notes | ✓ | ✓ | ✓ | ✗ |
| Change status | ✓ | ✓ | ✓ | ✗ |
| Assign tickets | ✓ | ✓ | ✓ | ✗ |
| Change priority | ✓ | ✓ | ✓ | ✗ |
| Delete tickets | ✓ | ✓ | ✗ | ✗ |
| Rate resolved tickets | ✓ | ✓ | ✗ | ✅ (own) |
| Create projects | ✓ | ✓ | ✗ | ✗ |
| Manage projects | ✓ | ✓ | ✗ | ✗ |
| Generate API keys | ✓ | ✓ | ✗ | ✗ |
| Manage users | ✓ | ✓ | ✗ | ✗ |
| Create agents/admins | ✓ | ✓ | ✗ | ✗ |
| View all reports | ✓ | ✓ | — limited | ✗ |
| Manage SLA policies | ✓ | ✓ | ✗ | ✗ |
| Manage categories | ✓ | ✓ | ✗ | ✗ |
| Canned responses | ✓ | ✓ | ✓ | ✗ |