Quickstart
From nothing to a live request. You will create an account, get a project, write one post, mint a read-only key, and fetch that post the way your website will.
Last updated
The whole thing, in five steps
Create an account
Signing up creates three things at once: you, a workspace, and a first project inside it. A workspace is the company; a project is one website. You are the owner of the workspace, on the free plan, with no card.
Examplebash curl -X POST https://api.cmskite.com/v1/auth/register \ -H 'content-type: application/json' \ -d '{ "email": "you@example.com", "password": "a-long-passphrase-you-will-remember", "name": "Your Name", "tenantName": "Northwind Journal" }'Find your project
Registration returns an access token. Use it to list your projects; the one created for you comes back first. Everything content-related happens inside a project, and its id is what you point a key at.
Examplebash curl https://api.cmskite.com/v1/projects \ -H "authorization: Bearer $ACCESS_TOKEN" \ -H "x-tenant-id: $WORKSPACE_ID"Write a post
Content is written with your dashboard session, not with an API key. That is deliberate: the key your website carries is read-only, so a leaked key cannot change what your readers see. Set the status to published or it stays a draft.
Examplebash curl -X POST https://api.cmskite.com/v1/blog/posts \ -H "authorization: Bearer $ACCESS_TOKEN" \ -H "x-tenant-id: $WORKSPACE_ID" \ -H "x-project-id: $PROJECT_ID" \ -H 'content-type: application/json' \ -d '{ "title": "Why the cold chain breaks at the last mile", "body": "# The last mile\n\nRefrigeration is solved until the van door opens.", "status": "published" }'Mint a read-only key
A key belongs to one project and can only read it. The secret is returned exactly once, in this response — it is stored as a keyed hash and cannot be recovered. Put it in your deployment environment, not in your repository.
Examplebash curl -X POST https://api.cmskite.com/v1/projects/$PROJECT_ID/api-keys \ -H "authorization: Bearer $ACCESS_TOKEN" \ -H "x-tenant-id: $WORKSPACE_ID" \ -H 'content-type: application/json' \ -d '{"name": "Production website"}'Read it from your site
This is the request your website makes. No workspace header, no project header — the key already knows which project it belongs to, which is why a client-supplied project id is refused rather than honoured.
Examplebash curl 'https://api.cmskite.com/v1/blog/posts?limit=10' \ -H "authorization: Bearer csk_live_..."
What comes back
Every response has the same envelope. A collection carries pagination; a single resource does not. requestId is on every response, success or failure, and is what to quote if you ever need to ask about one.
{
"success": true,
"data": [
{
"id": "post_01a0b9621db0700097df715c733e28b8",
"title": "Why the cold chain breaks at the last mile",
"slug": "why-the-cold-chain-breaks-at-the-last-mile",
"excerpt": null,
"body": "# The last mile\n\nRefrigeration is solved until the van door opens.",
"bodyFormat": "markdown",
"status": "published",
"publishedAt": "2026-09-19T11:16:50.226Z",
"author": null,
"category": null,
"tags": [],
"seo": {}
}
],
"pagination": { "hasNext": false, "nextCursor": null, "limit": 10 },
"requestId": "01a0b962-1da2-7000-8be2-6d650010f16d"
}In the framework you actually use
There is no SDK to install. It is HTTP with a bearer token, so the fetch built into your runtime is enough.
export const revalidate = 60
async function posts() {
const res = await fetch('https://api.cmskite.com/v1/blog/posts?limit=20', {
headers: { authorization: `Bearer ${process.env.CMSKITE_KEY}` },
next: { revalidate: 60 },
})
if (!res.ok) throw new Error(`CMSKite: ${res.status}`)
const { data } = await res.json()
return data
}
export default async function Blog() {
const all = await posts()
return (
<ul>
{all.map((post) => (
<li key={post.id}>
<a href={`/blog/${post.slug}`}>{post.title}</a>
</li>
))}
</ul>
)
}import os, httpx
client = httpx.Client(
base_url="https://api.cmskite.com/v1",
headers={"authorization": f"Bearer {os.environ['CMSKITE_KEY']}"},
)
response = client.get("/blog/posts", params={"limit": 20})
response.raise_for_status()
for post in response.json()["data"]:
print(post["slug"], post["title"])<?php
$ch = curl_init('https://api.cmskite.com/v1/blog/posts?limit=20');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['authorization: Bearer ' . getenv('CMSKITE_KEY')],
]);
$payload = json_decode(curl_exec($ch), true);
foreach ($payload['data'] as $post) {
echo $post['slug'], "\n";
}Before you go to production
- Keep the key out of your repository and out of client-side JavaScript. It is read-only, but it is still yours.
- Handle
429. The response carriesRetry-AfterandRateLimitheaders on every request, including successful ones, so you can pace yourself before you are refused. - Cache. A blog post changes far less often than it is read; a sixty-second revalidate removes almost all of your traffic.
- Restrict the key to your own origins if it will be used from a browser. See allowed origins under Authentication.