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.

Base URL: https://www.waqtum.qantrah.com/api/v1
Health Check: 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
You can generate API keys from your dashboard at /admin/api

Rate Limiting

Requests are rate-limited per API key based on your plan:

Plan Limit
Basic100 requests/minute
Business300 requests/minute
Enterprise1000 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
draftPUT, DELETE, POST /send
pending / in_progressPOST /void, GET /status
approvedGET /download (file is digitally signed)
rejected / expired / voidedRead-only

Documents

GET /documents List documents

Optional parameters:

statusdraft, pending, in_progress, approved, rejected, expired, voided
pagePage number
per_pageResults per page (default: 15)
GET /documents/{id} Get document

Returns document details with approvers and status.

POST /documents Create document (creates as draft)

Content-Type: multipart/form-data

Title fields (choose either format):

FieldTypeDescription
titlestringShorthand — used for both languages
— OR —
title_ar + title_enstringSeparate Arabic and English versions

Other fields:

FieldTypeRequiredDescription
filefile (PDF)YesPDF file (plan-based, max 20MB)
departmentstringDepartment key (e.g., hr, finance, legal)
workflow_typestringsequential | parallel (default: sequential)
expires_in_daysintegerExpiry in days (default: 7)
approvers[]arrayApprover list (see approver fields below)

Per-approver fields (approvers[N][...]):

FieldTypeRequiredDescription
name or (name_ar + name_en)stringYesApprover name (shorthand or separate)
phonestringYesWhatsApp number in E.164 format (e.g., 96812345678)
role_ar, role_enstringJob title
action_typestringsign | stamp | approve_only | review_only (default: sign)
order_index or orderintegerOrder (starts at 1, matters for sequential workflow)

Example (curl):

curl -X POST https://www.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"
Note: Document is created as draft. To start approval, call POST /documents/{id}/send.
PUT /documents/{id} Update document

Update document (only documents in draft status).

DELETE /documents/{id} Delete document

Delete document (only documents in draft status).

POST /documents/{id}/send Send for approval

Send document to approval workflow and notify approvers.

POST /documents/{id}/void Void document

Void a sent document (notifies approvers).

GET /documents/{id}/status Document status

Returns current document status and approval progress.

GET /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://www.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"
    }
}
Security note: The URL is permanent and publicly accessible (not temporary). Do not share publicly — anyone with the link can download.

Approvers

GET /documents/{documentId}/approvers List approvers
POST /documents/{documentId}/approvers Add approver (to a draft document)

Same fields as approvers[] in document creation:

FieldRequiredDescription
name or (name_ar + name_en)YesApprover name
phoneYesWhatsApp number in E.164 format
role_ar, role_enJob title
action_typesign | stamp | approve_only | review_only
order_index or orderApproval order
Approvers cannot be added after the document is sent (after POST /send).
PUT /approvers/{id} Update approver
DELETE /approvers/{id} Delete approver
POST /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.createdWhen a new document is created
document.sentWhen a document is sent for approval
document.approvedWhen a document is fully approved
document.rejectedWhen a document is rejected
approver.completedWhen an approver completes their action
Webhook Management
GET /webhooks/events Available events
GET /webhooks List webhooks
POST /webhooks Create webhook
urlstringURL to receive notifications
eventsarrayList of events to subscribe to
POST /webhooks/{id}/test Test webhook
POST /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');
}
Important: Use the raw payload (before json_decode). If you re-serialize the JSON, the bytes change and verification will fail.

Error Codes

Code Description
400Bad Request — check parameters
401Unauthorized — invalid API key
403Forbidden — no permission for this action
404Not Found — resource does not exist
422Validation Error — invalid data
429Too Many Requests — rate limit exceeded
500Server Error — contact support
Response Format
{
    "success": false,
    "error": {
        "code": 422,
        "message": "Validation failed",
        "details": {
            "title_ar": ["The title_ar field is required."]
        }
    }
}

Ready to get started?

Sign up now and start integrating with the WAQTUM API