ZerkoCMS
Zerko CMS documentation

Overview

Create and publish once, then deliver blogs, publications, projects, team profiles, media, settings, and forms to any website.

Published content only Tenant-isolated Works with any framework
Fetch your latest blogs
const response = await fetch(
    "https://cms.zerko.app/api/cms/public/blogs",
  {
    headers: {
      "X-Zerko-Api-Key": process.env.ZERKO_CMS_API_KEY
    }
  }
);

const { data: posts } = await response.json();
200 Published stories ready.
01

Quick start

Publish your first website-powered page in a few minutes.

Step 1

Publish content

Create a blog, publication, or project in Zerko CMS and set it to Published.

Step 2

Create a key

Open Site settings → API keys. Name the key after the website that will use it.

Step 3

Connect your website

Store the key safely on your website server and request the content you need.

Keep the key on your server

Use a server-only environment variable. Never commit an API key to Git or place it in public browser code.

02

Authentication

Every Delivery API request belongs to one Zerko workspace.

Send your key in the X-Zerko-Api-Key header. The key identifies the workspace automatically, so no tenant or company ID is needed in the URL.

.env.local
ZERKO_CMS_URL=https://cms.zerko.app
ZERKO_CMS_API_KEY=zrk_live_your_key_here
Request header
X-Zerko-Api-Key: zrk_live_your_key_here

Keys are shown once when created. If one is exposed, revoke it in Site settings and create a replacement.

Always call cms.zerko.app. It is the protected CMS gateway to Zerko's shared platform; websites never need the main operations API address or its private service credential.

03

API reference

All paths below follow the public Delivery API base URL.

Official base URLhttps://cms.zerko.app/api/cms/public
GET/bootstrap

Load site settings, recent content, team profiles, and active forms together.

GET/content

List all published content, with optional type and pagination filters.

GET/content/{slug}

Load one published item using its public slug.

GET/blogs

List published blog posts.

GET/publications

List publications and their downloadable documents.

GET/projects

List published projects.

GET/settings

Load site identity, SEO, social links, and appearance settings.

GET/team

List company profiles marked as published.

GET/media

List uploaded public media with pagination.

GET/forms/{slug}

Load an active form and its fields.

POST/forms/{slug}/submissions

Send a completed form to the CMS inbox.

Filtering and pagination

List endpoints accept page and limit. The general content endpoint also accepts type=blog, type=publication, or type=project.

GET /content?type=blog&page=1&limit=12

Page numbers start at 1. The API accepts between 1 and 100 items per page and safely adjusts values outside that range.

Response envelope
{
  "data": [],
  "meta": {
    "page": 1,
    "limit": 12,
    "total": 0,
    "pages": 0
  }
}
04

Content model

A stable shape shared by blogs, publications, and projects.

Identity$id, type, title, slug
Storyexcerpt, content, tags
PresentationcoverImage, featured
Detailsmetadata, publishedAt, updatedAt
Published content item
{
  "$id": "article_id",
  "type": "blog",
  "title": "Building better customer experiences",
  "slug": "building-better-customer-experiences",
  "excerpt": "A practical guide for growing teams.",
  "content": "<p>Rich, sanitized HTML content...</p>",
  "coverImage": "https://.../hero.jpg",
  "featured": true,
  "tags": ["customer experience", "growth"],
  "metadata": {},
  "downloads": [],
  "publishedAt": "2026-08-09T10:00:00Z",
  "updatedAt": "2026-08-09T10:00:00Z"
}

Publications

Publication items include a downloads array with document name, file type, size, and direct download URL.

Company team

The team endpoint returns only profiles marked as published, already sorted by display position.

Site settings

Use one response for brand identity, SEO details, social profiles, colours, logo, and browser icon.

05

Rich content

Turn the formatted article from Zerko into a polished page on your website.

The content field contains HTML created in the Zerko editor. Zerko cleans this HTML before saving it while preserving headings, paragraphs, lists, links, quotes, images, code blocks, highlights, and text alignment.

Render content from Zerko only

The saved article is cleaned by the CMS. Do not mix visitor form answers or other untrusted text into the HTML before rendering it.

Next.js article page

app/blog/[slug]/page.tsx
type PageProps = { params: Promise<{ slug: string }> };

export default async function ArticlePage({ params }: PageProps) {
  const { slug } = await params;
  const response = await fetch(
    `${process.env.ZERKO_CMS_URL}/api/cms/public/content/${slug}`,
    {
      headers: { "X-Zerko-Api-Key": process.env.ZERKO_CMS_API_KEY! },
      next: { revalidate: 60 }
    }
  );

  if (response.status === 404) return <p>Article not found.</p>;
  if (!response.ok) throw new Error("Could not load article");

  const { data: article } = await response.json();
  return (
    <article>
      <h1>{article.title}</h1>
      <div
        className="article-content"
        dangerouslySetInnerHTML={{ __html: article.content }}
      />
    </article>
  );
}

Give every article consistent styling

app/globals.css
.article-content { color: #27272a; font-size: 1.08rem; line-height: 1.8; }
.article-content h2 { margin: 2.5rem 0 1rem; font-size: 1.8rem; }
.article-content h3 { margin: 2rem 0 .75rem; font-size: 1.35rem; }
.article-content p, .article-content ul, .article-content ol { margin: 1rem 0; }
.article-content blockquote { border-left: 3px solid #c8a45d; padding-left: 1rem; }
.article-content img { display: block; max-width: 100%; height: auto; border-radius: 1rem; }
.article-content a { color: inherit; text-decoration: underline; }
.article-content pre { overflow-x: auto; padding: 1rem; border-radius: .75rem; }
Formatting keptHeadings, bold, emphasis, lists, quotes, code, links and uploaded images.
Unsafe markup removedScripts and unsupported markup do not become part of the saved article.
Website controls the lookYour site stylesheet decides typography, spacing, colours and responsive images.
06

Website resources

Use more than articles: power your brand, people, downloads, and media from one place.

Load the whole website in one request

GET /bootstrap returns four top-level values. It is ideal for a homepage or initial server render.

Bootstrap response
{
  "site": {
    "siteName": "Zerko",
    "tagline": "Build better, faster.",
    "logoUrl": "https://.../logo.png",
    "faviconUrl": "https://.../favicon.png",
    "primaryColor": "#C8A45D",
    "locale": "en",
    "timezone": "Africa/Dar_es_Salaam",
    "seoTitle": "Zerko",
    "seoDescription": "...",
    "imageOptimizationEnabled": true,
    "imageQuality": 85,
    "imageMaxWidth": 2560,
    "social": { "linkedin": "https://linkedin.com/company/zerko" }
  },
  "content": [],
  "team": [],
  "forms": []
}
Site settings

Read identity, domain, logo, browser icon, brand colour, language, timezone, search description, and social profiles from /settings.

Team profiles

Read published profiles from /team. Each profile can include name, role, biography, uploaded photo, contact details, social links, and display position.

Media library

Read uploaded public files from /media. Each item includes its name, type, size, public view URL, download URL, alt text, and folder.

Publication files

Every publication can expose a downloads list with document name, file type, byte size, and public URL.

Workspace admins can control automatic image optimization in Site settings → Images. Zerko uses Sharp before storage, preserves the original format and proportions, never enlarges small images, and leaves documents and animated GIFs unchanged.

Show downloadable publications

const { data: publications } = await cms.publications();

return publications.map((publication) => (
  <article key={publication.$id}>
    <h2>{publication.title}</h2>
    {publication.downloads.map((file) => (
      <a key={file.id} href={file.url} download>
        Download {file.name} ({file.mimeType})
      </a>
    ))}
  </article>
));
07

Website examples

Use the API directly or copy the typed Zerko client into your project.

Next.js server component

app/blog/page.tsx
async function getPosts() {
  const response = await fetch(
    `${process.env.ZERKO_CMS_URL}/api/cms/public/blogs`,
    {
      headers: {
        "X-Zerko-Api-Key": process.env.ZERKO_CMS_API_KEY!
      },
      next: { revalidate: 60 }
    }
  );

  if (!response.ok) throw new Error("Could not load posts");
  return response.json();
}

export default async function BlogPage() {
  const { data: posts } = await getPosts();

  return posts.map((post) => (
    <a key={post.$id} href={`/blog/${post.slug}`}>
      <h2>{post.title}</h2>
      <p>{post.excerpt}</p>
    </a>
  ));
}

Load one article by slug

const response = await fetch(
  `${process.env.ZERKO_CMS_URL}/api/cms/public/content/${slug}`,
  { headers: { "X-Zerko-Api-Key": process.env.ZERKO_CMS_API_KEY } }
);

const { data: article } = await response.json();

Bootstrap a company website

const response = await fetch(
  `${process.env.ZERKO_CMS_URL}/api/cms/public/bootstrap`,
  { headers: { "X-Zerko-Api-Key": process.env.ZERKO_CMS_API_KEY } }
);

const website = await response.json();
// website.site, website.content, website.team, website.forms

Plain JavaScript

For a small site, call the API from your own server endpoint and send only the published response to the browser.

server.js
const response = await fetch(
  "https://cms.zerko.app/api/cms/public/projects?page=1&limit=12",
  { headers: { "X-Zerko-Api-Key": process.env.ZERKO_CMS_API_KEY } }
);

if (!response.ok) throw new Error(`Zerko CMS returned ${response.status}`);
const { data: projects, meta } = await response.json();

Freshness and caching

Published pages: cache read requests for 30–60 seconds for fast pages and timely updates.

Preview or urgent changes: use cache: "no-store" temporarily.

Form submissions: never cache a POST request.

Images and files: use the public URL returned by Zerko; do not rebuild storage URLs yourself.

08

Contact forms

Display a CMS-managed form and send responses back to the workspace inbox.

Build questions visually in Zerko CMS, then load the active definition with GET /forms/{slug}. Render fields in the returned order and submit answers using their stable name values.

GET /forms/project-enquiry
{
  "data": {
    "$id": "form_id",
    "name": "Project enquiry",
    "slug": "project-enquiry",
    "successMessage": "Thanks — we received your request.",
    "active": true,
    "fields": [
      {
        "id": "full_name",
        "name": "full_name",
        "label": "Full name",
        "type": "text",
        "required": true,
        "placeholder": "Amina Said",
        "helpText": "",
        "options": []
      },
      {
        "id": "service",
        "name": "service",
        "label": "What do you need?",
        "type": "select",
        "required": true,
        "placeholder": "",
        "helpText": "Choose the closest option.",
        "options": ["Website", "Branding", "Consulting"]
      }
    ]
  }
}
POST /forms/contact-us/submissions
{
  "data": {
    "name": "Amina Said",
    "email": "[email protected]",
    "message": "I would like to discuss a project."
  },
  "sourceUrl": "https://example.com/contact"
}
Success response
{
  "submissionId": "submission_id",
  "message": "Thank you. We will be in touch shortly."
}

Supported field types are short answer, email, phone, long answer, choice list, checkbox, date, and number. Zerko rejects missing required answers, unknown fields, invalid email addresses, and choices that are not in the saved list.

Every valid response appears under Contact forms → Recent responses and the form's Responses view. Email alerts use the workspace's connected SMTP account, with Zerko's backend SMTP as fallback. Notification recipient addresses remain private and are never returned by the public form endpoint.

09

Errors and troubleshooting

Predictable responses make website integrations easier to operate.

400
Invalid request

Check filters, form fields, or the submitted JSON body.

401
Key missing or invalid

Add a valid, active API key to the request header.

404
Not found

The item may not exist, may still be a draft, or the form may be inactive.

5xx
Service unavailable

Keep the last cached page visible and retry the request after a short delay.

Rotate keys without downtime

Create the replacement key, deploy it to the website, confirm requests work, then revoke the old key.

Quick checks

Getting 401? Confirm the header name is exactly X-Zerko-Api-Key, the key begins with zrk_live_, and it has not been revoked or expired.

Getting 404 for an article? Confirm its slug and make sure the article is Published, not Draft, Scheduled, In review, or Archived.

Images do not appear? Use coverImage, photoUrl, or the media item's returned url exactly as supplied.

Updates seem delayed? Clear the website cache or wait for its configured revalidation time.