Pressly API v1.0

API Reference

A fully headless REST API for integrating Pressly content into any frontend, mobile app, or third-party service.

Base URL

https://www.pressly.co.ke/api/v1

Format

JSON (application/json)

Auth

Bearer Token (Sanctum)

Rate limiting: Public endpoints are limited to 60 requests/minute per IP. Authenticated endpoints allow 240 requests/minute per token.

Tenant Resolution

Every API request needs to resolve the tenant you want to read from or write to. You can do that by hostname, query string, or header.

Path-based site

https://www.pressly.co.ke/api/docs

Public site access uses the tenant short name in the URL.

API query style

https://www.pressly.co.ke/api/v1

Useful for external apps calling one shared API host.

Optional subdomain

Set a tenant subdomain to use this pattern.

If wildcard DNS is configured, the tenant can also resolve by subdomain.

Pass the tenant by query string

GET https://www.pressly.co.ke/api/v1/posts?tenant=demo

Or send the tenant as a header

X-Tenant: demo

Authentication

Use Bearer token authentication for all protected endpoints. Obtain a token by calling POST /auth/login.

Exchange credentials for a Bearer token.

Request Body (JSON)

email string required User email address
password string required User password

Request

{"email": "admin@demo.com", "password": "password"}

Response

{"token": "1|abc123...", "token_type": "Bearer", "user": {"id": 1, "name": "Admin", "email": "admin@demo.com"}}

Create a new subscriber account.

Request Body (JSON)

name string required Display name
email string required Email address
password string required Password (min 8 chars)

Request

{"name": "Jane Doe", "email": "jane@example.com", "password": "secret123"}

Response

{"token": "2|xyz789...", "user": {"id": 2, "name": "Jane Doe"}}

Returns the authenticated user's profile.

Response

{"data": {"id": 1, "name": "Admin", "email": "admin@demo.com", "roles": ["admin"]}}

Revoke the current Bearer token.

Response

{"message": "Logged out successfully"}

Posts

Retrieve published posts. Write operations require authentication and appropriate roles.

Returns a paginated list of published posts.

Query Parameters

page int Page number (default: 1)
per_page int Results per page, max 50 (default: 10)
category string Filter by category slug
tag string Filter by tag name
featured bool Return only featured posts
search string Full-text search across title, excerpt, content

Response

{"data": [{"id": 1, "title": "Post Title", "slug": "post-title", "excerpt": "...", "published_at": "2026-05-01T12:00:00Z"}], "meta": {"total": 1236, "current_page": 1, "last_page": 124}}

Returns a single published post by its URL slug.

Path Parameters

slug Post URL slug

Response

{"data": {"id": 1, "title": "Post Title", "slug": "post-title", "content": "...", "author": {"name": "Admin"}, "category": {"name": "Tech", "slug": "tech"}, "tags": []}}

Creates a new post. Requires editor or admin role.

Request Body (JSON)

title string required Post title
content string Post body HTML
status string draft or published (default: draft)

Request

{"title": "New Article", "content": "<p>Body text...</p>", "status": "draft"}

Response

{"data": {"id": 42, "title": "New Article", "slug": "new-article", "status": "draft"}}

Updates an existing post. Must be author, editor, or admin.

Path Parameters

id Post ID

Request

{"title": "Updated Title", "status": "published"}

Response

{"data": {"id": 42, "title": "Updated Title", "status": "published"}}

Permanently deletes a post. Requires admin role.

Path Parameters

id Post ID

Response

{"message": "Post deleted successfully"}

Categories

Read-only access to all categories.

Returns all categories with post counts.

Response

{"data": [{"id": 1, "name": "Technology", "slug": "technology", "posts_count": 142}]}

Returns a category and its paginated posts.

Path Parameters

slug Category slug

Query Parameters

page int Page number
per_page int Results per page, max 50

Response

{"data": {"category": {"id": 1, "name": "Technology"}, "posts": [...], "meta": {"total": 142}}}

Tags

Browse all tags used across published posts.

Returns all tags that have at least one published post.

Response

{"data": [{"id": 1, "name": "Laravel", "slug": "laravel", "url": "https://example.com/tag/laravel"}]}

Returns a tag and its paginated published posts.

Path Parameters

slug Tag slug

Query Parameters

per_page int Results per page, max 50 (default: 10)

Response

{"data": {"tag": {"name": "Laravel", "slug": "laravel"}, "posts": [...], "meta": {"total": 47}}}

Subscription Plans

Expose public subscription plans so external checkout flows or frontend apps can render pricing consistently.

Returns all active subscription plans ordered the same way they appear in the CMS.

Response

{"data": [{"name": "Monthly", "slug": "monthly", "price": "499.00", "currency": "KES", "interval": "monthly", "features": []}]}

Returns a single active subscription plan by slug.

Path Parameters

slug Plan slug

Response

{"data": {"name": "Monthly", "slug": "monthly", "price": "499.00", "currency": "KES", "subscribe_url": "https://example.com/demo/subscribe?plan=monthly"}}

E-paper

Read published edition metadata and, where allowed, the page manifest for free or accessible editions.

Returns paginated published editions with page counts, cover images, and reader URLs.

Query Parameters

page int Page number
per_page int Results per page, max 50 (default: 12)

Response

{"data": [{"title": "Weekend Edition", "slug": "weekend-edition", "page_count": 28, "is_free": true, "reader_url": "https://example.com/demo/epaper/weekend-edition"}], "meta": {"total": 8}}

Returns one edition. Free or accessible editions include page image URLs, while subscriber-only editions return metadata with `requires_subscription=true`.

Path Parameters

slug Edition slug

Response

{"data": {"title": "Weekend Edition", "slug": "weekend-edition", "requires_subscription": false, "pages": [{"page_number": 1, "image_url": "https://example.com/storage/..." }]}}

Streams

Surface live, scheduled, and ended stream metadata for apps, smart TV clients, and custom frontend pages.

Returns paginated streams ordered with live items first.

Query Parameters

page int Page number
per_page int Results per page, max 50 (default: 12)

Response

{"data": [{"title": "Morning Bulletin", "slug": "morning-bulletin", "is_live": true, "watch_url": "https://example.com/demo/streams/morning-bulletin"}], "meta": {"total": 3}}

Returns one stream with provider, embed code, stream URL, and current status.

Path Parameters

slug Stream slug

Response

{"data": {"title": "Morning Bulletin", "slug": "morning-bulletin", "provider": "youtube", "stream_url": "https://youtube.com/...", "embed_code": "<iframe ...>"}}

Comments

Read approved comments and post new ones (authenticated comments are auto-approved).

Returns all approved top-level comments with their approved replies.

Path Parameters

slug Post slug

Response

{"data": [{"id": 1, "content": "Great article!", "author_name": "Jane", "created_at": "2026-05-01T14:00:00Z", "replies": []}], "meta": {"total": 1}}

Adds a comment to a post. Authenticated comments are approved immediately; guest comments require moderation.

Path Parameters

slug Post slug

Request Body (JSON)

content string required Comment text (max 2000 chars)
parent_id int Parent comment ID for replies

Request

{"content": "Really useful, thanks!"}

Response

{"data": {"id": 5, "content": "Really useful, thanks!", "status": "approved"}, "message": "Comment posted."}

Error Codes

All error responses follow a consistent JSON structure.

Status Meaning When it occurs
200 OK Request succeeded
201 Created Resource created successfully
401 Unauthorized Missing or invalid Bearer token
403 Forbidden Token valid but lacks required role
404 Not Found Post, category, or tag does not exist
422 Unprocessable Validation failed check `errors` in response body
429 Too Many Requests Rate limit exceeded
500 Server Error Unexpected server error

Error response shape

{
  "message": "The given data was invalid.",
  "errors": {
    "email": ["The email field is required."]
  }
}