Keyword → Blog API

API reference

Webhooks

Jobs created via POST /v1/jobs deliver their result to the webhook URL you configured, instead of (or in addition to) polling.

Events

EventFired when
job.succeededThe job finished generating and result content is ready.
job.failedThe job could not complete — see the error object in the payload.

Headers

  • x-ktb-signature — `t=<unix timestamp>,v1=<HMAC-SHA256 hex digest>`, both in one header

Verifying a signature

Parse the timestamp and digest out of the x-ktb-signature header, reject it if the timestamp is more than 5 minutes old, then concatenate the timestamp, a period, and the raw request body, compute an HMAC-SHA256 digest using your webhook’s signing secret, and compare it to the provided digest using a constant-time comparison.

verify.js
const crypto = require("crypto");

// The header looks like: x-ktb-signature: t=1699999999,v1=<hex hmac>
function verifySignature(rawBody, signatureHeader, webhookSecret) {
  const match = /^t=(\d+),v1=([a-f0-9]+)$/i.exec(signatureHeader.trim());
  if (!match) return false;
  const [, timestamp, provided] = match;

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

  const signedPayload = `${timestamp}.${rawBody}`;
  const expected = crypto
    .createHmac("sha256", webhookSecret)
    .update(signedPayload)
    .digest("hex");

  const expectedBuf = Buffer.from(expected, "hex");
  const providedBuf = Buffer.from(provided, "hex");
  return (
    expectedBuf.length === providedBuf.length &&
    crypto.timingSafeEqual(expectedBuf, providedBuf)
  );
}

Payload — job.succeeded

json
{
  "event": "job.succeeded",
  "jobId": "job_5f2a9d3e1b",
  "requestId": "req_9f3a1c2e4b",
  "post": {
    "title": "AI Content Marketing for Small Businesses: A Practical Starting Point",
    "slugSuggestion": "ai-content-marketing-small-business",
    "meta": {
      "description": "A practical guide to using AI content marketing as a small business, without losing the voice your customers already trust.",
      "primaryKeyword": "ai content marketing"
    },
    "outline": {
      "h1": "AI Content Marketing for Small Businesses: A Practical Starting Point",
      "h2": [
        "Why small businesses are turning to AI content marketing",
        "Where AI helps most, and where it doesn't",
        "Keeping your brand voice consistent",
        "A simple weekly workflow",
        "Frequently asked questions"
      ]
    },
    "sections": [
      {
        "type": "introduction",
        "contentMarkdown": "Most small business owners don't have a marketing department. AI content marketing tools promise to fill that gap, but the real question isn't whether AI can write — it's whether it can write like *you*."
      },
      {
        "type": "body",
        "heading": "Why small businesses are turning to AI content marketing",
        "contentMarkdown": "Small business SEO used to mean choosing between hiring a writer or falling behind. AI content marketing changes that math by making a consistent publishing schedule achievable without a full-time hire."
      },
      {
        "type": "body",
        "heading": "Where AI helps most, and where it doesn't",
        "contentMarkdown": "AI is strongest at first drafts, outlines, and repetitive formats like FAQs. It's weakest at judgment calls that depend on knowing your actual customers — so keep a human review step for anything customer-facing."
      },
      {
        "type": "callout",
        "contentMarkdown": "A five-minute voice check before publishing catches most AI-sounding phrasing.",
        "callout": {
          "label": "Quick tip",
          "text": "Read the draft aloud. If it doesn't sound like something you'd actually say to a customer, rewrite that line."
        }
      },
      {
        "type": "body",
        "heading": "Keeping your brand voice consistent",
        "contentMarkdown": "Give the tool a short brand voice brief once, then reuse it: three adjectives, one thing you never say, and one real customer question you hear often."
      },
      {
        "type": "body",
        "heading": "A simple weekly workflow",
        "contentMarkdown": "Draft on Monday, review and edit Tuesday, publish Wednesday. Keeping a fixed cadence matters more than any individual post."
      },
      {
        "type": "faq",
        "heading": "Frequently asked questions",
        "contentMarkdown": ""
      },
      {
        "type": "conclusion",
        "contentMarkdown": "AI content marketing works best as a drafting partner, not a replacement for judgment. Start with one post a week and keep your own voice in the final pass."
      }
    ],
    "faqs": [
      {
        "question": "Will AI-written content hurt my SEO?",
        "answer": "Search engines rank content on quality and usefulness, not on how it was drafted. Editing for accuracy and voice before publishing is what matters."
      },
      {
        "question": "How much editing does an AI draft usually need?",
        "answer": "Expect to adjust specific claims, add real examples from your business, and tighten anything that reads generic."
      }
    ],
    "conclusion": "AI content marketing works best as a drafting partner, not a replacement for judgment. Start with one post a week and keep your own voice in the final pass.",
    "coverageNotes": {
      "keywordCoverage": [
        {
          "keyword": "ai content marketing",
          "covered": true,
          "evidence": "Appears in title, H1, and introduction."
        },
        {
          "keyword": "small business seo",
          "covered": true,
          "evidence": "Appears in the second H2 section."
        }
      ]
    }
  },
  "rendered": {
    "markdown": "# AI Content Marketing for Small Businesses: A Practical Starting Point\n\nMost small business owners don't have a marketing department..."
  },
  "quality": {
    "status": "pass",
    "score": 91,
    "revisionCount": 0,
    "qualityVersion": "1.0.0"
  }
}

Payload — job.failed

json
{
  "event": "job.failed",
  "jobId": "job_1a2b3c4d5e",
  "requestId": "req_2b3c4d5e6f",
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "constraints.maxWords exceeds the plan limit of 2000 words per request.",
    "details": {
      "field": "constraints.maxWords",
      "maxAllowed": 2000,
      "received": 5000
    }
  }
}