Keyword → Blog API

API reference

Content tokens — pull feed

A content token is a pre-configured destination feed — one token_id, one pull URL. Under that single token, one or more keyword rules can be configured, each with its own keywords and its own generation frequency (e.g. “tech news” every 2 hours and “evergreen tips” once a day) — all of them queue into the same feed. Your integration polls one endpoint, posts whatever it gets back, and then confirms it — pulling content does not mean it was used.

How delivery works: reserve, confirm, reject, or auto-abandon

Pulling an item puts it in a reserved state, not a used one. By default, calling the pull endpoint again resends that exact same item — simple, naive polling is always safe and will never silently skip content just because you called twice. There are four ways a reserved item stops being resent:

  • Confirm (accept) it — call POST .../items/{item_id}/confirm after you’ve actually posted it. This is the only way an item is marked permanently used.
  • Reject it — call POST .../items/{item_id}/reject if you’ve decided not to post it. This abandons it for good without fetching a replacement in the same call — the next pull call will claim a fresh item.
  • Reject-and-fetch-next in one call — pass ?next=true on the pull call itself to do both the rejection and the next claim atomically. Equivalent to calling reject then pulling again, just one round trip.
  • Auto-abandoned after 3 sends — if the same item gets resent 3 times without ever being confirmed or rejected, the next pull call automatically gives up on it and hands you a new item instead. This is a safety net for a consumer that polls but never confirms, or crashes mid-flow — it requires no action on your part.

Two concurrent or retried calls for the same token can never both claim different unused items out from under each other, nor double-confirm/double-reject the same one — every state transition happens in one atomic database transaction.

Pull the current (or next) article

GET/v1/content-tokens/{token_id}/latest

No Authorization header is needed — the token_id in the path is itself the secret. Treat your pull URL the same way you’d treat a webhook URL or an API key: don’t share it or commit it to a public repo. Add ?next=true to abandon whatever’s currently reserved and fetch the next item instead.

bash
curl ${NEXT_PUBLIC_API_BASE_URL}/v1/content-tokens/YOUR_TOKEN_ID/latest

Response — content available

200 — available
{
  "available": true,
  "item": {
    "id": "cti_a1b2c3",
    "post": {
      "title": "...",
      "slugSuggestion": "...",
      "sections": [
        "..."
      ],
      "conclusion": "..."
    },
    "rendered": {
      "markdown": "# ...",
      "html": "<h1>...</h1>"
    },
    "generatedAt": "2026-10-05T09:00:00.000Z",
    "deliveryCount": 1
  }
}

deliveryCount is how many times this exact item has been handed out so far (including this call) — it resets to 1 whenever a new item is claimed, and hits 3 right before the auto-abandon fallback kicks in on the next pull.

Response — nothing new yet

Still 200 OK — this is a normal, expected response, not an error. It means there’s nothing reserved for you right now and generation hasn’t produced anything new since your last pull (or isn’t due yet per the configured frequency). Wait and poll again later.

200 — nothing new
{
  "available": false
}

Response — unknown token

404
{
  "error": {
    "code": "NOT_FOUND",
    "message": "Unknown content token."
  }
}

Pull everything at once

GET/v1/content-tokens/{token_id}/items

Returns every pending article on the token in one call (up to 100), across all keyword rules, instead of one at a time. Each article is labeled with ruleId and ruleName. Add ?rule=TELLO (rule name, case-insensitive, or rule id) to get only one rule’s articles.

This uses the same lifecycle as /latest: every returned article is reserved and must be confirmed or rejected individually. Calling it again resends the ones you haven’t confirmed or rejected, plus any new ones, and the 3-sends auto-drop applies per article. Both modes share the same state, so you can mix them.

bash
curl "${NEXT_PUBLIC_API_BASE_URL}/v1/content-tokens/YOUR_TOKEN_ID/items?rule=TELLO"
200 — all pending
{
  "count": 1,
  "limit": 100,
  "items": [
    {
      "id": "cti_a1b2c3",
      "ruleId": "ctkr_x9y8z7",
      "ruleName": "TELLO",
      "post": {
        "title": "..."
      },
      "rendered": {
        "markdown": "# ...",
        "html": "<h1>...</h1>"
      },
      "generatedAt": "2026-10-05T09:00:00.000Z",
      "deliveryCount": 1
    }
  ]
}

Confirm an item was posted

POST/v1/content-tokens/{token_id}/items/{item_id}/confirm

Call this after you’ve actually published the item returned by the pull endpoint. Same auth model — the token_id in the path is the secret, no body required. Confirming an already-confirmed item is safe and still returns 200 (idempotent). Confirming an item that’s no longer the active reserved one (already superseded by a ?next=true call or the 3-send auto-abandon) returns 409 — there’s nothing left to confirm.

bash
curl -X POST ${NEXT_PUBLIC_API_BASE_URL}/v1/content-tokens/YOUR_TOKEN_ID/items/ITEM_ID/confirm
200 — confirmed
{
  "ok": true,
  "item": {
    "id": "cti_a1b2c3",
    "status": "used"
  }
}

Reject an item

POST/v1/content-tokens/{token_id}/items/{item_id}/reject

Call this when you’ve decided not to post the item returned by the pull endpoint — e.g. it doesn’t fit your editorial needs this cycle. Unlike pulling with ?next=true, this does not fetch a replacement in the same call; it only abandons the current one, and the next pull call claims a fresh item. Same auth model, same idempotency rules as confirm: rejecting an already-rejected item returns 200, rejecting an item that’s already confirmed used (or was never reserved) returns 409.

bash
curl -X POST ${NEXT_PUBLIC_API_BASE_URL}/v1/content-tokens/YOUR_TOKEN_ID/items/ITEM_ID/reject
200 — rejected
{
  "ok": true,
  "item": {
    "id": "cti_a1b2c3",
    "status": "rejected"
  }
}

Polling guidance

Poll on a schedule somewhat more frequent than the token’s configured generation frequency (e.g. every 15–30 minutes for an hourly feed) so you pick up new content promptly without hammering the endpoint. There’s no rate limit tied to a customer plan here — this is an internal delivery feed, not the metered /v1/generate//v1/jobs API — but please still poll reasonably, not in a tight loop, and confirm (or call with ?next=true) once you’re done with an item rather than relying on the 3-send fallback as your normal flow.

How content is generated

Each rule’s configured keywords (and any other generation settings) run through the exact same pipeline as /v1/generate — full source retrieval, freshness/relevance/quality/deduplication/evidence validation for current-event topics, originality and no-link checks, the Anthropic generation step, and final quality validation. A brand-new rule generates its first item right away in the background; after that it follows its own configured frequency via the scheduled cron job, so a pull never has to wait on generation. See the content quality pipeline docs for the full flow.

Offer & brand links

A keyword rule can carry a list of offers/brands, each with a link. Every generated piece mentions them by name, and the first mention of each is hyperlinked to its configured URL. Any offer the article doesn’t mention naturally is added to a short “Featured offers” list at the end, so every configured link always appears exactly once.

  • Links appear in rendered.html and rendered.markdown. The structured post JSON stays link-free.
  • In rendered.html, links carry rel="sponsored noopener" and open in a new tab, as Google requires for affiliate/paid links. Markdown can’t express rel — if you publish from rendered.markdown, add rel="sponsored" in your own renderer.
  • Only the configured URLs can ever appear — the generator itself never writes links.

Configuration

Tokens and their keyword rules are configured by us internally (Dashboard → Content tokens) — there is no public endpoint to create or modify one. A single token can hold several keyword rules, each with its own keywords and frequency; pausing a rule stops just that keyword group, while pausing the token itself stops every rule under it (already-queued items remain pullable either way). If you need a new feed, an additional keyword group, or a frequency change, just ask.

Scheduling note

Configured frequencyActual behavior
Any value, on a Vercel Pro+ deploymentChecked every 15 minutes; generates as soon as the configured frequency has elapsed.
Under ~24h, on a Vercel Hobby deploymentHobby plan cron jobs run at most once per day, regardless of the configured frequency.