# The LotsBlog REST API: Publish From Any Agent or Script

Source: https://blog.lots.blog/lotsblog-rest-api
Published: 2026-10-19T03:30:40.817772+00:00

The LotsBlog REST API lets any script, cloud agent or automation tool create, update, schedule and publish posts on your LotsBlog blog with one API key. Create the key at [lotstech.com/api](https://www.lotstech.com/api), send it in an `X-API-Key` header to `https://api.lots.blog/api/v1/lotsblog`, and you get the same 37 operations my AI agent uses through MCP.

If your agent supports remote MCP (Claude, ChatGPT, Codex, Cursor), connect the MCP server instead; it signs in with OAuth and needs no key. This guide is for everything else: CI jobs, cron scripts, n8n, Make or Zapier, and agents you build yourself. Every example below is copy-ready, checked against the [LotsBlog API docs](https://api.lots.blog/docs.md) on 18 October 2026.

## REST or MCP: which one to use

Both reach the same blog with the same permissions. The difference is who makes the call.

| You're working from | Use | How it signs in |
|---|---|---|
| Claude Code, Claude, ChatGPT, Codex, Cursor or another agent with remote MCP | MCP at `https://api.lots.blog/mcp` | OAuth in your browser, no key ([Claude Code guide](/publish-blog-posts-from-claude-code)) |
| A cloud agent that can only make HTTP calls | REST | API key |
| A GitHub Action, CI job or cron script | REST | API key stored as a secret |
| n8n, Make, Zapier or another workflow tool | REST, through its HTTP step | API key in a header |
| An agent you build with your own framework (your tools are HTTP functions) | REST, with `docs.md` as the tool reference | API key |
| Your own app or CMS integration | REST | API key |

The REST API isn't a cut-down version. The docs list 37 endpoints, matching the 37 tools in LotsBlog's published tool list: blogs, posts, topics, images, domains and `/blog` subdirectory hosting, analytics, team members, funding status and the optional quality check. If you've read the [list of blogging platforms with MCP support](/best-blogging-platforms-with-mcp-support), you know several gate their API or MCP to higher plans. LotsBlog includes both on every plan and on pay as you go.

## The basics

| Item | Value |
|---|---|
| Base URL | `https://api.lots.blog/api/v1/lotsblog` |
| Auth | `X-API-Key: YOUR_API_KEY`, or `Authorization: Bearer YOUR_API_KEY` |
| Default rate limit | 100 requests a minute per key |
| Lower limits on some writes | create, update, publish, schedule: 30 a minute; trash and restore: 20; upload from URL and quality check: 10 |
| Rate-limit headers | `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` on every response |
| Format | JSON in, JSON out: `{"success": true, "data": {...}, "meta": {...}}` |
| Full reference | `https://api.lots.blog/docs.md` (Markdown, written so an agent can read it) |

## Step 1: Create an API key

1. Open [lotstech.com/api](https://www.lotstech.com/api) and click **Manage API keys**.
2. Sign in with your Lots account.
3. Choose **Lots.Blog**, name the key (for example "ci-publisher") and create it.
4. Copy the key now. It's shown once.

One account holds the keys for every Lots app, and you can revoke a key from the same page. A key acts as you inside LotsBlog, so you need to have signed in to LotsBlog once before you create it. Keys can also be created at `https://api.lots.blog/dashboard`.

Never paste the key into code or a committed file. Put it in an environment variable or your CI's secret store. The examples below read it from one:

```bash
export LOTSBLOG_API_KEY="paste-your-key-here"   # better: load it from your secret manager
export LB="https://api.lots.blog/api/v1/lotsblog"
```

## Step 2: Find your blog and topics

```bash
curl -s -H "X-API-Key: $LOTSBLOG_API_KEY" "$LB/blogs"
```

The response lists every blog you own or belong to, with your role and its public URL. Save the blog's `id`:

```bash
export BLOG_ID="your-blog-uuid"
curl -s -H "X-API-Key: $LOTSBLOG_API_KEY" "$LB/blogs/$BLOG_ID/topics"
```

Topics come back with numeric IDs; you pass those as `topic_ids`. `GET $LB/blogs/$BLOG_ID` also returns the blog's private `blog_guide`, so an agent working over REST can read your voice and rules before it writes.

## Step 3: Create a draft with Markdown, a search title and FAQ schema

Put the post in a JSON file so the Markdown and schema stay readable:

```json
{
  "post_type": "article",
  "status": "draft",
  "title": "How We Back Up Postgres to S3 Every Night",
  "slug": "postgres-backup-to-s3",
  "content": "Our nightly backup runs `pg_dump`, compresses the file and copies it to S3.\n\n## The script\n\n...\n\n## Common questions\n\n### How long do you keep backups?\n\nThirty days, then S3 lifecycle rules delete them.",
  "description": "The nightly Postgres backup we run, with the script.",
  "meta_title": "Postgres Backup to S3: Our Nightly Script",
  "meta_description": "The nightly pg_dump to S3 backup we run, with the full script, retention rules and how we test a restore.",
  "meta_keywords": ["postgres backup", "pg_dump s3"],
  "topic_ids": [12],
  "structured_data": {
    "@context": "https://schema.org",
    "@type": "FAQPage",
    "mainEntity": [
      {
        "@type": "Question",
        "name": "How long do you keep backups?",
        "acceptedAnswer": {
          "@type": "Answer",
          "text": "Thirty days, then S3 lifecycle rules delete them."
        }
      }
    ]
  }
}
```

Then send it:

```bash
curl -s -X POST "$LB/blogs/$BLOG_ID/posts" \
  -H "X-API-Key: $LOTSBLOG_API_KEY" \
  -H "Content-Type: application/json" \
  --data @post.json
```

What each field does:

- **`status`** defaults to `draft`. A draft is private; the response includes a `preview_url` that asks the reader to sign in with an account on the blog.
- **`meta_title`** becomes the page `<title>`, `og:title` and `twitter:title` exactly as written. The visible headline stays `title`. Keep it to about 60 characters.
- **`meta_description`** is the search snippet. Keep it under 160 characters.
- **`structured_data`** takes supplemental JSON-LD such as FAQPage. LotsBlog already generates Article and BreadcrumbList schema, so don't send those. The FAQ must match questions visible on the page; I explain why in [Article, Breadcrumb and FAQ schema for blog posts](/blog-post-schema-article-breadcrumb-faq).

Save the post `id` from the response:

```bash
export POST_ID="the-new-post-uuid"
```

## Step 4: Upload a cover image

A local image goes up in three calls, so the bytes never pass through the API as base64:

```bash
FILE=cover.webp
SIZE=$(wc -c < "$FILE" | tr -d ' ')

# 1. Ask for an upload URL (valid 15 minutes)
curl -s -X POST "$LB/blogs/$BLOG_ID/images/upload" \
  -H "X-API-Key: $LOTSBLOG_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"filename\":\"$FILE\",\"file_size\":$SIZE,\"mime_type\":\"image/webp\"}"
```

The response carries `upload_url`, the `headers` to send (just `Content-Type`), a `file_key` and the future `public_url`.

```bash
# 2. PUT the bytes to upload_url with only the returned Content-Type header
curl -s -X PUT -H "Content-Type: image/webp" --data-binary @"$FILE" "$UPLOAD_URL"

# 3. Confirm it: LotsBlog checks the size and the file's real format
curl -s -X POST "$LB/blogs/$BLOG_ID/images/complete" \
  -H "X-API-Key: $LOTSBLOG_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"file_key\":\"$FILE_KEY\",\"file_size\":$SIZE}"
```

The last call returns the public CDN `url`. JPEG, PNG, WebP and GIF are accepted, up to 25 MB. If the image already sits at a public URL, skip all three and call `POST $LB/blogs/$BLOG_ID/images` with `image_url`. Uploaded images are public even on a private blog.

## Step 5: Update the post

`PATCH` changes only the fields you send. Everything else stays as it was:

```bash
curl -s -X PATCH "$LB/blogs/$BLOG_ID/posts/$POST_ID" \
  -H "X-API-Key: $LOTSBLOG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"featured_image":"https://media.lots.blog/YOUR_IMAGE.webp","meta_description":"A sharper snippet under 160 characters."}'
```

Send `"topic_ids": []` to clear topics, or leave the field out to keep them. Update the same `post_id` instead of creating a second post; slugs are unique within a blog, so a retry with the same slug fails rather than duplicating.

## Step 6: List posts

```bash
curl -s -H "X-API-Key: $LOTSBLOG_API_KEY" \
  "$LB/blogs/$BLOG_ID/posts?status=draft&limit=20"
```

Posts come back newest first. Filter by `status` (`draft`, `scheduled`, `published`, `archived`, `trashed`), `topic_id`, and page with `limit` (up to 100) and `offset`. To check one post exactly as saved, `GET $LB/blogs/$BLOG_ID/posts/$POST_ID`. Read a post back before retrying a write you aren't sure went through.

## Step 7: Schedule or publish

Writing a draft never publishes it. Going live is always its own call:

```bash
# Schedule: ISO 8601 with a timezone, at least 5 minutes ahead
curl -s -X POST "$LB/blogs/$BLOG_ID/posts/$POST_ID/schedule" \
  -H "X-API-Key: $LOTSBLOG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"scheduled_for":"2026-10-20T09:00:00+05:30"}'

# Publish now
curl -s -X POST "$LB/blogs/$BLOG_ID/posts/$POST_ID/publish" \
  -H "X-API-Key: $LOTSBLOG_API_KEY"
```

LotsBlog checks for scheduled posts every 15 minutes, so a post goes live on the first check after its time. A `scheduled` response isn't proof it's live; read the post back after the time passes. To pull an article back, `POST .../posts/$POST_ID/unpublish` returns it to a private draft and clears any schedule. `DELETE .../posts/$POST_ID` moves a post to Trash, out of the site, sitemap and RSS, and `POST .../posts/$POST_ID/restore` brings it back with its previous status.

## The same flow in Python

For a script or an agent framework, the whole draft flow is a few lines:

```python
import os, requests

LB = "https://api.lots.blog/api/v1/lotsblog"
H = {"X-API-Key": os.environ["LOTSBLOG_API_KEY"]}
blog_id = os.environ["BLOG_ID"]

post = requests.post(f"{LB}/blogs/{blog_id}/posts", headers=H, json={
    "post_type": "article",
    "title": "Release notes: October",
    "content": open("release-notes.md").read(),
    "meta_description": "What shipped in October and how to use it.",
}).json()

if not post.get("success"):
    raise SystemExit(post["error"])
print(post["data"])  # includes the post id and preview_url; it stays a draft
```

Add the schedule call only when a person has approved the draft, or when the whole pipeline (a release, a changelog) is the approval.

## No-code tools: n8n, Make and Zapier

Any step that sends an HTTP request with a custom header works. I checked each vendor's own docs on 18 October 2026:

- **n8n:** the HTTP Request node supports every method you need (GET, POST, PATCH, PUT, DELETE) and Header auth, so put `X-API-Key` in a Header Auth credential rather than in the node.
- **Make:** the HTTP app's "Make a request" module has an API key authentication type; Make recommends storing the key there instead of typing it into headers.
- **Zapier:** Webhooks by Zapier is available on every plan, Free included. Use POST or PUT for creating drafts, and Custom Request for `PATCH` and `DELETE`.

A pattern that works well: a form, a spreadsheet row or a finished document triggers the workflow, the workflow `POST`s a draft, and a person publishes from the preview link.

## Errors you'll see

| Code | HTTP | What it means | Fix |
|---|---|---|---|
| `AUTHENTICATION_REQUIRED` | 401 | Missing or invalid key | Check the header name and that the key wasn't revoked |
| `VALIDATION_ERROR` | 400 | A field is wrong: a meta description over 160 characters, a slug already used, a schedule under 5 minutes ahead | Read `error.message`; it names the field |
| `RATE_LIMIT_EXCEEDED` | 429 | Over the limit for that endpoint | Wait for `X-RateLimit-Reset`, then retry |
| `ENDPOINT_NOT_FOUND` | 404 | Wrong path | Copy the path from `docs.md` |
| (from storage) | 403 on the image PUT | Extra headers on the upload | Send only the `Content-Type` that `images/upload` returned |

## Host it on your own domain

Everything you publish over REST goes to the blog's address: a lots.blog subdomain, a custom domain like blog.example.com, or a path on your own site. The same API connects the domain (`POST $LB/blogs/$BLOG_ID/domain`, with `path: "/blog"` for a subdirectory) and checks it (`POST .../domain/check`). The full setup is in [How to host your blog at yourdomain.com/blog](/host-blog-at-yourdomain-com-blog).

## What it costs

The REST API and the MCP server are included on every plan and on pay as you go. Pay as you go is $9 per blog a month from credits. Plans are $24 a month for 3 blogs, $69 for 10 and $159 for 30, or 10 months' price for a year. Posts, page views and authors are unlimited, and Pro and Business also include LotsNotes Pro. New accounts get 10,000 free credits, valid 90 days, no card. The optional quality check (`posts/:post_id/quality-check`) uses credits only when you call it. Full details on the [pricing page](https://lots.blog/pricing).

## Start from your agent

Give this to any coding agent and let it build the integration with you:

> Read https://api.lots.blog/docs.md and help me integrate LotsBlog's REST API into my application. Read my API key from the LOTSBLOG_API_KEY environment variable, save posts as drafts, and never publish or schedule without asking me.

## How I checked this guide

I checked every request shape above against `docs.md` and the published tool list on 18 October 2026. Then I ran the same operations through the LotsBlog MCP server, which calls the same tools as the REST endpoints, on a throwaway private draft: create with Markdown, `meta_title`, `meta_description` and FAQPage `structured_data`; a cover upload in three calls; an update that changed two fields and kept the rest; a filtered list; and a move to Trash. Every field came back as sent.

## Common questions

### Do I need an API key to use LotsBlog from an AI agent?

Not if your agent supports remote MCP. Claude Code, Claude, ChatGPT, Codex and Cursor connect to `https://api.lots.blog/mcp` and sign in with OAuth. You need a key only for the REST API: scripts, CI, workflow tools and agents that make plain HTTP calls.

### Where do I create a LotsBlog API key?

At lotstech.com/api: click Manage API keys, sign in, choose Lots.Blog and create the key. It's shown once, so copy it into a secret store straight away. You can revoke it from the same page.

### What are the LotsBlog API rate limits?

100 requests a minute per key by default. Some writes are lower: creating, updating, publishing and scheduling posts allow 30 a minute, and uploading an image from a URL allows 10. Every response carries rate-limit headers.

### Can the REST API do everything the MCP server does?

Yes. The docs list 37 endpoints, one for each tool in LotsBlog's published tool list, from posts and images to domains, team members and analytics.

### Will a script publish my posts by accident?

No. New posts are private drafts unless you set a status or call publish or schedule. Keep publishing as a separate, deliberate step, and store the key as a secret, because a key acts as you.

### Is the REST API on every LotsBlog plan?

Yes. Every plan ($24, $69 or $159 a month) and pay as you go ($9 per blog a month) includes both the REST API and the MCP server.

*LotsBlog details checked on 18 October 2026 from [api.lots.blog/docs.md](https://api.lots.blog/docs.md), [lotstech.com/api](https://www.lotstech.com/api), [lots.blog/skill.md](https://lots.blog/skill.md) and [lots.blog/pricing](https://lots.blog/pricing). n8n, Make and Zapier details checked on 18 October 2026 from [n8n's HTTP Request node docs](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.httprequest/), [Make's HTTP app docs](https://apps.make.com/http) and [Zapier's webhooks help article](https://help.zapier.com/hc/en-us/articles/8496326446989-Send-webhooks-in-Zaps).*
