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 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
{
"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
{
"error": {
"code": "invalid_body",
"message": "\"title\" is required."
}
}| Field | Type | Notes |
|---|---|---|
401 missing_key / invalid_key optional | status | No key, or the key is wrong / revoked. |
403 insufficient_scope optional | status | The key is valid but lacks the needed scope. |
404 not_found optional | status | No resource with that id in this workspace. |
409 slug_taken optional | status | The slug collides with an existing item. |
400 invalid_body optional | status | The request body is missing a required field. |
Articles
Long-form written content — the writing module's core resource, including SEO fields and publish state.
Create a published article
{
"title": "Elections 2027: what to watch",
"body": "<p>The race begins…</p>",
"status": "published",
"tags": [
"politics",
"elections"
]
}Body fields
| Field | Type | Notes |
|---|---|---|
title required | string | Falls back to a slugified title if slug is omitted. |
slug optional | string | URL segment; must be unique per language. |
body optional | string (HTML) | Rich-text content, same format the editor produces. |
subtitle optional | string | Deck / standfirst. |
status optional | "draft" | "in_review" | "scheduled" | "published" | "archived" | Defaults to draft. |
category_id optional | uuid | One of your Categories. |
tags optional | string[] | Free-form tags. |
locale optional | string | Defaults to the workspace’s default language. |
Response
{
"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
}
}Videos
Video titles, whether streamed through Bunny Stream or linked from an external source.
Attach an already-uploaded Bunny video
{
"title": "Behind the Scenes: Episode 1",
"bunny_video_id": "8f3e2c1a-…",
"status": "published"
}| Field | Type | Notes |
|---|---|---|
title required | string | — |
bunny_video_id optional | string | GUID from the Bunny Stream upload flow. |
source_url optional | string | External URL, for content not hosted on Bunny. |
poster_url optional | string | Thumbnail 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.
{
"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.)
{
"name": "Culture",
"accent_color": "#8b5cf6"
}Media
Read-only for now: list what's already in the media library (images, video, audio) to reference elsewhere.
Subscribers
Newsletter subscribers — useful for wiring up a signup form on your own site.
{
"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.
| Field | Type | Notes |
|---|---|---|
article.published optional | event | Fires when an article’s status becomes published. |
article.updated optional | event | Fires when a published article changes. |
article.unpublished optional | event | Fires when a published article is unpublished. |
page.updated optional | event | Fires when a visually-edited page section saves. |
Verifying a payload
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.
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.
Comments
Reader comments, threaded, with a moderation status. Great for pulling in comments from your own front-end or an external form.
Submit a comment (lands in the moderation queue)
{ "article_id": "a1b2c3d4-…", "author_name": "Amina", "content": "Great breakdown, thank you." }article_idrequiredauthor_namerequiredcontentrequiredparent_idoptionalstatusoptionalApprove one
{ "status": "approved" }