# Post Markdown API guide for agents

Use this guide to publish, discover, and retrieve Markdown from Post Markdown.

**Base URL:** `https://post-md.vercel.app`

The machine-readable contract is at [`/openapi.json`](https://post-md.vercel.app/openapi.json).

## Fast path: read a post

Fetch the raw Markdown directly. No SDK or authentication is needed for public posts.

```bash
curl -i https://post-md.vercel.app/api/raw/abc123
```

The response uses `Content-Type: text/markdown; charset=utf-8`. Useful headers:

- `X-Post-Id`
- `X-Post-Title` — URL-encoded; decode it before displaying
- `X-Post-Created` and `X-Post-Updated` — ISO timestamps
- `X-Post-Linked-Count`

The body contains the post Markdown and may append the content of eligible linked public posts. For a compact response, use:

```bash
curl 'https://post-md.vercel.app/api/raw/abc123/compact?level=standard'
```

`level` accepts `light`, `standard` (default), or `aggressive`. The response is `text/plain`; `X-Compaction-Level` and `X-Compaction-Reduction` describe the result.

## Find public posts

```bash
curl 'https://post-md.vercel.app/api/posts/explore?page=1&limit=20&search=agents&tag=markdown'
```

The response is `{ "posts": [...], "total": 0, "page": 1, "totalPages": 0 }`. Each result includes `id`, `title`, `content`, `slug`, ISO `createdAt`/`updatedAt`, `userId`, and `tags`. Search uses full text when the FTS index is available; the fallback searches titles. Posts are newest first. `page` defaults to `1`; `limit` defaults to `20`.

To get JSON for a single public post, use `GET /api/posts/{id}`. It includes `tags` and `links` (with each link’s `targetPost`).

`GET /api/posts` returns a simple array of publicly readable posts. Its optional `userId` query parameter is private to that user and requires their signed-in session.

## Publish a post

Anonymous publishing is supported. `title` and `content` are required; the server creates an ID and slug if `slug` is omitted.

```bash
curl -X POST https://post-md.vercel.app/api/posts \
  -H 'Content-Type: application/json' \
  --data '{
    "title": "Notes for my agent",
    "content": "# Notes for my agent\n\nPublished from an API client.",
    "tags": ["agents", "notes"]
  }'
```

Success returns `201 Created` with the post object, including its generated `id`. Open `https://post-md.vercel.app/p/{id}` to view it and `https://post-md.vercel.app/api/raw/{id}` to read the Markdown.

Optional fields:

- `slug`: preferred URL slug (normalized; collisions receive a suffix)
- `tags`: string array
- `isPublic`, `isPrivate`, `isLocked`: visibility flags; public reads only expose posts that are public, not private, and not locked
- `lockPassword`: plaintext is accepted only as an input when creating/updating a locked post; it is hashed server-side and **never returned**
- `publishAt`: ISO date-time for scheduled publishing; scheduled posts are not publicly readable before publication

If you publish anonymously from a browser, the response sets a short-lived claim cookie so the author can sign in and claim the post. A command-line agent should retain the returned `id`; it cannot claim the post without completing the browser sign-in flow.

## Authentication and write access

- Public reads and anonymous creation do not need a key.
- Updating or deleting posts requires a Better Auth signed-in session and ownership of the post. The optional `userId` list filter also requires the matching user session.
- These post endpoints do **not** accept personal API keys as `Authorization: Bearer ...`. API keys are managed in the dashboard; they are not a general API authentication mechanism.
- Private posts require their owner’s session for JSON access. Locked posts and non-public/scheduled posts are not available through public JSON or raw Markdown endpoints. Password verification alone does not make the raw endpoint public.
- Do not send or store a post’s password in logs. The API never returns its stored hash.

## CORS, limits, and errors

Cross-origin browser access is enabled on `/api/posts`, `/api/posts/{id}`, `/api/raw/{id}`, and `/api/raw/{id}/compact`. It is not enabled on every route (for example, `/api/posts/explore`). Server-to-server clients can call routes regardless of browser CORS.

Current per-IP in-memory rate limits include:

| Operation | Limit |
| --- | ---: |
| List posts, get post, raw Markdown | 60/minute |
| Create post | 5/minute |
| Update/delete post | 20/minute |
| Explore | 30/minute |
| Compact Markdown | 30/minute |

When rate-limited, expect `429 Too Many Requests`; use the `Retry-After` response header. These limits are in-memory and may vary across server instances.

Common statuses: `400` invalid/missing input, `401` sign-in required, `403` content is locked or unavailable to public readers, `404` post not found (private posts may be hidden as not found), `429` rate limit, `500` unexpected server error.

## Other useful endpoints

- `GET /api/slug/check?slug=my-post` — returns `{ "available": true, "slug": "my-post" }`
- `POST /api/posts/{id}/verify-password` with `{ "password": "..." }` — checks a password and returns `{ "ok": true }`; it does not return the post body
- `GET /api/read/{token}` — reads a post through a dedicated access token; token expiry/read limits apply, and the response includes `readsRemaining`
- `GET /rss` — RSS 2.0 feed of recent public posts

## Minimal agent workflow

1. Search with `/api/posts/explore` or get an ID from a user.
2. Fetch `/api/raw/{id}` when you need the original Markdown; use `/compact` when context is constrained.
3. Cite the canonical page as `https://post-md.vercel.app/p/{id}`.
4. Publish with `POST /api/posts`; save the returned ID and URL.
