Connecting a Custom API
Complete guide to connecting any CMS or blog platform via Custom API, including required fields, request format, expected response structure, and troubleshooting tips.
Custom API Integration
ScaleBlogger's Custom API option lets you connect any CMS, blog platform, or content endpoint that accepts REST API requests. This guide covers the complete API contract your endpoint must implement, all content types ScaleBlogger sends, and how to render them.
Understanding the Two URL Fields
The Custom API form has two URL-related fields that serve different purposes:
| Field | What to enter | Example |
|---|---|---|
| Site URL | Your actual website address — the public URL where your blog or content lives. | https://myblog.com |
| Posts Endpoint | The API URL that receives article data — this is the technical endpoint ScaleBlogger sends requests to. | https://api.myblog.com/v1/posts or a webhook URL |
Key point: Do not put your API URL in the Site URL field. The Site URL is your public website; the Posts Endpoint is where ScaleBlogger sends article data.
How they interact:
- If you provide a Posts Endpoint, ScaleBlogger sends articles directly to that URL. The Site URL is stored for reference but is not used for publishing.
- If you leave the Posts Endpoint blank, ScaleBlogger falls back to
{site_url}/posts.
Impact on published article URLs: The Site URL does not determine the URL of published articles. The published URL comes entirely from what your API returns in its response (the url, link, or permalink field).
All Connection Fields
Required Fields
| Field | Description |
|---|---|
| Connection Name | A friendly label for this connection (e.g., "My Blog API") |
| Site URL | Your website's public URL (e.g., https://myblog.com). This is your actual website address, not the API endpoint. |
| API Token / Bearer Token | Your API authentication token. Sent as a Bearer token in the Authorization header. |
Optional Fields
| Field | Default | Description |
|---|---|---|
| Posts Endpoint | {site_url}/posts |
The full API URL where ScaleBlogger sends article data. |
| HTTP Method | POST |
The HTTP method for creating new articles (POST or PUT). |
| Custom Headers (JSON) | None | Additional HTTP headers as a JSON object. Example: {"X-Custom-Header": "value"} |
API Contract — Three Routes Your Endpoint Must Support
ScaleBlogger uses three HTTP methods against your endpoint. All three must be implemented for the full publishing workflow to function correctly.
1. GET — Connection Test & Slug Lookup
ScaleBlogger sends a GET request with a slug query parameter to test the connection and to check for existing articles before publishing.
GET {endpoint}?slug=your-article-slug
Authorization: Bearer {your_api_token}
Expected response:
- 200 OK with a JSON array of matching posts, or an empty array
[]if no match. - Each item in the array should include at least
idandslug.
// No match (slug is available):
[]
// Match found (article already exists):
[{"id": "abc-123", "slug": "your-article-slug"}]
When this is called:
- During the connection test (Step 1 in setup) — ScaleBlogger sends
?slug=__scaleblogger_connection_test__to verify connectivity and authentication. Your endpoint should return[]for this test slug. - Before publishing, to check if an article with the same slug already exists (slug collision detection).
2. POST — Create Article
ScaleBlogger sends a POST request to create a new article.
POST {endpoint}
Authorization: Bearer {your_api_token}
Content-Type: application/json
{
"title": "Your Article Title",
"content": "<p>Full HTML content...</p>",
"slug": "your-article-title",
"status": "published",
"excerpt": "Brief summary or meta description",
"meta_description": "SEO meta description text",
"featured_image_url": "https://example.com/image.jpg",
"author": "Jane Smith",
"seo_title": "SEO-Optimized Title (2026)",
"og_title": "SEO-Optimized Title (2026)",
"og_image": "https://example.com/social-card.jpg",
"canonical_url": null,
"schema_markup": {
"@context": "https://schema.org",
"@graph": [
{ "@type": "Article", "headline": "..." },
{ "@type": "BreadcrumbList", "itemListElement": [] },
{ "@type": "Organization", "name": "..." }
]
},
"tags": ["seo", "content marketing", "blogging"],
"categories": ["Marketing"],
"focus_keywords": ["seo", "content marketing"]
}
| Field | Type | Description |
|---|---|---|
title | string | The article title |
content | string | Full article body as rich HTML (see Content Types below) |
slug | string | URL-friendly slug for the article |
status | string | Always "published" |
excerpt | string | Short summary derived from the meta description (may be empty) |
meta_description | string | SEO meta description |
featured_image_url | string | null | URL of the hero/featured image, or null if none |
author | string | null | Article author name — use for bylines and E-E-A-T signals. Sourced from the draft's default author or the CMS connection's default author. |
seo_title | string | null | SEO-optimized title (may differ from title) — use for <title> tag |
og_title | string | null | Open Graph title for social sharing — use for <meta property="og:title"> |
og_image | string | null | Open Graph image URL for social cards — use for <meta property="og:image"> |
canonical_url | string | null | Canonical URL for syndicated content — use for <link rel="canonical">. Currently null (reserved for future use). |
schema_markup | object | null | JSON-LD structured data (Schema.org). May be a single schema object or a @graph array containing Article, BreadcrumbList, FAQPage, HowTo, and Organization schemas. Inject as <script type="application/ld+json">. |
tags | string[] | Combined target keywords and focus keywords (deduplicated) |
categories | string[] | Category names derived from the content cluster. Auto-create categories if they don't exist in your CMS. |
focus_keywords | string[] | Primary SEO keywords the article is optimized for |
Expected response (201 or 200):
{
"id": "abc-123",
"url": "https://myblog.com/blog/your-article-title"
}
ScaleBlogger checks for url, link, or permalink (in that order) to record the published URL. The id is stored for future updates.
3. PUT — Update Article
When ScaleBlogger updates a previously published article, it sends a PUT request to {endpoint}/{post_id}.
PUT {endpoint}/{post_id}
Authorization: Bearer {your_api_token}
Content-Type: application/json
{
"content": "<p>Updated HTML content...</p>",
"excerpt": "Updated excerpt",
"featured_image_url": "https://example.com/new-image.jpg",
"author": "Jane Smith",
"seo_title": "Updated SEO Title (2026)",
"og_title": "Updated SEO Title (2026)",
"og_image": "https://example.com/new-social-card.jpg",
"schema_markup": { "@context": "https://schema.org", "@type": "Article", "headline": "..." },
"tags": ["seo", "content marketing"],
"focus_keywords": ["seo", "content marketing"]
}
Note: title, slug, categories, and canonical_url are not included in update requests — only the content body, images, author, and SEO metadata are sent.
Expected response (200):
{
"id": "abc-123",
"url": "https://myblog.com/blog/your-article-title"
}
Important for Supabase Edge Functions: If your endpoint is a Supabase Edge Function, the post ID will appear in the URL path as /functions/v1/your-function-name/{post_id}. Your path parsing must extract the ID from the segment immediately after the function name — there is no /posts/ segment in the URL.
How to Use the SEO Fields
ScaleBlogger generates SEO, GEO, and AISEO-optimized content. Use these fields in your blog template to maximize search visibility:
HTML Head Tags
<!-- Page title (use seo_title if available, fall back to title) -->
<title>{{ seo_title || title }}</title>
<meta name="description" content="{{ meta_description }}">
<!-- Open Graph tags for social sharing -->
<meta property="og:title" content="{{ og_title || seo_title || title }}">
<meta property="og:description" content="{{ meta_description }}">
<meta property="og:image" content="{{ og_image || featured_image_url }}">
<!-- Canonical URL (only if provided) -->
{% if canonical_url %}
<link rel="canonical" href="{{ canonical_url }}">
{% endif %}
<!-- JSON-LD structured data -->
{% if schema_markup %}
<script type="application/ld+json">
{{ schema_markup | json }}
</script>
{% endif %}
Categories and Tags
The categories array contains category names (e.g., ["Web Development"]). Auto-create categories in your CMS if they don't already exist. The tags array contains deduplicated keywords from the article's target keywords and focus keywords — use these for tag clouds, related articles, or internal search.
Field Availability
All SEO fields are always present in the payload but may be null or empty arrays. Your API should accept and store them gracefully. For example:
seo_titleandog_titlemay benull— fall back totitleog_imagemay benull— fall back tofeatured_image_urlcanonical_urlis currently alwaysnull(reserved for future syndication support)schema_markupmay benullif no structured data was generatedtags,categories, andfocus_keywordsmay be empty arrays[]authormay benullif no default author is configured — use your own fallback or leave blank
What ScaleBlogger Sends — Rich Content Types
The content field contains full HTML that may include a variety of rich content types beyond plain text. Your website's blog template should be prepared to render all of these.
Top-Level Fields vs. HTML Content
The featured_image_url is a separate top-level field (not embedded in the HTML). Your template should render it above or alongside the article. Everything else below is embedded inside the content HTML string.
Text Content
- Headings:
<h2>through<h6>tags for section structure - Paragraphs:
<p>tags, may contain inline<strong>,<em>,<code>, and<a>links - Lists:
<ul>and<ol>with<li>items
Images (Inline)
Inline images appear inside the article body as:
<figure>
<img src="https://..." alt="description" />
<figcaption>Optional caption text</figcaption>
</figure>
These are different from the featured_image_url field. A single article may contain multiple inline images throughout the text.
Video Embeds
YouTube and other video embeds appear as:
<div class="video-embed" data-video-id="dQw4w9WgXcQ"
style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden">
<iframe src="https://www.youtube.com/embed/dQw4w9WgXcQ"
style="position:absolute;top:0;left:0;width:100%;height:100%"
frameborder="0" allowfullscreen></iframe>
</div>
Alternatively, if a custom embed HTML was provided, the raw embed HTML is used directly. The data-video-id attribute contains the YouTube video ID.
Quiz Embeds
Interactive quiz placeholders appear as:
<div class="quiz-embed" data-quiz-id="quiz-abc-123"></div>
Your frontend should use the data-quiz-id to load and render the quiz interactively (e.g., via JavaScript). If your site doesn't support quizzes, you can safely hide or ignore these elements.
Infographic Embeds
Data visualizations and infographics appear as:
<figure class="infographic">
<img src="https://..." alt="Infographic description" />
</figure>
Downloadable Templates
Downloadable files (spreadsheets, checklists, PDFs, etc.) appear as:
<div class="template-download">
<a href="https://..." target="_blank" rel="noopener">Download Template</a>
</div>
Tables
Data tables appear as standard HTML:
<table class="content-table">
<thead>
<tr><th>Header 1</th><th>Header 2</th></tr>
</thead>
<tbody>
<tr><td>Cell 1</td><td>Cell 2</td></tr>
</tbody>
</table>
Code Blocks
Code snippets with optional syntax highlighting:
<pre><code class="language-javascript">
const greeting = "Hello, world!";
</code></pre>
The class="language-{lang}" attribute indicates the programming language for syntax highlighting (e.g., via Prism.js or Highlight.js).
Blockquotes
Quotes with optional attribution:
<blockquote>
<p>Quote text here.</p>
<cite>Author Name</cite>
</blockquote>
Callouts & Quick Answers
Highlighted info boxes and quick-answer summaries:
<div class="callout callout-info">
<p>Important information here.</p>
</div>
<div class="callout callout-info" data-section-type="quick-answer">
<p><strong>Quick answer text</strong></p>
</div>
The callout-info, callout-warning, callout-tip classes indicate the callout style.
Recommended CSS for Rich Content
Add these styles to your blog template so embedded content renders correctly:
/* Video embeds - responsive 16:9 */
.video-embed {
position: relative;
padding-bottom: 56.25%;
height: 0;
overflow: hidden;
margin: 1.5rem 0;
}
.video-embed iframe {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
}
/* Quiz placeholders */
.quiz-embed {
border: 2px dashed #ccc;
padding: 2rem;
text-align: center;
margin: 1.5rem 0;
border-radius: 8px;
background: #f9f9f9;
}
/* Infographics */
.infographic img {
max-width: 100%;
height: auto;
margin: 1.5rem 0;
}
/* Downloadable templates */
.template-download {
background: #f0f7ff;
border: 1px solid #cce0ff;
border-radius: 8px;
padding: 1rem 1.5rem;
margin: 1.5rem 0;
}
.template-download a {
font-weight: 600;
text-decoration: none;
}
/* Tables */
.content-table {
width: 100%;
border-collapse: collapse;
margin: 1.5rem 0;
}
.content-table th, .content-table td {
border: 1px solid #ddd;
padding: 0.75rem;
text-align: left;
}
.content-table thead th {
background: #f5f5f5;
font-weight: 600;
}
/* Callouts */
.callout {
border-left: 4px solid #3b82f6;
background: #eff6ff;
padding: 1rem 1.5rem;
margin: 1.5rem 0;
border-radius: 0 8px 8px 0;
}
.callout-warning {
border-left-color: #f59e0b;
background: #fffbeb;
}
.callout-tip {
border-left-color: #10b981;
background: #ecfdf5;
}
/* Inline images */
figure {
margin: 1.5rem 0;
}
figure img {
max-width: 100%;
height: auto;
border-radius: 8px;
}
figcaption {
font-size: 0.875rem;
color: #666;
margin-top: 0.5rem;
text-align: center;
}
/* Code blocks */
pre {
background: #1e1e1e;
color: #d4d4d4;
padding: 1rem;
border-radius: 8px;
overflow-x: auto;
margin: 1.5rem 0;
}
code {
font-family: "Fira Code", "Consolas", monospace;
font-size: 0.9em;
}
How Authentication Works
ScaleBlogger sends two default headers with every request (GET, POST, and PUT):
Content-Type: application/jsonAuthorization: Bearer {your_api_token}
Custom headers you define are merged on top, so you can override these defaults or add additional headers.
Quick Setup Example
If your website is https://myblog.com and you have a webhook at https://hooks.example.com/api/posts:
- Connection Name: My Blog
- Site URL:
https://myblog.com - API Token: your-api-key-here
- Posts Endpoint:
https://hooks.example.com/api/posts - HTTP Method: POST
Testing Your Connection
After entering your credentials, click Test & Continue. ScaleBlogger will:
- Send
GET {endpoint}?slug=__scaleblogger_connection_test__ - Expect a 200 OK response with an empty array
[]
Make sure your API:
- Is accessible from the internet (not behind a firewall or VPN)
- Accepts the
Authorization: Bearer {token}header - Handles the
GET ?slug=...route and returns a JSON array - Returns 200 for unknown slugs with an empty
[]
Troubleshooting
Connection test fails (Step 1)
- Verify your Posts Endpoint URL is correct and publicly accessible
- Check that your API token is valid
- Ensure your endpoint handles
GETrequests with aslugquery parameter and returns a JSON array
Publishing defaults test fails (Step 2)
- This step tests the full create flow. Ensure your endpoint handles
POSTrequests - Verify your API returns JSON with at least an
idfield - Check that your API accepts
Content-Type: application/json
Articles fail to publish
- Verify your API can handle all the fields in the POST request payload (including SEO fields like
schema_markup,tags,categories) - The
contentfield may be very large (full HTML articles). Ensure your endpoint doesn't have a strict request size limit - Ensure your API returns JSON with an
idfield so ScaleBlogger can track the article for updates - If your API has strict schema validation, make sure it accepts the new SEO fields or ignores unknown fields
Published URL not showing in ScaleBlogger
- Make sure your API response includes a
url,link, orpermalinkfield with the full public URL - The published URL comes from your API response, not from the Site URL field
Updates not working
- Ensure your API supports
PUTrequests to{endpoint}/{id} - The post ID is appended directly to your endpoint URL. If using Supabase Edge Functions, extract the ID from the path segment after the function name
- Verify the post ID returned by your API is stable and can be used in URLs
Rich content not rendering
- The
contentfield is raw HTML — render it directly (with appropriate sanitization) - Add the recommended CSS classes above to style embedded content
- For code highlighting, integrate a library like Prism.js or Highlight.js
- For quiz embeds, implement a JavaScript handler that reads
data-quiz-idand loads the quiz
SEO fields not appearing
- All SEO and metadata fields (
author,seo_title,og_title,og_image,canonical_url,schema_markup,tags,categories,focus_keywords) are always present in the payload - Fields may be
nullor empty arrays — handle gracefully with fallbacks - If your database schema predates the SEO update, add the new columns (see Recommended Database Schema below)
Recommended Database Schema
If you are building a webhook to receive ScaleBlogger articles, here is a recommended database table structure:
CREATE TABLE blog_posts (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
title TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
content TEXT, -- Full HTML body
excerpt TEXT, -- Short summary
meta_description TEXT, -- SEO meta description
featured_image_url TEXT, -- Hero image URL
seo_title TEXT, -- SEO-optimized title for <title> tag
og_title TEXT, -- Open Graph title for social sharing
og_image TEXT, -- Open Graph image URL for social cards
canonical_url TEXT, -- Canonical URL (for syndicated content)
schema_markup JSONB, -- JSON-LD structured data (Schema.org)
tags TEXT[] DEFAULT '{}'::TEXT[], -- Keywords and tags
categories TEXT[] DEFAULT '{}'::TEXT[], -- Category names
focus_keywords TEXT[] DEFAULT '{}'::TEXT[], -- Primary SEO keywords
author TEXT,
published_at TIMESTAMPTZ DEFAULT now(),
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
The content column stores the full HTML including all embedded elements (images, videos, quizzes, tables, etc.). Your blog template reads and renders this HTML directly. The SEO columns enable your template to render proper <title>, Open Graph, and JSON-LD tags for search engine and social media optimization.
Complete Webhook Example (Supabase Edge Function)
Here is a full working example of a Supabase Edge Function that implements all three routes:
import { serve } from "https://deno.land/std/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
);
const API_KEY = Deno.env.get("SCALEBLOGGER_API_KEY") || "";
const SITE_URL = (Deno.env.get("SITE_URL") || "https://yoursite.com").replace(/\/$/, "");
const json = (data: unknown, status = 200) =>
new Response(JSON.stringify(data), {
status,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "authorization, content-type",
"Access-Control-Allow-Methods": "GET, POST, PUT, OPTIONS",
},
});
serve(async (req) => {
// Handle CORS preflight
if (req.method === "OPTIONS") return json({ ok: true });
// Authenticate
const authHeader = req.headers.get("authorization") || "";
const token = authHeader.replace("Bearer ", "");
if (!token || token !== API_KEY) {
return json({ error: "unauthorized" }, 401);
}
const url = new URL(req.url);
// Extract post ID from path:
// URL structure: /functions/v1/your-function-name/{id}
const pathParts = url.pathname.split("/").filter(Boolean);
const funcIndex = pathParts.indexOf("your-function-name");
const postId = funcIndex >= 0 && funcIndex + 1 < pathParts.length
? pathParts[funcIndex + 1]
: null;
try {
// GET ?slug=... - Connection test & slug lookup
if (req.method === "GET") {
const slug = url.searchParams.get("slug");
if (!slug) return json({ error: "slug query param required" }, 400);
const { data } = await supabase
.from("blog_posts")
.select("id, slug")
.eq("slug", slug);
return json(data || []);
}
// POST - Create article
if (req.method === "POST") {
const body = await req.json();
const { data, error } = await supabase
.from("blog_posts")
.insert({
title: body.title,
slug: body.slug,
content: body.content,
excerpt: body.excerpt || "",
meta_description: body.meta_description || "",
featured_image_url: body.featured_image_url || null,
seo_title: body.seo_title || null,
og_title: body.og_title || null,
og_image: body.og_image || null,
canonical_url: body.canonical_url || null,
schema_markup: body.schema_markup || null,
tags: body.tags || [],
categories: body.categories || [],
focus_keywords: body.focus_keywords || [],
author: body.author || null,
published_at: new Date().toISOString(),
})
.select("id, slug")
.single();
if (error) throw error;
return json({ id: data.id, url: `${SITE_URL}/blog/${data.slug}` }, 201);
}
// PUT /{id} - Update article
if (req.method === "PUT") {
if (!postId) return json({ error: "post ID required in path" }, 400);
const body = await req.json();
const updates: Record<string, unknown> = {
updated_at: new Date().toISOString(),
};
// Accept all fields ScaleBlogger may send
for (const key of [
"title", "content", "slug", "excerpt",
"featured_image_url", "meta_description",
"seo_title", "og_title", "og_image",
"canonical_url", "schema_markup",
"tags", "categories", "focus_keywords",
"author", "published_at"
]) {
if (body[key] !== undefined) updates[key] = body[key];
}
const { data, error } = await supabase
.from("blog_posts")
.update(updates)
.eq("id", postId)
.select("id, slug")
.single();
if (error) throw error;
return json({ id: data.id, url: `${SITE_URL}/blog/${data.slug}` });
}
return json({ error: "method not allowed" }, 405);
} catch (err) {
return json({ error: err.message }, 500);
}
});
Remember: Replace your-function-name with your actual Edge Function name when extracting the post ID from the URL path. Set the SITE_URL environment variable in your Supabase project to your public website URL (e.g., https://myblog.com) so the webhook returns absolute URLs.
Content Types Summary
Here is a quick reference of every HTML structure ScaleBlogger may embed in the content field:
| Content Type | HTML Element / Class | Key Attributes |
|---|---|---|
| Headings | <h2> – <h6> | Optional id for anchor links |
| Paragraphs | <p> | May contain <strong>, <em>, <a>, <code> |
| Lists | <ul>, <ol> | — |
| Inline Images | <figure> <img> | src, alt, optional <figcaption> |
| Video Embeds | <div class="video-embed"> | data-video-id, contains <iframe> |
| Quiz Embeds | <div class="quiz-embed"> | data-quiz-id |
| Infographics | <figure class="infographic"> | Contains <img> with src, alt |
| Templates | <div class="template-download"> | Contains download <a> link |
| Tables | <table class="content-table"> | Standard <thead>/<tbody> structure |
| Code Blocks | <pre><code> | Optional class="language-{lang}" |
| Blockquotes | <blockquote> | Optional <cite> for attribution |
| Callouts | <div class="callout"> | callout-info, callout-warning, callout-tip |
| Quick Answers | <div class="callout" data-section-type="quick-answer"> | Bold summary text |