API Reference

Scriptlog provides a RESTful API that allows external applications to interact with blog content. The API follows OpenAPI 3.0 specification and returns JSON responses.

Home / Documentation / API Reference

Authentication

The API supports two authentication methods:

API Key Authentication

HTTP headers
GET /api/v1/posts HTTP/1.1
Host: blogware.site
X-API-Key: your-api-key-here

Bearer Token Authentication

HTTP headers
GET /api/v1/posts HTTP/1.1
Host: blogware.site
Authorization: Bearer your-bearer-token

Authentication Requirements

Endpoint Type Authentication Required
Read (GET) - Public contentNo
Create/Update/Partial Update/Delete (POST/PUT/PATCH/DELETE)Yes

API Information

Method Endpoint Auth Description
GET/api/v1/NoGet API metadata and available endpoints

Example Response

JSON 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 requests60 seconds
Write (POST/PUT/DELETE/PATCH)20 requests60 seconds
Header Description
X-RateLimit-LimitMaximum requests allowed per window
X-RateLimit-RemainingRemaining requests in current window
X-RateLimit-ResetUnix timestamp when the rate limit resets
Retry-AfterSeconds 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/postsNoList published posts
GET/api/v1/posts/{id}NoGet single post
GET/api/v1/posts/{id}/commentsNoGet post comments
POST/api/v1/postsYesCreate post
PUT/api/v1/posts/{id}YesUpdate post
PATCH/api/v1/posts/{id}YesPartially update post
DELETE/api/v1/posts/{id}YesDelete post

Query Parameters (List Posts)

Parameter Type Default Description
pageinteger1Page number
per_pageinteger10Items per page (max: 100)
sort_bystringIDSort field (ID, post_date, post_modified, post_title)
sort_orderstringDESCSort direction (ASC, DESC)

Path Parameters (Get Single Post)

Parameter Type Description
idintegerPost ID

Example Paginated Response

JSON 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

JSON request
{
  "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
pageinteger1Page number
per_pageinteger10Items per page

Categories API

Method Endpoint Auth Description
GET/api/v1/categoriesNoList categories
GET/api/v1/categories/{id}NoGet category
GET/api/v1/categories/{id}/postsNoGet posts in category
POST/api/v1/categoriesYesCreate category
PUT/api/v1/categories/{id}YesUpdate category
PATCH/api/v1/categories/{id}YesPartially update category
DELETE/api/v1/categories/{id}YesDelete category

Query Parameters (List Categories)

Parameter Type Default Description
pageinteger1Page number
per_pageinteger10Items per page
sort_bystringIDSort field
sort_orderstringDESCSort direction

Example Response (List Categories)

JSON response
{
  "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

JSON request
{
  "topic_title": "Category Name",
  "topic_status": "Y"
}

Comments API

Method Endpoint Auth Description
GET/api/v1/commentsNoList approved comments
GET/api/v1/comments/{id}NoGet comment
POST/api/v1/commentsNoSubmit comment
PUT/api/v1/comments/{id}YesUpdate comment
PATCH/api/v1/comments/{id}YesPartially update comment
DELETE/api/v1/comments/{id}YesDelete comment

Query Parameters (List Comments)

Parameter Type Description
post_idintegerFilter by post ID
pageintegerPage number
per_pageintegerItems per page
sort_bystringSort field
sort_orderstringSort direction

Create Comment - Request Body

JSON request
{
  "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/archivesNoList archive dates
GET/api/v1/archives/{year}NoPosts from year
GET/api/v1/archives/{year}/{month}NoPosts from month

Example Response (List Archives)

JSON response
{
  "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
yearintegerYear (e.g., 2024)
monthintegerMonth (1-12)

Search API

Method Endpoint Auth Description
GET/api/v1/searchNoSearch all content (posts + pages)
GET/api/v1/search/postsNoSearch posts only
GET/api/v1/search/pagesNoSearch pages only

Search Parameters

Parameter Type Required Description
qstringYesSearch keyword (min 2, max 100 chars)
typestringNoall, posts, or pages (default: all)

Example Request

HTTP request
GET /api/v1/search?q=cicero&type=all

Protected Posts API

Method Endpoint Auth Description
POST/api/v1/posts/{id}/unlockNoUnlock password-protected post
POST/api/v1/posts/{id}/verifyNoVerify password for protected post

Unlock Post - Request Body

JSON request
{
  "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/languagesNoList active languages
GET/api/v1/languages/activeNoList active languages
GET/api/v1/languages/defaultNoGet default language
GET/api/v1/languages/{code}NoGet single language
POST/api/v1/languagesYesCreate language
PUT/api/v1/languages/{code}YesUpdate language
PATCH/api/v1/languages/{code}YesPartially update language
PUT/api/v1/languages/{code}/defaultYesSet as default language
DELETE/api/v1/languages/{code}YesDelete language

Create Language - Request Body

JSON request
{
  "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}NoList translations for language
GET/api/v1/translations/{code}/{key}NoGet single translation
GET/api/v1/translations/{code}/exportNoExport translations as key-value map
POST/api/v1/translations/{code}YesCreate translation
POST/api/v1/translations/{code}/importYesBulk import translations
POST/api/v1/translations/{code}/cacheYesClear translation cache
PUT/api/v1/translations/{id}YesUpdate translation
PATCH/api/v1/translations/{id}YesPartially update translation
DELETE/api/v1/translations/{id}YesDelete translation

Create Translation - Request Body

JSON request
{
  "trans_key": "nav.dashboard",
  "trans_value": "Dashboard",
  "trans_locale": "en"
}

Import Translations - Request Body

JSON request
{
  "translations": {
    "nav.dashboard": "Dashboard",
    "nav.posts": "Posts",
    "nav.settings": "Settings"
  }
}

GDPR API

Method Endpoint Auth Description
POST/api/v1/gdpr/consentNoSubmit cookie consent
GET/api/v1/gdpr/consentNoGet consent status

Submit Consent - Request Body

JSON request
{
  "status": "accepted",
  "type": "cookie"
}

Health Check

Method Endpoint Auth Description
GET/api/v1/healthNoAPI health status for monitoring

Example Response

JSON 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-tokenNoGet CSRF token for write operations

Media Upload

Method Endpoint Auth Description
POST/api/v1/media/uploadYesUpload 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/queryNoQuery posts + pages (JSON body)
QUERY/api/v1/query/postsNoQuery posts only
QUERY/api/v1/query/pagesNoQuery 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.

JSON request body
{
  "type": "all",
  "q": "search keyword"
}

Query Parameters

All list endpoints support the following parameters:

Parameter Type Default Description
pageinteger1Page number
per_pageinteger10Items per page (max: 100)
sort_bystringIDSort field
sort_orderstringDESCSort direction (ASC/DESC)

Response Format

Success Response

JSON response
{
  "success": true,
  "status": 200,
  "message": "Operation description",
  "data": { ... }
}

Created Response

JSON 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

JSON 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

JSON error
{
  "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
selfThe current resource URL
collectionThe parent collection URL
firstFirst page of paginated results
prevPrevious page of paginated results
nextNext page of paginated results
lastLast page of paginated results
canonicalThe canonical HTML URL for the resource
commentsComments for a post
postThe parent post for a comment
postsPosts in a category
yearYear archive for a month
searchSearch endpoint (templated URL)
service-descOpenAPI specification URL

Example Single Resource with HATEOAS

JSON response
{
  "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
200OK
201Created
204No Content
304Not Modified (conditional GET)
400Bad Request
401Unauthorized
403Forbidden
404Not Found
405Method Not Allowed
406Not Acceptable
409Conflict
415Unsupported Media Type
422Unprocessable Entity
429Too Many Requests
500Internal Server Error

Error Codes

Code Description
BAD_REQUESTInvalid request parameters
UNAUTHORIZEDAuthentication required
FORBIDDENInsufficient permissions
NOT_FOUNDResource not found
CONFLICTResource already exists
VALIDATION_ERRORValidation failed
MISSING_QUERYSearch query parameter required
RATE_LIMIT_EXCEEDEDToo many requests
INTERNAL_SERVER_ERRORServer error

SDK Examples

JavaScript / Fetch

JavaScript example.js
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

PHP example.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

Python example.py
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

BASH commands
# 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:

  1. Copy the API_OPENAPI.json file to a web server
  2. Navigate to Swagger Editor
  3. Paste the JSON content
  4. Explore the interactive API documentation

Using with Postman

To import into Postman:

  1. Open Postman
  2. Click Import
  3. Select "Import from link"
  4. 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 lib/dto/api/MyResourceApiDto.php
<?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 lib/dao/MyResourceDao.php
<?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 lib/service/MyResourceService.php
<?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 lib/controller/api/MyResourceApiController.php
<?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

PHP api/index.php
$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');

Support

For issues and questions: