Xyzee Developers
Blog management API
Give your AI agent the same editorial controls as an admin: create, publish, update, archive, upload media, manage SEO, and moderate comments.
Endpoints
| Method and path | Purpose |
|---|---|
GET /api/blog | List/filter posts |
POST /api/blog | Create a post, optionally publish |
GET /api/blog/{id} | Read a post and engagement counts |
PUT, PATCH /api/blog/{id} | Edit any post field or status |
DELETE /api/blog/{id} | Permanently delete a post |
GET /api/blog/slug/{slug} | Read by exact slug, including drafts with a key |
GET, POST /api/blog/media | List or upload media |
PATCH /api/blog/media/{id} | Edit image title or alt text |
DELETE /api/blog/media/{id} | Delete unused registered blog media |
GET, POST /api/blog/comments | List or create comments/replies |
PUT, PATCH, DELETE /api/blog/comments/{id} | Edit or delete comments and replies |
POST /api/blog/generate | Generate content, metadata, or images |
GET /api/blog/openapi | Public machine-readable OpenAPI 3.1 reference |
Every editor field
| Field | Type | Rules |
|---|---|---|
title | string, required on create | 1–200 characters |
slug | string, required on create | 1–200; lowercase letters/digits separated by hyphens; unique |
content | string, required on create | Markdown, 1–100000 characters |
category | string, required on create | 1–100 characters; custom categories allowed |
featuredImageAlt | string, optional | Descriptive cover alt text, up to 500 characters |
excerpt | string | Up to 500 characters; empty string clears |
featuredImage | string | HTTP(S) image URL or empty string to clear; multipart file overrides |
tags | string[] | Up to 100 tags, 1–100 characters each; default [] |
status | DRAFT | PUBLISHED | ARCHIVED | Default DRAFT |
isFeatured | boolean | Default false |
metaTitle | string | Up to 70 characters; empty string clears |
metaDescription | string | Up to 160 characters; empty string clears |
file | multipart binary | Optional cover image, up to 10MB; upload instead of featuredImage URL |
Create a key
Sign in with a verified admin account and open Dashboard → Admin → Blog → Blog API keys. Name the key, optionally set an expiry, and choose Create key. Copy the complete key once into your agent’s secret settings. The list shows only its prefix, expiry, status, and last use. Revoke a key there at any time. To rotate, create a replacement, switch your agent, then revoke the old key.
A key grants full blog management: every editable post field, all posts regardless of author or status, uploaded media, comment moderation, and the built-in AI generator. It does not grant access to users, projects, settings, or key creation. Keys stop working if revoked, expired, or their issuing admin is deleted, loses the ADMIN role or email verification, or is actively banned. Posts and comments created by an agent are attributed to the issuing admin.
Authenticate
Use your website’s canonical HTTPS origin and send Authorization: Bearer <key> on every management request. Do not put keys in URLs, browser code, blog content, or source control. There is no cookie login or CSRF token to obtain for agents. Browser admins continue to use their existing session. Key management itself requires an admin session.
export XYZEE_BASE_URL="https://www.xyzee.dev"
# Set XYZEE_BLOG_API_KEY in your environment using your secret manager.
curl --fail-with-body "$XYZEE_BASE_URL/api/blog?limit=50&page=1" \
-H "Authorization: Bearer $XYZEE_BLOG_API_KEY"
With a valid key, listing includes drafts, published posts, and archived posts. Without a key, the same listing returns only published posts; ?admin=true requires an admin. An invalid supplied key always returns 401, including on public read endpoints.
Create a complete post
Save this as post.json. content is a Markdown string, including embedded image links and any supported media references. Do not send an editor document object. Content is limited to 100,000 characters.
{
"title": "Building reliable APIs",
"slug": "building-reliable-apis",
"excerpt": "A practical guide to reliable API design.",
"content": "# Building reliable APIs\n\nStart with a clear contract.\n\n",
"featuredImage": "https://example.com/cover.png",
"category": "Development",
"tags": ["API", "Engineering"],
"status": "DRAFT",
"isFeatured": false,
"metaTitle": "Building reliable APIs | Xyzee",
"metaDescription": "Learn how to design reliable APIs with clear contracts and validation."
}
curl --fail-with-body "$XYZEE_BASE_URL/api/blog" \
-H "Authorization: Bearer $XYZEE_BLOG_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @post.json
A 201 response contains { "success": true, "message": "...", "post": { ... } } and a Location header. Save post.id for updates and post.slug for its public URL. A draft is not public. Use status: "PUBLISHED" when publishing is intended; publishing sets publishedAt on the first transition to published. Republishing preserves that original date. ARCHIVED removes a post from public listings and pages without deleting it.
Upload media and a cover
Upload up to five files totaling 50MB per request, as multipart file fields:
curl --fail-with-body "$XYZEE_BASE_URL/api/blog/media" \
-H "Authorization: Bearer $XYZEE_BLOG_API_KEY" \
-F "files=@cover.png" -F "files=@diagram.png"
The response is { "files": [{ "id": "...", "storageId": "...", "url": "...", "name": "...", "size": 123, "type": "image/png", "bucketId": "...", "createdAt": "..." }] }. Use the returned URL for featuredImage or links inside content. Uploaded blog media is publicly readable even if its post is a draft. Keep confidential material out of blog uploads.
Supported types: JPEG, PNG, GIF, WebP, AVIF; PDF; MP4, WebM, QuickTime/MOV, AVI, MKV, WMV, FLV, 3GPP; MP3/MPEG, WAV, AAC, OGG, WebM audio, FLAC, M4A. SVG, HTML, and executable files are not accepted. The OpenAPI document lists the exact MIME types. Your host or reverse proxy may impose a smaller upload limit.
Alternatively, attach one cover file directly while creating or editing a post. Covers are limited to 10MB and the supported image types. A file overrides featuredImage in the same request. Multipart tags is a JSON array string (comma-separated tags also work); isFeatured must be exactly true or false.
curl --fail-with-body "$XYZEE_BASE_URL/api/blog" \
-H "Authorization: Bearer $XYZEE_BLOG_API_KEY" \
-F "title=Building reliable APIs" \
-F "slug=building-reliable-apis" \
-F "category=Development" \
-F "content=<article.md" \
-F 'tags=["API","Engineering"]' \
-F "status=DRAFT" -F "isFeatured=false" -F "file=@cover.png"
Replacing or clearing a cover detaches its URL; it does not delete storage objects that another post may use. List registered media with GET /api/blog/media?page=1&limit=50. Delete an unused asset by its returned id, not storageId, using DELETE /api/blog/media/{id}. A referenced asset returns 409: detach it from every cover and content body first. Files uploaded before this API or through other upload endpoints are not registered as blog assets and cannot be deleted through this endpoint. External image URLs are never deleted.
Edit, publish, unpublish, archive, and delete
PATCH and PUT /api/blog/{id} both apply partial updates. Omitted fields stay unchanged. Clear excerpt, featuredImage, metaTitle, or metaDescription with ""; clear tags with []. Send actual JSON booleans, not string booleans. Unknown fields, null values, and empty updates are rejected. All post fields above are supported in JSON and multipart requests; binary file input is multipart only.
curl --fail-with-body -X PATCH "$XYZEE_BASE_URL/api/blog/$POST_ID" \
-H "Authorization: Bearer $XYZEE_BLOG_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status":"PUBLISHED","isFeatured":true}'
curl --fail-with-body "$XYZEE_BASE_URL/api/blog/$POST_ID" \
-H "Authorization: Bearer $XYZEE_BLOG_API_KEY"
Check the returned status and then visit /blog/{slug} to verify publication. Use status: "DRAFT" to unpublish or status: "ARCHIVED" to archive. To change the slug, PATCH slug; the old URL will no longer resolve. To permanently delete a post, send DELETE /api/blog/{id}. Deletion also removes its engagement records and comments; registered media remains available for explicit cleanup. Successful updates return { success, message, post }; successful deletes return { success, message }.
Author ID, IDs, timestamps, view/share counts, likes, and comment counts are server-managed fields, not editable post fields. Read them from the API; do not include them in a create/update body.
Find posts and paginate
Read by ID at GET /api/blog/{id}, or by slug at GET /api/blog/slug/{slug}. Both return the post directly, including draft content for authorized admins and agents. The ID route includes numeric likes and comments counts. Anonymous callers receive 404 for non-published posts.
Authenticated listing returns a JSON array, with X-Total-Count, X-Page, and X-Has-More headers. Tokens default to 50 results per page; set page (1–100000) and limit (1–100). Continue while X-Has-More is true. Filters: status=DRAFT|PUBLISHED|ARCHIVED, exact category, featured=true|false, and case-insensitive search across title/content. Use the slug read endpoint for exact retry checks.
A slug must be unique. A conflicting create or slug update returns 409. After a timeout or uncertain create response, read the intended slug before retrying to avoid duplicate publication. Do not automatically overwrite an existing post just because it has the same slug. Requests do not currently support an idempotency-key header or optimistic concurrency; coordinate agents editing the same post, and fetch current content before applying a patch.
Moderate comments
These management endpoints require a key or verified admin session:
GET /api/blog/comments?postId={id}&page=1&limit=50returns{ comments, total, page, limit }. Omit postId to list all comments. Results are flat and include replies with theirparentId.POST /api/blog/commentsaccepts{ "postId": "...", "content": "...", "parentId": "..." }. Omit parentId for a top-level comment. Replies must belong to the same post. Content is 1–2000 characters. Returns{ success, comment }with 201.PATCHorPUT /api/blog/comments/{id}accepts{ "content": "..." }and can edit any comment. Returns{ success, comment }.DELETE /api/blog/comments/{id}deletes that comment and its entire reply tree. Returns{ success: true }.
Use built-in AI generation
POST /api/blog/generate provides the same generation actions as the admin UI. This uses Xyzee’s configured Gemini/OpenRouter credentials and can incur provider charges. It generates content, metadata, or an image; it does not create or publish a post.
{
"action": "content",
"provider": "gemini",
"model": "gemini-2.5-flash",
"title": "Building reliable APIs",
"prompt": "Write a practical tutorial with examples.",
"category": "Development",
"tags": ["API"]
}
Actions: content (default), metadata, image. Supply a title or prompt. Optional fields: provider, model, imageModel, category, tags, and existing content. For metadata, send the article content. For image generation, set imageModel and a matching provider. Use the model enums and input limits in the OpenAPI document. Responses contain content and provider, metadata and provider, or image and provider, respectively. Save generated images through the media upload API and pass their URL to a post.
Errors and deployment
Errors use { "error": "...", "details": ... } (details is optional). Handle 400 invalid input, 401 missing/invalid/expired/revoked key, 403 insufficient permission or blocked browser origin, 404 missing resource, 409 unique slug conflict or referenced media, 413 oversized upload, 415 unsupported request encoding, 500 unexpected failure, and 503 unconfigured storage. Correct 4xx requests before retrying. For uncertain writes, read current state before retrying. Responses containing credentials and blog management data use Cache-Control: no-store.
Before deploying this feature, run npx prisma generate and npx prisma db push with the deployment’s existing MongoDB connection. This adds BlogApiKey and BlogAsset collections and unique indexes; Prisma 6 uses db push for MongoDB. Do not use a reset or accept-data-loss flag. Existing posts need no conversion. Existing server Appwrite endpoint, project, bucket, and API key settings are reused. The optional generator continues to use the existing AI provider configuration. No blog API token belongs in a public environment variable.
The runnable regression suite is npm run test:blog-api. The reference at /api/blog/openapi derives post and generation input schemas from the same validators used by the handlers.
