Public API & API keys

The public API lets any website read a workspace's published content — posts, pages, posts, taxonomy, team profiles, redirects, site config. It is read-only, JSON everywhere, and authenticated with a publishable key.

Base URL

https://fullbleed.basilpot.com/api/v1

On a self-hosted install this is {your-app}/api/v1. Every endpoint below is relative to this base.

The API returns published content only — drafts and deleted items never appear.

Responses are never cached by Fullbleed: every response carries Cache-Control: no-store and reflects the database at request time, so an edit in the dashboard shows up on the next request. Caching is up to your website — e.g. Next.js fetch(url, { next: { revalidate: 60 } }), your framework's ISR, or your own CDN rules.

1. Create an API key

  1. In the dashboard open API Access (/workspace/{slug}/api-access).
  2. Click Create API key. A key is generated and shown once:
    kb_pub_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    
  3. Copy it immediately — it is not shown again. You can always create more.

Keys are workspace-scoped, prefixed kb_pub_, 30 random characters. Store the key as an environment variable or build secret on your website — it is publishable (read-only), but keep it out of public git history.

Revoking

In API Access, revoke any key to immediately reject its requests. Requests succeed only while revoked_at is null.

2. Authenticate

Send the key as a bearer token:

Authorization: Bearer kb_pub_…

Every request without a valid key returns 401:

{ "error": "Invalid API key" }

3. Requests from browsers (CORS)

GET endpoints support cross-origin (CORS) requests from your website:

  • A browser request carries an Origin header. Fullbleed checks it against the workspace's allowed-origin allowlist (allowed_origins) and returns 403 Origin not allowed for unknown origins.
  • Requests with no Origin (curl, server-to-server, mobile apps) skip the check.
  • Preflight OPTIONS is answered automatically.

Managed cloud: the origin allowlist is currently administered at the data layer, being surfaced in the dashboard soon. Self-hosted installs manage it directly — see Allowed origins in the Self-hosting guide.

4. Endpoints

Content lists

GET /pages, GET /services, GET /posts

Each returns a paginated list, newest published first. Posts support filters.

Query parameters

Param Applies Meaning
page all Page number, 1-based. Default 1.
limit all Page size. Default 20, max 100.
category posts Only posts in category with this slug.
author posts Only posts by author (author slug).
tag posts Only posts carrying this tag slug.

Response envelope

{
  "data": [ /* content items */ ],
  "meta": { "page": 1, "limit": 20, "total": 37 }
}

Default list page size is 20 (dashboard admin endpoints default to 10).

Single items

GET /pages/:slug, GET /services/:slug, GET /posts/:slug

{ "data": { /* content item */ } }

404 { "error": "Not found" } when the slug has no published non-deleted item.

Content item shape

Field Type Notes
id string UUID
type page | service | post Content kind
title string
slug string
bodyHtml string Rendered HTML for the content body
status string Always "published" here
publishedAt string ISO timestamp
metaTitle string | null SEO title
metaDescription string | null SEO description
canonicalUrl string | null
coverImage string | null Absolute URL on your media domain, ready for <img>
author { id, name, slug } | null Posts
category { id, name, slug } | null Posts
tags { id, name, slug }[] Posts
createdAt string ISO timestamp
updatedAt string ISO timestamp

Taxonomy

GET /authors, GET /categories, GET /tags

{ "data": [ { "id": "…", "name": "…", "slug": "…" } ] }

Authors additionally include description (bio) and image_media_id.

Team

GET /team

Staff profiles managed under Team in the dashboard, sorted by position (0 = first). Owners and editors set the order by dragging members on the Team page; new members are added last. Render them in the order returned. Not paginated.

{ "data": [ {
  "id": "…", "position": 0, "name": "Dr. Asha Rai", "role": "Consultant",
  "profession": "Gynaecologist", "qualification": "MBBS, MD",
  "description": "…", "license": "NMC 4455",
  "image": "https://media.example.com/…/asha.webp"
} ] }

name and role are always set. profession, qualification, description, license and image are null when left blank; image is an absolute URL on your media domain.

Redirects

GET /redirects/resolve?from=/old-path

/old-path must start with /. Returns the target if it matches:

{ "data": { "source_path": "/old-path", "target_path": "/new-path", "permanent": 1 } }

or 404 { "error": "Not found" }.

Site config

GET /site-config

Returns the business details set in dashboard Settings — name, description, contact email, phone numbers, address, opening hours and social links. logo and image come back as absolute URLs on your media domain, so they can be used directly in <img src>. Returns {} when nothing is configured.

5. Example: rendering your blog

GET /posts?limit=50 then render bodyHtml as trusted, server-produced HTML:

const res = await fetch("https://fullbleed.basilpot.com/api/v1/posts?limit=50", {
  headers: { Authorization: "Bearer " + process.env.FULLBLEED_API_KEY },
});
const { data: posts } = await res.json();
{posts.map((post) => (
  <article key={post.id}>
    <h2>{post.title}</h2>
    {post.coverImage && <img src={post.coverImage} alt="" />}
    <div dangerouslySetInnerHTML={{ __html: post.bodyHtml }} />
    <small>
      {post.publishedAt} · {post.author?.name} · {post.category?.name}
    </small>
  </article>
))}

6. Errors

All errors use the { "error": "…" } shape:

Status Meaning
400 Invalid request (e.g. redirects/resolve without a leading /)
401 Missing or invalid API key
403 Origin header not on the workspace allowlist
404 Unknown endpoint, or nothing published for the slug/path
405 Method not allowed (all content endpoints are GET)

Quick curl check

curl -H "Authorization: Bearer kb_pub_…" \
  https://fullbleed.basilpot.com/api/v1/posts?limit=1