Blog

A keyword plan, articles written in the background, a review window, and a target they publish to.

The blog writes articles for a website, not posts for a network. You describe the site once, a keyword plan fills a calendar, the writer drafts each article in the background, and after a review window the draft publishes to the target you chose: a connected account, your own website, or the product blog.

Every route lives under /v1/blog and names its workspace: workspace_id in the query for GET and for every route with an id, workspaceId in the body otherwise. Responses use snake_case keys, a single resource comes back bare, and a list comes back as { "data": [...] }. Request bodies keep the camelCase field names each route documents.

ScopeCovers
postsReading anything, settings, keyword edits, editing, rejecting and deleting articles
aiAnything that spends AI credits: planning keywords, requesting an article, regenerating one
publishPublishing an article

How an article moves

suggested keyword → planned → queued → writing → draft → scheduled → publishing → published
  1. Plan. POST /keywords/plan researches keywords for the site and marks the best ones planned, each with a planned_for date. You can add your own with POST /keywords
  2. Write. When a planned date comes within review_hold_days, or when you call POST /articles, the writer runs in the background: research, outline, body, meta and images. The article is queued, then writing, then lands as a draft
  3. Review. A draft waits until review_until. Edit it, reject it, or publish it early. With auto_publish on, a draft that nobody touched publishes on its own when the window closes
  4. Publish. The article goes to the target. scheduled means a date is set, publishing means the send is in flight, published means it landed, with published_url when the target reported one
Article statusMeaning
queuedWaiting for the writer
writingThe writer is running
draftWritten, in review until review_until
scheduledWill publish at publish_at
publishingBeing sent to the target
publishedLive. published_at is set, published_url when known
failedThe write or the publish failed; error says why. Edit and publish again, or regenerate
rejectedRejected by a reviewer. It stays readable and can be edited back into review

Keyword statuses are suggested, planned, written and dismissed.

Targets

One target per workspace, set in the settings. Without one, every publish answers 409 no_target.

target_kindWhere the article goes
accountA connected account (target_account_id), published as an article post on that network. Pick an account on a network that takes articles, such as WordPress, Medium, dev.to, Hashnode or Listmonk
websiteYour own site. The article is POSTed to website_url as a signed article.published payload. See receiving blog articles
product_blogThe FoPost product blog. Available to admins only

Whatever the target, the workspace's webhooks receive article.published as well.

Settings

GET /v1/blog/settings?workspace_id=
curl "https://api.fopost.com/v1/blog/settings?workspace_id=7d2b8c11-4e5a-4a8f-b0d9-3c5e6f7a8b90" \
  -H "X-API-Key: $FOPOST_API_KEY"
{
  "site_url": "https://yourbrand.com",
  "topic": "Running shoes and training plans",
  "audience": "Amateur runners training for their first marathon",
  "language": "en",
  "location_code": 2840,
  "cadence_per_week": 2,
  "review_hold_days": 3,
  "auto_publish": false,
  "image_tier": "standard",
  "in_body_images": 2,
  "min_words": 1200,
  "max_words": 1800,
  "brand_voice_id": "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
  "target_kind": "website",
  "target_account_id": null,
  "target_platform": null,
  "website_url": "https://yourbrand.com/api/fopost/articles",
  "has_website_secret": true,
  "keyword_data_available": true,
  "updated_at": "2026-10-01T08:12:00.000Z"
}
FieldDescription
site_url, topic, audienceWhat the site is about and who reads it. The planner derives its seed keywords from these when you pass none
language, location_codeLanguage of the articles and the market keyword data is read for
cadence_per_weekArticles planned per week, 0 to 7. 0 turns planning off
review_hold_daysHow long a draft waits in review, 0 to 14 days. Writing starts this many days before the planned date, so the draft is reviewable for the whole window and publishes on the day
auto_publishPublish a draft on its own when its review window closes. Needs a target
image_tierdraft, standard or best, the quality tier for generated images
in_body_imagesImages placed under outline headings, 0 to 3, on top of the featured image
min_words, max_wordsThe length the writer aims for
brand_voice_idThe brand the writer writes in
target_kind, target_account_id, website_urlThe target, above. target_platform is read-only and names the target account's network
has_website_secretWhether a website signing secret exists. The secret itself is never readable
keyword_data_availableWhether search volume and difficulty can be fetched on this deployment. Returned by GET only

Update settings

PATCH /v1/blog/settings
curl -X PATCH "https://api.fopost.com/v1/blog/settings" \
  -H "X-API-Key: $FOPOST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "workspaceId": "7d2b8c11-4e5a-4a8f-b0d9-3c5e6f7a8b90",
    "targetKind": "website",
    "websiteUrl": "https://yourbrand.com/api/fopost/articles"
  }'
{
  "target_kind": "website",
  "website_url": "https://yourbrand.com/api/fopost/articles",
  "has_website_secret": true,
  "...": "...",
  "website_secret": "whsec_K3n9xq2YpB7vLh0dR4sWfT8mZc1aE6uJ"
}

The body takes the settings fields above in camelCase (siteUrl, cadencePerWeek, targetKind, websiteUrl and so on). Every field is optional except workspaceId; send the ones you want to change. The response is the settings. Setting websiteUrl for the first time issues a signing secret, returned once as website_secret and null on every other update. Clearing websiteUrl drops the secret. targetKind: "product_blog" answers 403 unless the key belongs to an admin.

Rotate the website secret

POST /v1/blog/settings/website-secret
{ "website_secret": "whsec_Q8vL2mB5xN1tY7cR0wP4kH9sA3dF6gJz" }

Body { "workspaceId": "..." }. The old secret stops verifying immediately, and the new one is shown once.

Test the website

POST /v1/blog/settings/website-test

Body { "workspaceId": "..." }. Sends a signed article.test envelope to website_url and reports what the site said.

{ "http_status": 200 }

The response is 200 when the site accepted it. When it did not, the response is 502:

{ "error": "website_test_failed", "message": "The website answered 500", "http_status": 500 }

http_status is what the site answered, or null when nothing came back, and message says why.

Keywords

GET /v1/blog/keywords?workspace_id=
ParameterDescription
workspace_idRequired
statusComma-separated: suggested, planned, written, dismissed
limitCaps the list
{
  "data": [
    {
      "id": "3f8a1d92-6c40-4b7e-a15d-90b2c4e6f8a1",
      "keyword": "marathon training plan for beginners",
      "search_volume": 9900,
      "difficulty": 42,
      "cpc": 1.35,
      "intent": "informational",
      "source": "provider",
      "status": "planned",
      "planned_for": "2026-10-14",
      "created_at": "2026-10-01T08:15:00.000Z"
    }
  ]
}

search_volume, difficulty (0 to 100), cpc and intent are null when the deployment has no keyword data (keyword_data_available is false) or the keyword was added by hand. source is provider, user or agent.

Plan keywords

POST /v1/blog/keywords/plan
curl -X POST "https://api.fopost.com/v1/blog/keywords/plan" \
  -H "X-API-Key: $FOPOST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "workspaceId": "7d2b8c11-4e5a-4a8f-b0d9-3c5e6f7a8b90", "seeds": ["marathon training", "running shoes"] }'
{ "status": "queued" }

Answers 202 and runs in the background: it researches the seeds (derived from site_url, topic and audience when omitted), stores the results as suggested, and marks enough of them planned to fill the calendar at cadence_per_week. Spends AI credits; see 402 below. Needs the ai scope.

Add a keyword

POST /v1/blog/keywords

Body { "workspaceId": "...", "keyword": "best trail running shoes", "plannedFor": "2026-10-21" }. Answers 201 with the keyword, source: "user", planned when plannedFor is given and suggested otherwise.

Update a keyword

PATCH /v1/blog/keywords/{id}?workspace_id=

Body takes status and plannedFor (null takes it off the calendar). Set status: "dismissed" to keep the planner from picking it again.

Remove a keyword

DELETE /v1/blog/keywords/{id}?workspace_id=

Answers 204. An article already written from it is untouched.

Articles

GET /v1/blog/articles?workspace_id=
ParameterDescription
workspace_idRequired
statusComma-separated article statuses
limitCaps the list
{
  "data": [
    {
      "id": "b41e7c05-9d38-4a2f-8e60-1c7d5b3a9f24",
      "title": "A Marathon Training Plan for First-Time Runners",
      "slug": "marathon-training-plan-for-first-time-runners",
      "keyword": "marathon training plan for beginners",
      "excerpt": "Sixteen weeks, three runs a week, and one long run that grows slowly.",
      "meta_title": "Marathon Training Plan for Beginners (16 Weeks)",
      "meta_description": "A sixteen-week marathon plan for first-time runners, with weekly mileage and the long run that gets you to the start line.",
      "featured_image": {
        "media_id": "6b0e9a4c-2d71-4f3a-9c18-5e7d0a1b2c34",
        "url": "https://api.fopost.com/v1/media/6b0e9a4c-2d71-4f3a-9c18-5e7d0a1b2c34/file",
        "alt": "A runner lacing shoes at dawn"
      },
      "tags": ["marathon", "training"],
      "word_count": 1540,
      "status": "draft",
      "origin": "plan",
      "review_until": "2026-10-14T06:00:00.000Z",
      "publish_at": null,
      "published_at": null,
      "published_url": null,
      "error": null,
      "created_at": "2026-10-11T06:00:00.000Z",
      "updated_at": "2026-10-11T06:09:00.000Z"
    }
  ]
}

origin is plan (the calendar), user (requested by hand or through the API) or agent. The list carries the summary; the single read adds the body.

Request an article

POST /v1/blog/articles
curl -X POST "https://api.fopost.com/v1/blog/articles" \
  -H "X-API-Key: $FOPOST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "workspaceId": "7d2b8c11-4e5a-4a8f-b0d9-3c5e6f7a8b90",
    "keyword": "best trail running shoes",
    "instructions": "Compare five shoes. Close with a sizing note."
  }'
FieldDescription
workspaceIdRequired
titleWrite to this title
keywordWrite for this search term
keywordIdA keyword from the plan; its term becomes the target and it is marked written
instructionsUp to 4,000 characters of direction for the writer

Exactly one of title, keyword or keywordId is required. Answers 202 with the article in queued; poll the single read or subscribe to article.published. Spends AI credits and needs the ai scope.

Get an article

GET /v1/blog/articles/{id}?workspace_id=

Everything the list shows, plus:

FieldDescription
body_markdownThe article body in Markdown, inline image references included
body_htmlThe same body rendered to HTML
outlineThe headings the writer worked from
briefThe research behind the article (competitors, questions, search_volume, difficulty), or null
instructionsWhat was asked for
imagesIn-body images: media_id, url, alt and position, the index of the outline heading each sits under

Image urls here are dashboard URLs that re-check your membership on every request. The URLs that reach a target and a webhook are different: fetchable by anyone for six hours, then gone.

Edit an article

PATCH /v1/blog/articles/{id}?workspace_id=
curl -X PATCH "https://api.fopost.com/v1/blog/articles/b41e7c05-9d38-4a2f-8e60-1c7d5b3a9f24?workspace_id=7d2b8c11-4e5a-4a8f-b0d9-3c5e6f7a8b90" \
  -H "X-API-Key: $FOPOST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "title": "The 16-Week Beginner Marathon Plan", "publishAt": "2026-10-14T07:00:00.000Z" }'
FieldDescription
title, slugslug is lowercase letters, digits and hyphens
excerpt, metaTitle, metaDescriptionEach nullable
bodyMarkdownReplaces the body; body_html is re-rendered from it
tagsReplaces the list
publishAtA future time puts the draft on the calendar as scheduled; null returns it to review

Only a draft, scheduled, failed or rejected article can be edited. Anything else answers 409.

Regenerate an article

POST /v1/blog/articles/{id}/regenerate?workspace_id=

Runs the writer again on the same brief and instructions; the current body is replaced when it finishes. Answers 202 with the article back in queued, 409 while it is writing or publishing. Spends AI credits and needs the ai scope.

Publish an article

POST /v1/blog/articles/{id}/publish?workspace_id=
curl -X POST "https://api.fopost.com/v1/blog/articles/b41e7c05-9d38-4a2f-8e60-1c7d5b3a9f24/publish?workspace_id=7d2b8c11-4e5a-4a8f-b0d9-3c5e6f7a8b90" \
  -H "X-API-Key: $FOPOST_API_KEY"
{ "id": "b41e7c05-...", "status": "scheduled", "publish_at": "2026-10-11T09:30:04.000Z", "...": "..." }

Sends the draft to the target now, without waiting for the review window. Answers 202; the article moves through publishing to published in the background, or to failed with an error. Needs the publish scope.

{ "error": "no_target", "message": "Choose where articles publish first" }

409 no_target means the workspace has no target_kind. Set one in the settings and publish again. A 409 with another message means the article is not in a state that can publish, such as writing.

Reject a draft

POST /v1/blog/articles/{id}/reject?workspace_id=

Marks the article rejected and takes it off the calendar. Nothing is sent. Edit it to put it back into review.

Delete an article

DELETE /v1/blog/articles/{id}?workspace_id=

Answers 204. An article already published stays wherever it was published; only FoPost's copy goes. This cannot be undone.

When the balance is empty

Planning, requesting and regenerating spend AI credits. When the balance cannot cover the run, the request is refused before anything starts:

{
  "error": "insufficient_credits",
  "message": "Your AI balance is empty. Add funds to keep using AI.",
  "upgrade_url": "/settings/billing"
}

A 402 with an upgrade_url, the same answer every AI feature gives. Nothing is queued and nothing is charged. Reads, settings, keyword edits and publishing never spend credits.

Next

Related documentation
  • API Overview

    Base URL, envelopes, pagination, and errors for the FoPost REST API.

  • Authentication

    API keys, scopes, and workspace binding.

  • Publishing

    Create a post, target accounts, publish it, and read the per-account result.

  • Scheduling

    Schedule a post, repeat it, and import a batch from a spreadsheet.

  • Media

    Upload files, list the media library, and attach media to a post.

  • Validation

    Check content, text length, and media against platform rules before a post exists.

  • Accounts

    List connected social accounts, check their health, and refresh credentials.

  • Workspaces

    Workspaces, labels, and how isolation works across them.

Was this helpful?

On this page