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