Connect a custom backend

blog-maker can publish articles to any backend that exposes a JSON HTTP endpoint, not just WordPress. This page is the technical reference for connecting your own PHP, Node, or other server.

When to use this

Use a custom REST endpoint when your site runs on a proprietary CMS, a custom PHP backend, or anything else that is not WordPress/WooCommerce. Set it up once from your brand's dashboard, then every article publishes straight to your own storage.

Config schema

The connection is described by a small JSON config with these fields:

FieldTypeDescription
endpointstring, requiredThe URL we send the published post to.
method"POST" | "PUT" | "PATCH", optionalHTTP method for the request. Defaults to POST.
authobject, requiredHow we authenticate: bearer token, basic auth, a custom header, or none.
payloadTemplateJSON, requiredThe JSON body we send, with {{tokens}} filled in from the article.
successPathstring, optionalDotted path in the response that must be truthy for the publish to count as successful.
postIdPathstring, optional (default "id")Dotted path to the new post id in the response.
postUrlPathstring, optionalDotted path to the published URL in the response.
pingEndpointstring, optionalA separate URL used only for the connection test.
timeoutMsnumber, optional (default 30000)Request timeout in milliseconds.

Authentication

Four ways to authenticate, chosen per connection:

TypeFieldsSent asNote
bearertokenAuthorization: Bearer <token>Recommended.
basicusername, passwordAuthorization: Basic base64(user:pass)Legacy systems only.
headerheaderName, headerValue<headerName>: <headerValue>Any custom header your backend expects.
none(none)no auth headerFor endpoints without their own auth.

Available tokens

Use these inside your payload template; every string in the template is scanned and tokens are replaced with the article's data.

TokenDescription
{{id}}Numeric article id.
{{title}} / {{seoTitle}}SEO title.
{{seoDescription}}Meta description.
{{html}} / {{htmlContent}}Full HTML body.
{{slug}} / {{urlSlug}}URL slug.
{{primaryKeyword}}Focus keyword.
{{secondaryKeywords}}Additional keywords, comma-separated.
{{postId}}Post id, present only when updating an existing post.

Example payload template

{
  "title": "{{title}}",
  "slug": "{{slug}}",
  "html": "{{html}}",
  "metaDescription": "{{seoDescription}}",
  "primaryKeyword": "{{primaryKeyword}}"
}

Example request and expected response

POST https://example.com/api/receive-post
Content-Type: application/json
Accept: application/json
Authorization: Bearer sk_live_xxxxx

{
  "title": "How to set up blog automation",
  "slug": "how-to-set-up-blog-automation",
  "html": "<p>...</p>",
  "metaDescription": "Learn step by step how content automation works.",
  "primaryKeyword": "blog automation"
}
{
  "ok": true,
  "id": "post-123",
  "url": "https://example.com/blog/how-to-set-up-blog-automation"
}
  • successPath (e.g. "ok"): must be truthy, otherwise the publish is treated as failed.
  • postIdPath (default "id"): path to the post id, nested paths allowed, e.g. "data.id".
  • postUrlPath (optional): path to the published URL.

On error (no 2xx, or successPath falsy), the first characters of your response show up as the error message in the dashboard, so you can debug from there.

Receiver example

A ready-made minimal server you can hand to your developer. It checks the bearer token, stores the article, and responds in the expected shape.

PHP

<?php
// Minimal blog-maker custom_rest receiver.
// Expects: Authorization: Bearer <token>, JSON body per your payload template.

$expectedToken = getenv('BLOG_MAKER_TOKEN') ?: 'change-me';
$headers = getallheaders();
$auth = $headers['Authorization'] ?? '';

if ($auth !== "Bearer $expectedToken") {
    http_response_code(401);
    echo json_encode(['ok' => false, 'error' => 'Unauthorized']);
    exit;
}

$body = json_decode(file_get_contents('php://input'), true);
if (!$body || empty($body['title']) || empty($body['html'])) {
    http_response_code(400);
    echo json_encode(['ok' => false, 'error' => 'Missing title or html']);
    exit;
}

// postId is present on updates; create a new id otherwise.
$postId = $body['postId'] ?? bin2hex(random_bytes(6));
$posts = json_decode(@file_get_contents('posts.json') ?: '{}', true) ?: [];

$posts[$postId] = [
    'title' => $body['title'],
    'slug' => $body['slug'] ?? '',
    'html' => $body['html'],
    'metaDescription' => $body['metaDescription'] ?? '',
];

file_put_contents('posts.json', json_encode($posts));

echo json_encode([
    'ok' => true,
    'id' => $postId,
    'url' => "https://example.com/blog/{$body['slug']}",
]);

Node.js

// Minimal blog-maker custom_rest receiver (plain Node, no framework).
// Run with: node receiver.js
import { createServer } from "node:http";
import { randomBytes } from "node:crypto";

const TOKEN = process.env.BLOG_MAKER_TOKEN || "change-me";
const posts = new Map();

const server = createServer((req, res) => {
  if (req.method !== "POST" && req.method !== "PUT") {
    res.writeHead(405).end();
    return;
  }
  if (req.headers.authorization !== `Bearer ${TOKEN}`) {
    res.writeHead(401, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ ok: false, error: "Unauthorized" }));
    return;
  }

  let raw = "";
  req.on("data", (chunk) => (raw += chunk));
  req.on("end", () => {
    const body = JSON.parse(raw || "{}");
    if (!body.title || !body.html) {
      res.writeHead(400, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ ok: false, error: "Missing title or html" }));
      return;
    }

    // postId is present on updates; create a new id otherwise.
    const postId = body.postId || randomBytes(6).toString("hex");
    posts.set(postId, body);

    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({
      ok: true,
      id: postId,
      url: `https://example.com/blog/${body.slug}`,
    }));
  });
});

server.listen(3999, () => console.log("Receiver listening on :3999"));

Security

  • Credentials (tokens, passwords, header values) are encrypted at rest with AES-256-GCM.
  • A connection is scoped to your brand; only you or users with write access to that brand can create or test it.
  • We block requests to private and internal network addresses, including when your endpoint redirects to one.
  • Connection tests are rate-limited per user.
  • Custom REST endpoints are available on the Basic plan and above.