Publish API — Push Content to WordPress, Ghost, Webflow, and Custom CMS Hooks
REST API for publishing blog posts and landing pages to connected CMS platforms. Supports WordPress, Wisp, Ghost, Webflow, and custom REST hooks with approval gating and auto-indexing on success.
Publish a blog post or landing page to a connected CMS — WordPress, Wisp, Ghost, Webflow, or a custom REST hook. Auto-fires indexing submissions (GSC + IndexNow) on success.
Conceptual overview
Publishing is the last mile. Once content is drafted, refined, and evaluated, you push it to a platform. CitationBench maintains workspace-level platform configs — connected accounts that publishing references. One workspace can have many platforms (a WordPress blog + a Wisp landing-page CMS, for example) and one publish call targets one of them.
By default, publishing is approval-gated in agency workspaces. The agent pauses before sending; you review the rendered preview, approve, and the actual push happens.
Endpoints
| Method | Path | Purpose |
|---|---|---|
| POST | /v1/produce/publish | Publish content |
| POST | /v1/produce/publish/{contentId}/unpublish | Take it down |
| GET | /v1/produce/publish/history | List publishing events |
| GET | /v1/produce/publish/platforms | List platform configs |
| POST | /v1/produce/publish/platforms | Connect a platform |
| GET | /v1/produce/publish/platforms/{id} | Get one |
| PATCH | /v1/produce/publish/platforms/{id} | Update credentials / settings |
| DELETE | /v1/produce/publish/platforms/{id} | Disconnect |
| POST | /v1/produce/publish/platforms/{id}/test | Test a platform connection |
POST /v1/produce/publish
{
"contentType": "blog_post",
"contentId": "bp_***",
"platformConfigId": "pfm_wordpress-main",
"isDraft": false,
"scheduledAt": "2026-05-25T09:00:00-04:00",
"alsoIndex": true
}| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
contentType | "blog_post" | "landing_page" | yes | — | — |
contentId | string | yes | — | — |
platformConfigId | string | yes | — | Target platform |
isDraft | boolean | no | false | Publish as draft instead of live |
scheduledAt | ISO timestamp | no | now | Schedule for later |
alsoIndex | boolean | no | workspace autoQueueOnPublish | Auto-fire indexing submissions after publish |
categoryIds / tagIds | string[] | no | — | Platform-specific |
featuredImageId | string | no | — | If IMAGE_REFINER produced one, reference it here |
Response (approval-gated, default)
{
"invocationId": "inv_***",
"agentId": "agt_***",
"skill": "produce.publish",
"status": "WAITING_APPROVAL",
"approvalId": "appr_***",
"preview": {
"platform": "wordpress",
"title": "How engineering teams track capacity",
"slug": "engineering-team-capacity-tracking",
"url": "https://acme.com/blog/engineering-team-capacity-tracking",
"categories": ["Engineering"],
"tags": ["capacity", "engineering"],
"featuredImageUrl": "https://cdn.citationbench.com/images/img_***.png",
"scheduledAt": "2026-05-25T09:00:00-04:00"
}
}Approve via Agent · approval. On approval the publish actually fires.
Response (auto-publish, workspace policy allows)
{
"invocationId": "inv_***",
"agentId": "agt_***",
"skill": "produce.publish",
"status": "SUCCEEDED",
"creditsUsed": 1,
"result": {
"publishedUrl": "https://acme.com/blog/engineering-team-capacity-tracking",
"platformResponse": {
"id": "wp_post_12345",
"publishedAt": "2026-05-24T08:30:00Z",
"permalink": "https://acme.com/blog/engineering-team-capacity-tracking"
},
"indexingTriggered": [
{ "platform": "gsc", "submissionId": "sub_***" },
{ "platform": "indexnow", "submissionId": "sub_***" }
]
},
"raw": "Pushed to WordPress (post id wp_post_12345). Indexing fired ...",
"files": ["agent-output/publish-receipt.json"]
}POST /v1/produce/publish/{contentId}/unpublish
curl -X POST .../v1/produce/publish/bp_***/unpublish -d '{
"platformConfigId": "pfm_wordpress-main",
"redirectTo": "https://acme.com/blog/superseded-by-foo"
}'If supported by the platform, sets the post to private/draft and optionally configures a redirect.
GET /v1/produce/publish/history
curl -G .../v1/produce/publish/history \
--data-urlencode "platformConfigId=pfm_wordpress-main" \
--data-urlencode "since=2026-05-01T00:00:00Z"{
"data": [
{
"id": "ph_***",
"contentType": "blog_post",
"contentId": "bp_***",
"platform": "wordpress",
"publishedUrl": "https://acme.com/blog/...",
"publishedAt": "2026-05-24T08:30:00Z",
"status": "SUCCESS",
"invocationId": "inv_***"
}
]
}CRUD: /v1/produce/publish/platforms
List
curl .../v1/produce/publish/platforms{
"data": [
{
"id": "pfm_wordpress-main",
"type": "wordpress",
"name": "Acme blog",
"baseUrl": "https://acme.com",
"connected": true,
"lastTestedAt": "2026-05-20T...",
"settings": { "defaultCategoryId": 5, "defaultStatus": "publish" }
},
{
"id": "pfm_wisp-pricing",
"type": "wisp",
"name": "Acme pricing pages",
"connected": true
}
]
}Connect a new platform
curl -X POST .../v1/produce/publish/platforms -d '{
"type": "wordpress",
"name": "Acme blog",
"baseUrl": "https://acme.com",
"credentials": {
"username": "admin",
"applicationPassword": "***"
},
"settings": {
"defaultCategoryId": 5,
"defaultStatus": "publish"
}
}'Supported types: wordpress, wisp, ghost, webflow, custom_rest.
Test
curl -X POST .../v1/produce/publish/platforms/pfm_***/testReturns success/failure of the auth + a sample API call.
MCP
> Publish bp_*** to our WordPress.Claude calls produce.publish.send. If workspace policy requires approval, surfaces the preview for you to approve first.
> Publish all DRAFT blog posts in the performance pillar to WordPress, scheduled one per day at 9am Eastern.Claude lists drafts, then calls produce.publish.send for each with staggered scheduledAt.
Errors
| Status | Code | Cause |
|---|---|---|
| 400 | validation_error | Missing fields |
| 403 | platform_not_configured | platformConfigId not in this workspace |
| 422 | platform_publish_failed | Platform rejected the post; see details.platformError |
| 503 | platform_unavailable | Connection timed out |
Cost
| Action | Credits |
|---|---|
| Per publish | 1 (covers downstream indexing as well, if enabled) |
| Unpublish | free |
| Platform CRUD | free |
| Platform test | free |
Use cases (string things together)
A. Auto-publish on draft completion
The content_factory skill's last step is produce.publish (approval-gated). Once you approve, it ships.
B. Schedule a content calendar
DAY=0
for BP in $(curl -sf -G .../v1/produce/blog-post --data-urlencode "status=DRAFT" --data-urlencode "tag=q2-2026" | jq -r '.data[].id'); do
SCHED=$(date -u -d "+$DAY day 09:00" +"%Y-%m-%dT%H:%M:%SZ")
curl -X POST .../v1/produce/publish -d "{
\"contentType\": \"blog_post\",
\"contentId\": \"$BP\",
\"platformConfigId\": \"pfm_wordpress-main\",
\"scheduledAt\": \"$SCHED\"
}"
DAY=$((DAY+1))
doneC. Multi-platform publish
Different content types go to different platforms.
curl -X POST .../v1/produce/publish -d '{
"contentType": "blog_post",
"contentId": "bp_***",
"platformConfigId": "pfm_wordpress-main"
}'
curl -X POST .../v1/produce/publish -d '{
"contentType": "landing_page",
"contentId": "lp_***",
"platformConfigId": "pfm_webflow-pricing"
}'D. Gate publishing on quality
Use an eval gate that blocks publish until evaluation score ≥ 75:
- applies_to: produce.publish
when: { field_lt: { evaluationScore: 75 } }
then: { action: escalate_to_approval, reason: "Quality below threshold" }Related
- API: Production · blog post
- API: Production · landing page
- API: Production · evaluate (eval-gate-driven)
- API: Indexing · gsc index (auto-fires on publish)
- API: Indexing · indexnow (auto-fires on publish)
- Concept: Approval Workflows
- Playbook: Client-approval gated publishing
Image
REST API for generating brand-aligned images from reusable templates. Produce OG cards, hero illustrations, CTA banners, and inline graphics rendered via Satori — no prompt engineering required.
GSC index
REST API to submit URLs to Google for indexing and check their current crawl/index status. Wraps the GSC Indexing API with auth handling, rate-limit smoothing, and auto-mirroring to IndexNow.