16 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.
MCP Servers
A directory of Model Context Protocol servers shown at /mcp-servers on the public site. Fields: id, name, slug, description, url, docs_url, logo_url, category, category_label, install_command, config_example, transport, transport_label, language, capabilities (array of strings), tags (array of strings), views, clicks, featured, approved, submitted_by, created_at, updated_at.
An MCP server is only visible on the public site once approved is true. New servers default to approved: false unless you explicitly set approved: true in the create request — this is a moderation gate, use it deliberately.
GET /api/mcp-servers
Query params (all optional): category (one of the keys below), approved (true/false).
Valid category values: official, filesystem, databases, developer-tools, productivity, search, browser-automation, communication, data-analytics, other.
POST /api/mcp-servers
| 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 repository or homepage |
docs_url |
string (URL) | no | dedicated documentation link, if different from url |
logo_url |
string (URL) | no | |
category |
string | yes | one of the category keys above |
install_command |
string | no | e.g. "npx -y @modelcontextprotocol/server-filesystem /path" |
config_example |
string | no | a config snippet for an MCP client (e.g. the mcpServers JSON block) |
transport |
string | no | one of stdio, sse, http |
language |
string | no | e.g. "TypeScript", "Python" |
capabilities |
array<string> | no | short bullet points of what it exposes, e.g. ["Read files", "Write files"] |
tags |
array<string> | no | e.g. ["official", "read-only"] |
featured |
boolean | no | defaults to false |
approved |
boolean | no | defaults to false — see note above |
Submit an MCP server listing:
curl -X POST {{BASE_URL}}/api/mcp-servers \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{
"name": "Example Filesystem Server",
"description": "Gives an MCP client read/write access to a local directory.",
"url": "https://github.com/example/example-mcp-filesystem",
"docs_url": "https://github.com/example/example-mcp-filesystem#readme",
"category": "filesystem",
"install_command": "npx -y @example/mcp-filesystem /path/to/dir",
"config_example": "{\n \"mcpServers\": {\n \"filesystem\": {\n \"command\": \"npx\",\n \"args\": [\"-y\", \"@example/mcp-filesystem\", \"/path/to/dir\"]\n }\n }\n}",
"transport": "stdio",
"language": "TypeScript",
"capabilities": ["Read files", "Write files", "List directories"],
"tags": ["official", "filesystem"],
"approved": true
}'
views and clicks are tracked automatically by the public site and cannot be set via the API.
GET /api/mcp-servers/{id} · PUT/PATCH /api/mcp-servers/{id} · DELETE /api/mcp-servers/{id}
Same fields as create, all optional.
Claude Skills
A directory of Claude Skills shown at /skills on the public site. Fields: id, name, slug, description, url, docs_url, logo_url, category, category_label, install_instructions, example_prompt, requirements, tags (array of strings), views, clicks, featured, approved, submitted_by, created_at, updated_at.
A skill is only visible on the public site once approved is true. New skills default to approved: false unless you explicitly set approved: true in the create request — this is a moderation gate, use it deliberately.
GET /api/skills
Query params (all optional): category (one of the keys below), approved (true/false).
Valid category values: document-processing, development, design, productivity, data-analysis, communication, other.
POST /api/skills
| 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 repository or homepage |
docs_url |
string (URL) | no | dedicated documentation link, if different from url |
logo_url |
string (URL) | no | |
category |
string | yes | one of the category keys above |
install_instructions |
string | no | e.g. "Copy into ~/.claude/skills/ and restart Claude" |
example_prompt |
string | no | a sample prompt that would trigger/use this skill |
requirements |
string | no | e.g. "Claude Code", "Claude with Code Execution tool" |
tags |
array<string> | no | e.g. ["official", "anthropic"] |
featured |
boolean | no | defaults to false |
approved |
boolean | no | defaults to false — see note above |
Submit a skill listing:
curl -X POST {{BASE_URL}}/api/skills \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{
"name": "Example PDF Skill",
"description": "Extracts text and tables from PDF files and can fill out PDF forms.",
"url": "https://github.com/example/example-pdf-skill",
"docs_url": "https://github.com/example/example-pdf-skill#readme",
"category": "document-processing",
"install_instructions": "Copy this folder into ~/.claude/skills/pdf/ and restart Claude.",
"example_prompt": "Extract the line items from this invoice PDF into a table.",
"requirements": "Claude Code",
"tags": ["official", "pdf"],
"approved": true
}'
views and clicks are tracked automatically by the public site and cannot be set via the API.
GET /api/skills/{id} · PUT/PATCH /api/skills/{id} · DELETE /api/skills/{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>.