Base URLs
| Production | http://blogware.site/api/v1 |
| Development | http://localhost/blogware/public_html/api/v1 |
API Version: 1.1.1 | Format: JSON
Authentication
The API supports two authentication methods:
API Key Authentication
GET /api/v1/posts HTTP/1.1
Host: blogware.site
X-API-Key: your-api-key-here
Bearer Token Authentication
GET /api/v1/posts HTTP/1.1
Host: blogware.site
Authorization: Bearer your-bearer-token
Authentication Requirements
| Endpoint Type |
Authentication Required |
| Read (GET) - Public content | No |
| Create/Update/Partial Update/Delete (POST/PUT/PATCH/DELETE) | Yes |
API Information
| Method |
Endpoint |
Auth |
Description |
| GET | /api/v1/ | No | Get API metadata and available endpoints |
Example Response
{
"success": true,
"status": 200,
"message": "Welcome to Blogware RESTful API",
"data": {
"name": "Blogware RESTful API",
"version": "1.1.1",
"description": "RESTful API for Blogware content management system",
"base_url": "/api/v1",
"authentication": {
"type": "API Key or Bearer Token",
"header": "X-API-Key or Authorization: Bearer ",
"required": true
}
},
"_links": {
"self": { "href": "http://blogware.site/api/v1", "rel": "self", "type": "GET" },
"posts": { "href": "http://blogware.site/api/v1/posts", "rel": "posts", "type": "GET" },
"categories": { "href": "http://blogware.site/api/v1/categories", "rel": "categories", "type": "GET" },
"comments": { "href": "http://blogware.site/api/v1/comments", "rel": "comments", "type": "GET" },
"archives": { "href": "http://blogware.site/api/v1/archives", "rel": "archives", "type": "GET" },
"search": { "href": "http://blogware.site/api/v1/search?q={query}", "rel": "search", "type": "GET", "templated": true },
"gdpr": { "href": "http://blogware.site/api/v1/gdpr/consent", "rel": "gdpr", "type": "GET" },
"languages": { "href": "http://blogware.site/api/v1/languages", "rel": "languages", "type": "GET" },
"translations": { "href": "http://blogware.site/api/v1/translations/en", "rel": "translations", "type": "GET" },
"media": { "href": "http://blogware.site/api/v1/media/upload", "rel": "media", "type": "POST" },
"openapi": { "href": "http://blogware.site/api/v1/openapi.json", "rel": "service-desc", "type": "application/json" }
}
}
Permission Levels
| Level |
Create Posts |
Edit Posts |
Delete Posts |
Manage Categories |
Moderate Comments |
| administrator | | | | | |
| editor | | | | | |
| author | | (own only) | | | |
| subscriber | | | | | |
Rate Limiting
API requests are rate limited to ensure fair usage and prevent abuse. Rate limiting is applied per-client using IP address, API key, or Bearer token as the identifier.
| Endpoint Type |
Limit |
Window |
| Read (GET) | 60 requests | 60 seconds |
| Write (POST/PUT/DELETE/PATCH) | 20 requests | 60 seconds |
| Header |
Description |
X-RateLimit-Limit | Maximum requests allowed per window |
X-RateLimit-Remaining | Remaining requests in current window |
X-RateLimit-Reset | Unix timestamp when the rate limit resets |
Retry-After | Seconds to wait before retrying (only on 429 responses) |
If you exceed the rate limit, you'll receive a 429 Too Many Requests response.
Posts API
| Method |
Endpoint |
Auth |
Description |
| GET | /api/v1/posts | No | List published posts |
| GET | /api/v1/posts/{id} | No | Get single post |
| GET | /api/v1/posts/{id}/comments | No | Get post comments |
| POST | /api/v1/posts | Yes | Create post |
| PUT | /api/v1/posts/{id} | Yes | Update post |
| PATCH | /api/v1/posts/{id} | Yes | Partially update post |
| DELETE | /api/v1/posts/{id} | Yes | Delete post |
Query Parameters (List Posts)
| Parameter |
Type |
Default |
Description |
page | integer | 1 | Page number |
per_page | integer | 10 | Items per page (max: 100) |
sort_by | string | ID | Sort field (ID, post_date, post_modified, post_title) |
sort_order | string | DESC | Sort direction (ASC, DESC) |
Path Parameters (Get Single Post)
| Parameter |
Type |
Description |
id | integer | Post ID |
Example Paginated Response
{
"success": true,
"status": 200,
"data": [...],
"pagination": {
"current_page": 1,
"per_page": 10,
"total_items": 50,
"total_pages": 5,
"has_next_page": true,
"has_previous_page": false
},
"_links": {
"self": { "href": "http://blogware.site/api/v1/posts?page=1", "rel": "self", "type": "GET" },
"next": { "href": "http://blogware.site/api/v1/posts?page=2", "rel": "next", "type": "GET" },
"last": { "href": "http://blogware.site/api/v1/posts?page=5", "rel": "last", "type": "GET" }
}
}
Create Post - Request Body
{
"post_title": "My New Post",
"post_content": "Full content of the post",
"post_summary": "Optional summary",
"post_status": "draft",
"post_visibility": "public",
"post_tags": "php, api",
"comment_status": "open",
"topics": [1, 2]
}
Required Fields
post_title (string)
post_content (string)
Optional Fields
post_summary (string)
post_status (string: "publish", "draft")
post_visibility (string: "public", "private", "protected")
post_tags (string, comma-separated)
comment_status (string: "open", "closed")
topics (array of integers)
Query Parameters (Get Comments for Post)
| Parameter |
Type |
Default |
Description |
page | integer | 1 | Page number |
per_page | integer | 10 | Items per page |
Categories API
| Method |
Endpoint |
Auth |
Description |
| GET | /api/v1/categories | No | List categories |
| GET | /api/v1/categories/{id} | No | Get category |
| GET | /api/v1/categories/{id}/posts | No | Get posts in category |
| POST | /api/v1/categories | Yes | Create category |
| PUT | /api/v1/categories/{id} | Yes | Update category |
| PATCH | /api/v1/categories/{id} | Yes | Partially update category |
| DELETE | /api/v1/categories/{id} | Yes | Delete category |
Query Parameters (List Categories)
| Parameter |
Type |
Default |
Description |
page | integer | 1 | Page number |
per_page | integer | 10 | Items per page |
sort_by | string | ID | Sort field |
sort_order | string | DESC | Sort direction |
Example Response (List Categories)
{
"success": true,
"status": 200,
"data": [
{
"id": 1,
"title": "Technology",
"slug": "technology",
"status": "Y",
"post_count": 15,
"url": "http://blogware.site/category/technology",
"_links": {
"self": { "href": "http://blogware.site/api/v1/categories/1", "rel": "self", "type": "GET" },
"posts": { "href": "http://blogware.site/api/v1/categories/1/posts", "rel": "posts", "type": "GET" },
"canonical": { "href": "http://blogware.site/category/technology", "rel": "canonical", "type": "text/html" },
"collection": { "href": "http://blogware.site/api/v1/categories", "rel": "collection", "type": "GET" }
}
}
],
"pagination": {...},
"_links": { ... }
}
Create Category - Request Body
{
"topic_title": "Category Name",
"topic_status": "Y"
}
| Method |
Endpoint |
Auth |
Description |
| GET | /api/v1/comments | No | List approved comments |
| GET | /api/v1/comments/{id} | No | Get comment |
| POST | /api/v1/comments | No | Submit comment |
| PUT | /api/v1/comments/{id} | Yes | Update comment |
| PATCH | /api/v1/comments/{id} | Yes | Partially update comment |
| DELETE | /api/v1/comments/{id} | Yes | Delete comment |
Query Parameters (List Comments)
| Parameter |
Type |
Description |
post_id | integer | Filter by post ID |
page | integer | Page number |
per_page | integer | Items per page |
sort_by | string | Sort field |
sort_order | string | Sort direction |
Create Comment - Request Body
{
"comment_author_name": "John Doe",
"comment_author_email": "[email protected]",
"comment_content": "Great article!",
"comment_post_id": 1,
"comment_parent_id": 0
}
Note: Comments are submitted with 'pending' status for moderation.
Archives API
| Method |
Endpoint |
Auth |
Description |
| GET | /api/v1/archives | No | List archive dates |
| GET | /api/v1/archives/{year} | No | Posts from year |
| GET | /api/v1/archives/{year}/{month} | No | Posts from month |
Example Response (List Archives)
{
"success": true,
"status": 200,
"data": {
"archives": [
{
"year": 2024,
"months": [
{
"month": 6,
"month_name": "June",
"post_count": 5
}
],
"total_posts": 25
}
],
"total_years": 3
},
"_links": {
"self": { "href": "http://blogware.site/api/v1/archives", "rel": "self", "type": "GET" },
"collection": { "href": "http://blogware.site/api/v1/archives", "rel": "collection", "type": "GET" }
}
}
Path Parameters
| Parameter |
Type |
Description |
year | integer | Year (e.g., 2024) |
month | integer | Month (1-12) |
Search API
| Method |
Endpoint |
Auth |
Description |
| GET | /api/v1/search | No | Search all content (posts + pages) |
| GET | /api/v1/search/posts | No | Search posts only |
| GET | /api/v1/search/pages | No | Search pages only |
Search Parameters
| Parameter |
Type |
Required |
Description |
q | string | Yes | Search keyword (min 2, max 100 chars) |
type | string | No | all, posts, or pages (default: all) |
Example Request
GET /api/v1/search?q=cicero&type=all
Protected Posts API
| Method |
Endpoint |
Auth |
Description |
| POST | /api/v1/posts/{id}/unlock | No | Unlock password-protected post |
| POST | /api/v1/posts/{id}/verify | No | Verify password for protected post |
Unlock Post - Request Body
{
"password": "post-password"
}
Returns decrypted content on success, 401 Unauthorized on wrong password. Rate-limited to prevent brute-force attacks.
Languages API
| Method |
Endpoint |
Auth |
Description |
| GET | /api/v1/languages | No | List active languages |
| GET | /api/v1/languages/active | No | List active languages |
| GET | /api/v1/languages/default | No | Get default language |
| GET | /api/v1/languages/{code} | No | Get single language |
| POST | /api/v1/languages | Yes | Create language |
| PUT | /api/v1/languages/{code} | Yes | Update language |
| PATCH | /api/v1/languages/{code} | Yes | Partially update language |
| PUT | /api/v1/languages/{code}/default | Yes | Set as default language |
| DELETE | /api/v1/languages/{code} | Yes | Delete language |
Create Language - Request Body
{
"lang_code": "de",
"lang_name": "German",
"lang_locale": "de_DE",
"is_default": false,
"is_active": true
}
Translations API
| Method |
Endpoint |
Auth |
Description |
| GET | /api/v1/translations/{code} | No | List translations for language |
| GET | /api/v1/translations/{code}/{key} | No | Get single translation |
| GET | /api/v1/translations/{code}/export | No | Export translations as key-value map |
| POST | /api/v1/translations/{code} | Yes | Create translation |
| POST | /api/v1/translations/{code}/import | Yes | Bulk import translations |
| POST | /api/v1/translations/{code}/cache | Yes | Clear translation cache |
| PUT | /api/v1/translations/{id} | Yes | Update translation |
| PATCH | /api/v1/translations/{id} | Yes | Partially update translation |
| DELETE | /api/v1/translations/{id} | Yes | Delete translation |
Create Translation - Request Body
{
"trans_key": "nav.dashboard",
"trans_value": "Dashboard",
"trans_locale": "en"
}
Import Translations - Request Body
{
"translations": {
"nav.dashboard": "Dashboard",
"nav.posts": "Posts",
"nav.settings": "Settings"
}
}
GDPR API
| Method |
Endpoint |
Auth |
Description |
| POST | /api/v1/gdpr/consent | No | Submit cookie consent |
| GET | /api/v1/gdpr/consent | No | Get consent status |
Submit Consent - Request Body
{
"status": "accepted",
"type": "cookie"
}
Health Check
| Method |
Endpoint |
Auth |
Description |
| GET | /api/v1/health | No | API health status for monitoring |
Example Response
{
"success": true,
"status": 200,
"data": {
"status": "healthy",
"timestamp": "2026-07-30 12:00:00",
"php_version": "8.1.28"
}
}
CSRF Token
| Method |
Endpoint |
Auth |
Description |
| GET | /api/v1/csrf-token | No | Get CSRF token for write operations |
| Method |
Endpoint |
Auth |
Description |
| POST | /api/v1/media/upload | Yes | Upload image (Summernote editor) |
Request: multipart/form-data with field image. Supported types: JPEG, PNG, GIF, WebP, BMP. Max size: 5MB.
Query API (RFC 10008)
| Method |
Endpoint |
Auth |
Description |
| QUERY | /api/v1/query | No | Query posts + pages (JSON body) |
| QUERY | /api/v1/query/posts | No | Query posts only |
| QUERY | /api/v1/query/pages | No | Query pages only |
The QUERY method (RFC 10008) is a safe, idempotent alternative to GET for complex queries. The query parameters are sent in the request body as JSON.
{
"type": "all",
"q": "search keyword"
}
Query Parameters
All list endpoints support the following parameters:
| Parameter |
Type |
Default |
Description |
page | integer | 1 | Page number |
per_page | integer | 10 | Items per page (max: 100) |
sort_by | string | ID | Sort field |
sort_order | string | DESC | Sort direction (ASC/DESC) |
Success Response
{
"success": true,
"status": 200,
"message": "Operation description",
"data": { ... }
}
Created Response
{
"success": true,
"status": 201,
"message": "Resource created",
"data": {
"id": 42
}
}
Includes Location header with the resource URL.
No Content Response
Used for DELETE operations. Status 204 with empty body.
Paginated Response
{
"success": true,
"status": 200,
"data": [...],
"pagination": {
"current_page": 1,
"per_page": 10,
"total_items": 50,
"total_pages": 5,
"has_next_page": true,
"has_previous_page": false
},
"_links": {
"self": { "href": "http://blogware.site/api/v1/posts?page=1", "rel": "self", "type": "GET" },
"first": { "href": "http://blogware.site/api/v1/posts?page=1", "rel": "first", "type": "GET" },
"next": { "href": "http://blogware.site/api/v1/posts?page=2", "rel": "next", "type": "GET" },
"last": { "href": "http://blogware.site/api/v1/posts?page=5", "rel": "last", "type": "GET" }
}
}
Error Response
{
"success": false,
"status": 400,
"error": {
"code": "BAD_REQUEST",
"message": "Error description"
}
}
HATEOAS (Hypermedia as the Engine of Application State)
All API responses include HATEOAS links following RFC 5988 (Web Linking). This allows clients to discover available actions dynamically without hardcoding URLs.
Common Link Relations
| Relation |
Description |
self | The current resource URL |
collection | The parent collection URL |
first | First page of paginated results |
prev | Previous page of paginated results |
next | Next page of paginated results |
last | Last page of paginated results |
canonical | The canonical HTML URL for the resource |
comments | Comments for a post |
post | The parent post for a comment |
posts | Posts in a category |
year | Year archive for a month |
search | Search endpoint (templated URL) |
service-desc | OpenAPI specification URL |
Example Single Resource with HATEOAS
{
"success": true,
"status": 200,
"data": {
"id": 1,
"title": "My First Blog Post",
"slug": "my-first-blog-post"
},
"_links": {
"self": { "href": "http://blogware.site/api/v1/posts/1", "rel": "self", "type": "GET" },
"comments": { "href": "http://blogware.site/api/v1/posts/1/comments", "rel": "comments", "type": "GET" },
"canonical": { "href": "http://blogware.site/post/1/my-first-blog-post", "rel": "canonical", "type": "text/html" },
"collection": { "href": "http://blogware.site/api/v1/posts", "rel": "collection", "type": "GET" }
}
}
HTTP Status Codes
| Code |
Meaning |
| 200 | OK |
| 201 | Created |
| 204 | No Content |
| 304 | Not Modified (conditional GET) |
| 400 | Bad Request |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | Not Found |
| 405 | Method Not Allowed |
| 406 | Not Acceptable |
| 409 | Conflict |
| 415 | Unsupported Media Type |
| 422 | Unprocessable Entity |
| 429 | Too Many Requests |
| 500 | Internal Server Error |
Error Codes
| Code |
Description |
BAD_REQUEST | Invalid request parameters |
UNAUTHORIZED | Authentication required |
FORBIDDEN | Insufficient permissions |
NOT_FOUND | Resource not found |
CONFLICT | Resource already exists |
VALIDATION_ERROR | Validation failed |
MISSING_QUERY | Search query parameter required |
RATE_LIMIT_EXCEEDED | Too many requests |
INTERNAL_SERVER_ERROR | Server error |
SDK Examples
JavaScript / Fetch
const baseUrl = 'http://blogware.site/api/v1';
// Get posts
const response = await fetch(`${baseUrl}/posts`);
const data = await response.json();
// Get single post
const post = await fetch(`${baseUrl}/posts/1`);
// Create comment (no auth required)
const comment = await fetch(`${baseUrl}/comments`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
comment_author_name: 'John Doe',
comment_author_email: '[email protected]',
comment_content: 'Great article!',
comment_post_id: 1
})
});
PHP
$baseUrl = 'http://blogware.site/api/v1';
// Get posts
$response = file_get_contents($baseUrl . '/posts');
$posts = json_decode($response, true);
// Get posts with authentication
$context = stream_context_create([
'http' => [
'header' => "X-API-Key: your-api-key\r\n"
]
]);
$response = file_get_contents($baseUrl . '/posts', false, $context);
Python
import requests
base_url = 'http://blogware.site/api/v1'
# Get posts
response = requests.get(f'{base_url}/posts')
posts = response.json()
# Get posts with authentication
headers = {'X-API-Key': 'your-api-key'}
response = requests.get(f'{base_url}/posts', headers=headers)
# Create comment
data = {
'comment_author_name': 'John Doe',
'comment_author_email': '[email protected]',
'comment_content': 'Great article!',
'comment_post_id': 1
}
response = requests.post(f'{base_url}/comments', json=data)
cURL
# Get posts
curl http://blogware.site/api/v1/posts
# Get posts with authentication
curl -H "X-API-Key: your-api-key" http://blogware.site/api/v1/posts
# Create comment
curl -X POST http://blogware.site/api/v1/comments \
-H "Content-Type: application/json" \
-d '{
"comment_author_name": "John Doe",
"comment_author_email": "[email protected]",
"comment_content": "Great article!",
"comment_post_id": 1
}'
OpenAPI Specification
The complete OpenAPI 3.0 specification is available via the dynamic endpoint or as static files:
Dynamic Endpoint (Recommended)
Access the live specification at GET /api/v1/openapi.json — it substitutes runtime server URLs automatically.
Static Files
Use these files to generate client SDKs, validate API responses, import into API testing tools (Postman, Swagger UI), or auto-generate documentation.
Using with Swagger UI
To view the API documentation in Swagger UI:
- Copy the
API_OPENAPI.json file to a web server
- Navigate to Swagger Editor
- Paste the JSON content
- Explore the interactive API documentation
Using with Postman
To import into Postman:
- Open Postman
- Click Import
- Select "Import from link"
- Enter:
http://blogware.site/docs/API_OPENAPI.json
Creating API Controllers
All API controllers follow a consistent pattern: Controller → Service → DAO → Database, with DTOs for response formatting.
Step 1: Create DTO
<?php
namespace Scriptlog\Dto\Api;
class MyResourceApiDto
{
public static function transform(array $resource, string $appUrl): array
{
return [
'id' => (int)$resource['id'],
'title' => $resource['title'],
'slug' => $resource['slug'],
'url' => $appUrl . '/resource/' . $resource['id'] . '/' . $resource['slug'],
'date' => $resource['created_at'],
'_links' => [
'self' => ApiHateoas::resourceLink($resource['id']),
'collection' => ApiHateoas::collectionLink()
]
];
}
public static function transformCollection(array $resources, string $appUrl): array
{
return array_map(function ($r) use ($appUrl) {
return self::transform($r, $appUrl);
}, $resources);
}
}
Step 2: Create DAO
<?php
namespace Scriptlog\Dao;
class MyResourceDao
{
private $db;
public function __construct($db)
{
$this->db = $db;
}
public function findAll(int $page, int $perPage, int $offset): array
{
$stmt = $this->db->prepare(
"SELECT * FROM tbl_resources ORDER BY id DESC LIMIT ? OFFSET ?"
);
$stmt->execute([$perPage, $offset]);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
public function count(): int
{
return (int)$this->db->query("SELECT COUNT(*) FROM tbl_resources")->fetchColumn();
}
public function findById(int $id): ?array
{
$stmt = $this->db->prepare("SELECT * FROM tbl_resources WHERE id = ?");
$stmt->execute([$id]);
$row = $stmt->fetch(\PDO::FETCH_ASSOC);
return $row ?: null;
}
public function create(array $data): int
{
$stmt = $this->db->prepare(
"INSERT INTO tbl_resources (title, content) VALUES (?, ?)"
);
$stmt->execute([$data['title'], $data['content']]);
return (int)$this->db->lastInsertId();
}
}
Step 3: Create Service
<?php
namespace Scriptlog\Service;
class MyResourceService
{
private $resourceDao;
public function __construct($resourceDao)
{
$this->resourceDao = $resourceDao;
}
public function getPaginatedResources(int $page, int $perPage): array
{
$offset = ($page - 1) * $perPage;
return [
'items' => $this->resourceDao->findAll($page, $perPage, $offset),
'total' => $this->resourceDao->count()
];
}
public function getResource(int $id): ?array
{
return $this->resourceDao->findById($id);
}
public function createResource(array $data): int
{
return $this->resourceDao->create($data);
}
}
Step 4: Create Controller
<?php
namespace Scriptlog\Controller\Api;
use Scriptlog\Core\ApiResponse;
use Scriptlog\Dto\Api\MyResourceApiDto;
class MyResourceApiController extends \Scriptlog\Controller\ApiController
{
private $resourceService;
public function __construct($resourceService)
{
parent::__construct();
$this->resourceService = $resourceService;
}
public function index($params = [])
{
$this->requiresAuth = false;
$pagination = $this->getPagination($params);
try {
$result = $this->resourceService->getPaginatedResources(
$pagination['page'],
$pagination['per_page']
);
$transformed = MyResourceApiDto::transformCollection(
$result['items'],
$this->getAppUrl()
);
ApiResponse::paginated(
$transformed,
$pagination['page'],
$pagination['per_page'],
$result['total']
);
} catch (\Throwable $e) {
ApiResponse::error($e->getMessage(), 500, 'FETCH_ERROR');
}
}
public function show($params = [])
{
$this->requiresAuth = false;
$id = isset($params['id']) ? (int)$params['id'] : 0;
if (!$id) {
ApiResponse::badRequest('Resource ID is required');
return;
}
$resource = $this->resourceService->getResource($id);
if (!$resource) {
ApiResponse::notFound('Resource not found');
return;
}
ApiResponse::success(
MyResourceApiDto::transform($resource, $this->getAppUrl())
);
}
public function store($params = [])
{
$this->requiresAuth = true;
if (!$this->hasPermission(['administrator', 'editor'])) {
ApiResponse::forbidden('Permission denied');
return;
}
$errors = $this->validateRequired($this->requestData, ['title', 'content']);
if ($errors) {
ApiResponse::unprocessableEntity('Validation failed', $errors);
return;
}
$id = $this->resourceService->createResource($this->requestData);
ApiResponse::created(['id' => $id], 'Resource created');
}
}
Step 5: Register Routes
$router->get('resources', 'MyResourceApiController@index');
$router->get('resources/(?P<id>[0-9]+)', 'MyResourceApiController@show');
$router->post('resources', 'MyResourceApiController@store');
$router->put('resources/(?P<id>[0-9]+)', 'MyResourceApiController@update');
$router->patch('resources/(?P<id>[0-9]+)', 'MyResourceApiController@update');
$router->delete('resources/(?P<id>[0-9]+)', 'MyResourceApiController@destroy');
Note: Routes use named capture groups (?P<id>[0-9]+) for parameter resolution. Always reference parameters as $params['id'], never $params[0]. When adding new classes, use PSR-4 namespaces and register them in both lib/autoload-aliases.php and lib/autoload-aliases-map.php.
Support
For issues and questions: