The jekcms REST API runs from a single PHP file. Using the real code, we show how a request is parsed, how authentication works, how lists are paginated and where a new endpoint goes.
How the API Is Built
The jekcms REST API lives in one file: api/v1/index.php. Every request starting with /api/v1/ reaches it through a single rule in .htaccess:
RewriteRule ^api/v1/(.*)$ api/v1/index.php [QSA,L]
The Api class in that file splits the address into parts. For /api/v1/posts/42/revisions, the first part (posts) is the endpoint name and the rest (42, revisions) become parameters. The request body is read as JSON; for GET requests the query string values are merged in as well.
Every request is handled in the same order: the rate limit is checked, the caller is authenticated, and then the function for that endpoint name is called:
private function route()
{
switch ($this->endpoint) {
case 'posts': return $this->handlePosts();
case 'media': return $this->handleMedia();
case 'categories': return $this->handleCategories();
case 'tags': return $this->handleTags();
case 'comments': return $this->handleComments();
case 'stats': return $this->handleStats();
case 'search': return $this->handleSearch();
case 'health': return $this->handleHealth();
// users, settings, webhook, trends, sitemap ...
default:
$this->error('Endpoint not found', 404);
return null;
}
}
Adding a New Endpoint
A new endpoint needs two things: a case line in route() and a function that handles the request. The function returns an array; the class takes care of encoding it as JSON inside the {"success": true, "data": ...} envelope. On an error you call $this->error() and return null.
As an example, here is a /api/v1/category-counts endpoint that returns the number of published posts in each category:
// Inside route():
case 'category-counts': return $this->handleCategoryCounts();
// New function in the class:
private function handleCategoryCounts()
{
if ($this->method !== 'GET') {
$this->error('Method not allowed', 405);
return null;
}
return $this->db->fetchAll(
"SELECT c.slug, c.name, COUNT(p.id) AS posts
FROM categories c
LEFT JOIN post_categories pc ON pc.category_id = c.id
LEFT JOIN posts p ON p.id = pc.post_id AND p.status = 'published'
GROUP BY c.id ORDER BY c.name"
);
}
If you need something similar, check the existing endpoints first. /api/v1/stats, for example, already returns counts for posts, comments, media, categories, tags and users.
One warning: api/v1/index.php is part of the core, and a jekcms update rewrites it. Keep your additions in a separate file and add them back after updating.
Authentication
Authentication runs once per request, in one place, before routing. Your new endpoint is therefore protected without any extra code. The key can arrive three ways: the X-API-Key header, an Authorization: Bearer ... header, or the Api-Key header some clients send. Because some Apache setups do not pass the Authorization header to PHP, the code tries several sources for it.
Keys are stored in the database as SHA-256 hashes, not plain text, and the incoming key is hashed before comparison. Keys created for webhook signatures are not accepted as API credentials.
Being authenticated is not enough to write. Operations that change content call authorizeWrite(), which checks the role of the user the key belongs to; a role without the capability gets a 403. If your new endpoint changes data, call the same check.
Pagination and Filters
The post list accepts page and per_page, with per_page capped at 100. Available filters are status, type, category (the category slug, child categories included), tag, author_id, search, date_from and date_to. Every value goes to the database through a parameterised query, never pasted into the SQL string.
GET /api/v1/posts?category=news&status=published&page=2&per_page=20
The response looks like this:
{
"success": true,
"data": {
"items": [ ... ],
"total": 134,
"pages": 7,
"current_page": 2,
"per_page": 20
}
}
Using the same field names in your own list endpoints lets one piece of client-side pagination code work everywhere.
Rate Limit
By default the API accepts 100 requests per hour per IP address and returns 429 once the limit is reached. The value can be changed with the API_RATE_LIMIT setting.
Sending Notifications Out
The webhook support in the core is for incoming requests: a tool such as n8n can send posts to jekcms through api/v1/webhook/*, and the request is verified with an HMAC-SHA256 signature. There is no general outgoing webhook system that tells other services when something happens on the site.
If your endpoint needs to notify an external service, you write that part yourself. Store the target address and a shared secret somewhere suitable, sign the payload the same way and have the receiving side verify the signature:
$payload = json_encode(['data' => $data, 'timestamp' => time()]);
$signature = hash_hmac('sha256', $payload, $secret);
// Send it with the request:
// X-Webhook-Signature: sha256=<signature>
Give the request a short timeout so the API response does not wait on a service that is down, and log the failure. If you need retries, a queue is the better place for them.