REST API · Webhooks · SCIM 2.0

Maktab Developer Portal

Full REST API, 22 webhook events, SCIM 2.0 provisioning, and Power Automate connector. Build integrations in minutes.

Authentication

Maktab uses Laravel Sanctum token authentication for all API endpoints. Each agent can generate multiple API tokens with scoped permissions.

Generate a Token

Navigate to Profile → API Tokens → Create Token in the admin panel. Select the scopes your integration needs.

HTTP
POST /api/v1/tokens
Content-Type: application/json
Accept: application/json

{
  "name": "My Integration",
  "abilities": ["tickets:read", "tickets:write"]
}

// Response
{
  "token": "1|xXxXxX..."
}
Using the Token

Include the token in the Authorization header as a Bearer token on every request.

HTTP
GET /api/v1/tickets
Authorization: Bearer 1|xXxXxX...
Accept: application/json

// All responses include pagination:
{
  "data": [...],
  "meta": {
    "current_page": 1,
    "per_page": 25,
    "total": 847
  }
}
Token Scopes: tickets:read, tickets:write, users:read, users:write, kb:read, kb:write, assets:read, assets:write, reports:read, admin:read. Tokens without explicit scopes can access all endpoints.

API Reference

Base URL: https://your-domain.com/api/v1 · All responses are JSON · All dates are ISO 8601 UTC

Tickets
GET
/tickets
List tickets with filters: status, priority, department_id, assignee_id, per_page (max 100)
POST
/tickets
Create a new ticket. Required: subject, description, requester_email
GET
/tickets/{id}
Get a single ticket with all replies and timeline events
PUT
/tickets/{id}
Update ticket fields: status, priority, assignee_id, department_id, tags
POST
/tickets/{id}/replies
Add a reply or internal note to a ticket
POST
/tickets/{id}/merge
Merge this ticket into another. Body: {"merge_into_id": 123}
Users & ITSM
GET
/users
List agents. Filters: role_id, department_id, is_active
POST
/users
Create an agent account
GET
/assets
List assets with filters: type, status, department_id
GET
/changes
List change requests. Filters: status, risk, assignee_id
GET
/problems
List problem records with linked incident count
GET
/knowledge-base
List KB articles. Filters: category_id, is_published, lang

Webhook Events

Maktab fires 22 webhook events. Configure endpoints at Settings → Integrations → Webhooks. All payloads are signed with HMAC-SHA256.

Signature Verification

Every webhook includes an X-Maktab-Signature header. Verify it before processing to prevent replay attacks.

PHP
function verifyWebhook(string $payload, string $signature, string $secret): bool
{
    $expected = 'sha256=' . hash_hmac('sha256', $payload, $secret);
    return hash_equals($expected, $signature);
}

// In your webhook handler:
$signature = $_SERVER['HTTP_X_MAKTAB_SIGNATURE'] ?? '';
if (!verifyWebhook($payload, $signature, $webhookSecret)) {
    http_response_code(401);
    exit;
}
Example Payload

All events share the same envelope. The data object is event-specific.

JSON
{
  "event": "ticket.created",
  "timestamp": "2026-05-24T10:30:00Z",
  "delivery_id": "wh_01HXZ9...",
  "data": {
    "id": 1847,
    "subject": "Printer not working",
    "status": "open",
    "priority": "medium",
    "requester_email": "user@company.com",
    "department_id": 3,
    "created_at": "2026-05-24T10:30:00Z"
  }
}
All Webhook Events
Event Name Description
ticket.created Fired when a new ticket is created (any channel)
ticket.updated Fired when ticket fields change (status, priority, assignee)
ticket.assigned Fired when a ticket is assigned or reassigned to an agent
ticket.resolved Fired when a ticket status changes to resolved
ticket.closed Fired when a ticket is closed
ticket.escalated Fired when a ticket breaches SLA and is escalated
ticket.merged Fired when two tickets are merged
reply.created Fired when an agent or client posts a reply
reply.note_added Fired when an internal note is added to a ticket
user.created Fired when a new agent account is created
user.deactivated Fired when an agent account is deactivated
client.created Fired when a new client user registers
client.company.created Fired when a new client company is added
sla.breached Fired when an SLA deadline is missed
sla.at_risk Fired when a ticket is within 20% of SLA deadline
csat.submitted Fired when a client submits a CSAT survey
kb.article.published Fired when a KB article is published
incident.created Fired when a major incident is declared
incident.resolved Fired when a major incident is marked resolved
change.approved Fired when a change request is approved by CAB
on_call.escalated Fired when an on-call escalation is triggered
asset.discovered Fired when a new asset is discovered or manually added

SCIM 2.0 Provisioning

Automate user provisioning and deprovisioning via SCIM 2.0. Compatible with Okta, Azure AD, OneLogin, and any SCIM 2.0-compliant IdP.

SCIM Base URL & Auth
HTTP
Base URL: https://your-domain.com/scim/v2
Auth:     Bearer token (generate in Settings → SCIM)
Version:  SCIM 2.0 (RFC 7642–7644)
Supported Endpoints
GET
/scim/v2/Users
List provisioned users with filter support
POST
/scim/v2/Users
Provision a new agent account
PUT
/scim/v2/Users/{id}
Update user attributes
DEL
/scim/v2/Users/{id}
Deprovision (deactivate) user — immediate access revocation
GET
/scim/v2/Groups
List groups (maps to Maktab Roles)
Okta Setup (5 Steps)
  1. In Okta, go to Applications → Add Application → Create New App
  2. Set Sign-on method to SAML 2.0. Configure with Maktab's SSO metadata at /app/saml/metadata
  3. Enable Provisioning → To App → Enable SCIM
  4. Set SCIM base URL to https://your-domain.com/scim/v2. Set Auth to Bearer token (from Maktab Settings → SCIM)
  5. Assign users/groups in Okta. They appear in Maktab within seconds. Deassigning immediately deactivates access.
JSON
// POST /scim/v2/Users — provision new agent
{
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
  "userName": "john@company.com",
  "name": { "givenName": "John", "familyName": "Doe" },
  "emails": [{ "value": "john@company.com", "primary": true }],
  "active": true
}

Rate Limits

Rate limits are per API token. Exceeded limits return HTTP 429 with a Retry-After header.

Endpoint Rate Limit
GET /api/v1/* 60 req/min per token
POST /api/v1/tickets 30 req/min per token
POST /api/v1/*/replies 60 req/min per token
Webhook delivery 1,000 events/min outbound
SCIM endpoints 100 req/min per IdP

Code Examples

Common operations in curl, PHP, Python, and JavaScript.

Create a Ticket
curl
curl -X POST https://your-domain.com/api/v1/tickets \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "subject": "VPN not connecting",
    "description": "Cannot connect to office VPN since morning",
    "requester_email": "user@company.com",
    "priority": "high",
    "department_id": 2
  }'
PHP
$response = Http::withToken('YOUR_TOKEN')->post(
    'https://your-domain.com/api/v1/tickets',
    [
        'subject'         => 'VPN not connecting',
        'description'     => 'Cannot connect to office VPN',
        'requester_email' => 'user@company.com',
        'priority'        => 'high',
        'department_id'   => 2,
    ]
);
$ticket = $response->json('data');
JavaScript
const response = await fetch(
  'https://your-domain.com/api/v1/tickets',
  {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${TOKEN}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      subject: 'VPN not connecting',
      description: 'Cannot connect to office VPN',
      requester_email: 'user@company.com',
      priority: 'high',
      department_id: 2,
    }),
  }
);
const { data: ticket } = await response.json();
Python
import requests

response = requests.post(
    "https://your-domain.com/api/v1/tickets",
    headers={"Authorization": f"Bearer {TOKEN}"},
    json={
        "subject":         "VPN not connecting",
        "description":     "Cannot connect to office VPN",
        "requester_email": "user@company.com",
        "priority":        "high",
        "department_id":   2,
    }
)
ticket = response.json()["data"]
Receive a Webhook
PHP (Laravel)
Route::post('/maktab-webhook', function(Request $request) {
    $secret    = config('services.maktab.webhook_secret');
    $signature = $request->header('X-Maktab-Signature');
    $expected  = 'sha256=' . hash_hmac('sha256', $request->getContent(), $secret);

    if (!hash_equals($expected, $signature)) {
        return response()->json(['error' => 'Invalid signature'], 401);
    }

    $event = $request->input('event');
    $data  = $request->input('data');

    match($event) {
        'ticket.created'  => handleNewTicket($data),
        'ticket.resolved' => handleResolvedTicket($data),
        default           => null,
    };

    return response()->json(['ok' => true]);
})->withoutMiddleware([VerifyCsrfToken::class]);
Python (Flask)
import hmac, hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def webhook():
    secret    = b'your_webhook_secret'
    body      = request.get_data()
    signature = request.headers.get('X-Maktab-Signature', '')
    expected  = 'sha256=' + hmac.new(secret, body, hashlib.sha256).hexdigest()

    if not hmac.compare_digest(expected, signature):
        return jsonify({'error': 'Invalid signature'}), 401

    data = request.json
    print(f"Event: {data['event']}")
    return jsonify({'ok': True})
Node.js (Express)
const crypto = require('crypto');

app.post('/webhook', express.raw({ type: '*/*' }), (req, res) => {
  const secret    = process.env.MAKTAB_WEBHOOK_SECRET;
  const signature = req.headers['x-maktab-signature'];
  const expected  = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(req.body)
    .digest('hex');

  if (!crypto.timingSafeEqual(
    Buffer.from(signature), Buffer.from(expected))) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const { event, data } = JSON.parse(req.body);
  console.log(`Event: ${event}`, data);
  res.json({ ok: true });
});