For Developers

REST API Development

9 min read

> **What you'll learn:** How the TallCMS REST API is architected, how to authenticate, and how to extend it.

For detailed endpoint documentation, see the OpenAPI docs. To generate the docs, see Generate API Documentation.


Overview

TallCMS provides a full REST API for headless CMS usage. The API follows JSON:API conventions and supports:

FeatureDescription
AuthenticationLaravel Sanctum token-based auth
AuthorizationFilament Shield permissions + token abilities
ResourcesPages, Posts, Categories, Media, Webhooks
i18nPer-locale or multi-locale read/write
Soft DeletesPages and Posts support trash/restore
WebhooksEvent-driven notifications with retry logic

Architecture

Base URL

/api/v1/tallcms

Route Structure

packages/tallcms/cms/
├── routes/
│   └── api.php                    # All API routes
├── src/Http/
│   ├── Controllers/Api/V1/
│   │   ├── Controller.php         # Base with response helpers
│   │   ├── AuthController.php
│   │   ├── PageController.php
│   │   ├── PostController.php
│   │   ├── CategoryController.php
│   │   ├── MediaController.php
│   │   ├── MediaCollectionController.php
│   │   └── WebhookController.php
│   ├── Middleware/
│   │   ├── CheckTokenExpiry.php
│   │   └── CheckTokenAbilities.php
│   ├── Requests/Api/V1/           # Form request validation
│   └── Resources/Api/V1/          # JSON transformers

Controller Concerns

Controllers use shared traits for common functionality:

TraitPurpose
HandlesFilteringQuery parameter filtering with allowlist
HandlesSortingSort field validation
HandlesIncludesEager loading + withCount
HandlesPaginationPage/per_page with max limit
HandlesLocalei18n response formatting

Authentication

Token Creation

curl -X POST /api/v1/tallcms/auth/token \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "password": "secret",
    "device_name": "My App",
    "abilities": ["pages:read", "posts:read"]
  }'

Response:

{
  "data": {
    "token": "1|abc123...",
    "expires_at": "2027-01-27T10:30:00Z",
    "abilities": ["pages:read", "posts:read"]
  }
}

Using Tokens

curl /api/v1/tallcms/pages \
  -H "Authorization: Bearer 1|abc123..."

Token Abilities

AbilityGrants Access To
pages:readList/view pages and revisions
pages:writeCreate/update/publish pages
pages:deleteSoft-delete and force-delete pages
posts:readList/view posts and revisions
posts:writeCreate/update/publish posts
posts:deleteSoft-delete and force-delete posts
categories:readList/view categories
categories:writeCreate/update categories
categories:deleteDelete categories
media:readList/view media and collections
media:writeUpload/update media and collections
media:deleteDelete media and collections
webhooks:manageFull webhook management

Authorization Flow

API requests pass through two authorization layers:

Request
   │
   ▼
┌─────────────────────────┐
│ 1. Token Ability Check  │  CheckTokenAbilities middleware
│    "Does token have     │  e.g., pages:write
│     required ability?"  │
└───────────┬─────────────┘
            │
            ▼
┌─────────────────────────┐
│ 2. Policy Check         │  $this->authorize() in controller
│    "Does user have      │  e.g., Update:CmsPage Shield permission
│     Shield permission?" │
└───────────┬─────────────┘
            │
            ▼
        Response

Both checks must pass. A token with pages:write ability still requires the user to have Update:CmsPage Shield permission.


Query Parameters

Filtering

GET /pages?filter[status]=published&filter[author_id]=1

Each resource defines allowed filters:

ResourceAllowed Filters
Pagesstatus, author_id, parent_id, is_homepage, created_at, updated_at, trashed
Postsstatus, author_id, category_id, is_featured, created_at, updated_at, trashed
Categoriesparent_id
Mediamime_type, collection_id, has_variants, created_at

Sorting

GET /pages?sort=created_at&order=desc

Includes

GET /pages?include=author,children&with_counts=children

Pagination

GET /pages?page=2&per_page=25

Maximum per_page is 100.


Translations (i18n)

Reading

Single locale:

GET /pages/1?locale=en
{ "title": "About Us", "slug": "about-us" }

All translations:

GET /pages/1?with_translations=true
{
  "title": { "en": "About Us", "de": "Über uns" },
  "slug": { "en": "about-us", "de": "ueber-uns" }
}

Writing

Single-locale mode (use ?locale= or X-Locale header):

POST /pages?locale=en
{ "title": "About Us", "content": [...] }

Multi-locale mode (use translations object):

PUT /pages/1
{
  "translations": {
    "title": { "en": "About Us", "de": "Über uns" }
  }
}

Mixing both modes in one request returns a 400 error.


Webhooks

Event Types

EventTriggered When
page.createdPage created
page.updatedPage updated
page.publishedPage published
page.deletedPage soft-deleted
post.createdPost created
post.updatedPost updated
post.publishedPost published
post.deletedPost soft-deleted

Payload Format

{
  "id": "wh_del_abc123",
  "event": "page.published",
  "attempt": 1,
  "max_attempts": 3,
  "timestamp": "2026-01-27T10:30:00Z",
  "data": {
    "id": 123,
    "type": "page",
    "attributes": { "title": "About Us", "status": "published" }
  },
  "meta": {
    "triggered_by": { "id": 1, "name": "Admin User" }
  }
}

Security Headers

HeaderDescription
X-TallCMS-EventEvent type
X-TallCMS-SignatureHMAC-SHA256 signature
X-TallCMS-DeliveryUnique delivery ID
X-TallCMS-AttemptRetry attempt number

Signature Verification

$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_TALLCMS_SIGNATURE'];
$secret = 'your-webhook-secret';

$expected = 'sha256=' . hash_hmac('sha256', $payload, $secret);

if (!hash_equals($expected, $signature)) {
    abort(401, 'Invalid signature');
}

SSRF Protection

Webhook URLs are validated for security:

  • HTTPS only (port 443)
  • No IP literals in hostname
  • No private/reserved IP ranges
  • No localhost or .local domains
  • DNS re-validated at delivery time

Configuration

Note: For initial API setup and environment variables, see Installation.

// config/tallcms.php

'api' => [
    'enabled' => env('TALLCMS_API_ENABLED', false),
    'prefix' => env('TALLCMS_API_PREFIX', 'api/v1/tallcms'),
    'rate_limit' => env('TALLCMS_API_RATE_LIMIT', 60),
    'auth_rate_limit' => env('TALLCMS_API_AUTH_RATE_LIMIT', 5),
    'auth_lockout_minutes' => env('TALLCMS_API_AUTH_LOCKOUT', 15),
    'token_expiry_days' => env('TALLCMS_API_TOKEN_EXPIRY', 365),
    'max_per_page' => 100,
],

'webhooks' => [
    'enabled' => env('TALLCMS_WEBHOOKS_ENABLED', false),
    'timeout' => env('TALLCMS_WEBHOOK_TIMEOUT', 30),
    'max_retries' => env('TALLCMS_WEBHOOK_MAX_RETRIES', 3),
    'retry_backoff' => [60, 300, 900],
],

Extending the API

Adding a New Resource

  1. Create the controller:
namespace TallCms\Cms\Http\Controllers\Api\V1;

class CustomController extends Controller
{
    use HandlesFiltering, HandlesPagination;

    protected function allowedFilters(): array
    {
        return ['status', 'created_at'];
    }
}
  1. Create the resource transformer:
namespace TallCms\Cms\Http\Resources\Api\V1;

class CustomResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
        ];
    }
}
  1. Register routes in routes/api.php:
Route::middleware('tallcms.abilities:custom:read')->group(function () {
    Route::get('/custom', [CustomController::class, 'index']);
});
  1. Add the ability to TokenAbilityValidator::VALID_ABILITIES.

Rate Limiting

EndpointLimitKey
POST /auth/token5 attemptsIP + email hash
All other endpoints60/minuteUser ID

Rate limit headers are included in responses:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
Retry-After: 45  (on 429 responses)

Common Pitfalls

"Token missing required ability"The token doesn't include the needed ability. Request a new token with the required abilities.

"This action is unauthorized" (403)The user has the token ability but lacks the Shield permission. Grant the permission in Admin > Shield.

"Invalid include(s)"The requested include relation isn't in the controller's allowedIncludes() array.

Webhook not receiving eventsCheck that TALLCMS_WEBHOOKS_ENABLED=true and the webhook is active.


Next Steps

Choose Theme