9.3 KiB
API Guide for AI Agents
This app is a personal content-operations hub. It stores Sites, Social Accounts, and Posts, and exposes them over a local JSON API so an AI agent (Claude Code, Mistral, or any other LLM/tool-calling agent) can read the user's data and drive the content lifecycle — draft, schedule, publish, report back.
There is no authentication on this API. It is designed to run on the local network only. Do not expose it to the public internet. Because there is no auth, credentials (social account passwords, WordPress Application Passwords) are returned in plain text by design — the agent is expected to use them directly (e.g. to log into Reddit, or call WordPress's REST API) without an extra decryption step.
Base URL: {APP_URL}/api — e.g. http://localhost:8000/api in local dev.
All requests/responses are JSON. Send Accept: application/json (Laravel
defaults to JSON error responses for API routes regardless, but set it to
be safe on validation-error bodies).
Mental model
Site (a website the user owns)
└─ cms_type: wordpress | custom | other
└─ if wordpress: wp_username + wp_app_password (WP Application Password)
SocialAccount (login for a social network)
└─ network: twitter | instagram | facebook | reddit | tiktok | linkedin | youtube | other
Post (one piece of content, belongs to a Site, optionally a SocialAccount)
└─ type: same vocabulary as SocialAccount.network, plus "blog"
└─ status: draft -> scheduled -> published | failed
└─ published_url: set once it's actually live somewhere
The typical agent loop is:
- Read Sites/Social Accounts to know what you're working with (
GET /api/sites,GET /api/accounts). - Create a Post as a draft (
POST /api/posts), or find one that's alreadydue(GET /api/posts?due=1). - Publish it externally, by whatever mechanism fits its
type/site (see "How to actually publish" below). - Report the outcome back with
PATCH /api/posts/{id}— setstatustopublishedorfailed, andpublished_urlif you have one, unless you used theposts:publishcommand, which already does this step for you.
Sites
Read-only. No create/update/delete via API — sites are managed through the
app's UI at /sites.
GET /api/sites
Optional query param: ?name= (partial, case-sensitive-per-DB-collation match on name).
GET /api/sites/{id}
Response shape:
{
"data": {
"id": 1,
"name": "My Blog",
"url": "https://example.com",
"description": "A blog about things",
"cms_type": "wordpress",
"wp_username": "admin",
"wp_app_password": "abcd 1234 efgh 5678",
"created_at": "2026-07-19T12:00:00.000000Z",
"updated_at": "2026-07-19T12:00:00.000000Z"
}
}
cms_type is one of wordpress, custom, other. wp_username/
wp_app_password are only meaningful (non-null) when cms_type is
wordpress — they're a WordPress Application Password pair (see
"WordPress publishing" below), not a regular account password.
Social Accounts
Read-only, same reasoning as Sites — manage via /social-accounts in the UI.
GET /api/accounts
Optional query params: ?network= (exact match), ?type= (exact match).
GET /api/accounts/{id}
Response shape:
{
"data": {
"id": 1,
"network": "reddit",
"username": "someuser",
"email": "someuser@example.com",
"password": "plaintext-password",
"type": "post",
"created_at": "...",
"updated_at": "..."
}
}
network is one of twitter, instagram, facebook, reddit, tiktok,
linkedin, youtube, other. type is one of post, comment, like,
or null. The password field is the real plain-text login password for
that network — treat it as a live credential.
Posts
Full CRUD — this is the resource the agent actively drives.
GET /api/posts
Query params (all optional, combine freely):
?status=— exact match:draft|scheduled|published|failed?site_id=— exact match?social_account_id=— exact match?due=1— the important one for automation: returns posts wherestatus=scheduled AND scheduled_at <= now(). Poll this to find work.
GET /api/posts/{id}
POST /api/posts — create a post.
Body:
{
"site_id": 1,
"social_account_id": null,
"type": "blog",
"title": "Optional title",
"body": "Required content",
"status": "draft",
"scheduled_at": null
}
Validation: site_id required, must exist. social_account_id nullable,
must exist if given. type required, one of twitter, instagram,
facebook, reddit, tiktok, linkedin, youtube, blog, other.
body required. title nullable. status defaults to draft if omitted.
scheduled_at nullable datetime.
Note: type and the linked Social Account's network are not
cross-validated — you can link a type=blog post to a Twitter account, or
no account at all. Set type to whatever actually describes the content;
it's what later determines how the post gets published (see below).
PATCH /api/posts/{id} — partial update. This is how you report a
publish result back for anything you didn't publish via the
posts:publish command:
{ "status": "published", "published_url": "https://example.com/2026/07/my-post" }
or on failure:
{ "status": "failed" }
DELETE /api/posts/{id}
How to actually publish a post
This app is a data store and scheduler — it does not publish content by itself except via one specific command below. Publishing mechanism depends on the post's target:
WordPress sites (type=blog, site has cms_type=wordpress)
Don't call the WordPress REST API yourself from a generic HTTP request —
use the built-in Artisan command, which handles auth, validation, and
status/published_url bookkeeping for you:
php artisan posts:publish {post_id}
What it does:
- Loads the Post and its Site.
- Validates:
post.typemust beblog,post.titlemust be set,site.cms_typemust bewordpress, and the site must havewp_username/wp_app_passwordset. Any failure here exits 1 with a descriptive message and does not touchPost.status. - POSTs to
{site.url}/wp-json/wp/v2/postsusing HTTP Basic Auth with the site's WordPress Application Password,status: publish(goes live immediately, not a WP draft). - On success: sets
Post.status = published,Post.published_urlto the new post's permalink, prints{"success": true, "url": "..."}, exits 0. - On failure (bad credentials, unreachable site, WordPress error): sets
Post.status = failed, prints{"success": false, "error": "..."}, exits 1.
This is a shell command, not an HTTP endpoint — run it from wherever
you have shell access to this app (e.g. php artisan posts:publish 42).
You do not need to call PATCH /api/posts/{id} afterward; the command
already updates the Post's status and published_url.
If a WordPress site is missing wp_username/wp_app_password, the user
needs to generate an Application Password in their WP admin (Users →
Profile → Application Passwords) and set it on the Site via this app's
/sites UI — this is not something an agent can do on the user's behalf.
Reddit (type=reddit)
Use the standalone sidecar at sidecars/reddit-post/, a Playwright script
(not part of this Laravel app, no shared DB access):
./sidecars/reddit-post/run.sh \
--username=USERNAME --password=PASSWORD \
--subreddit=test --title="..." --body="..."
It logs into Reddit and creates a text post, printing the same
{"success": ..., "url"/"error": ...} JSON contract on stdout. Unlike
posts:publish, this sidecar does not know about the Posts table at
all — get the Social Account credentials from GET /api/accounts
first, then after the sidecar returns, report the result yourself via
PATCH /api/posts/{id} (status + published_url from the sidecar's
url, or status: failed if it errored).
Any other network/CMS (custom, other, or a type with no
integration yet)
There is no built-in publishing path. Do it however is appropriate for
that platform (its own API, its own automation), using the credentials
from GET /api/sites / GET /api/accounts, then report back via
PATCH /api/posts/{id} exactly as described above. This is the general
fallback contract: whatever publishes a post, always ends with either
the posts:publish command's automatic bookkeeping, or a manual PATCH
setting status and, if applicable, published_url.
Quick reference
| Resource | List | Get one | Create | Update | Delete |
|---|---|---|---|---|---|
| Sites | GET /api/sites |
GET /api/sites/{id} |
— | — | — |
| Social Accounts | GET /api/accounts |
GET /api/accounts/{id} |
— | — | — |
| Posts | GET /api/posts |
GET /api/posts/{id} |
POST /api/posts |
PATCH/PUT /api/posts/{id} |
DELETE /api/posts/{id} |
| Task | How |
|---|---|
| Find work to publish | GET /api/posts?due=1 |
| Publish to a WordPress site | php artisan posts:publish {post_id} (handles status/published_url itself) |
| Publish to Reddit | sidecars/reddit-post/run.sh ..., then PATCH /api/posts/{id} yourself |
| Publish anywhere else | do it yourself, then PATCH /api/posts/{id} yourself |
| Report a failed publish | PATCH /api/posts/{id} with {"status": "failed"} |