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/.

esc