If you prefer not to use the React SDK or TypeScript client, you can interact directly with the Vlozi API using standard HTTP requests. This works from any language or framework — Python, Go, PHP, cURL, mobile apps, or plain JavaScript fetch.
Base URL
All requests go through your Gateway URL, which you can find in Settings → API Keys in the Vlozi Dashboard.
https://api.vlozi.appAuthentication
Include your public API key in every request. You can use either of these headers:
# Option 1: x-api-key header (recommended)
x-api-key: pk_live_abc123...
# Option 2: Authorization Bearer header
Authorization: Bearer pk_live_abc123...IMPORTANT
Public (publishable) keys are read-only — they only allow GET requests. If you need write access, use a secret key from the server side only. Never expose secret keys in frontend code.
Domain locking
If you configured allowed domains on your API key, the gateway validates the Origin header. Browser requests from unlisted domains will receive a 403 Domain not allowed error. Server-to-server requests (no Origin header) are also blocked when domains are configured.
Error responses
All errors follow a consistent JSON format:
// 401 — Missing API key
{ "error": "API key missing" }
// 403 — Invalid, expired, or domain-locked key
{ "error": "Invalid API key" }
{ "error": "API key expired" }
{ "error": "Domain not allowed" }
// 404 — Resource not found
{ "error": "Post not found" }Endpoints
List posts
Get a paginated list of published posts with optional filtering, search, and sorting.
GET /blog/public/postsQuery parameters
| Param | Type | Default | Description |
|---|---|---|---|
page |
number | 1 | Page number (min: 1) |
limit |
number | 10 | Items per page (min: 1, max: 100) |
category |
string | — | Filter by category slug (e.g., tutorials) |
tag |
string | — | Filter by tag slug (e.g., react) |
search |
string | — | Search title and excerpt (case-insensitive, max 200 chars) |
sort |
string | publishedAt | Sort field: publishedAt, title, or createdAt |
order |
string | desc | Sort direction: asc or desc |
cURL example
# Basic — latest 10 posts
curl -H "x-api-key: pk_live_abc123" \
"https://api.vlozi.app/blog/public/posts"
# With filters — tutorials about React, sorted by title
curl -H "x-api-key: pk_live_abc123" \
"https://api.vlozi.app/blog/public/posts?category=tutorials&search=react&sort=title&order=asc&limit=5"
# Pagination — page 2
curl -H "x-api-key: pk_live_abc123" \
"https://api.vlozi.app/blog/public/posts?page=2&limit=10"JavaScript (fetch) example
const API_KEY = "pk_live_abc123";
const BASE_URL = "https://api.vlozi.app";
async function listPosts({ page = 1, limit = 10, category, tag, search, sort, order } = {}) {
const params = new URLSearchParams();
params.set("page", String(page));
params.set("limit", String(limit));
if (category) params.set("category", category);
if (tag) params.set("tag", tag);
if (search) params.set("search", search);
if (sort) params.set("sort", sort);
if (order) params.set("order", order);
const res = await fetch(`${BASE_URL}/blog/public/posts?${params}`, {
headers: { "x-api-key": API_KEY },
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.error || "Failed to fetch posts");
}
return res.json();
}
// Usage
const result = await listPosts({ category: "tutorials", search: "react" });
console.log(result.data); // Post[]
console.log(result.meta); // { page, limit, total, totalPages }Response shape
{
"data": [
{
"title": "Getting Started with React",
"slug": "getting-started-with-react",
"excerpt": "Learn the basics of React…",
"featuredImageUrl": "https://…/image.jpg",
"publishedAt": "2024-01-15T10:00:00Z",
"category": {
"name": "Tutorials",
"slug": "tutorials"
},
"tags": [
{ "name": "React", "slug": "react" },
{ "name": "JavaScript", "slug": "javascript" }
]
}
],
"meta": {
"page": 1,
"limit": 10,
"total": 42,
"totalPages": 5
}
}NOTE
When no posts match the given filters, data will be an empty array [] and total will be 0. This is not an error.
Get single post
Get a single post by slug, including its full HTML content. This endpoint returns rendered HTML (converted from the TipTap editor JSON).
GET /blog/public/posts/:slugcURL example
curl -H "x-api-key: pk_live_abc123" \
"https://api.vlozi.app/blog/public/posts/getting-started-with-react"JavaScript (fetch) example
async function getPost(slug) {
const res = await fetch(`${BASE_URL}/blog/public/posts/${slug}`, {
headers: { "x-api-key": API_KEY },
});
if (!res.ok) {
if (res.status === 404) return null;
const err = await res.json();
throw new Error(err.error || "Failed to fetch post");
}
return res.json();
}
// Usage
const post = await getPost("getting-started-with-react");
if (!post) {
console.log("Post not found");
} else {
document.getElementById("title").textContent = post.title;
document.getElementById("content").innerHTML = post.content; // HTML
}Response shape
{
"title": "Getting Started with React",
"slug": "getting-started-with-react",
"excerpt": "Learn the basics of React…",
"content": "<h2>Introduction</h2><p>React is a JavaScript library…</p>",
"seoTitle": "React Tutorial for Beginners",
"seoDescription": "A comprehensive guide to getting started with React…",
"featuredImageUrl": "https://…/image.jpg",
"publishedAt": "2024-01-15T10:00:00Z",
"category": {
"name": "Tutorials",
"slug": "tutorials"
},
"tags": [
{ "name": "React", "slug": "react" },
{ "name": "JavaScript", "slug": "javascript" }
]
}CAUTION
The content field contains rendered HTML. Always sanitize it before injecting into the DOM. Use a library like DOMPurify to prevent XSS attacks.
List categories
Get all categories for your workspace with published post counts. Returns a flat array (not paginated).
GET /blog/public/categoriescURL example
curl -H "x-api-key: pk_live_abc123" \
"https://api.vlozi.app/blog/public/categories"JavaScript (fetch) example
async function listCategories() {
const res = await fetch(`${BASE_URL}/blog/public/categories`, {
headers: { "x-api-key": API_KEY },
});
if (!res.ok) throw new Error("Failed to fetch categories");
const { data } = await res.json();
return data; // Category[]
}
// Usage — Build a category nav
const categories = await listCategories();
categories.forEach(cat => {
console.log(`${cat.name} (${cat.postCount} posts)`);
});Response shape
{
"data": [
{ "name": "Tutorials", "slug": "tutorials", "postCount": 12 },
{ "name": "News", "slug": "news", "postCount": 5 },
{ "name": "Case Studies", "slug": "case-studies", "postCount": 3 }
]
}List tags
Get all tags for your workspace with published post counts. Returns a flat array (not paginated).
GET /blog/public/tagscURL example
curl -H "x-api-key: pk_live_abc123" \
"https://api.vlozi.app/blog/public/tags"JavaScript (fetch) example
async function listTags() {
const res = await fetch(`${BASE_URL}/blog/public/tags`, {
headers: { "x-api-key": API_KEY },
});
if (!res.ok) throw new Error("Failed to fetch tags");
const { data } = await res.json();
return data; // Tag[]
}
// Usage — Build a tag cloud
const tags = await listTags();
tags.forEach(tag => {
console.log(`#${tag.name} (${tag.postCount})`);
});Response shape
{
"data": [
{ "name": "React", "slug": "react", "postCount": 8 },
{ "name": "Next.js", "slug": "nextjs", "postCount": 6 },
{ "name": "TypeScript", "slug": "typescript", "postCount": 4 }
]
}Complete example: build a blog with fetch
A full working example that builds a blog page without the SDK — using only the raw API and plain JavaScript:
API client helper
// lib/blog-api.js
const API_KEY = "pk_live_abc123";
const BASE_URL = "https://api.vlozi.app";
const headers = { "x-api-key": API_KEY };
export async function fetchAPI(path) {
const res = await fetch(`${BASE_URL}${path}`, { headers });
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error || `HTTP ${res.status}`);
}
return res.json();
}
export const blogAPI = {
// List posts with all query options
listPosts: (params = {}) => {
const qs = new URLSearchParams();
Object.entries(params).forEach(([k, v]) => {
if (v !== undefined && v !== null && v !== "") qs.set(k, String(v));
});
return fetchAPI(`/blog/public/posts?${qs}`);
},
// Get a single post by slug
getPost: (slug) => fetchAPI(`/blog/public/posts/${slug}`),
// List all categories
listCategories: () => fetchAPI("/blog/public/categories").then(r => r.data),
// List all tags
listTags: () => fetchAPI("/blog/public/tags").then(r => r.data),
};Blog list page
// app/blog/page.jsx (or any framework)
import { blogAPI } from "@/lib/blog-api";
export default async function BlogPage({ searchParams }) {
// Read filters from URL query string
const category = searchParams?.category;
const tag = searchParams?.tag;
const search = searchParams?.search;
const page = Number(searchParams?.page) || 1;
// Fetch data in parallel
const [postsResult, categories, tags] = await Promise.all([
blogAPI.listPosts({ page, limit: 12, category, tag, search, sort: "publishedAt", order: "desc" }),
blogAPI.listCategories(),
blogAPI.listTags(),
]);
return (
<div>
{/* Search */}
<form action="/blog" method="GET">
<input name="search" defaultValue={search} placeholder="Search posts…" />
<button type="submit">Search</button>
</form>
{/* Category Filter */}
<nav>
<a href="/blog">All</a>
{categories.map(cat => (
<a key={cat.slug} href={`/blog?category=${cat.slug}`}>
{cat.name} ({cat.postCount})
</a>
))}
</nav>
{/* Tag Filter */}
<nav>
{tags.map(t => (
<a key={t.slug} href={`/blog?tag=${t.slug}`}>
#{t.name}
</a>
))}
</nav>
{/* Posts */}
{postsResult.data.map(post => (
<article key={post.slug}>
{post.featuredImageUrl && <img src={post.featuredImageUrl} alt={post.title} />}
<h2><a href={`/blog/${post.slug}`}>{post.title}</a></h2>
<p>{post.excerpt}</p>
<time>{new Date(post.publishedAt).toLocaleDateString()}</time>
{post.category && <span>{post.category.name}</span>}
{post.tags.map(t => <span key={t.slug}>#{t.name}</span>)}
</article>
))}
{/* Pagination */}
{postsResult.meta.totalPages > 1 && (
<nav>
{page > 1 && <a href={`/blog?page=${page - 1}`}>← Previous</a>}
<span>Page {postsResult.meta.page} of {postsResult.meta.totalPages}</span>
{page < postsResult.meta.totalPages && <a href={`/blog?page=${page + 1}`}>Next →</a>}
</nav>
)}
</div>
);
}Blog post detail page
// app/blog/[slug]/page.jsx
import { blogAPI } from "@/lib/blog-api";
import DOMPurify from "isomorphic-dompurify";
export default async function BlogPostPage({ params }) {
let post;
try {
post = await blogAPI.getPost(params.slug);
} catch (err) {
return <p>Post not found.</p>;
}
// Sanitize HTML content for safe rendering
const safeContent = DOMPurify.sanitize(post.content);
return (
<article>
{/* SEO-friendly header */}
<h1>{post.title}</h1>
<time>{new Date(post.publishedAt).toLocaleDateString()}</time>
{/* Category & Tags */}
{post.category && (
<a href={`/blog?category=${post.category.slug}`}>{post.category.name}</a>
)}
{post.tags.map(t => (
<a key={t.slug} href={`/blog?tag=${t.slug}`}>#{t.name}</a>
))}
{/* Featured Image */}
{post.featuredImageUrl && (
<img src={post.featuredImageUrl} alt={post.title} />
)}
{/* Content */}
<div dangerouslySetInnerHTML={{ __html: safeContent }} />
</article>
);
}Other languages
Python
import requests
API_KEY = "pk_live_abc123"
BASE_URL = "https://api.vlozi.app"
HEADERS = {"x-api-key": API_KEY}
# List posts with search
response = requests.get(
f"{BASE_URL}/blog/public/posts",
headers=HEADERS,
params={"search": "react", "sort": "title", "order": "asc", "limit": 5}
)
data = response.json()
for post in data["data"]:
category = post["category"]["name"] if post["category"] else "Uncategorized"
tags = ", ".join(f"#{t['name']}" for t in post["tags"])
print(f"[{category}] {post['title']} — {tags}")
print(f"Page {data['meta']['page']} of {data['meta']['totalPages']}")PHP
<?php
$apiKey = "pk_live_abc123";
$baseUrl = "https://api.vlozi.app";
// List posts filtered by category
$query = http_build_query([
"category" => "tutorials",
"limit" => 10,
"sort" => "publishedAt",
"order" => "desc",
]);
$response = file_get_contents(
"$baseUrl/blog/public/posts?$query",
false,
stream_context_create([
"http" => [
"header" => "x-api-key: $apiKey",
],
])
);
$result = json_decode($response, true);
foreach ($result["data"] as $post) {
echo $post["title"] . " — " . $post["excerpt"] . "\n";
}
// Get a single post
$slug = "getting-started-with-react";
$post = json_decode(file_get_contents(
"$baseUrl/blog/public/posts/$slug",
false,
stream_context_create(["http" => ["header" => "x-api-key: $apiKey"]])
), true);
echo $post["content"]; // HTML
?>TIP
Categories and tags endpoints return all items at once (not paginated). They're lightweight and designed for building navigation. You can cache them aggressively — they only change when you create or delete taxonomy in the dashboard.