● Live Documentation

SupportDesk
API Reference

Complete REST API documentation for the centralized multi-project support ticket platform. Covers JWT portal authentication, external project API key integration, tickets, SLA management, and analytics.

Base URL http://api-support.test/api
Version v1.0.0
Format JSON
Auth JWT Bearer + API Key Pair
📋

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.

Base URL
http://api-support.test/api
Content-Type
application/json
Accept
application/json
Portal Auth
Authorization: Bearer <jwt>
External Auth
X-Api-Key + X-Api-Secret
Health Check
GET /health
💡
Always include 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.

Strategy A — JWT (Portal Users)
Used by admins, agents, and customer reps accessing the support portal.

Header: Authorization: Bearer <token>
Strategy B — API Key Pair (External Projects)
Used by the 60+ external apps submitting tickets via /api/external/*.

Headers:
X-Api-Key: sk_xxx
X-Api-Secret: ss_xxx
⚠️
JWT tokens expire after 60 minutes (configurable via 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.

HTTP Status Codes
200 Success
201 Created
401 Unauthenticated
403 Forbidden / insufficient role
404 Resource not found
422 Validation failed
500 Server error
Error Response Shape
JSON
// 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).

POST /auth/login Authenticate and get JWT token
Public
Request Body
Field Type Req Description
email string * User email address
password string * Account password
Request Example
JSON
{
  "email": "admin@support.local",
  "password": "password"
}
Response — 200 OK
200 OK401403 Inactive
JSON
{
  "access_token": "eyJ0eXAiOiJKV1QiLCJhb...",
  "token_type": "bearer",
  "expires_in": 3600,
  "user": {
    "id": 1,
    "name": "Support Admin",
    "email": "admin@support.local",
    "role": "admin"
  }
}
POST /auth/register Register customer account
Public
Request Body
Field Type Req Description
name string * Full name
email string * Must be unique
password string * Min 8 characters
password_confirmation string * Must match password
phone string Phone number
Response — 201 Created
201422 Validation
ℹ️
Returns same shape as login. New accounts always receive the customer role. Admins create agent/admin accounts via POST /admin/users.
POST /auth/refresh Refresh expired JWT token
JWT Required

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.

Response — 200 OK
200 OK401 Expired refresh
JSON
{ "access_token": "eyJ...", "token_type": "bearer", "expires_in": 3600 }
POST /auth/logout Invalidate JWT token
JWT Required

Blacklists the current token. No body required.

Response — 200 OK
JSON
{ "message": "Logged out successfully" }
GET /auth/me Get authenticated user info
JWT Required

Returns the current authenticated user including their assigned projects.

Response — 200 OK
JSON
{ "user": { "id":1, "name":"Admin", "role":"admin", "projects":[...] } }
PUT /auth/profile Update own profile / change password
JWT Required
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
Response — 200 OK
200 OK422
JSON
{ "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.

GET /tickets List tickets (paginated, filterable)
JWT RequiredAll Roles
Query Parameters
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
Response — 200 OK (Paginated)
200 OK
JSON
{
  "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
}
GET /tickets/{id} Get full ticket detail
JWT RequiredScoped by Role
Path Parameters
Param Type Req Description
id integer * Ticket database ID

Returns the full ticket with messages, attachments, activities, SLA data, watchers, and rating.

Response includes
200 OK403404
JSON
{
  "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": { /* ... */ }
  }
}
POST /tickets Create a new ticket
JWT RequiredAll Roles
Request Body (multipart or JSON)
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)
Response — 201 Created
201 Created422
JSON
{
  "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"
  }
}
✅
SLA deadlines are automatically calculated and stamped based on the project's policy for the selected priority.
PUT /tickets/{id} Update ticket subject/category/tags
JWT RequiredAgent+
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
Response — 200 OK
200 OK403422
JSON
{ "ticket": { /* updated ticket */ } }
POST /tickets/{id}/status Change ticket status
JWT RequiredAgent+
Field Type Req Description
status string * open | in_progress | pending_customer | resolved | closed
ℹ️
Status changes are automatically logged in the activity trail with timestamp and actor.
Response — 200 OK
200 OK403
JSON
{ "ticket": { "status": "resolved", "resolved_at": "2024-04-21T14:00:00Z" } }
POST /tickets/{id}/assign Assign / unassign ticket
JWT RequiredAgent+
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.
Response — 200 OK
JSON
{ "ticket": { "assigned_to": 3, "assignee": { "name": "Alice Okafor" } } }
POST /tickets/{id}/messages Add reply or internal note
JWT RequiredAll Roles
Field Type Req Description
body string * Message content
type string reply (default, customer-visible) | note (agents only)
attachments[] file File uploads, max 10MB each
⚠️
Customers can only send type=reply. The note type is restricted to agents and admins and is never visible to the customer.
Response — 201 Created
201403422
JSON
{
  "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"
  }
}
POST /tickets/{id}/rate Rate resolved ticket (CSAT)
JWT RequiredCreator Only
Field Type Req Description
score integer * 1–5 star rating
comment string Optional feedback text
ℹ️
Only the original ticket creator can rate. Ticket must be resolved. Rating can be submitted once only.
Response — 200 OK
JSON
{ "rating": { "score": 5, "comment": "Great support!" } }
POST /tickets/{id}/watch  |  DELETE /tickets/{id}/watch Watch / Unwatch ticket
JWT Required

Subscribe or unsubscribe to ticket activity notifications. No body required.

Response — 200 OK
JSON
{ "watching": true }
DELETE /tickets/{id} Soft-delete ticket (Admin only)
JWT RequiredAdmin Only

Soft-deletes the ticket. Records are recoverable; hard purge runs monthly via the scheduler.

Response — 200 OK
JSON
{ "message": "Ticket deleted" }
🏗️

Projects

Manage the connected external projects. Each project gets its own API key pair for external integration.

GET /admin/projects List all projects (paginated)
JWT RequiredAdmin+
Param Type Description
search string Search name or slug
active boolean Filter by active status
per_page integer Default 20
Response — 200 OK
JSON
{ "data": [{ "id":1, "name":"NaijaHomes", "slug":"naijaHomes", "tickets_count":42, "agents_count":3, "is_active":true }] }
POST /admin/projects Create a new project
JWT RequiredAdmin+
Field Type Req Description
name string * Project name. Slug is auto-generated.
description string Short description
support_email email Project support email
website_url url Project website
timezone string Valid timezone (default: UTC)
settings object Arbitrary project-level JSON config
Response — 201 Created
201 Created422
JSON
{ "project": { "id":6, "name":"AgriMarket", "slug":"agrimarket" } }
GET /admin/projects/{id} Get project with agents & API keys
JWT RequiredAdmin+

Returns the project with agents, sla_policies, and api_keys (excluding secrets).

Response — 200 OK
JSON
{ "project": { "agents":[...], "api_keys":[{ "id":1, "name":"Production Key", "last_used_at":"..." }] } }
PUT /admin/projects/{id} Update project details
JWT RequiredAdmin+

Same fields as create. All optional (PATCH semantics). Include "is_active": false to disable the project.

Response — 200 OK
JSON
{ "project": { /* updated */ } }
POST /admin/projects/{id}/agents  |  DELETE /agents/{userId} Assign / remove agents
JWT RequiredAdmin+
POST — Assign Agent
Field Type Req Description
user_id integer * User ID to assign
is_lead boolean Mark as project lead agent
Responses
200 OK
JSON
{ "message": "Agent assigned to project" }
// DELETE returns:
{ "message": "Agent removed from project" }
POST /admin/projects/{id}/api-keys Generate API key pair ⚠️ Secret shown once
JWT RequiredAdmin+
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)
🔴
The api_secret is returned only once at creation and is not stored in readable form. Save it immediately.
Response — 201 Created
201
JSON
{
  "api_key": "sk_Abc123xyzAbc123xyz...",
  "api_secret": "ss_XyzLongSecretValue...",
  "key_id": 7,
  "message": "Store the api_secret securely."
}
GET / DELETE
ENDPOINTS
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.

GET /admin/users List users with filters
JWT RequiredAdmin+
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
Response — 200 OK
JSON
{ "data": [{ "id":3, "name":"Alice Okafor", "role":"agent", "is_active":true, "last_login_at":"..." }] }
POST /admin/users Create user (agent / admin / customer)
JWT RequiredAdmin+
Field Type Req Description
name string * Full name
email email * Unique email
password string * Min 8 chars
role string * admin | agent | customer
department string Department label
phone string Phone number
Also available
ENDPOINTS
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.

GET /admin/reports/dashboard KPI overview
JWT RequiredAgent+

Returns total counts by status, unassigned count, SLA breached count, and average response/resolution times.

Response — 200 OK
JSON
{
  "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
}
GET /admin/reports/by-project Ticket volume & resolve rate per project
JWT RequiredAdmin+

Accepts from and to query params.

Response
JSON
[{ "project":"NaijaHomes", "total":84, "resolved":72, "breached":2, "resolve_rate":85.7 }]
GET /admin/reports/agents Agent performance report
JWT RequiredAdmin+

Per-agent assigned/resolved counts, resolve rate, avg resolution time.

Response
JSON
[{ "agent":"Alice Okafor", "total_assigned":34, "total_resolved":31, "resolve_rate":91.2, "avg_resolution_minutes":280.5 }]
GET /admin/reports/volume Ticket volume trend over time
JWT RequiredAdmin+
Param Type Description
group_by string day (default) | week | month
Response
JSON
[{ "period":"2024-04-21", "created":12, "resolved":8 }]
GET /admin/reports/sla SLA breach analysis
JWT RequiredAdmin+
Response
JSON
{ "total_tickets":284, "fr_breach_rate":3.2, "res_breach_rate":1.4, "by_priority":{ "critical":{"total":5,"fr_breached":1,"res_breached":0} } }
GET /admin/reports/categories Category ticket distribution
JWT RequiredAdmin+
Response
JSON
[{ "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.

GET /categories List categories (hierarchical)
JWT Required

Returns root categories with nested children. Also: POST /categories, PUT /categories/{id}, DELETE /categories/{id} (Admin only).

Response
JSON
{ "categories": [{ "id":1, "name":"Technical Issue", "color":"#EF4444", "children":[] }] }
GET /admin/sla List SLA policies
JWT RequiredAdmin+

Filter with ?project_id=N for project-specific policies. Policies with project_id: null are global defaults.

SLA Policy Fields
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
Default Global SLAs
Priority First Response Resolution
critical 15 min 2 hours
high 1 hour 8 hours
medium 4 hours 24 hours
low 8 hours 80 hours
Also available
ENDPOINTS
POST   /admin/sla                   // Create policy
PUT    /admin/sla/{id}              // Update policy
DELETE /admin/sla/{id}              // Delete policy
GET /canned-responses List canned responses (Agent+)
JWT RequiredAgent+

Returns own responses plus all shared responses. Filter with ?project_id=N or ?search=refund.

Also available
ENDPOINTS
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.

🔑
Required on every external request:
X-Api-Key: sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
X-Api-Secret: ss_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
GET /external/ping Health check / validate API key
API Key Required

Use this to test that your API key pair works and to confirm which project it belongs to.

Response — 200 OK
JSON
{ "status":"ok", "project":{ "id":1, "name":"NaijaHomes", "slug":"naijaHomes" }, "timestamp":"2024-04-21T09:00:00Z" }
POST /external/tickets Submit a ticket from external project
API Key Required
Request Body
Field Type Req Description
customer_name string * End user's full name
customer_email 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…)
Request Example
JSON
{
  "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...
Response — 201 Created
JSON
{
  "ticket_number": "NAIJ-2024-00043",
  "ticket_id": 43,
  "status": "open",
  "priority": "high",
  "created_at": "2024-04-21T09:00:00Z"
}
GET /external/tickets List project tickets (scoped to API key)
API Key Required
Param Type Description
customer_email email Filter by customer email
status string Filter by status
priority string Filter by priority
per_page integer Default 20
Note on scope
ℹ️
Results are automatically scoped to the project that owns the API key. You cannot access tickets from other projects.
GET /external/tickets/{ticketNumber} Get ticket with public replies
API Key Required

Path param is the ticket number string e.g. NAIJ-2024-00043, not the integer ID. Internal notes are hidden from external responses.

Response — 200 OK
JSON
{
  "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
  }
}
POST /external/tickets/{ticketNumber}/reply Customer adds reply from external app
API Key Required
Field Type Req Description
body string * Reply text
customer_email email * Must match ticket's customer email
Response — 201 Created
JSON
{ "message_id":89, "ticket":"NAIJ-2024-00043", "status":"pending_customer" }
POST /external/tickets/{ticketNumber}/close Customer closes ticket
API Key Required
Field Type Req Description
customer_email email * Must match ticket's customer email
Response — 200 OK
JSON
{ "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.

PHP
// 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✓✓✓✗