Introduction

The Andishi API lets you read and write everything in a workspace — articles, comments, video, podcasts, categories, media and subscribers — over plain JSON. It's the same data your studio uses; nothing is a second-class citizen.

Base URL: your deployment's own origin, e.g. https://your-workspace.example.com/api/v1. Every request and response body is JSON; there is no XML, no SOAP, no surprises.

Authentication

Every request carries an API key as a Bearer token. Keys are created under Developers in your workspace (admins only) and shown exactly once.

curl
curl https://your-app/api/v1/articles \
  -H "Authorization: Bearer andishi_live_51fA9c…" \
  -H "Content-Type: application/json"

Scopes

Each key carries a list of scopes, e.g. articles:read, articles:write, comments:write. A request fails with 403 insufficient_scope if the key lacks the scope the endpoint needs. Issue narrow keys per integration — a public website only ever needs read scopes.

Pagination & errors

Every endpoint follows the same two shapes, so you only need to learn them once.

List responses

GET /api/v1/articles?limit=2
{
  "data": [
    {
      "id": "…",
      "title": "Elections 2027",
      "status": "published"
    },
    {
      "id": "…",
      "title": "Budget explainer",
      "status": "draft"
    }
  ],
  "pagination": {
    "total": 84,
    "limit": 2,
    "offset": 0
  }
}

Use limit (max 100) and offset query params to page through results.

Errors

422 example
{
  "error": {
    "code": "invalid_body",
    "message": "\"title\" is required."
  }
}
FieldTypeNotes
401 missing_key / invalid_key optionalstatusNo key, or the key is wrong / revoked.
403 insufficient_scope optionalstatusThe key is valid but lacks the needed scope.
404 not_found optionalstatusNo resource with that id in this workspace.
409 slug_taken optionalstatusThe slug collides with an existing item.
400 invalid_body optionalstatusThe request body is missing a required field.

Articles

Long-form written content — the writing module's core resource, including SEO fields and publish state.

GET/api/v1/articlesList articles
POST/api/v1/articlesCreate an article
GET/api/v1/articles/:idFetch one
PATCH/api/v1/articles/:idUpdate fields
DELETE/api/v1/articles/:idMove to trash (soft delete)

Create a published article

POST /api/v1/articles
{
  "title": "Elections 2027: what to watch",
  "body": "<p>The race begins…</p>",
  "status": "published",
  "tags": [
    "politics",
    "elections"
  ]
}

Body fields

FieldTypeNotes
title requiredstringFalls back to a slugified title if slug is omitted.
slug optionalstringURL segment; must be unique per language.
body optionalstring (HTML)Rich-text content, same format the editor produces.
subtitle optionalstringDeck / standfirst.
status optional"draft" | "in_review" | "scheduled" | "published" | "archived"Defaults to draft.
category_id optionaluuidOne of your Categories.
tags optionalstring[]Free-form tags.
locale optionalstringDefaults to the workspace’s default language.

Response

201 Created
{
  "data": {
    "id": "a1b2c3d4-…",
    "title": "Elections 2027: what to watch",
    "slug": "elections-2027-what-to-watch",
    "status": "published",
    "published_at": "2026-07-12T09:00:00Z",
    "read_time_minutes": 3,
    "comment_count": 0
  }
}

Comments

Reader comments, threaded, with a moderation status. Great for pulling in comments from your own front-end or an external form.

GET/api/v1/commentsList (filter by article, status)
POST/api/v1/commentsCreate a comment
PATCH/api/v1/comments/:idApprove / mark spam / edit
DELETE/api/v1/comments/:idDelete

Submit a comment (lands in the moderation queue)

POST /api/v1/comments
{
  "article_id": "a1b2c3d4-…",
  "author_name": "Amina",
  "content": "Great breakdown, thank you."
}
FieldTypeNotes
article_id requireduuidThe article being commented on.
author_name requiredstringDisplay name.
content requiredstringComment body.
parent_id optionaluuidReply to another comment.
status optional"pending" | "approved"Defaults to pending unless you pass approved.

Approve one

PATCH /api/v1/comments/:id
{
  "status": "approved"
}

Videos

Video titles, whether streamed through Bunny Stream or linked from an external source.

GET/api/v1/videosList videos
POST/api/v1/videosCreate a video record

Attach an already-uploaded Bunny video

POST /api/v1/videos
{
  "title": "Behind the Scenes: Episode 1",
  "bunny_video_id": "8f3e2c1a-…",
  "status": "published"
}
FieldTypeNotes
title requiredstring
bunny_video_id optionalstringGUID from the Bunny Stream upload flow.
source_url optionalstringExternal URL, for content not hosted on Bunny.
poster_url optionalstringThumbnail image URL.
status optional"draft" | "published" | "archived"Defaults to draft.

Podcast episodes

Episodes belong to a show created in the studio; the API publishes into an existing show.

GET/api/v1/podcast-episodesList episodes (filter by show)
POST/api/v1/podcast-episodesPublish an episode
POST /api/v1/podcast-episodes
{
  "show_id": "f4a1…",
  "title": "Ep. 12 — The Budget, Explained",
  "audio_url": "https://…/ep12.mp3",
  "episode_number": 12,
  "status": "published"
}

Categories

The built-in taxonomy. (Custom taxonomies you define in the studio aren't yet exposed over the API — ask us if you need them.)

GET/api/v1/categoriesList categories
POST/api/v1/categoriesCreate a category
POST /api/v1/categories
{
  "name": "Culture",
  "accent_color": "#8b5cf6"
}

Media

Read-only for now: list what's already in the media library (images, video, audio) to reference elsewhere.

GET/api/v1/mediaList media items (filter by type)

Subscribers

Newsletter subscribers — useful for wiring up a signup form on your own site.

GET/api/v1/subscribersList subscribers
POST/api/v1/subscribersAdd a subscriber (upsert by email)
POST /api/v1/subscribers
{
  "email": "reader@example.com",
  "name": "Amina O."
}

Webhooks

Subscribe to events instead of polling. Every payload is signed so you can verify it really came from Andishi.

FieldTypeNotes
article.published optionaleventFires when an article’s status becomes published.
article.updated optionaleventFires when a published article changes.
article.unpublished optionaleventFires when a published article is unpublished.
page.updated optionaleventFires when a visually-edited page section saves.

Verifying a payload

node
const crypto = require('crypto')

const expected = crypto
  .createHmac('sha256', WEBHOOK_SECRET)
  .update(rawRequestBody)
  .digest('hex')

if (expected !== req.headers['x-andishi-signature']) {
  throw new Error('Invalid signature')
}

SDKs & libraries

A small, dependency-free TypeScript client ships in the repo — copy it in, or call the REST endpoints directly from any language.

TypeScript
import { Andishi } from 'andishi'

const andishi = new Andishi(process.env.ANDISHI_KEY, {
  baseUrl: 'https://your-app',
})

await andishi.articles.create({ title: 'Hi', status: 'published' })
await andishi.comments.moderate(commentId, 'approved')
await andishi.subscribers.add({ email: 'reader@example.com' })

No official library for your language yet? The API is plain REST + JSON — any HTTP client works.