Files
omg-wtf-ai/resources/docs/api.md
T
2026-08-11 00:42:46 +03:00

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 id in 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 422 with 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>.