# Create or upsert post

> Create a draft or published post, or upsert by slug with an account API key.

HTML version: https://blogizi.com/docs/api-create-post
Markdown version: https://blogizi.com/docs/api-create-post.md

Primary write endpoint for the CLI and Obsidian plugin. Project is selected via `X-Blogizi-Project`, body fields, or automatically when the account has exactly one project. See [Authentication](/docs/api-authentication).

## Request

```http
POST /api/posts
Authorization: Bearer YOUR_ACCOUNT_API_KEY
X-Blogizi-Project: your-project-slug
Content-Type: application/json
```

```json
{
  "title": "How we built our auth flow",
  "slug": "how-we-built-our-auth-flow",
  "description": "≤160 char meta description",
  "keyword": "indie app authentication",
  "content": "Markdown body…",
  "status": "draft",
  "scheduledAt": "2026-10-26T09:00",
  "upsert": false
}
```

## Fields

- `title`, `slug` — required
- `description`, `keyword`, `content` — optional but recommended for SEO
- `coverImageUrl` — optional public cover image URL (also accepted as frontmatter `coverImageUrl` / `coverImage`). On upsert, omit to keep the existing cover. Upload bytes with [Upload post image](/docs/api-upload-image) first if needed.
- `ctaTitle`, `ctaSubtitle`, `ctaButtonLabel`, `ctaButtonUrl` — optional per-post CTA override (same rules as [Write in Markdown](/docs/markdown-frontmatter)); on upsert, omit a field to keep the existing value
- `sidebarCtaTitle`, `sidebarCtaText`, `sidebarCtaBullets` (string array), `sidebarCtaButtonLabel`, `sidebarCtaButtonUrl` — optional per-post sidebar CTA override (title, button label, and URL required together); omit to keep existing values on update
- `status` — `draft` or `published` (`published` publishes immediately and cancels any schedule)
- `scheduledAt` — optional ISO datetime (or naive local interpreted in the project timezone). Future → draft + schedule; `null` clears a schedule; omit leaves it unchanged on upsert. Response includes `scheduledAt` (UTC ISO); internal queue ids are never returned
- `upsert` — when `true`, update an existing post with the same slug (Obsidian plugin pattern). Default create fails if the slug already exists.
- `projectSlug` / `project` / `projectId` — optional if the header or sole-project rule already selects a blog

## Inline images in `content`

Use standard Markdown after uploading a content image:

```markdown
![Architecture diagram](https://media.blogizi.com/content/<postId>/<timestamp>.png)
```

Flow: create/upsert the post → [upload](/docs/api-upload-image) with `type=content` → upsert again with the markdown snippet in `content`.

## Response

Returns the post payload plus `{ project: { id, slug }, created: boolean }`. Status is `201` on create and `200` on upsert update. When scheduled, `data.status` stays `draft` and `data.scheduledAt` is a UTC ISO string.

```json
{
  "data": {
    "slug": "how-we-built-our-auth-flow",
    "status": "draft",
    "scheduledAt": "2026-10-26T13:00:00.000Z"
  },
  "project": { "id": "665f…", "slug": "my-app" },
  "created": true
}
```

> Prefer [CLI publishing](/docs/cli-publishing) for local markdown files. Use this endpoint when building your own client or when the Obsidian plugin upserts by slug.
