A fast, public, no-auth content API — power your blog on any stack without running WordPress.
Using Claude Code, Cursor, or another AI coding agent? Copy the prompt above, paste it into your agent, add your website slug — it has everything needed to wire up your blog.
https://api.seobeast.io/v1/publicNo authentication required. All endpoints are publicly accessible for published content only.
{
"success": true,
"data": { /* response data */ },
"meta": {
"websiteSlug": "example-blog",
"requestedAt": "2024-12-15T10:00:00Z",
"version": "v1"
}
}{
"success": false,
"error": "Error message",
"details": "Additional error details (if applicable)"
}/{websiteSlug}/postsGet a paginated list of published posts
| Parameter | Type | Description |
|---|---|---|
| page | integer | Page number (default: 1) |
| limit | integer | Posts per page, 1-50 (default: 10) |
| category | string | Filter by category slug |
| tag | string | Filter by tag slug |
| post_type | string | Filter by post type |
| search | string | Search in title and content (min 2 chars) |
| sort | string | Sort by: published_at, title, view_count |
| order | string | Sort order: asc, desc (default: desc) |
GET /v1/public/strong-curves/posts?limit=5&category=fitness/{websiteSlug}/posts/{slug}Get a single post by slug
| Parameter | Type | Description |
|---|---|---|
| include_content | boolean | Include full content (default: true) |
| include_related | boolean | Include related posts (default: false) |
| related_limit | integer | Number of related posts, 1-10 (default: 3) |
GET /v1/public/strong-curves/posts/getting-started?include_related=true/{websiteSlug}/categoriesGet list of categories with hierarchical structure
| Parameter | Type | Description |
|---|---|---|
| include_counts | boolean | Include post counts (default: true) |
| parent_id | string | Filter by parent category UUID |
| sort | string | Sort by: name, sort_order, post_count |
GET /v1/public/strong-curves/categories/{websiteSlug}/feedGet RSS, Atom, or JSON feed of published posts
| Parameter | Type | Description |
|---|---|---|
| format | string | Feed format: rss, atom, json (default: rss) |
| limit | integer | Max posts in feed, 1-50 (default: 20) |
| category | string | Filter by category slug |
| tag | string | Filter by tag slug |
GET /v1/public/strong-curves/feedGET /v1/public/strong-curves/feed?format=atomGET /v1/public/strong-curves/feed?format=json/{websiteSlug}/posts/{slug}/comments/{commentId}/reactionsAdd, update, or remove a reaction on a comment
{
"type": "like", // "like" or "dislike"
"visitorId": "visitor_123"
}{
"success": true,
"data": {
"message": "Reaction updated successfully",
"comment": { /* updated comment with new like_count */ },
"reaction": "like", // Current reaction or null if removed
"action": "added" // "added", "updated", or "removed"
}
}POST /v1/public/strong-curves/posts/hip-dips-guide/comments/c2f98dec-b3ea-4c9f-a336-26538a2c0b11/reactionsCurrently no rate limiting is enforced. This will be added in future versions with 1000 requests per hour for free tier.
// Fetch latest posts
const response = await fetch('/v1/public/strong-curves/posts?limit=5');
const data = await response.json();
if (data.success) {
// Process posts
// data.data.posts
// data.data.pagination.totalPages
}// app/blog/[slug]/page.tsx
export const revalidate = 600; // re-fetch every 10 minutes
export default async function PostPage({ params }) {
const { slug } = await params;
const res = await fetch(
`https://api.seobeast.io/v1/public/your-site/posts/${slug}`
);
const { data: post } = await res.json();
return <article>{/* render post.title, post.content */}</article>;
}---
// src/pages/blog/[slug].astro
const { slug } = Astro.params;
const res = await fetch(
`${import.meta.env.API_BASE}/v1/public/your-site/posts/${slug}`
);
const { data: post } = await res.json();
---
<article>
<h1>{post.title}</h1>
<Fragment set:html={post.contentHtml} />
</article><!-- pages/blog/[slug].vue -->
<script setup>
const route = useRoute();
const { data } = await useFetch(
`${useRuntimeConfig().public.apiBase}/v1/public/your-site/posts/${route.params.slug}`
);
const post = computed(() => data.value?.data);
</script>
<template>
<article><h1>{{ post.title }}</h1></article>
</template>// src/routes/blog/[slug]/+page.server.js
export async function load({ params, fetch }) {
const res = await fetch(
`https://api.seobeast.io/v1/public/your-site/posts/${params.slug}`
);
const { data: post } = await res.json();
return { post };
}import { useState, useEffect } from 'react';
function usePosts(websiteSlug, params = {}) {
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchPosts = async () => {
const searchParams = new URLSearchParams(params);
const response = await fetch(`/v1/public/${websiteSlug}/posts?${searchParams}`);
const data = await response.json();
if (data.success) {
setPosts(data.data.posts);
}
setLoading(false);
};
fetchPosts();
}, [websiteSlug, params]);
return { posts, loading };
}The read API is public and needs no key. Webhooks are the optional other half: SEO Beast calls your site the moment a post is published, updated, or deleted, so your cache clears instantly instead of waiting for a revalidation window. This is the only part of the integration that uses a secret.
Every webhook POST carries an X-Webhook-Signature header: an HMAC-SHA256 of the raw request body, keyed with your secret. The body includes a timestamp field — verify the signature, then reject anything older than a few minutes to prevent replays.
// app/api/webhooks/seobeast/route.ts
import crypto from "crypto";
export async function POST(request: Request) {
const secret = process.env.SEOBEAST_WEBHOOK_SECRET!;
const signature = request.headers.get("x-webhook-signature") ?? "";
const rawBody = await request.text();
// 1. Verify the HMAC signature (timing-safe)
const expected = crypto.createHmac("sha256", secret)
.update(rawBody).digest("hex");
const ok = signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!ok) return new Response("Invalid signature", { status: 401 });
const payload = JSON.parse(rawBody);
// 2. Replay protection: reject payloads older than 5 minutes.
// payload.timestamp is inside the signed body, so it can't be forged.
const sentAt = new Date(payload.timestamp).getTime();
if (!Number.isFinite(sentAt) || Math.abs(Date.now() - sentAt) > 5 * 60_000) {
return new Response("Stale webhook", { status: 401 });
}
// 3. Revalidate the affected content
// e.g. revalidateTag("blog-posts"); revalidatePath(`/blog/${payload.data.slug}`)
return new Response("ok");
}For more information or support, visit the API info endpoint for machine-readable API details.
Comments
/{websiteSlug}/posts/{slug}/commentsGet comments for a specific post with user reaction data
Query Parameters
Response Fields
like_count- Number of likes on the commentuser_reaction- Current user's reaction ("like", "dislike", or null)replies- Nested array of reply commentsExample
GET /v1/public/strong-curves/posts/hip-dips-guide/comments?visitorId=visitor_123/{websiteSlug}/posts/{slug}/commentsCreate a new comment on a post
Request Body
{ "content": "Great article! Very helpful.", "author_name": "John Doe", "author_email": "john@example.com", "visitorId": "visitor_123", "parent_id": null // Optional: UUID for reply comments }Example
POST /v1/public/strong-curves/posts/hip-dips-guide/comments