Public API & API keys
The public API lets any website read a workspace's published content — posts, pages, posts, taxonomy, redirects — and submit contact-form inquiries. It is read-only (plus inquiry submission), 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.
1. Create an API key
- In the dashboard open API Access (
/workspace/{slug}/api-access). - Click Create API key. A key is generated and shown once:
kb_pub_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX - 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
Originheader. Fullbleed checks it against the workspace's allowed-origin allowlist (allowed_origins) and returns403 Origin not allowedfor unknown origins. - Requests with no Origin (curl, server-to-server, mobile apps) skip the check.
- Preflight
OPTIONSis 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 to10).
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.
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" }.
Inquiries (contact form)
POST /inquiries
Accepts JSON:
{
"name": "Ada Lovelace",
"email": "ada@example.com",
"phone": "+1 555 0100",
"subject": "Project inquiry",
"message": "We’d love to work with you."
}
Only name, email and message are required (email must contain @). If the request
carries an Origin, it must be in the workspace's allowed-origin allowlist.
Success (201) → { "data": { "received": true } }. The message then appears in the
dashboard under Inquiries.
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. Example: contact form
From a script, page, or edge function on your own origin (no Origin header, so no allowlist needed):
const res = await fetch("https://fullbleed.basilpot.com/api/v1/inquiries", {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.FULLBLEED_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({ name, email, message }),
});
To submit from a browser directly, the page's origin must be on the workspace's allowed-origin allowlist.
7. 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