Keyword → Blog API

Guides

Examples

There is no official SDK package for this API today — every example below is plain fetch/requests/curl against the real /v1 endpoints documented in the API reference.

Agency / content-team batch workflow

Submitting several briefs as async jobs (POST /v1/jobs) instead of holding several synchronous connections open, with a per-brief idempotency key so a re-run after a partial failure never bills the same brief twice.

batch.js
// Content-team batch workflow: submit several posts as async jobs so you
// never hold N connections open, then let webhooks tell you when each is done.
// There is no official SDK — this is plain fetch() against the real API.

const KEYWORD_TO_BLOG_API_KEY = process.env.KEYWORD_TO_BLOG_API_KEY;
const BASE_URL = process.env.KEYWORD_TO_BLOG_BASE_URL ?? "https://keyword-to-blog-docs.vercel.app";

const briefs = [
  { keywords: ["ai content marketing"], topic: "AI content marketing for agencies" },
  { keywords: ["seo for saas"], topic: "SEO fundamentals for SaaS teams" },
  { keywords: ["content calendar template"], topic: "Building a quarterly content calendar" },
];

async function submitBatch(briefs) {
  const jobs = [];
  for (const brief of briefs) {
    const res = await fetch(`${BASE_URL}/v1/jobs`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${KEYWORD_TO_BLOG_API_KEY}`,
        "Content-Type": "application/json",
        // One key per brief, derived deterministically, so re-running this
        // script after a partial failure never re-bills work already done.
        "Idempotency-Key": `batch-${brief.keywords[0]}`,
      },
      body: JSON.stringify({
        generateRequest: {
          keywords: brief.keywords,
          topic: brief.topic,
          language: "en",
          tone: "professional",
          constraints: { maxWords: 1200, includeFAQs: true },
          format: { responseTypes: ["json", "markdown"] },
        },
        webhook: {
          url: "https://your-app.example.com/webhooks/keyword-to-blog",
          events: ["job.succeeded", "job.failed"],
        },
        format: { responseTypes: ["json", "markdown"] },
      }),
    });

    if (!res.ok) {
      const error = await res.json();
      console.error(`Failed to queue "${brief.topic}":`, error.error.code, error.error.message);
      continue;
    }

    const job = await res.json();
    jobs.push(job);
    console.log(`Queued ${job.jobId} for "${brief.topic}" — status: ${job.status}`);
  }
  return jobs;
}

await submitBatch(briefs);
// Each job's result now arrives at your webhook URL independently — see the
// "Developer pipeline" example below for verifying and handling that delivery.

Developer pipeline: queue → API → webhook

Your own worker enqueues a job, then a webhook receiver verifies the signature (see webhooks) before trusting the payload — including rejecting a stale/replayed delivery.

pipeline.js
// Developer pipeline: your own queue enqueues work, calls this API
// asynchronously, and reacts to the signed webhook when a job finishes.
// Node.js, plain fetch()/crypto — no framework assumed.

import { createHmac, timingSafeEqual } from "node:crypto";

const KEYWORD_TO_BLOG_API_KEY = process.env.KEYWORD_TO_BLOG_API_KEY;
const BASE_URL = process.env.KEYWORD_TO_BLOG_BASE_URL ?? "https://keyword-to-blog-docs.vercel.app";

// 1. Your own worker picks up a task and calls this API.
async function createGenerationJob(task) {
  const res = await fetch(`${BASE_URL}/v1/jobs`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${KEYWORD_TO_BLOG_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": task.id, // reuse YOUR task id as the idempotency key
    },
    body: JSON.stringify({
      generateRequest: task.generateRequest,
      webhook: { url: task.webhookUrl, events: ["job.succeeded", "job.failed"] },
      format: { responseTypes: ["json"] },
    }),
  });

  if (res.status === 429) {
    const { error } = await res.json();
    // RATE_LIMITED / QUOTA_EXCEEDED both carry Retry-After — respect it
    // instead of retrying immediately.
    const retryAfterSeconds = Number(res.headers.get("Retry-After") ?? "5");
    throw new Error(`${error.code}: retry after ${retryAfterSeconds}s`);
  }
  if (!res.ok) {
    const { error } = await res.json();
    throw new Error(`${error.code}: ${error.message}`);
  }
  return res.json(); // JobV1 — status is "queued" or already "succeeded"/"failed" if processed synchronously
}

// 2. Your webhook receiver verifies the signature before trusting the payload.
function verifyWebhookSignature(rawBody, signatureHeader, webhookSecret) {
  const match = /^t=(\d+),v1=([a-f0-9]+)$/i.exec((signatureHeader ?? "").trim());
  if (!match) return false;
  const [, timestamp, provided] = match;

  const ageSeconds = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
  if (ageSeconds > 5 * 60) return false; // reject replayed/stale deliveries

  const expected = createHmac("sha256", webhookSecret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");
  const expectedBuf = Buffer.from(expected, "hex");
  const providedBuf = Buffer.from(provided, "hex");
  return expectedBuf.length === providedBuf.length && timingSafeEqual(expectedBuf, providedBuf);
}

// 3. Example receiver (e.g. inside an Express/Next.js route handler).
async function handleWebhook(req, webhookSecretForThisJob) {
  const rawBody = await req.text();
  const signature = req.headers.get("x-ktb-signature");

  if (!verifyWebhookSignature(rawBody, signature, webhookSecretForThisJob)) {
    return new Response("invalid signature", { status: 401 });
  }

  const payload = JSON.parse(rawBody); // WebhookSucceededPayloadV1 | WebhookFailedPayloadV1
  if (payload.event === "job.succeeded") {
    await saveGeneratedPost(payload.jobId, payload.post, payload.rendered);
  } else {
    // payload.event === "job.failed" — payload.error.code is a normal ErrorCode
    await markTaskFailed(payload.jobId, payload.error);
  }
  return new Response("ok", { status: 200 });
}

Single request — curl / JavaScript / Python

The same synchronous call in each of the three most common client contexts. Credentials always come from an environment variable, never a literal in code.

curl
curl -X POST "$KEYWORD_TO_BLOG_BASE_URL/v1/generate" \
  -H "Authorization: Bearer $KEYWORD_TO_BLOG_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "keywords": ["ai content marketing"],
    "language": "en",
    "tone": "professional",
    "constraints": { "maxWords": 900 },
    "format": { "responseTypes": ["json", "markdown"] }
  }'
generate.js
const KEYWORD_TO_BLOG_API_KEY = process.env.KEYWORD_TO_BLOG_API_KEY;
const BASE_URL = process.env.KEYWORD_TO_BLOG_BASE_URL ?? "https://keyword-to-blog-docs.vercel.app";

const res = await fetch(`${BASE_URL}/v1/generate`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${KEYWORD_TO_BLOG_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({
    keywords: ["ai content marketing"],
    language: "en",
    tone: "professional",
    constraints: { maxWords: 900 },
    format: { responseTypes: ["json", "markdown"] },
  }),
});

if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${error.code}: ${error.message}`);
}

const { post, rendered } = await res.json();
console.log(post.title, rendered.markdown);
generate.py
import os
import uuid
import requests

API_KEY = os.environ["KEYWORD_TO_BLOG_API_KEY"]
BASE_URL = os.environ.get("KEYWORD_TO_BLOG_BASE_URL", "https://keyword-to-blog-docs.vercel.app")

response = requests.post(
    f"{BASE_URL}/v1/generate",
    headers={
        "Authorization": f"Bearer {API_KEY}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "keywords": ["ai content marketing"],
        "language": "en",
        "tone": "professional",
        "constraints": {"maxWords": 900},
        "format": {"responseTypes": ["json", "markdown"]},
    },
    timeout=60,
)

if not response.ok:
    error = response.json()["error"]
    raise RuntimeError(f"{error['code']}: {error['message']}")

body = response.json()
print(body["post"]["title"])
print(body["rendered"]["markdown"])