Partner Publish API (Hope)

Server-to-server endpoint for partner apps to publish a live article and matching post card in one call. No Kind Joe staff session required.

Status

Implemented at POST /api/partner/publish. Deploy with HOPE_PUBLISH_API_KEY set in production.

Endpoints

  • POST /api/partner/upload-image — store the photo and get the hero and square links
  • POST /api/partner/publish — create the live article

Authentication

Same secret on Kind Joe and Hope (HOPE_PUBLISH_API_KEY). Use either:

  • Authorization: Bearer <HOPE_PUBLISH_API_KEY>
  • x-api-key: <HOPE_PUBLISH_API_KEY>

Upload an image first

Same API key. Send a public https image URL, a data:image/...;base64,... URI, or a multipart field named file (max 8MB). Kind Joe uploads it to Cloudinary and returns both crops. Paste those two URLs into the publish body. You do not need your own Cloudinary keys.

curl -X POST "$KINDJOE_API_BASE_URL/api/partner/upload-image" \
  -H "Authorization: Bearer $HOPE_PUBLISH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "imageUrl": "https://example.com/storm-photo.jpg" }'
{
  "success": true,
  "originalUrl": "https://res.cloudinary.com/…/kindjoe/articles/storm.jpg",
  "coverImageUrl": "https://res.cloudinary.com/…/c_fill,g_auto,w_1600,h_900,…",
  "postCard": { "imageUrl": "https://res.cloudinary.com/…/c_fill,g_auto,w_800,h_800,…" }
}

Request body

{
  "slug": "my-article-slug",
  "title": "Article title",
  "excerpt": "At least twelve words in the excerpt for validation.",
  "contentMarkdown": "# Full markdown body…",
  "coverImageUrl": "https://…",
  "category": "Local News",
  "tags": ["ventura-county"],
  "seo": {
    "title": "SEO title",
    "description": "Meta description — at least 70 characters required.",
    "sourceUrl": "https://original-source",
    "sourceArticleId": 12345,
    "sourceType": "news_rss_articles"
  },
  "postCard": {
    "imageUrl": "https://res.cloudinary.com/…/tile.jpg",
    "caption": "Optional"
  },
  "isVentura": true
}

Response (201)

{
  "success": true,
  "articleId": "…",
  "slug": "my-article-slug",
  "articleUrl": "https://www.kindjoe.com/my-article-slug",
  "destinationUrl": "https://www.kindjoe.com/my-article-slug",
  "postId": 42,
  "feed": "ventura",
  "authorId": "cms-writer-id"
}

What Kind Joe does

RuleBehavior
PublishLive article (isDraft: false, publishedAt = now)
AuthorRandom CMS Writer or Editor (not system-ai / rss-auto-writer)
Venturatag ventura-county, post card in ventura_posts. Public URL stays /{slug}
KindJoe/{slug}, row in posts
Post carddestinationUrl = article URL (same as admin post modal)

Hope env (your app)

Image upload uses Kind Joe's Cloudinary. Hope only needs the shared publish key.

KINDJOE_API_BASE_URL=https://www.kindjoe.com
HOPE_PUBLISH_API_KEY=<same secret as Kind Joe>

Quick test

curl -X POST "$KINDJOE_API_BASE_URL/api/partner/publish" \
  -H "Authorization: Bearer $HOPE_PUBLISH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"slug":"hope-test-…","title":"Test","excerpt":"Twelve word minimum excerpt for validation.","contentMarkdown":"# Test","seo":{"description":"Seventy character minimum meta description for publish validation on Kind Joe."},"postCard":{"imageUrl":"https://res.cloudinary.com/demo/image/upload/sample.jpg"}}'

Related

  • KindJoe post cards — feed tile model
  • Read-only RSS pool: see markdown doc docs/developers/rss-news-read-api.md in the repo

Errors

  • 401 — invalid/missing API key
  • 400 — validation (excerpt, SEO, body)
  • 409 — slug conflict