API reference

Raw HTTP API

Endpoints, query parameters, and response shapes for non-React clients.

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.app

Authentication

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/posts

Query 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/:slug

cURL 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/categories

cURL 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/tags

cURL 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.

Blog · API referenceEdit on GitHub