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.
Product API base path is /v1/api(studio session routes use /v1). Example: https://api.example.com/v1/api. Every request and response body is JSON. API keys are workspace-scoped — no extra workspace header is required.
Setup & API keys
Before the first request you need one thing: a key. Which kind depends on where your code runs — the wrong choice is the most common way to leak a credential.
Base URL
Every request goes to https://api.andishi.nexuslabsstudio.com. There is nothing to install or deploy — your workspace is already there, and your API key identifies it. Paths below are relative to that host, so the articles endpoint in full is https://api.andishi.nexuslabsstudio.com/v1/api/articles.
The three surfaces
Requests land on one of three paths, and they authenticate differently. Most integrations only need the middle one.
| Path | Authenticates with | Use it for |
|---|---|---|
/v1 | Session cookie or JWT | The studio UI itself. Not intended for integrations. |
/v1/api | Secret key andishi_live_… | Your backend: reading and writing workspace content. |
/v1/public | Publishable key pk_… | Browsers. Published site-builder content only. |
Creating a secret key
In the studio, open Developers → New API Key. Give it a name you will recognise later, tick the scopes it needs, and create it. Only workspace owners and admins can mint keys.
{
"api_key": {
"id": "9f1c…",
"name": "Marketing site",
"key_prefix": "andishi_live_51fA9",
"scopes": [
"articles:read",
"media:read"
],
"created_at": "2026-01-14T09:12:00Z"
},
"secret": "andishi_live_51fA9c…"
}The secret field appears in this one response and nowhere else. Andishi stores only a hash of it and the visible prefix, so it cannot be shown to you again or recovered by support. Put it in your environment config immediately; if you lose it, revoke the key and mint another.
Keys are bound to the workspace that created them, which is why /v1/apineeds no workspace header — the key already says which workspace it speaks for.
Secret keys vs publishable keys
| Secret key | Publishable key | |
|---|---|---|
| Looks like | andishi_live_… | pk_live_… |
| Created in | Developers → New API Key | Automatically, with each Site |
| Scope of access | Every module its scopes allow, read and write | Published page content, one site, read only |
| Safe in a browser bundle? | No | Yes — it is designed for it |
| If it leaks | Revoke it now; it can write to your workspace | Rotate it; a reader only ever saw public content |
Where content can be read from
This is the distinction that catches people out. A publishable key reads site-builder page content only — the fields you wrapped with <AE> from @andishi/react. Articles, videos, podcasts, categories, media and subscribers are available only on /v1/api, behind the secret key.
A secret key is a read and write credential for the whole workspace, so it must never reach a browser bundle. In practice that means a purely client-side site cannot list your articles directly — fetch them somewhere the key stays private and pass the result down:
| If your site is | Fetch articles in |
|---|---|
| Next.js / Remix / SvelteKit | A server component, loader, or route handler |
| Static (Astro, Hugo, 11ty) | The build step |
| A client-only SPA (Vite, CRA) | A small backend or serverless function that proxies to Andishi |
Your first request
curl "https://api.andishi.nexuslabsstudio.com/v1/api/articles?limit=5" \ -H "Authorization: Bearer $ANDISHI_API_KEY"
const res = await fetch(
'https://api.andishi.nexuslabsstudio.com/v1/api/articles?limit=5',
{ headers: { Authorization: `Bearer ${process.env.ANDISHI_API_KEY}` } },
);
if (!res.ok) throw new Error(`andishi: ${res.status}`);
const { articles, pagination } = await res.json();Rotating and revoking
Revoking a key in Developers takes effect on the next request — there is no cache to wait out. Keys cannot be edited, so changing scopes means creating a replacement and revoking the old one. Publishable keys are rotated from Sites → your site; the previous value stops resolving immediately, so redeploy the site with the new one.
Authentication
Every request carries an API key as a Bearer token. Keys are created under Developers in your workspace (admins only), shown exactly once, and bound to that workspace.
curl https://your-app/v1/api/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 missing_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
{
"articles": [
{
"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. Collection keys match the resource (articles, comments, …).
Errors
{
"error": "title is required",
"code": "title_required"
}| Field | Type | Notes |
|---|---|---|
401 invalid_api_key optional | status | No key, or the key is wrong / revoked. |
403 missing_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 title_required / invalid_request 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
{
"article": {
"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', // calls {baseUrl}/v1/api/…
})
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" }