# 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](https://laravel.com/docs/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** ```bash 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`** ```json { "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 ` 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: ```json { "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: ```json { "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\ | no | tag IDs, replaces the post's tag list | **Publish a post right now:** ```bash 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\ | no | e.g. `["free", "api", "open-source"]` | | `pricing` | string | no | free text, e.g. `"Free"`, `"$10/month"` | | `features` | array\ | no | short bullet points | | `featured` | boolean | no | defaults to `false` | | `approved` | boolean | no | defaults to `false` — see note above | **Submit a tool listing:** ```bash 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 ```bash 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/`.