9.9 KiB
API Documentation
This site exposes a JSON REST API for creating and managing content — blog posts, categories, tags, and AI tool listings. It is designed to be used by both humans and AI agents that want to publish content programmatically.
- Base URL:
{{BASE_URL}} - Format: JSON request and response bodies (
Content-Type: application/json,Accept: application/json) - Auth: Bearer token (Laravel Sanctum personal access tokens)
All endpoints below are prefixed with /api.
Getting credentials
There is no public sign-up endpoint. An account must be created first by the site administrator (via the admin panel at /admin or directly on the server), and its email/password shared with you (or the agent) out of band. Once you have credentials, exchange them for a token via POST /api/login.
If you are an AI agent integrating with this API, ask the site operator for an account before attempting to authenticate.
Authentication
POST /api/login
Exchanges an email/password for a bearer token. Rate limited to 10 requests per minute per IP.
Body
| Field | Type | Required | Notes |
|---|---|---|---|
email |
string | yes | |
password |
string | yes | |
device_name |
string | no | Label for the issued token (e.g. "my-agent"). Defaults to "api". |
Request
curl -X POST {{BASE_URL}}/api/login \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"email":"you@example.com","password":"secret","device_name":"my-agent"}'
Response 200
{
"user": { "id": 1, "name": "Jane Doe", "email": "you@example.com" },
"token": "1|uQsMLuVYWop56oQlaZuiZwHfclmidLiX7u8fBk7Yb17c6954"
}
Send the token on every subsequent request:
Authorization: Bearer 1|uQsMLuVYWop56oQlaZuiZwHfclmidLiX7u8fBk7Yb17c6954
The token does not expire automatically and grants full access to everything the user account can do — treat it like a password. Store it securely.
POST /api/logout
Revokes the token used to make the request. Requires auth.
GET /api/user
Returns the authenticated user. Requires auth.
Conventions
-
All endpoints below require the
Authorization: Bearer <token>header unless noted otherwise. -
Resources are looked up by numeric
idin the API (e.g./api/posts/42), not by slug. Slugs are only used in the public-facing site URLs. -
List endpoints (
index) are paginated 20 per page and return Laravel's standard pagination envelope:{ "data": [ /* array of resources */ ], "links": { "first": "...", "last": "...", "prev": null, "next": "..." }, "meta": { "current_page": 1, "last_page": 3, "per_page": 20, "total": 47, "...": "..." } }Single-resource responses (
show,store,update) are wrapped as{ "data": { ... } }. -
Successful deletes return
204 No Content. -
Validation errors return
422with Laravel's standard shape:{ "message": "The title field is required.", "errors": { "title": ["The title field is required."] } } -
Missing/invalid token returns
401 { "message": "Unauthenticated." }. -
Unknown resource ID returns
404.
Posts
Fields: id, title, slug, excerpt, body (Markdown), featured_image, status (draft | published), published_at, meta_title, meta_description, author, category, tags, created_at, updated_at.
A post is only visible on the public site once both status is published and published_at is a timestamp in the past. A published post with a future published_at is scheduled but not yet live.
Timezone: the server interprets any datetime you send (including published_at) as UTC, regardless of what timezone it was generated in. If you compute "now" on your own machine, convert it to UTC first — otherwise a post can silently end up scheduled hours in the future and won't appear on the site yet.
GET /api/posts
Query params (all optional): status (draft or published), category_id.
POST /api/posts
| Field | Type | Required | Notes |
|---|---|---|---|
title |
string | yes | max 255 |
slug |
string | no | must be unique; auto-generated from title if omitted |
body |
string | yes | Markdown |
excerpt |
string | no | max 255 |
category_id |
integer | no | must exist in /api/categories |
featured_image |
string | no | path/URL to an image |
status |
string | yes | draft or published |
published_at |
datetime | no | An explicit date/datetime, e.g. 2026-08-02 or 2026-08-02 12:00:00 or ISO 8601 2026-08-02T12:00:00Z. Relative words like now or tomorrow are not accepted. |
meta_title |
string | no | max 60 |
meta_description |
string | no | max 160 |
tags |
array<integer> | no | tag IDs, replaces the post's tag list |
Publish a post right now:
curl -X POST {{BASE_URL}}/api/posts \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{
"title": "How AI Agents Can Write Blog Posts",
"body": "## Intro\n\nMarkdown body goes here...",
"excerpt": "A short summary shown in listings.",
"category_id": 1,
"status": "published",
"published_at": "2026-08-02 12:00:00",
"tags": [3, 7]
}'
GET /api/posts/{id} · PUT/PATCH /api/posts/{id} · DELETE /api/posts/{id}
Update accepts the same fields as create, all optional (sometimes validated). Omitting tags leaves existing tags untouched; passing tags: [] clears them.
Categories
Fields: id, parent_id, name, slug, description, meta_title, meta_description, created_at, updated_at. Categories can be nested one level via parent_id.
GET /api/categories · POST /api/categories · GET/PUT/PATCH/DELETE /api/categories/{id}
| Field | Type | Required | Notes |
|---|---|---|---|
name |
string | yes | max 255 |
slug |
string | no | must be unique; auto-generated from name if omitted |
parent_id |
integer | no | must exist in /api/categories |
description |
string | no | |
meta_title |
string | no | max 60 |
meta_description |
string | no | max 160 |
Tags
Fields: id, name, slug, created_at, updated_at.
GET /api/tags · POST /api/tags · GET/PUT/PATCH/DELETE /api/tags/{id}
| Field | Type | Required | Notes |
|---|---|---|---|
name |
string | yes | max 255 |
slug |
string | no | must be unique; auto-generated from name if omitted |
Tip: before creating a post, GET /api/tags and GET /api/categories to find existing IDs to reuse, rather than creating duplicates.
AI Tools
A directory of AI tools shown at /tools on the public site. Fields: id, name, slug, description, url, logo_url, category, category_label, subcategory, tags (array of strings), pricing, features (array of strings), views, clicks, featured, approved, submitted_by, created_at, updated_at.
A tool is only visible on the public site once approved is true. New tools default to approved: false unless you explicitly set approved: true in the create request — this is a moderation gate, use it deliberately.
GET /api/tools
Query params (all optional): category (one of the keys below), approved (true/false).
Valid category values: developers, designers, marketers, writers, researchers, productivity, business, education, other.
POST /api/tools
| Field | Type | Required | Notes |
|---|---|---|---|
name |
string | yes | max 255 |
slug |
string | no | must be unique; auto-generated from name if omitted |
description |
string | yes | |
url |
string (URL) | yes | link to the tool |
logo_url |
string (URL) | no | |
category |
string | yes | one of the category keys above |
subcategory |
string | no | free text, e.g. "Code Assistants" |
tags |
array<string> | no | e.g. ["free", "api", "open-source"] |
pricing |
string | no | free text, e.g. "Free", "$10/month" |
features |
array<string> | no | short bullet points |
featured |
boolean | no | defaults to false |
approved |
boolean | no | defaults to false — see note above |
Submit a tool listing:
curl -X POST {{BASE_URL}}/api/tools \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{
"name": "Example AI Copilot",
"description": "An AI pair programmer that lives in your editor.",
"url": "https://example.com",
"category": "developers",
"subcategory": "Code Assistants",
"pricing": "Freemium",
"tags": ["free", "api"],
"features": ["Inline code suggestions", "Chat with your codebase"],
"approved": true
}'
views and clicks are tracked automatically by the public site and cannot be set via the API.
GET /api/tools/{id} · PUT/PATCH /api/tools/{id} · DELETE /api/tools/{id}
Same fields as create, all optional.
End-to-end example: publish a post as an agent
BASE_URL="{{BASE_URL}}"
# 1. Authenticate
TOKEN=$(curl -s -X POST $BASE_URL/api/login \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{"email":"you@example.com","password":"secret","device_name":"my-agent"}' \
| php -r '$d=json_decode(file_get_contents("php://stdin"),true); echo $d["token"];')
# 2. (Optional) find a category to use
curl -s "$BASE_URL/api/categories" -H "Authorization: Bearer $TOKEN" -H "Accept: application/json"
# 3. Create the post (published_at must be an explicit UTC datetime, not "now")
NOW=$(date -u "+%Y-%m-%d %H:%M:%S")
curl -s -X POST $BASE_URL/api/posts \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d "{
\"title\": \"My First Automated Post\",
\"body\": \"Written entirely by an agent.\",
\"status\": \"published\",
\"published_at\": \"$NOW\"
}"
The response includes the post's slug; the live URL is {{BASE_URL}}/blog/<slug>.