Headless blog

Content API reference

Read-only endpoints for one blog. JSON in, JSON out, GET only.

Basics

  • Base address: https://drive.example.com/api/content/v1/blogs/:blog, where :blog is the blog's address name (like weeknight-kitchen) or its id.
  • The key goes in ?key= or a header Authorization: Key stc_....
  • All URLs in answers are absolute and use the blog's custom domain once it has one.
  • HTML is rendered and cleaned by the server: no scripts, no inline handlers, only safe links. Cards keep stable classes (card card-recipe, card-product, card-gallery, card-embed, card-bookmark, card-button, callout callout-tip) so you can style them.
shell
curl "https://drive.example.com/api/content/v1/blogs/weeknight-kitchen/posts?key=stc_YOUR_CONTENT_KEY&limit=5"

Posts

GET /posts

Published posts, newest first.

ParameterWhat it does
pagePage number, from 1.
limitPosts per page, 1 to 100 (default 15). all means 100.
tagOnly posts with this tag (its slug, like pasta).
authorOnly posts by this username.
featuredtrue or false.
orderpublished_at desc (default), published_at asc, title asc, updated_at desc.
fieldsOnly these fields, like id,title,slug,url.
includetags,authors adds the full lists.
formatshtml (default), markdown, or both.
json
{
  "posts": [
    {
      "id": "bp_8fQ2kL1mNp0x",
      "slug": "lemon-pasta",
      "type": "post",
      "title": "Lemon pasta",
      "url": "https://food.example.com/lemon-pasta",
      "html": "<p>Weeknight lemon pasta.</p><section class=\"card card-recipe\" id=\"recipe\">...</section>",
      "excerpt": "Weeknight lemon pasta.",
      "custom_excerpt": null,
      "featured": false,
      "visibility": "public",
      "published_at": "2026-10-08T12:00:00.000Z",
      "updated_at": "2026-10-08T12:30:00.000Z",
      "reading_time": 3,
      "word_count": 512,
      "toc": [{ "level": 2, "id": "the-sauce", "text": "The sauce" }],
      "feature_image": "https://food.example.com/media/Xq7pL2a/k3Jd9sLqP0aZbYcX/pasta.jpg",
      "feature_image_alt": "A bowl of pasta",
      "feature_image_width": 2400,
      "feature_image_height": 1600,
      "feature_image_sizes": { "thumbnail": "...?size=thumb", "full": "..." },
      "feature_image_srcset": "...?size=thumb 400w, ... 2400w",
      "meta_title": null,
      "meta_description": null,
      "og_image": null,
      "canonical_url": null,
      "json_ld": [{ "@context": "https://schema.org", "@type": "BlogPosting", "headline": "Lemon pasta" }],
      "rating": { "average": 4.8, "count": 126, "reviews": 31 },
      "made": 340,
      "recipe": { "total_label": "22 min", "total_min": 22, "course": ["main"], "cuisine": ["italian"], "diet": ["vegetarian"], "difficulty": "easy" },
      "primary_tag": { "slug": "pasta", "name": "Pasta" },
      "primary_author": { "username": "sam", "name": "Sam Rivera" },
      "tags": [{ "slug": "pasta", "name": "Pasta", "url": "https://food.example.com/tag/pasta" }]
    }
  ],
  "meta": { "pagination": { "page": 1, "limit": 15, "pages": 4, "total": 52, "next": 2, "prev": null } }
}

Recipe posts also carry rating (readers' average stars, how many ratings and written reviews), made (how many readers said they made it) and recipe (time, course, cuisine, diet, difficulty). Their json_ld includes a Recipe with aggregateRating, so search results can show the stars. Other posts have rating: null.

GET /posts/:slugOrId

One post, with its tags and authors. Answers { "posts": [post] }, or 404 when there's no such published post. Unlisted posts only answer to their slug.

shell
curl -H "Authorization: Key stc_YOUR_CONTENT_KEY" "https://drive.example.com/api/content/v1/blogs/weeknight-kitchen/posts/lemon-pasta"

Pages

GET /pages and GET /pages/:slugOrId work like posts, for pages such as About or Contact. Answers use pages instead of posts.

Tags

GET /tags lists tags that have published posts, most used first. GET /tags/:slug answers one.

json
{ "tags": [{ "slug": "pasta", "name": "Pasta", "description": "", "url": "https://food.example.com/tag/pasta", "count": { "posts": 12 } }] }

Authors

GET /authors lists the people shown on the blog's team page. GET /authors/:username answers one.

json
{ "authors": [{ "username": "sam", "name": "Sam Rivera", "bio": "Cooks on weeknights.", "profile_image": "https://drive.example.com/u/sam/avatar.png", "url": "https://food.example.com/author/sam", "links": [{ "label": "Website", "url": "https://example.com" }], "role": "owner", "count": { "posts": 40 } }] }

Settings

GET /settings answers the blog's name, look and navigation, to build your header and footer.

json
{ "settings": { "title": "Weeknight Kitchen", "description": "Recipes that fit a Tuesday.", "url": "https://food.example.com", "lang": "en", "accent_color": "#E4572E", "logo": null, "icon": null, "cover_image": null, "meta_title": null, "meta_description": null, "navigation": [{ "label": "Home", "url": "https://food.example.com" }], "secondary_navigation": [], "rss_url": "https://food.example.com/rss.xml", "atom_url": "https://food.example.com/atom.xml", "posts_per_page": 10 } }

Errors

json
{ "errors": [{ "type": "NotFoundError", "code": "not_found", "message": "Post not found." }] }
StatusCodeWhen
401missing_keyNo key sent.
401invalid_keyUnknown, revoked, or a key for another blog.
403origin_not_allowedA browser on a site the key doesn't allow.
404not_foundNo such published item, or the blog is private.
429rate_limitedToo many requests; wait for Retry-After seconds.

CORS and caching

  • GET and OPTIONS only. Preflights answer 204 and are cached for a day.
  • Access-Control-Allow-Origin is *, or your site's origin when the key lists allowed sites.
  • ETag on every answer; send If-None-Match for a 304. Cache-Control: public, max-age=60, stale-while-revalidate=300.