Contents
API Documentation
RESTful API for managing documents and approvals programmatically
Overview
The WAQTUM API provides full programmatic access to all platform features. All requests and responses use JSON format.
https://waqtum.qantrah.com/api/v1
GET /api/v1/health
Public
⚠️ Important Notes Before You Start
Creating a document does not start approval
POST /documents only saves the document as draft. To start the approval flow and notify approvers via WhatsApp, you must separately call POST /documents/{id}/send.
Bilingual fields
Fields like title and name have two versions in the database: title_ar / title_en and name_ar / name_en. To simplify integration, the API accepts either the shorthand (just title — used for both languages) or both language versions separately.
Digital signing is automatic
Once all approvers complete, the system automatically generates a final PDF with signatures, stamps, and an audit certificate page, then embeds an encrypted X.509 digital signature via OpenSSL. No manual call needed.
Download URL is permanent & public
The /documents/{id}/download endpoint returns a direct public storage URL (not a 30-min temporary URL). The URL is unguessable but works from any client. Do not share the URL publicly.
"approved" is the final state
Download works only when status == approved (not completed or signed). Check the status before attempting download.
Authentication
All protected endpoints require your API key and secret in the Authorization header:
Authorization: Bearer YOUR_API_KEY:YOUR_API_SECRET
Content-Type: application/json
Accept: application/json
Rate Limiting
Requests are rate-limited per API key based on your plan:
| Plan | Limit |
|---|---|
| Basic | 100 requests/minute |
| Business | 300 requests/minute |
| Enterprise | 1000 requests/minute |
When exceeded, a 429 (Too Many Requests) error is returned.
Document Lifecycle
A document moves through these states in order:
POST /documents ──► draft (editable, deletable)
POST /documents/{id}/send ──► pending ──► in_progress (sent to approvers)
all approvers complete ──► approved (digitally signed PDF available)
any approver rejects ──► rejected
expires_at exceeded ──► expired
POST /documents/{id}/void ──► voided (manual cancel)
| Status | Allowed Actions |
|---|---|
draft | PUT, DELETE, POST /send |
pending / in_progress | POST /void, GET /status |
approved | GET /download (file is digitally signed) |
rejected / expired / voided | Read-only |
Documents
/documents
List documents
Optional parameters:
status | draft, pending, in_progress, approved, rejected, expired, voided |
page | Page number |
per_page | Results per page (default: 15) |
/documents/{id}
Get document
Returns document details with approvers and status.
/documents
Create document (creates as draft)
Content-Type: multipart/form-data
Title fields (choose either format):
| Field | Type | Description |
|---|---|---|
title | string | Shorthand — used for both languages |
| — OR — | ||
title_ar + title_en | string | Separate Arabic and English versions |
Other fields:
| Field | Type | Required | Description |
|---|---|---|---|
file | file (PDF) | Yes | PDF file (plan-based, max 20MB) |
department | string | — | Department key (e.g., hr, finance, legal) |
workflow_type | string | — | sequential | parallel (default: sequential) |
expires_in_days | integer | — | Expiry in days (default: 7) |
approvers[] | array | — | Approver list (see approver fields below) |
Per-approver fields (approvers[N][...]):
| Field | Type | Required | Description |
|---|---|---|---|
name or (name_ar + name_en) | string | Yes | Approver name (shorthand or separate) |
phone | string | Yes | WhatsApp number in E.164 format (e.g., 96812345678) |
role_ar, role_en | string | — | Job title |
action_type | string | — | sign | stamp | approve_only | review_only (default: sign) |
order_index or order | integer | — | Order (starts at 1, matters for sequential workflow) |
Example (curl):
curl -X POST https://waqtum.qantrah.com/api/v1/documents \
-H "Authorization: Bearer KEY:SECRET" \
-F "title_ar=توقيع التقرير" \
-F "title_en=Report Signature" \
-F "department=hr" \
-F "workflow_type=sequential" \
-F "expires_in_days=7" \
-F "file=@document.pdf" \
-F "approvers[0][name_ar]=علي" \
-F "approvers[0][name_en]=Ali" \
-F "approvers[0][phone]=96812345678" \
-F "approvers[0][action_type]=sign" \
-F "approvers[0][order_index]=1"
/documents/{id}
Update document
Update document (only documents in draft status).
/documents/{id}
Delete document
Delete document (only documents in draft status).
/documents/{id}/send
Send for approval
Send document to approval workflow and notify approvers.
/documents/{id}/void
Void document
Void a sent document (notifies approvers).
/documents/{id}/status
Document status
Returns current document status and approval progress.
/documents/{id}/download
Download approved document
Returns the download URL for the digitally-signed final PDF. Works only when status == approved.
Response:
{
"data": {
"download_url": "https://waqtum.qantrah.com/storage/final/.../WAQTUM_WQT-2026-00001_Final.pdf",
"expires_at": "2026-04-09T20:00:00+00:00",
"filename": "WQT-2026-00001.pdf"
}
}
Approvers
/documents/{documentId}/approvers
List approvers
/documents/{documentId}/approvers
Add approver (to a draft document)
Same fields as approvers[] in document creation:
| Field | Required | Description |
|---|---|---|
name or (name_ar + name_en) | Yes | Approver name |
phone | Yes | WhatsApp number in E.164 format |
role_ar, role_en | — | Job title |
action_type | — | sign | stamp | approve_only | review_only |
order_index or order | — | Approval order |
/approvers/{id}
Update approver
/approvers/{id}
Delete approver
/approvers/{id}/remind
Send reminder
Resend WhatsApp notification to the approver.
Webhooks
Receive real-time notifications when events occur in the system.
Available Events
| Event | Description |
|---|---|
document.created | When a new document is created |
document.sent | When a document is sent for approval |
document.approved | When a document is fully approved |
document.rejected | When a document is rejected |
approver.completed | When an approver completes their action |
Webhook Management
/webhooks/events
Available events
/webhooks
List webhooks
/webhooks
Create webhook
url | string | URL to receive notifications |
events | array | List of events to subscribe to |
/webhooks/{id}/test
Test webhook
/webhooks/{id}/rotate-secret
Rotate secret
Signature Verification
An HMAC-SHA256 signature + timestamp are sent in headers of every webhook request. The signature covers timestamp + dot (.) + payload to prevent replay attacks:
Headers sent:
X-Waqtum-Event: document.completed
X-Waqtum-Timestamp: 1733527200
X-Waqtum-Signature: sha256=HMAC_HASH
X-Waqtum-Delivery: 12345
Content-Type: application/json
Signature formula:
signed_payload = timestamp + "." + payload
signature = hex(hmac_sha256(signed_payload, secret))
Verification (PHP/Laravel):
$payload = $request->getContent();
$timestamp = $request->header('X-Waqtum-Timestamp');
$received = $request->header('X-Waqtum-Signature'); // "sha256=..."
$signedPayload = $timestamp . '.' . $payload;
$expected = 'sha256=' . hash_hmac('sha256', $signedPayload, env('WAQTUM_WEBHOOK_SECRET'));
if (!hash_equals($expected, $received)) {
abort(401, 'Invalid signature');
}
// Optional: reject old requests (anti-replay)
if (abs(time() - (int) $timestamp) > 300) {
abort(401, 'Stale timestamp');
}
Verification (Node.js):
const crypto = require('crypto');
const payload = req.rawBody; // raw bytes, NOT parsed JSON
const timestamp = req.headers['x-waqtum-timestamp'];
const received = req.headers['x-waqtum-signature'];
const signed = `${timestamp}.${payload}`;
const expected = `sha256=${crypto.createHmac('sha256', SECRET).update(signed).digest('hex')}`;
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))) {
return res.status(401).send('Invalid signature');
}
Error Codes
| Code | Description |
|---|---|
400 | Bad Request — check parameters |
401 | Unauthorized — invalid API key |
403 | Forbidden — no permission for this action |
404 | Not Found — resource does not exist |
422 | Validation Error — invalid data |
429 | Too Many Requests — rate limit exceeded |
500 | Server Error — contact support |
Response Format
{
"success": false,
"error": {
"code": 422,
"message": "Validation failed",
"details": {
"title_ar": ["The title_ar field is required."]
}
}
}