ZerkoCMS
Zerko CMS documentation

Overview

Create and publish once, then read blogs, publications, projects, services, team profiles, partners, products, media, settings, and forms from any website. Securely write visitor actions back to the same workspace.

Public reads and writes 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, project, or service 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 public 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 API base URL.

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

Load site settings, recent content, team profiles, partners, active forms, and installed extensions.

GET/content

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

GET/content/{slug}

Load one published item using its public slug.

POST/content

Create a CMS content item from a trusted server integration.

PATCH/content/{contentId}

Update a CMS content item using its ID.

DELETE/content/{contentId}

Archive a CMS content item using its ID.

GET/blogs

List published blog posts.

GET/publications

List publications and their downloadable documents.

GET/projects

List published projects.

GET/services

List published services.

GET/settings

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

GET/team

List company profiles marked as published.

GET/partners

List visible partner names and uploaded logos.

GET/products

List active products when Ecommerce is installed.

GET/products/{productId}

Load one active product from the shared Zerko catalog.

GET/service-catalog

List active services when Services is installed.

GET/service-catalog/{serviceId}

Load one active service from the shared Zerko catalog.

POST/payments/checkout

Start a Snippe mobile payment using the saved product or service price.

GET/payments/{reference}

Check the current status of a Snippe payment.

GET/bookings/types

List public appointment options when Bookings is installed.

GET/bookings/types/{bookingTypeId}/slots

Generate available appointment slots for a date.

POST/bookings

Create an appointment request from a website.

GET/seo

Load SEO extension settings for metadata, sitemap, and robots behavior.

GET/seo/meta

Load resolved metadata for the homepage or a content slug.

GET/seo/sitemap

List published URLs for sitemap generation.

POST/analytics/events

Record a page view or conversion event.

POST/newsletter/subscribe

Add or update a newsletter subscriber.

GET/membership/access

Check whether an email has active member access.

POST/membership/request

Let visitors request member access.

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, type=project, or type=service.

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
  }
}
03A

Visitor actions

Accept visitor actions without giving websites CMS admin access.

Public write endpoints use the same X-Zerko-Api-Key header as reads. The backend scopes every request to the key's workspace, validates the request body, and uses saved prices and extension settings instead of accepting them from the visitor.

POST /forms/contact/submissions
{
  "data": {
    "full_name": "Asha Mushi",
    "email": "[email protected]",
    "message": "I would like a proposal."
  },
  "sourceUrl": "https://example.com/contact"
}
POST /bookings
{
  "bookingTypeId": "consultation_id",
  "customerName": "Asha Mushi",
  "customerEmail": "[email protected]",
  "customerPhone": "+255700000000",
  "startsAt": "2026-08-12T09:00:00+03:00",
  "notes": "First consultation"
}
POST /payments/checkout
{
  "productId": "product_id",
  "quantity": 1,
  "firstName": "Asha",
  "lastName": "Mushi",
  "email": "[email protected]",
  "phoneNumber": "+255700000000"
}
Newsletter, membership, and analytics
POST /newsletter/subscribe
{ "email": "[email protected]", "name": "Asha", "sourceUrl": "https://example.com" }

POST /membership/request
{ "email": "[email protected]", "name": "Asha", "level": "member" }

POST /analytics/events
{ "event": "page_view", "path": "/services", "referrer": "https://google.com" }

Keep the key on your website server. For browser forms, send data to your own server route first, then call Zerko from that route. Do not accept a price, payment status, tenant ID, or API key from the browser as trusted input.

03B

Content writes

Create, edit, publish, and archive content through the CMS management API.

Content authoring does not use the public delivery key. It goes through /api/cms with a signed-in Zerko CMS session or trusted server management headers, so websites can read published content without receiving admin powers.

POST /api/cms/content
{
  "type": "blog",
  "title": "Launching our new service",
  "slug": "launching-our-new-service",
  "excerpt": "A short introduction for listing pages.",
  "content": "<p>The full rich HTML article.</p>",
  "coverImage": "https://cdn.example.com/launch.jpg",
  "featured": true,
  "status": "published"
}
PATCH /api/cms/content/{contentId}
{
  "title": "Launching our updated service",
  "status": "published"
}
Archive or permanently delete
PATCH /api/cms/content/{contentId}
{ "status": "archived" }

PATCH /api/cms/content/{contentId}
{ "status": "draft" }

DELETE /api/cms/content/{contentId}

Archive is a soft delete. It hides the item from public website reads and keeps revision history safe. Delete permanently removes the content item and its attached content records, so use it only when the content should be gone for good.

04

Content model

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

Identity$id, type, title, slug
Storyexcerpt, content, tags
PresentationcoverImage, featured, galleryImages
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": {
    "client": "Acme Energy",
    "year": "2026",
    "location": "Dar es Salaam, Tanzania",
    "industry": "Energy",
    "scope": "Installation and commissioning",
    "projectStatus": "Completed",
    "websiteUrl": "https://example.com"
  },
  "downloads": [],
  "galleryImages": [
    {
      "id": "gallery_image_id",
      "url": "https://.../project-detail.jpg",
      "altText": "Project installation detail",
      "position": 0
    }
  ],
  "publishedAt": "2026-08-09T10:00:00Z",
  "updatedAt": "2026-08-09T10:00:00Z"
}

Projects include extra website-ready facts in metadata, including client, year, location, industry, delivered services, status, and an optional project link. Project entries can also include galleryImages for case-study galleries.

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 the main public website resources together. 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": "...",
    "heroSlides": [
      "https://.../hero-1.jpg",
      "https://.../hero-2.jpg"
    ],
    "heroSlideshowEnabled": true,
    "heroSlideIntervalSeconds": 3,
    "imageOptimizationEnabled": true,
    "imageQuality": 85,
    "imageMaxWidth": 2560,
    "social": { "linkedin": "https://linkedin.com/company/zerko" }
  },
  "content": [],
  "team": [],
  "partners": [],
  "forms": [],
  "extensions": ["ecommerce", "services"]
}
Site settings

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

Public gallery

Images marked with publicGallery in the media library can be shown on the website, ordered by galleryPosition.

Team profiles

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

Partners

Read visible partners from /partners. Each partner contains a name, uploaded logo, 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.partners, website.forms, website.extensions

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();

Show services on your website

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

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

return services.map((service) => ({
  title: service.title,
  summary: service.excerpt,
  page: `/services/${service.slug}`,
  image: service.coverImage
}));

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

Extensions

Optional extensions let each workspace add only the website tools it needs, then call them through the same Delivery API.

Ecommerce

Install Ecommerce in the CMS extension library. Products already created in Zerko appear automatically, and products added in the CMS join that same catalog.

Services

Install Services to expose Zerko service catalog items. Services created in Zerko or CMS stay in the same tenant catalog.

Snippe Payments

Install Payments and connect the workspace's own Snippe key. Zerko encrypts the key and never exposes it to the website.

Bookings

Publish appointment options and accept booking requests from any connected website. Paid appointment options start a real Snippe checkout through the Payments extension.

SEO tools

Fetch search metadata, canonical domain, share image, sitemap, and robots preferences from the CMS.

Analytics

Send lightweight page views and conversion events to the CMS without exposing the main Zerko backend.

Newsletter

Let visitors subscribe from the website and manage the list in Zerko CMS.

Membership

Check member access by email before rendering gated pages or downloads. Paid membership requests also require the tenant to connect Snippe Payments first.

Price protected

Checkout accepts a product ID or service ID and quantity. The backend reads the current saved price, so a website visitor cannot submit a different amount.

Load products
const { data: products } = await cms.products(1, 24);
Load services
const { data: services } = await cms.serviceCatalog(1, 24);
Start mobile checkout
const payment = await cms.checkout({
  productId: product.$id,
  // or serviceId: service.$id,
  quantity: 1,
  firstName: "Amina",
  lastName: "Said",
  email: "[email protected]",
  phoneNumber: "255781000000"
});

// Save payment.reference, then poll until paid or failed.
const latest = await cms.paymentStatus(payment.reference);
Bookings
const { data: types } = await cms.bookingTypes();
const { data: slots } = await cms.bookingSlots(types[0].$id, "2026-08-15");

await cms.createBooking({
  bookingTypeId: types[0].$id,
  customerName: "Amina Said",
  customerEmail: "[email protected]",
  startsAt: slots.find((slot) => slot.available)!.startsAt
});
Marketing extensions
const { data: seo } = await cms.seoMeta("blog-slug");
const { data: sitemap } = await cms.sitemap();

await cms.track({ event: "page_view", path: "/pricing" });

await cms.subscribe({
  email: "[email protected]",
  name: "Reader"
});

const access = await cms.membershipAccess("[email protected]");
await cms.requestMembership({ email: "[email protected]", name: "New Member" });

Extension routes return 404 until their extension is installed. Uninstalling an extension disables its public routes but does not delete existing content, subscribers, appointments, members, catalog items, or orders.

10

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.