API Documentation
This API gives you programmatic access to your own blogs, nothing more. Every request is authenticated, and every response is scoped to the account that made it. There is no way to list or pull another user's blogs through this API, public or private. Content lives on True Blogger API; this API is for pulling your own writing into a portfolio site or tool you control, not for mirroring the platform.
Base URL: https://true-blogger-api.app. Every path below is relative to it.
Authentication
Every endpoint requires an API key, sent as Authorization: Bearer <key>. There is no unauthenticated tier: a request with no key, or an invalid or revoked one, always gets a 401.
Endpoints tagged Professional require a paid Professional account. account_manager and owner accounts have Professional-level API access automatically, on top of their moderation permissions.
Professional is $25/year, or $3/month. It unlocks creating, editing, and deleting blogs through the API (reading is always free, on every account), plus a "Pro" badge shown anywhere your name appears publicly. Manage or start a subscription from Account → Settings.
Generate a key from Account → API Keys. It's shown once, at creation time, so save it somewhere safe. Revoke it any time from the same page if it's ever exposed.
Quick Start
Pick whatever you're building with. Every tab below pulls the same data (GET /api/v1/blogs), wired into that language or framework's own idioms. The full endpoint reference with every method and every OS/language combination is further down.
import { useEffect, useState } from "react";
// Client-side React: the API key ships in the browser bundle and is
// visible in the network tab. Fine for a personal project only you'll
// open, but for anything public-facing, proxy the request through your
// own backend instead (see the Next.js tab) so the key never reaches
// the browser.
function useMyBlogs(apiKey) {
const [blogs, setBlogs] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("https://true-blogger-api.app/api/v1/blogs", {
headers: { Authorization: `Bearer ${apiKey}` },
})
.then((res) => res.json())
.then((data) => setBlogs(data.blogs))
.finally(() => setLoading(false));
}, [apiKey]);
return { blogs, loading };
}
export default function MyBlogList() {
const { blogs, loading } = useMyBlogs(import.meta.env.VITE_TB_API_KEY);
if (loading) return <p>Loading...</p>;
return (
<ul>
{blogs.map((blog) => (
<li key={blog._id}>
<a href={`https://true-blogger-api.app/blogs/${blog.slug}`}>{blog.title}</a>
</li>
))}
</ul>
);
}Endpoints
The whole API surface, at a glance. Click a path to jump to its full example. Switch the language tab in any request below and it applies to every endpoint on this page, not just the one you clicked.
| Method | Path | Access | Description |
|---|---|---|---|
| GET | /api/v1/me | Auth required | Your blogs + engagement data, in one call. |
| GET | /api/v1/blogs | Auth required | Your blogs as their own resource, with full content. |
| GET | /api/v1/blogs/:slug | Auth required | A single blog of yours. |
| POST | /api/v1/blogs | Professional | Create a blog. |
| PATCH | /api/v1/blogs/:slug | Professional | Update any field on one of your blogs. |
| DELETE | /api/v1/blogs/:slug | Professional | Delete a blog (restorable from your dashboard for 24 hours). |
/api/v1/meAuth requiredEvery blog you own (public and private both), including its view count, like/dislike counts, and how many comments it has. Comments, likes, and views are separate, relationally linked collections in the database (each references the blog they belong to); this endpoint joins that data onto your own blogs. It never includes anyone else's content.
Request
curl "https://true-blogger-api.app/api/v1/me" \
-H "Authorization: Bearer lba_your_key_here"Response
{
"user": { "firstName": "Jordan", "lastName": "Blake", "username": "jordanb" },
"blogs": [
{
"_id": "670f...",
"title": "Getting Started with JavaScript Closures",
"slug": "getting-started-with-javascript-closures",
"excerpt": "A closure is a function that remembers the variables from where it was created.",
"visibility": "public",
"tags": ["javascript", "programming"],
"viewCount": 15,
"likeCount": 4,
"dislikeCount": 0,
"commentCount": 2,
"publishedAt": "2026-08-27T04:35:04.315Z"
}
]
}
// Missing or revoked key:
// 401 { "error": "Invalid or missing API key" }/api/v1/blogsAuth requiredThe same blog list as /api/v1/me, as its own resource endpoint, plus content and coverImageUrl on each blog. Also available at GET /api/v1/blogs/:slug for a single blog.
No pagination. Every one of your blogs comes back in a single response, sorted newest first. Fine at personal-project scale; if you have hundreds of posts, filter client-side or ask before relying on this staying unpaginated.
Request
curl "https://true-blogger-api.app/api/v1/blogs" \
-H "Authorization: Bearer lba_your_key_here"/api/v1/blogsProfessionalCreate a blog under your account. Same fields and same content rules (profanity filter, image URL allowlist) as writing from the site itself.
Body
| Field | Type | Required | Notes |
|---|---|---|---|
title | string | Required | Must be non-empty after trimming. Up to 140 characters. |
content | object | Required | A Tiptap JSON document ({ type: "doc", content: [...] }). Only a limited set of node/mark types are allowed -- paragraphs, line breaks, lists, images, code blocks, blockquotes, horizontal rules, and bold/italic/strike/code marks. Headings and links are not allowed. |
excerpt | string | Optional | Plain text summary shown on blog cards. Up to 200 characters. |
coverImageUrl | string | Optional | Must be an http(s) URL, or an uploaded image from /api/uploads/:id. |
coverFocus | object | Optional | Where the cover image is centered and how far it is zoomed: { x: 0-100, y: 0-100, zoom: 1-3 }. x and y are percentages across and down the image; defaults to { x: 50, y: 50, zoom: 1 } (a plain center crop). Out-of-range values are clamped. Changing coverImageUrl without sending coverFocus resets it. To display it yourself, use object-position: x% y% and, when zoom is above 1, transform: scale(zoom) with the same transform-origin, inside a container with overflow hidden. |
tags | string[] | Optional | Up to 4 tags, each up to 32 characters. Lowercased and de-duplicated automatically. |
visibility | string | Optional | One of "public" or "private". Defaults to "private" if omitted. |
commentsEnabled | boolean | Optional | true lets readers comment on the post, false turns comments off. Defaults to true if omitted. |
Request
curl -X POST "https://true-blogger-api.app/api/v1/blogs" \
-H "Authorization: Bearer lba_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"title": "Posted from the API",
"excerpt": "A short summary.",
"content": {
"type": "doc",
"content": [
{
"type": "paragraph"
}
]
},
"tags": [
"javascript"
],
"visibility": "public"
}'Response
201 { "blog": { "_id": "...", "slug": "posted-from-the-api", ... } }
// Free tier:
// 403 { "error": "Creating blogs through the API is a Professional feature" }/api/v1/blogs/:slugProfessionalUpdate any field on one of your own blogs: title, excerpt, content, tags, cover image, or visibility. Flipping visibility to public stamps a fresh publishedAt at that moment, every time. Send only the fields you want to change.
Body
| Field | Type | Required | Notes |
|---|---|---|---|
title | string | Optional | Must be non-empty after trimming. Up to 140 characters. Omit to leave unchanged. |
content | object | Optional | [object Object] Omit to leave unchanged. |
excerpt | string | Optional | Plain text summary shown on blog cards. Up to 200 characters. |
coverImageUrl | string | Optional | Must be an http(s) URL, or an uploaded image from /api/uploads/:id. |
coverFocus | object | Optional | Where the cover image is centered and how far it is zoomed: { x: 0-100, y: 0-100, zoom: 1-3 }. x and y are percentages across and down the image; defaults to { x: 50, y: 50, zoom: 1 } (a plain center crop). Out-of-range values are clamped. Changing coverImageUrl without sending coverFocus resets it. To display it yourself, use object-position: x% y% and, when zoom is above 1, transform: scale(zoom) with the same transform-origin, inside a container with overflow hidden. |
tags | string[] | Optional | Up to 4 tags, each up to 32 characters. Lowercased and de-duplicated automatically. |
visibility | string | Optional | One of "public" or "private". Flipping a private post to public always stamps a fresh publishedAt at that moment. |
commentsEnabled | boolean | Optional | true or false. Omit to leave unchanged. |
Request
curl -X PATCH "https://true-blogger-api.app/api/v1/blogs/posted-from-the-api" \
-H "Authorization: Bearer lba_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"visibility": "private"
}'/api/v1/blogs/:slugProfessionalDeletes one of your own blogs. It isn't gone right away: it moves to Recently Deleted on your dashboard for 24 hours, so a script that deletes the wrong posts can be undone. Restoring is done from the dashboard, not the API. The response includes recoverableUntil. While it's there, its URL stays reserved; after 24 hours it's permanently deleted along with its comments and reactions.
Request
curl -X DELETE "https://true-blogger-api.app/api/v1/blogs/posted-from-the-api" \
-H "Authorization: Bearer lba_your_key_here"Errors
Every error response is { "error": "message" } with a matching HTTP status: 401 for missing/invalid/revoked keys, 403 for a write attempt on a free-tier key or a suspended account, 404 for a path or blog that doesn't exist, 429 if you're sending requests too fast (see Rate Limits below).
Getting a CORS error in the browser? Every /api/v1 response allows requests from any website, so a CORS error almost always means the address itself is slightly off and got redirected. Log the exact URL you're fetching and check for a double slash (/api/v1//blogs, usually from writing ${API}/blogs when API already ends in /), http:// instead of https://, or www. in front. Use exactly https://true-blogger-api.app/api/v1/ plus the endpoint path.
Rate Limits
120 requests per minute per IP address, and 60 requests per minute per API key, across all /api/v1/* endpoints. Going over either returns 429 with a Retry-After header (seconds until the window resets). These numbers may change as real usage patterns emerge. A burst here and there is fine, sustained hammering isn't. The 60-per-key limit still applies per student, but the shared-IP limit is raised automatically for accounts registered with a recognized school/organization email domain, so a whole classroom on the same network doesn't compete for one public IP's budget.
Writing is also capped per account, counted across the site and the API together: creating blogs at 4 per hour and 30 per day (the day resets at midnight UTC), and editing blogs at 30 per hour. Only writes that actually save count, so a request rejected for a missing title doesn't use one up. Going over returns 429 with a Retry-After header and a message saying how long to wait.
Roadmap
Writing through the API (creating, editing, deleting your own blogs) is now live for Professional accounts. HTTP security headers and secret rotation are still on the to-do list.
Still have questions?
FAQ's