Skip to content

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

  1. 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"
      }'
  2. 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"
  3. 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"
      }'
  4. 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"}'
  5. 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.

GET /v1/blog/postsjson
{
  "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.

Next.js — app/blog/page.tsxts
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>
  )
}
Pythonpython
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"])
PHPphp
<?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 carries Retry-After and RateLimit headers 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.