Full REST API, 22 webhook events, SCIM 2.0 provisioning, and Power Automate connector. Build integrations in minutes.
Maktab uses Laravel Sanctum token authentication for all API endpoints. Each agent can generate multiple API tokens with scoped permissions.
Navigate to Profile → API Tokens → Create Token in the admin panel. Select the scopes your integration needs.
POST /api/v1/tokens Content-Type: application/json Accept: application/json { "name": "My Integration", "abilities": ["tickets:read", "tickets:write"] } // Response { "token": "1|xXxXxX..." }
Include the token in the Authorization header as a Bearer token on every request.
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 } }
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.Base URL: https://your-domain.com/api/v1 · All responses are JSON · All dates are ISO 8601 UTC
Maktab fires 22 webhook events. Configure endpoints at Settings → Integrations → Webhooks. All payloads are signed with HMAC-SHA256.
Every webhook includes an X-Maktab-Signature header. Verify it before processing to prevent replay attacks.
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; }
All events share the same envelope. The data object is event-specific.
{
"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"
}
}
| 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 |
Automate user provisioning and deprovisioning via SCIM 2.0. Compatible with Okta, Azure AD, OneLogin, and any SCIM 2.0-compliant IdP.
Base URL: https://your-domain.com/scim/v2 Auth: Bearer token (generate in Settings → SCIM) Version: SCIM 2.0 (RFC 7642–7644)
/app/saml/metadatahttps://your-domain.com/scim/v2. Set Auth to Bearer token (from Maktab Settings → SCIM)// 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 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 |
Common operations in curl, PHP, Python, and JavaScript.
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 }'
$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');
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();
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"]
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]);
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})
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 }); });