TextStem API Documentation
Overview
This document provides comprehensive documentation for the TextStem API. The API follows RESTful principles and uses JSON for request and response bodies.
Authentication
The API uses Laravel Sanctum for authentication. All endpoints except those under /api/v1/public/ require a bearer token.
Authorization: Bearer your-token-here
Current User
GET /user
Returns the authenticated user record. Requires auth:sanctum.
Response Formats
Most resources return plain JSON wrapped in a data key. Wrangler Pages (the CRUD resource at /api/v1/pages) use the JSON:API format (timacdonald/json-api), with data.type, data.id, data.attributes, and data.relationships keys.
The public endpoint (/api/v1/public/pages/by-url) returns plain JSON without an envelope.
Public Endpoints
These endpoints do not require authentication.
Load Wrangler Page by URL
GET /api/v1/public/pages/by-url
| Name | Type | Required | Description |
|---|---|---|---|
| url | string | Yes | Full page URL as stored in wrangler_pages.url |
| include_components | boolean | No | Include attached components grouped by slot (default: true) |
Example:
GET /api/v1/public/pages/by-url?url=%2Fabout&include_components=true
Success (200):
{
"id": 12,
"title": "About Us",
"url": "/about",
"slug": "about-us",
"parent_id": null,
"thumbnail": null,
"enabled": true,
"menu_order": 1,
"menu_name": "About",
"listed": 1,
"template": "default",
"access": "public",
"description": "About our company.",
"keywords": "about, company",
"redirect": null,
"redirect_permanent": false,
"category_id": null,
"canonical_url": "/about",
"components": [
{
"id": 101,
"type": "prose",
"title": "Intro",
"content": "...",
"config": {},
"slot": "main",
"position": 1,
"enabled": 1,
"thumbnail": null,
"wrangler_page_id": 12
}
]
}
Not found or disabled (404):
{"message": "Page not found"}
Assets
Assets represent media files. The single-asset show endpoint is not exposed -- use the list endpoint with a search filter instead.
List Assets
GET /api/v1/assets
| Name | Type | Required | Description |
|---|---|---|---|
| search | string | No | Filter by title or path |
| page | integer | No | Page number |
Response (200):
{
"data": [
{
"id": 1,
"path": "images/photo.jpg",
"disk": "public",
"type": "image",
"title": "Sample Image",
"caption": "A caption",
"description": "A description",
"thumbnail": "images/photo-thumb.jpg",
"meta": {},
"data": {},
"category_id": null,
"created_at": "2024-01-01T00:00:00.000000Z",
"updated_at": "2024-01-01T00:00:00.000000Z"
}
],
"links": { "first": "...", "last": "...", "prev": null, "next": null },
"meta": { "current_page": 1, "last_page": 1, "per_page": 15, "total": 1 }
}
Create Asset
POST /api/v1/assets
| Name | Type | Required | Description |
|---|---|---|---|
| path | string | Yes | Relative path to the file |
| title | string | Yes | Display title |
| type | string | No | Asset type (e.g. image, video) |
| caption | string | No | Short caption |
| description | string | No | Longer description |
| disk | string | No | Storage disk name |
| thumbnail | string | No | Thumbnail path |
| meta | json | No | Arbitrary metadata object |
| data | json | No | Additional structured data |
| category_id | integer | No | Category to associate the asset with |
Returns the created asset in the same shape as the list item, wrapped in "data".
Update Asset
PUT /api/v1/assets/{id}
Accepts the same fields as Create. All required fields must still be supplied.
Returns the updated asset wrapped in "data".
Delete Asset
DELETE /api/v1/assets/{id}
Response: 204 No Content
Categories
Categories group pages and posts.
List Categories
GET /api/v1/categories
| Name | Type | Required | Description |
|---|---|---|---|
| search | string | No | Filter by name |
| page | integer | No | Page number |
Response (200):
{
"data": [
{
"id": 1,
"name": "News",
"type": "post",
"colour": "#3b82f6",
"created_at": "2024-01-01T00:00:00.000000Z",
"updated_at": "2024-01-01T00:00:00.000000Z"
}
],
"links": {},
"meta": {}
}
Get Category
GET /api/v1/categories/{id}
Returns a single category wrapped in "data".
Create Category
POST /api/v1/categories
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Category name (max 255) |
| type | string | No | Arbitrary type label (max 255) |
| colour | string | No | Hex colour code (e.g. #3b82f6) |
Returns the created category wrapped in "data".
Update Category
PUT /api/v1/categories/{id}
Accepts the same fields as Create.
Returns the updated category wrapped in "data".
Delete Category
DELETE /api/v1/categories/{id}
The category cannot be deleted if it is currently associated with any pages or posts. In that case, the API returns 204 No Content without deleting (a flash message is set server-side but no JSON error body is returned).
Response: 204 No Content
List Pages in a Category
GET /api/v1/categories/{category}/wrangler-pages
Returns a paginated list of wrangler pages belonging to the given category.
Create a Page within a Category
POST /api/v1/categories/{category}/wrangler-pages
Creates a new wrangler page and associates it with the category. Accepts the same fields as POST /api/v1/pages (see Wrangler Pages below), with all fields required rather than optional.
List Posts in a Category
GET /api/v1/categories/{category}/posts
Returns a paginated list of posts belonging to the given category.
Create a Post within a Category
POST /api/v1/categories/{category}/posts
Creates a new post and associates it with the category. Accepts the same fields as POST /api/v1/posts (see Posts below).
Posts
Posts are content items such as articles or news entries.
List Posts
GET /api/v1/posts
| Name | Type | Required | Description |
|---|---|---|---|
| search | string | No | Filter by title |
| page | integer | No | Page number |
Response (200):
{
"data": [
{
"id": 1,
"title": "My Article",
"slug": "my-article",
"url": "/blog/my-article",
"post_type": "article",
"description": "Short description",
"content": "Full HTML content",
"published": 1,
"thumbnail": null,
"post_parent": null,
"menu_order": 0,
"post_status": "public",
"meta": {},
"category_id": null,
"created_at": "2024-01-01T00:00:00.000000Z",
"updated_at": "2024-01-01T00:00:00.000000Z"
}
],
"links": {},
"meta": {}
}
Get Post
GET /api/v1/posts/{id}
Returns a single post wrapped in "data".
Create Post
POST /api/v1/posts
| Name | Type | Required | Description |
|---|---|---|---|
| title | string | Yes | Post title (max 255) |
| post_type | string | Yes | Type identifier (e.g. article, news) |
| post_status | string | Yes | Visibility: public, private, or protected |
| slug | string | No | URL slug |
| url | string | No | Full URL path (max 191) |
| description | string | No | Short description (max 2000) |
| content | string | No | Full HTML content |
| published | integer | No | Published flag (1 = published) |
| thumbnail | string | No | Thumbnail path (max 191) |
| post_parent | integer | No | Parent post ID |
| menu_order | integer | No | Sort order |
| meta | json | No | Arbitrary metadata (JSON string or object) |
| category_id | integer | No | Associated category ID |
Returns the created post wrapped in "data".
Update Post
PUT /api/v1/posts/{id}
Accepts the same fields as Create. When meta is supplied on update, values are merged with existing metadata rather than replaced entirely.
Returns the updated post wrapped in "data".
Delete Post
DELETE /api/v1/posts/{id}
Deletes the post and its stored thumbnail (if any).
Response: 204 No Content
Post Tags
GET /api/v1/posts/{post}/tags
POST /api/v1/posts/{post}/tags/{tag}
DELETE /api/v1/posts/{post}/tags/{tag}
GET returns a paginated list of tags attached to the post.
POST attaches an existing tag (by ID) to the post without detaching others.
DELETE detaches the tag from the post.
Both POST and DELETE return 204 No Content.
Wrangler Pages
The page-builder pages resource. The CRUD routes use the /api/v1/pages prefix; the sub-resource routes (tags, components) use /api/v1/wrangler-pages.
Responses from the CRUD endpoints use the JSON:API envelope format. The public endpoint at /api/v1/public/pages/by-url returns plain JSON.
List Wrangler Pages
GET /api/v1/pages
| Name | Type | Required | Description |
|---|---|---|---|
| search | string | No | Filter by title or URL |
| page | integer | No | Page number |
Get Wrangler Page
GET /api/v1/pages/{id}
Response uses JSON:API format:
{
"data": {
"type": "wrangler-pages",
"id": "12",
"attributes": {
"title": "About Us",
"url": "/about",
"greedy": 0,
"parent_id": null,
"thumbnail": null,
"enabled": 1,
"menu_order": 1,
"menu_name": "About",
"listed": 1,
"template": "default",
"access": "public",
"description": "About our company.",
"keywords": "about, company",
"redirect": null,
"redirect_permanent": 0,
"meta": {}
},
"relationships": {
"parent": { "data": null },
"category": { "data": null }
}
}
}
Create Wrangler Page
POST /api/v1/pages
| Name | Type | Required | Description |
|---|---|---|---|
| title | string | Yes | Page title (max 255) |
| template | string | Yes | Template identifier (max 255) |
| access | string | Yes | Visibility: public, private, or protected |
| url | string | No | URL path (max 255) |
| greedy | mixed | No | Whether URL matching is greedy |
| parent_id | integer | No | Parent page ID (cannot be a descendant) |
| thumbnail | file | No | Thumbnail image upload |
| enabled | mixed | No | Whether the page is active |
| menu_order | integer | No | Sort order in menus |
| menu_name | string | No | Label used in menus (max 255) |
| listed | mixed | No | Whether the page appears in listings |
| password | string | No | Password for protected pages (stored hashed) |
| description | string | No | Meta description (max 255) |
| keywords | string | No | Meta keywords (max 255) |
| redirect | string | No | Redirect destination URL (max 255) |
| redirect_permanent | mixed | No | Whether the redirect is permanent (301) |
| category_id | integer | No | Associated category ID |
| meta | array | No | Arbitrary metadata |
Returns the created page in JSON:API format.
Update Wrangler Page
PUT /api/v1/pages/{id}
Accepts the same fields as Create. title, greedy, enabled, menu_order, listed, and access are required on update.
Returns the updated page in JSON:API format.
Delete Wrangler Page
DELETE /api/v1/pages/{id}
Deletes the page and its stored thumbnail (if any).
Response: 204 No Content
Wrangler Page Tags
GET /api/v1/wrangler-pages/{wranglerPage}/tags
POST /api/v1/wrangler-pages/{wranglerPage}/tags/{tag}
DELETE /api/v1/wrangler-pages/{wranglerPage}/tags/{tag}
GET returns a paginated list of tags attached to the page.
POST attaches an existing tag to the page without detaching others.
DELETE detaches the tag from the page.
Both POST and DELETE return 204 No Content.
Wrangler Page Components
GET /api/v1/wrangler-pages/{wranglerPage}/wrangler-components
POST /api/v1/wrangler-pages/{wranglerPage}/wrangler-components
GET returns a paginated list of components attached to the page.
POST creates a new component and attaches it to the page.
| Name | Type | Required | Description |
|---|---|---|---|
| type | string | Yes | Component type identifier (max 255) |
| title | string | Yes | Component title (max 255) |
| content | string | Yes | Component content |
| config | json | Yes | Configuration JSON object |
| thumbnail | file | No | Thumbnail image upload |
Wrangler Components
Standalone CRUD for wrangler components.
GET /api/v1/wrangler-components
POST /api/v1/wrangler-components
GET /api/v1/wrangler-components/{id}
PUT /api/v1/wrangler-components/{id}
DELETE /api/v1/wrangler-components/{id}
GET (list) supports a search query parameter.
Create / Update Component Fields
| Name | Type | Required | Description |
|---|---|---|---|
| type | string | Yes | Component type identifier (max 255) |
| wrangler_page_id | integer | Yes | ID of the owning page (create only) |
| title | string | No | Component title (max 255) |
| content | string | No | Component content |
| config | json | No | Configuration JSON object (depth-limited for safety) |
| thumbnail | file | No | Thumbnail image upload |
Messages
View Message
GET /api/v1/view-message/{message}
Returns the message record. Marks the message as read (state = 1) as a side effect.
Roles
Roles are managed via Spatie Laravel Permission.
List Roles
GET /api/v1/roles
Supports a search query parameter.
Response (200):
{
"data": [
{
"id": 1,
"name": "admin",
"guard_name": "web",
"created_at": "2024-01-01T00:00:00.000000Z",
"updated_at": "2024-01-01T00:00:00.000000Z"
}
],
"links": {},
"meta": {}
}
Get Role
GET /api/v1/roles/{id}
Create Role
POST /api/v1/roles
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Unique role name (max 32) |
| permissions | array | No | Array of permission IDs to assign |
Update Role
PUT /api/v1/roles/{id}
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Role name (max 32, unique except self) |
| permissions | array | No | Permission IDs -- replaces all current assignments |
Delete Role
DELETE /api/v1/roles/{id}
Response: 204 No Content
Permissions
Permissions are managed via Spatie Laravel Permission.
List Permissions
GET /api/v1/permissions
Supports a search query parameter.
Response (200):
{
"data": [
{
"id": 1,
"name": "view-users",
"guard_name": "web",
"created_at": "2024-01-01T00:00:00.000000Z",
"updated_at": "2024-01-01T00:00:00.000000Z"
}
],
"links": {},
"meta": {}
}
Get Permission
GET /api/v1/permissions/{id}
Create Permission
POST /api/v1/permissions
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Permission name (max 64) |
| roles | array | No | Array of role IDs to assign |
Update Permission
PUT /api/v1/permissions/{id}
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Permission name (max 40, unique except self) |
| roles | array | No | Role IDs -- replaces all current role assignments |
Delete Permission
DELETE /api/v1/permissions/{id}
Response: 204 No Content
Error Handling
The API uses standard HTTP status codes.
| Code | Meaning |
|---|---|
| 200 | OK |
| 201 | Created |
| 204 | No Content |
| 401 | Unauthorized -- missing or invalid token |
| 403 | Forbidden -- authenticated but insufficient permissions |
| 404 | Not Found |
| 422 | Unprocessable Entity -- validation failed |
| 429 | Too Many Requests -- rate limit exceeded |
| 500 | Server Error |
Validation error response body:
{
"message": "The given data was invalid.",
"errors": {
"title": ["The title field is required."]
}
}
Pagination
All list endpoints return paginated results using Laravel's default pagination. The response includes links (first, last, prev, next) and a meta object with current_page, last_page, per_page, and total.
Versioning
The current API version is v1. All authenticated endpoints are prefixed with /api/v1/.