Publish content
Create a blog, publication, project, or service in Zerko CMS and set it to Published.
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.
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();Publish your first website-powered page in a few minutes.
Create a blog, publication, project, or service in Zerko CMS and set it to Published.
Open Site settings → API keys. Name the key after the website that will use it.
Store the key safely on your website server and request the content you need.
Use a server-only environment variable. Never commit an API key to Git or place it in public browser code.
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.
ZERKO_CMS_URL=https://cms.zerko.app
ZERKO_CMS_API_KEY=zrk_live_your_key_hereX-Zerko-Api-Key: zrk_live_your_key_hereKeys 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.
All paths below follow the public API base URL.
https://cms.zerko.app/api/cms/public/bootstrapLoad site settings, recent content, team profiles, partners, active forms, and installed extensions.
/contentList all published content, with optional type and pagination filters.
/content/{slug}Load one published item using its public slug.
/contentCreate a CMS content item from a trusted server integration.
/content/{contentId}Update a CMS content item using its ID.
/content/{contentId}Archive a CMS content item using its ID.
/blogsList published blog posts.
/publicationsList publications and their downloadable documents.
/projectsList published projects.
/servicesList published services.
/settingsLoad site identity, SEO, social links, and appearance settings.
/teamList company profiles marked as published.
/partnersList visible partner names and uploaded logos.
/productsList active products when Ecommerce is installed.
/products/{productId}Load one active product from the shared Zerko catalog.
/service-catalogList active services when Services is installed.
/service-catalog/{serviceId}Load one active service from the shared Zerko catalog.
/payments/checkoutStart a Snippe mobile payment using the saved product or service price.
/payments/{reference}Check the current status of a Snippe payment.
/bookings/typesList public appointment options when Bookings is installed.
/bookings/types/{bookingTypeId}/slotsGenerate available appointment slots for a date.
/bookingsCreate an appointment request from a website.
/seoLoad SEO extension settings for metadata, sitemap, and robots behavior.
/seo/metaLoad resolved metadata for the homepage or a content slug.
/seo/sitemapList published URLs for sitemap generation.
/analytics/eventsRecord a page view or conversion event.
/newsletter/subscribeAdd or update a newsletter subscriber.
/membership/accessCheck whether an email has active member access.
/membership/requestLet visitors request member access.
/mediaList uploaded public media with pagination.
/forms/{slug}Load an active form and its fields.
/forms/{slug}/submissionsSend a completed form to the CMS inbox.
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=12Page numbers start at 1. The API accepts between 1 and 100 items per page and safely adjusts values outside that range.
{
"data": [],
"meta": {
"page": 1,
"limit": 12,
"total": 0,
"pages": 0
}
}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.
{
"data": {
"full_name": "Asha Mushi",
"email": "[email protected]",
"message": "I would like a proposal."
},
"sourceUrl": "https://example.com/contact"
}{
"bookingTypeId": "consultation_id",
"customerName": "Asha Mushi",
"customerEmail": "[email protected]",
"customerPhone": "+255700000000",
"startsAt": "2026-08-12T09:00:00+03:00",
"notes": "First consultation"
}{
"productId": "product_id",
"quantity": 1,
"firstName": "Asha",
"lastName": "Mushi",
"email": "[email protected]",
"phoneNumber": "+255700000000"
}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.
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.
{
"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"
}{
"title": "Launching our updated service",
"status": "published"
}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.
A stable shape shared by blogs, publications, projects, and services.
$id, type, title, slugexcerpt, content, tagscoverImage, featured, galleryImagesmetadata, publishedAt, updatedAt{
"$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.
Publication items include a downloads array with document name, file type, size, and direct download URL.
The team endpoint returns only profiles marked as published, already sorted by display position.
Use one response for brand identity, SEO details, social profiles, colours, logo, and browser icon.
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.
The saved article is cleaned by the CMS. Do not mix visitor form answers or other untrusted text into the HTML before rendering it.
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>
);
}.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; }Use more than articles: power your brand, people, downloads, and media from one place.
GET /bootstrap returns the main public website resources together. It is ideal for a homepage or initial server render.
{
"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"]
}Read identity, domain, logo, browser icon, brand colour, language, timezone, search description, hero images, slideshow timing, and social profiles from /settings.
Images marked with publicGallery in the media library can be shown on the website, ordered by galleryPosition.
Read published profiles from /team. Each profile can include name, role, biography, uploaded photo, contact details, social links, and display position.
Read visible partners from /partners. Each partner contains a name, uploaded logo, and display position.
Read uploaded public files from /media. Each item includes its name, type, size, public view URL, download URL, alt text, and folder.
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.
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>
));Use the API directly or copy the typed Zerko client into your project.
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>
));
}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();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.extensionsFor a small site, call the API from your own server endpoint and send only the published response to the browser.
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();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
}));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.
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.
{
"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"]
}
]
}
}{
"data": {
"name": "Amina Said",
"email": "[email protected]",
"message": "I would like to discuss a project."
},
"sourceUrl": "https://example.com/contact"
}{
"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.
Optional extensions let each workspace add only the website tools it needs, then call them through the same Delivery API.
Install Ecommerce in the CMS extension library. Products already created in Zerko appear automatically, and products added in the CMS join that same catalog.
Install Services to expose Zerko service catalog items. Services created in Zerko or CMS stay in the same tenant catalog.
Install Payments and connect the workspace's own Snippe key. Zerko encrypts the key and never exposes it to the website.
Publish appointment options and accept booking requests from any connected website. Paid appointment options start a real Snippe checkout through the Payments extension.
Fetch search metadata, canonical domain, share image, sitemap, and robots preferences from the CMS.
Send lightweight page views and conversion events to the CMS without exposing the main Zerko backend.
Let visitors subscribe from the website and manage the list in Zerko CMS.
Check member access by email before rendering gated pages or downloads. Paid membership requests also require the tenant to connect Snippe Payments first.
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.
const { data: products } = await cms.products(1, 24);const { data: services } = await cms.serviceCatalog(1, 24);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);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
});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.
Predictable responses make website integrations easier to operate.
400Check filters, form fields, or the submitted JSON body.
401Add a valid, active API key to the request header.
404The item may not exist, may still be a draft, or the form may be inactive.
5xxKeep the last cached page visible and retry the request after a short delay.
Create the replacement key, deploy it to the website, confirm requests work, then revoke the old key.
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.