BlogProduce

BlogProduce Studio

İçerik operasyonlarını tek panelden yönetin

Developer Documentation

BlogProduce API

Generate AI-written, SEO-ready blog posts and serve them anywhere — your website, app or CMS. REST, JSON, cursor pagination, idempotent writes.

Base URLhttps://api.blogproduce.com

Overview

How the BlogProduce API works and what you can build with it.

The API is organized around REST. Every request and response is JSON. Successful responses share a single envelope:

Response envelope
{
  "success": true,
  "data": { ... }
}

Asynchronous content generation

Blog generation is asynchronous: you queue a job, poll its status, then publish and fetch the finished post. A typical integration is four calls:

1

Queue

POST /api/v1/posts:generate returns a jobId instantly.

2

Poll

GET /api/v1/jobs/{jobId} until status is "succeeded".

3

Publish

POST /api/v1/posts/{postId}/publish makes it live.

4

Serve

GET /api/v1/published/{slug}?format=html on your site.

API Keys

Create and manage the keys your integration uses.

API Anahtarların

Anahtarlar workspace'e bağlıdır. Secret anahtar yalnızca oluşturulduğu anda gösterilir.

Bu workspace için henüz API anahtarı yok.

Authentication

Authenticate every request with your secret API key.

Create a key from the panel above. The secret key (prefixed bg_secret_) is shown once at creation. Send it on every request — the recommended way is the Authorization header:

curl https://api.blogproduce.com/api/v1/posts \
  -H "Authorization: Bearer bg_secret_YOUR_KEY"
Keep your secret key server-side. Never ship it in browser JavaScript or mobile apps — anyone can read it there. For rendering published posts directly from the browser, use the Public API, which needs no key.

Keys are scoped to a workspace: every request only sees that workspace's posts. Revoking a key disables it immediately.

Quickstart

Generate, publish and fetch your first article in under five minutes.

const BASE_URL = 'https://api.blogproduce.com';
const API_KEY = process.env.BLOGPRODUCE_API_KEY; // bg_secret_...

async function api(path, options = {}) {
  const res = await fetch(BASE_URL + path, {
    ...options,
    headers: {
      'Authorization': 'Bearer ' + API_KEY,
      'Content-Type': 'application/json',
      ...options.headers,
    },
  });
  const body = await res.json();
  if (!res.ok || body.success === false) {
    throw new Error(body.message || 'Request failed: ' + res.status);
  }
  return body.data;
}

// 1) Queue a blog post
const { jobId } = await api('/api/v1/posts:generate', {
  method: 'POST',
  headers: { 'Idempotency-Key': 'first-post-001' },
  body: JSON.stringify({
    topic: 'How to choose a CRM for a small business',
    language: 'en',
    tone: 'professional',
    length: 'medium',
    model: 'pro',          // 'standard' | 'pro' | 'premium'
    seoMode: true,
  }),
});

// 2) Poll until the job finishes (typically 30-90 seconds)
let job;
do {
  await new Promise((r) => setTimeout(r, 4000));
  job = await api('/api/v1/jobs/' + jobId);
} while (job.status === 'queued' || job.status === 'running');

if (job.status !== 'succeeded') {
  throw new Error('Generation failed: ' + (job.error || 'unknown'));
}

// 3) Publish it
const published = await api('/api/v1/posts/' + job.postId + '/publish', {
  method: 'POST',
  body: JSON.stringify({}),
});

// 4) Fetch the finished article as HTML
const post = await api('/api/v1/published/' + published.slug + '?format=html');
console.log(post.title);
console.log(post.content_html);

Generate Content

Queue an AI blog generation job. Returns immediately with a job id.

POST/api/v1/posts:generate
Headers
Authorization
stringrequired
Bearer bg_secret_... — your secret API key.
Idempotency-Key
stringoptional
Recommended. Retrying with the same key and identical body within 24h returns the original response instead of creating a duplicate job.
Body parameters
topic
string (5-200 chars)required
What the article should be about. The more specific, the better the result.
language
stringoptional
ISO code: 'tr', 'en', 'de', 'fr', 'es' and 30+ more. Default: 'tr'.
tone
string (≤100 chars)optional
Writing tone, e.g. 'professional', 'friendly', 'authoritative'. Default: 'professional'.
length
stringoptional
'short' (~600-900 words), 'medium' (~1100-1500), 'long' (~1800-2600). Default: 'medium'.
model
stringoptional
Quality tier: 'standard' (fast), 'pro' (higher quality), 'premium' (Claude Opus — best long-form quality). Default: 'standard'.
seoMode
booleanoptional
Adds SEO optimization: meta tags, FAQ targeting long-tail queries, keyword placement, JSON-LD schema.
additionalContext
string (≤1000 chars)optional
Brand facts, product details or angle instructions the writer must respect.
targetAudience
string (≤100 chars)optional
Who the article is for, e.g. 'startup founders'.
keywords
string[]optional
Target keywords to weave in naturally.
outline
stringoptional
Optional outline to follow instead of letting the AI plan the structure.
coverImageUrl
stringoptional
Pre-selected cover image URL to attach to the post.
200 OK
{
  "success": true,
  "data": {
    "jobId": "3f6b2c1e-8a4d-4c2f-9e1b-7d5a2c8f4e6a",
    "status": "queued"
  }
}
Generation runs through a multi-stage pipeline (outline → draft → editorial pass) and typically completes in 30-90 seconds depending on length and tier.

Job Status

Poll a generation job until it finishes.

GET/api/v1/jobs/{jobId}
200 OK
{
  "success": true,
  "data": {
    "status": "succeeded",          // queued | running | succeeded | failed | cancelled
    "postId": "9c8d7e6f-...",       // present once the post exists
    "error": "..."                  // present only when status is "failed"
  }
}
Poll every 3-5 seconds. When status is succeeded, use postId to review, update or publish the post.

Posts

List, read, edit and publish the posts in your workspace.

List posts

GET/api/v1/posts
Query parameters
status
stringoptional
Filter by status: queued | generating | draft | review | published | failed.
limit
numberoptional
Items per page. Default 50, max 100.
cursor
stringoptional
Opaque cursor from the previous response for the next page.
200 OK
{
  "success": true,
  "data": {
    "posts": [
      {
        "id": "9c8d7e6f-...",
        "title": "How to Choose a CRM for a Small Business",
        "slug": "how-to-choose-a-crm-for-a-small-business",
        "excerpt": "...",
        "status": "draft",
        "language": "en",
        "coverImageUrl": null,
        "publishedAt": null,
        "createdAt": "2026-08-04T10:12:00.000Z",
        "updatedAt": "2026-08-04T10:13:30.000Z",
        "author": { "name": "...", "email": "..." },
        "tags": []
      }
    ],
    "cursor": "MjAyNi0wOC0wNFQxMDoxMjowMC4wMDBa",
    "hasMore": true
  }
}
Pass data.cursor back as ?cursor= to fetch the next page. When hasMore is false you have everything.

Get a post

GET/api/v1/posts/{id}?format=json|md|html
200 OK (format=html)
{
  "success": true,
  "data": {
    "id": "9c8d7e6f-...",
    "title": "...",
    "slug": "...",
    "excerpt": "...",
    "content": "<h1>...</h1><p>...</p>",     // in the requested format
    "contentFormat": "html",
    "content_html": "<h1>...</h1><p>...</p>", // always-available HTML rendering
    "status": "draft",
    "language": "en",
    "coverImageUrl": null,
    "publishedAt": null,
    "createdAt": "2026-08-04T10:12:00.000Z",
    "updatedAt": "2026-08-04T10:14:00.000Z",
    "author": { "name": "...", "email": "..." },
    "tags": [],
    "seo": {
      "metaTitle": "...",
      "metaDescription": "...",
      "ogTitle": "...",
      "ogDescription": "...",
      "schemaJsonld": { "@context": "https://schema.org" }
    }
  }
}

Update a post

PATCH/api/v1/posts/{id}
Body parameters
title
string (≤200)optional
New title.
excerpt
string (≤500)optional
New excerpt.
content
stringoptional
Replacement content (stored as a new revision).
slug
stringoptional
New slug — must be unique within the workspace.
Posts can only be updated while their status is draft, generated or review.

Delete a post

DELETE/api/v1/posts/{id}
Request / response
curl -X DELETE "https://api.blogproduce.com/api/v1/posts/POST_ID" \
  -H "Authorization: Bearer bg_secret_YOUR_KEY"

# 200 OK
# {
#   "success": true,
#   "data": { "id": "9c8d7e6f-...", "deleted": true }
# }
Deletion is permanent: the post, all of its revisions and its SEO metadata are removed and cannot be recovered. Usage records are kept for billing. A post that belongs to another project returns 404.

Publish a post

POST/api/v1/posts/{id}/publish
Request / response
curl -X POST "https://api.blogproduce.com/api/v1/posts/POST_ID/publish" \
  -H "Authorization: Bearer bg_secret_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "slug": "optional-custom-slug" }'

# 200 OK
# {
#   "success": true,
#   "data": {
#     "id": "9c8d7e6f-...",
#     "status": "published",
#     "slug": "how-to-choose-a-crm-for-a-small-business",
#     "publishedAt": "2026-08-04T10:20:00.000Z",
#     "message": "Post published successfully"
#   }
# }

Published Content

Fetch only live content — ideal for rendering your blog.

GET/api/v1/published?limit=50&cursor=...
GET/api/v1/published/{slug}?format=json|md|html
Next.js App Router example
// npm i isomorphic-dompurify
import DOMPurify from 'isomorphic-dompurify';

// app/blog/page.tsx — list published posts (server component)
export default async function BlogPage() {
  const res = await fetch('https://api.blogproduce.com/api/v1/published?limit=20', {
    headers: { Authorization: `Bearer ${process.env.BLOGPRODUCE_API_KEY}` },
    next: { revalidate: 300 }, // ISR: refresh every 5 minutes
  });
  const { data } = await res.json();
  const posts = data.posts; // data also carries { cursor, hasMore }

  return (
    <main>
      {posts.map((post) => (
        <a key={post.id} href={`/blog/${post.slug}`}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
        </a>
      ))}
    </main>
  );
}

// app/blog/[slug]/page.tsx — render a single post
export default async function PostPage({ params }) {
  const res = await fetch(
    `https://api.blogproduce.com/api/v1/published/${params.slug}?format=html`,
    {
      headers: { Authorization: `Bearer ${process.env.BLOGPRODUCE_API_KEY}` },
      next: { revalidate: 300 },
    },
  );
  const { data: post } = await res.json();

  // content_html is sanitized server-side, but sanitize again at the render
  // boundary — it is the one place that protects you regardless of what
  // reaches the content upstream.
  const html = DOMPurify.sanitize(post.content_html);

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: html }} />
    </article>
  );
}
content_html is sanitized server-side before delivery. The seo object contains ready-to-use meta tags and JSON-LD — wire them into your page <head> for full SEO benefit.

Public API (No Auth)

Read-only endpoints for client-side rendering — no API key required.

Safe to call directly from the browser: they expose only published posts of a project, addressed by its public project slug (shown in your workspace settings).

GET/api/v1/public/projects/{projectSlug}/posts?limit=50&cursor=...
GET/api/v1/public/projects/{projectSlug}/posts/{slug}?format=html
Browser example
// No key needed — safe for client-side code
const res = await fetch(
  'https://api.blogproduce.com/api/v1/public/projects/YOUR_PROJECT_SLUG/posts?limit=10',
);
const { data } = await res.json();

for (const post of data.posts) {
  console.log(post.title, post.slug, post.publishedAt);
}
// data.cursor + data.hasMore -> cursor pagination, same as the Posts API

Content Formats

Every post is stored canonically and converted on demand.

Endpoints that return full content accept a ?format= query parameter:

json
defaultoptional
Canonical structure: { "content": "<markdown>", "metadata": { "toc": [...], "faq": [...], "seo": {...} } }. Best when you want structured access to FAQ, table of contents and SEO fields.
md
markdownoptional
The article as plain Markdown — ideal for static site generators or storing in your own CMS.
html
htmloptional
Sanitized, render-ready HTML. Responses also always include content_html regardless of the requested format.

Errors

Standard HTTP status codes with a JSON body.

Error response
{
  "statusCode": 400,
  "message": "topic must be longer than or equal to 5 characters",
  "error": "Bad Request"
}
400
Bad Requestoptional
Validation failed — the message field lists what is wrong. Unknown body fields are also rejected.
401
Unauthorizedoptional
Missing, invalid, expired or revoked API key.
403
Forbiddenoptional
The key is valid but not allowed to perform this action.
404
Not Foundoptional
Resource does not exist or belongs to another workspace.
429
Too Many Requestsoptional
Rate limit exceeded — check the rate limit headers and retry with backoff.
500
Server Erroroptional
Something went wrong on our side. Safe to retry with the same Idempotency-Key.

Rate Limits

Per-key limits keep the platform fast for everyone.

Requests are rate-limited per API key. Current usage is reported on responses via headers:

Response headers
X-RateLimit-Limit: 150
X-RateLimit-Remaining: 149
When you receive 429, wait and retry with exponential backoff. Generation calls also count against your plan's monthly content quota — see the Usage page.