A simple Typecho API plugin
Base URL: https://your-site.com/typecho-api/v1
Content-Type: application/json (except uploads: multipart/form-data)
Response Header: X-Typecho-Api-Version: 1.0.0
All endpoints require authentication. Two methods are supported, controlled by the authMode plugin setting.
Authorization: Basic <base64(username:password)>Uses Typecho's user database. Only users with roles listed in allowedRoles (default: administrator,editor) are permitted.
X-Typecho-Token: <token-secret>Tokens are configured in plugin settings as label:secret pairs (one per line).
If configured, only listed IPv4/IPv6 addresses can access any endpoint. All others receive 403.
GET /typecho-api/v1/postsQuery Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
page | int | 1 | Page number |
pageSize | int | 20 | Items per page (max 100) |
status | string | — | Filter: publish, draft, private, waiting, hidden |
authorId | int | — | Filter by author ID |
search | string | — | Full-text search in title and content |
Response 200:
{
"data": [
{
"id": 12,
"title": "Hello World",
"slug": "hello-world",
"status": "publish",
"text": "<!--markdown-->\n\n# Hello",
"markdown": "# Hello",
"contentFormat": "markdown",
"authorId": 1,
"authorName": "Admin",
"allowComment": true,
"allowPing": true,
"allowFeed": true,
"created": 1773102541,
"createdAt": "2026-03-10T00:29:01+00:00",
"modified": 1773102541,
"modifiedAt": "2026-03-10T00:29:01+00:00",
"categories": [
{ "id": 2, "name": "News", "slug": "news", "type": "category" }
],
"tags": [
{ "id": 5, "name": "api", "slug": "api", "type": "tag" }
],
"fields": {},
"excerpt": "Hello content excerpt..."
}
],
"meta": {
"page": 1,
"pageSize": 20,
"total": 42
}
}GET /typecho-api/v1/post?id={id}Response 200: Single post object wrapped in { "data": { ... } } (same shape as list items, without excerpt).
Response 404: { "error": { "code": 404, "message": "Post not found." } }
POST /typecho-api/v1/postsRequest Body:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
title | string | yes | — | Post title |
markdown | string | yes* | — | Markdown content (highest priority) |
text | string | yes* | — | Raw text content (second priority) |
content | string | yes* | — | Content fallback (lowest priority) |
status | string | no | "draft" | publish / draft / private / waiting / hidden |
authorId | int | no | config default | Author user ID |
created | int | no | now | Unix timestamp |
slug | string | no | auto-generated | URL slug (uniquified automatically) |
password | string | no | — | Password-protect the post |
allowComment | bool | no | true | Allow comments |
allowPing | bool | no | true | Allow pingbacks |
allowFeed | bool | no | true | Include in RSS feed |
categories | array or string | no | — | Category names, slugs, IDs, or objects |
tags | array or string | no | — | Tag names, slugs, IDs, or objects |
fields | object | no | — | Custom fields { "key": "value" } |
*One of markdown, text, or content is required.
Example:
curl -X POST https://your-site.com/typecho-api/v1/posts \
-H "X-Typecho-Token: my-secret-token" \
-H "Content-Type: application/json" \
-d '{
"title": "Hello from API",
"markdown": "# Heading\n\n**Bold** content here.",
"status": "publish",
"categories": ["News", "Announcements"],
"tags": ["api", "typecho"],
"fields": { "priority": 5 }
}'Response 201:
{
"message": "Post created.",
"data": { /* full post object */ }
}Categories and tags accept flexible input:
// Comma-separated string
"categories": "News, Announcements"
// Array of names
"categories": ["News", "Announcements"]
// Array of objects
"categories": [{ "name": "News", "slug": "news" }, { "id": 2 }]
// Mixed
"categories": ["News", { "name": "Announcements", "slug": "announcements" }]Non-existent categories/tags are auto-created.
PUT /typecho-api/v1/post?id={id}Replaces all fields. Same body format as Create.
Response 200: { "message": "Post updated.", "data": { ... } }
PATCH /typecho-api/v1/post?id={id}Updates only the provided fields; all others are preserved.
Example — change only status:
curl -X PATCH https://your-site.com/typecho-api/v1/post?id=12 \
-H "X-Typecho-Token: my-secret-token" \
-H "Content-Type: application/json" \
-d '{ "status": "publish" }'Response 200: { "message": "Post updated.", "data": { ... } }
DELETE /typecho-api/v1/post?id={id}Hard-deletes the post and cascades to relationships, comments, and custom fields.
Response 200:
{
"message": "Post deleted.",
"data": { "id": 12 }
}POST /typecho-api/v1/upload
Content-Type: multipart/form-dataForm Fields:
| Field | Type | Required | Description |
|---|---|---|---|
image | file | yes | Image file |
destination | string | no | local (default), bunny, r2, or both |
Allowed types: JPEG, PNG, GIF, WebP, SVG, BMP, AVIF
Max size: 10 MB
Example:
curl -X POST https://your-site.com/typecho-api/v1/upload \
-H "X-Typecho-Token: my-secret-token" \
-F "image=@photo.jpg" \
-F "destination=r2"Response 200:
{
"message": "Image uploaded.",
"data": {
"url": "https://cdn.example.com/2026/03/photo-a1b2c3d4.jpg",
"path": "/usr/uploads/2026/03/photo-a1b2c3d4.jpg",
"filename": "photo-a1b2c3d4.jpg",
"size": 45678,
"mimeType": "image/jpeg",
"destination": "r2",
"embed": {
"markdown": "",
"html": "<img src=\"https://cdn.example.com/2026/03/photo-a1b2c3d4.jpg\" alt=\"\" />"
},
"urls": {
"r2": "https://r2cdn.example.com/2026/03/14/photo.jpg",
"bunny": null
}
}
}Storage destinations:
| Value | Behavior |
|---|---|
local | Saves to Typecho's local /usr/uploads/ directory |
bunny | Uploads to BunnyCDN storage zone |
r2 | Uploads to Cloudflare R2 bucket |
both | Uploads to R2 and BunnyCDN concurrently; returns BunnyCDN URL as primary |
POST /typecho-api/v1/agreeIncrements the agree counter on a random published post. Has a 10% chance of skipping (no-op).
Response 200:
{
"data": {
"updated": true,
"postId": 42,
"agree": 7,
"skipped": false
}
}All errors follow a consistent format:
{
"error": {
"code": 401,
"message": "Authentication failed."
}
}| Code | Message | When |
|---|---|---|
| 400 | Query parameter 'id' is required. | Missing ?id= on single-post endpoints |
| 400 | Request body must be valid JSON. | Malformed JSON body |
| 400 | No image uploaded or upload error | Missing file in upload request |
| 401 | Authentication failed. | Invalid credentials |
| 403 | The authenticated user role is not allowed for API access. | User role not in allowedRoles |
| 403 | This IP address is not allowed. | IP not in allow-list |
| 404 | Post not found. | Post ID doesn't exist |
| 405 | Method not allowed. | Wrong HTTP method for endpoint |
| 422 | Field 'title' is required. | Missing required field |
| 422 | Unsupported image type: {type} | File MIME not in allowed list |
| 422 | Image exceeds maximum size of 10 MB. | Upload too large |
| 422 | Invalid destination. | Unknown storage destination |
| 422 | Invalid post status. | Status not in allowed list |
| 500 | Post was created but could not be reloaded. | DB write succeeded, read failed |
| 500 | Failed to create upload directory. | Server filesystem error |
| 500 | Failed to save uploaded file. | move_uploaded_file() failed |
| 502 | Cloud upload failed: {reason} | R2/BunnyCDN upload error |
Configure in Typecho admin panel under Plugins → TypechoApi → Settings.
| Setting | Type | Default | Description |
|---|---|---|---|
authMode | radio | both | basic, bearer, or both |
allowedRoles | text | administrator,editor | Comma-separated allowed user roles |
bearerTokens | textarea | — | label:secret pairs, one per line |
allowedIps | textarea | — | IPv4/IPv6 addresses, one per line |
defaultAuthorId | text | 1 | Default author for bearer token requests |
| Setting | Type | Default | Description |
|---|---|---|---|
storageDestination | radio | local | Default upload destination |
bunnyAccessKey | text | — | BunnyCDN API password |
bunnyStorageZone | text | — | BunnyCDN storage zone name |
bunnyStorageHost | text | https://syd.storage.bunnycdn.com | BunnyCDN region endpoint |
bunnyPublicHost | text | — | BunnyCDN public URL prefix |
r2AccessKeyId | text | — | Cloudflare R2 access key ID |
r2AccessKeySecret | text | — | Cloudflare R2 secret key |
r2Bucket | text | — | R2 bucket name |
r2Endpoint | text | — | R2 S3-compatible endpoint |
r2PublicUrl | text | — | R2 public URL prefix |
r2RootFolder | text | — | Optional path prefix inside bucket |
| Action | Method | Endpoint | Auth Required |
|---|---|---|---|
| List posts | GET | /typecho-api/v1/posts | Yes |
| Get post | GET | /typecho-api/v1/post?id=N | Yes |
| Create post | POST | /typecho-api/v1/posts | Yes |
| Replace post | PUT | /typecho-api/v1/post?id=N | Yes |
| Partial update | PATCH | /typecho-api/v1/post?id=N | Yes |
| Delete post | DELETE | /typecho-api/v1/post?id=N | Yes |
| Upload image | POST | /typecho-api/v1/upload | Yes |
| Increment agree | POST | /typecho-api/v1/agree | Yes |