Writing content

Create, update, publish and unpublish posts, and upload media, with the Writizzy public API.

The write side of the v1 API lets you manage your content from your own tools: publish from a Git repository, run an inbound migration script, or wire Writizzy into a custom editorial pipeline. It stays in the same /v1 contract as the read endpoints, no separate version.

Use a write key

Writing requires a write key. A read key can only read: any create, update, publish or upload request made with a read key returns 403. A write key includes read access, so a single script can fetch a post and then patch it.

Generate the write key from Blog Settings → Developer API, then pass it as a Bearer token like any other request:

POST /v1/blogs/yourblog/posts
Authorization: Bearer wz_yourblog_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

The write flow

A post is always created as a draft. Publishing is a separate, explicit step. A typical flow is:

  1. POST /posts to create the draft, you get back its id and final slug
  2. optionally PATCH /posts/{id} to refine it
  3. POST /posts/{id}/publish to make it visible

The author of any post created through the API is the blog owner.

Endpoints

MethodPathDescription
POST/v1/blogs/{subdomain}/postsCreate a draft
PATCH/v1/blogs/{subdomain}/posts/{id}Update a post (partial)
POST/v1/blogs/{subdomain}/posts/{id}/publishPublish a post
POST/v1/blogs/{subdomain}/posts/{id}/unpublishRevert a post to draft
POST/v1/blogs/{subdomain}/mediaUpload a media file
GET/v1/blogs/{subdomain}/redirectsList redirects
POST/v1/blogs/{subdomain}/redirectsCreate up to 500 redirects at once
DELETE/v1/blogs/{subdomain}/redirects/{id}Delete a redirect
DELETE/v1/blogs/{subdomain}/tags/{slug}Delete a tag and remove it from its posts

Create a draft

title and content are required. slug is optional: when omitted it is generated from the title, and de-duplicated automatically if it collides with an existing post. Tags are created on the fly.

curl -X POST https://api.writizzy.com/v1/blogs/yourblog/posts \
  -H "Authorization: Bearer wz_yourblog_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Getting started with Kotlin",
    "content": "# Hello\n\nThis is my first post.",
    "excerpt": "A short intro to Kotlin.",
    "coverImageUrl": "https://cdn.writizzy.com/yourblog/kotlin.png",
    "tags": ["kotlin", "tutorial"],
    "accessMode": "FREE"
  }'
FieldRequiredDescription
titleyesPost title, 255 characters max
contentyesPost body, in Markdown
slugnoGenerated from the title when omitted
excerptnoShort teaser text
coverImageUrlnoCover image of the post. Upload the file first (see below) and pass back the URL you get, or point to any public URL
tagsnoTag names, created on the fly
accessModenoFREE (default) or PAID

The response is the full post, including the resolved slug and id.

All of these can be changed later with PATCH /posts/{id}.

Replaying a create: onConflict

By default a replayed create makes a second post. Add onConflict=UPDATE and the slug becomes the identity of the post instead: if it is already taken on your blog, the existing post is updated in place and the response is 200 instead of 201.

curl -X POST "https://api.writizzy.com/v1/blogs/yourblog/posts?onConflict=UPDATE" \
  -H "Authorization: Bearer wz_yourblog_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Getting started with Kotlin",
    "slug": "getting-started-with-kotlin",
    "content": "# Hello\n\nRevised body."
  }'

This is what makes a publishing pipeline simple: a script that pushes a Markdown file on every commit does not have to remember any id, and does not have to check whether the post exists first. Send the same slug, get the same post.

onConflictSlug already taken
DEDUPLICATE (default)The slug is suffixed (-2, -3…) and a new post is created, 201
UPDATEThe existing post is updated in place, 200

Fields absent from the payload are left unchanged, like a partial update. The publication state is never touched: publishing stays an explicit call. A slug held by a static page is left alone, and the create path runs as usual.

{
  "id": "p_abc123",
  "title": "Getting started with Kotlin",
  "slug": "getting-started-with-kotlin",
  "status": "DRAFT",
  "content": "# Hello\n\nThis is my first post.",
  "publishedAt": null,
  "accessMode": "FREE",
  "...": "..."
}

Update a post

Send only the fields you want to change, the rest are left untouched. This call never changes the publication state.

curl -X PATCH https://api.writizzy.com/v1/blogs/yourblog/posts/p_abc123 \
  -H "Authorization: Bearer wz_yourblog_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "excerpt": "A short intro to Kotlin." }'

Changing the slug after publication is discouraged, it breaks the public URL. A slug collision returns 400.

Read your drafts back

The read endpoints return published posts only. Add status to read the other states: DRAFT, IN_REVIEW, SCHEDULED, or ALL for every state. This needs a write key, a read key gets 403.

curl "https://api.writizzy.com/v1/blogs/yourblog/posts?status=DRAFT" \
  -H "Authorization: Bearer wz_yourblog_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

curl "https://api.writizzy.com/v1/blogs/yourblog/posts/getting-started-with-kotlin?status=ALL" \
  -H "Authorization: Bearer wz_yourblog_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Every post returned by the API carries its status.

Publish and unpublish

Publishing makes the post visible. That is its only effect: no newsletter is sent and no cross-posting is triggered, those are separate, explicit actions. A future publishedAt schedules the post: it stays hidden and is published at 00:00 UTC that day. publishedAt defaults to today when omitted. Publishing an already-published post is a no-op, and giving it a future date is refused. Publishing a scheduled post without a date publishes it right away.

curl -X POST https://api.writizzy.com/v1/blogs/yourblog/posts/p_abc123/publish \
  -H "Authorization: Bearer wz_yourblog_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Unpublishing reverts the post to a draft. It is reversible and safe, and a no-op on a draft.

curl -X POST https://api.writizzy.com/v1/blogs/yourblog/posts/p_abc123/unpublish \
  -H "Authorization: Bearer wz_yourblog_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Upload media

Upload an image as multipart/form-data and get back its public CDN URL. Use that URL as a post coverImageUrl or inside the Markdown content.

curl -X POST https://api.writizzy.com/v1/blogs/yourblog/media \
  -H "Authorization: Bearer wz_yourblog_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -F "file=@cover.png" \
  -F "altText=Cover image"
{
  "url": "https://cdn.writizzy.com/blogs/.../cover.png",
  "filename": "cover.png",
  "size": 48213
}

Uploads count against your plan's media storage quota. A request that would exceed it returns 403.

Redirects

Moving a blog from another domain changes its URLs. Send the old and new paths in one call, up to 500 per request, and old links keep working with a 301.

curl -X POST https://api.writizzy.com/v1/blogs/yourblog/redirects   -H "Authorization: Bearer wz_yourblog_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"   -H "Content-Type: application/json"   -d '{
    "redirects": [
      { "sourcePath": "/2019/05/getting-started.html", "destinationPath": "/getting-started-with-kotlin" },
      { "sourcePath": "/category/kotlin", "destinationPath": "/tags/kotlin" }
    ]
  }'
{
  "created": [
    { "id": "r_123", "sourcePath": "/2019/05/getting-started.html", "destinationPath": "/getting-started-with-kotlin" }
  ],
  "rejected": [
    { "sourcePath": "/category/kotlin", "destinationPath": "/tags/kotlin", "reason": "Destination path does not exist or is not published" }
  ]
}

Each redirect is checked as in the dashboard: paths start with /, a source can only be redirected once, and the destination must be a published post (/slug), a tag page (/tags/slug) or the home page (/). A redirect that fails a check comes back in rejected with its reason, the others are created. So import and publish the posts first, then send the redirects.

GET /redirects lists them, newest first, with page, limit (100 max) and search. DELETE /redirects/{id} removes one.

Tags

Tags are created by name when you create or update a post. To remove one, send DELETE /v1/blogs/{subdomain}/tags/{slug} with the slug returned by GET /tags. The tag is removed from every post using it, drafts included, and the posts are kept. Menu links to it are removed too, and its page shows no posts. In GET /tags, a tag with postCount at 0 is used by no post.

Notes

  • The post type is always POST. Static pages are not editable through the API.
  • The author is the blog owner, there is no third-party author to associate.
  • Hard deletion is not exposed. Use unpublish to remove a post from the public site.
  • All write endpoints return the standard error format.