Skip to main content

Developer documentation

The Facet Creator API

Read your portfolio, posts, projects, courses, and talks; create and edit content; pull member and analytics data; and get notified the moment something happens on your site. This page explains how, in plain language first, with the exact detail underneath.

Prefer a machine-readable contract? Download the OpenAPI 3.1 specification file (openapi.yaml) and load it into your tool of choice. Throughout this page, API means Application Programming Interface, JSON means JavaScript Object Notation, and an endpoint is one web address you can call.

Getting started: create your first API key

In short: open your admin panel, make a key, choose what it is allowed to do, copy it once, and send it on every request.

An API key is a secret password for your program. You create one in your site's admin panel under Tools → API. When you create a key you choose three things:

  1. A name — a label so you can recognise the key later (for example, "Zapier" or "my export script").
  2. Scopes — the permissions the key carries. A scope limits what the key may do. Pick the smallest set that does the job: read to read content, write to create and edit posts and projects, and webhooks to register webhooks. See the scopes section for the full list.
  3. Allowed origins (optional)allowed_origins is a list of websites that are allowed to use the key from a browser. If you set it, a request must come from one of those origins (checked using the browser's Origin or Referer header). Leave it empty for server-to-server use.

The full key value is shown only once, right after you create it. Copy it somewhere safe immediately — you cannot read it again. A Facet key always starts with fct_live_.

Make your first call

Replace your-site with your Facet subdomain and paste your key after Bearer. This reads your published posts:

Example request that lists posts using curl
curl https://your-site.facetcloud.io/api/v1/posts \
  -H "Authorization: Bearer fct_live_YOUR_KEY_HERE"

A successful response is JSON, shaped like this:

Example JSON response for a list of posts
{
  "data": [
    {
      "id": "rec0123456789abc",
      "title": "Hello world",
      "slug": "hello-world",
      "excerpt": "My first post",
      "visibility": "public",
      "created": "2026-01-01 12:00:00.000Z"
    }
  ],
  "total": 1,
  "page": 1,
  "per_page": 20
}

Rotating a key

To rotate a key means to replace it with a new one — do this on a schedule, or right away if a key might have leaked. The safe order is:

  1. Create a new key with the same scopes.
  2. Update your program to use the new key.
  3. Confirm everything still works.
  4. Delete (revoke) the old key from the admin panel.

You can also set an expiry date on a key, or switch it off with the active flag, without deleting it.

Glossary of terms

Short, plain definitions of the words used on this page.

API (Application Programming Interface)
A set of web addresses (endpoints) your program can call to read or change data on your Facet site.
Endpoint
A single web address plus method (for example GET /api/v1/posts) that does one job.
Scope
A permission stamped on an API key that limits what the key may do, such as read or write.
Webhook
A message Facet sends to a URL you choose when something happens, so you do not have to keep asking.
HMAC (Hash-based Message Authentication Code)
A signature, computed from the message body and a shared secret, that lets you confirm a webhook really came from Facet and was not changed.
JSON (JavaScript Object Notation)
The plain-text data format every request and response on this API uses.
MCP (Model Context Protocol)
An open standard that lets AI assistants call tools and APIs. The Facet API is designed to be usable from MCP tools.
Cursor
An opaque marker returned with a page of results; pass it back to fetch the next page without missing or repeating rows.
Bearer token
A credential sent in the Authorization header that proves who is making the request. Here, your API key is the bearer token.

Authentication: sending your key

In short: put your key in one header on every request. There is no other way to sign in.

Every request must include an Authorization header with the word Bearer, a space, and your key. A bearer token is just a credential that proves who is calling.

The Authorization header format
Authorization: Bearer fct_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

There is no X-API-Key header. The Authorization: Bearer header above is the only accepted credential. Requests are made over HTTPS only; never put your key in a URL or in client-side code you ship to visitors.

If the header is missing or the key is wrong, you get a 401. If the key is real but not allowed to do what you asked, you get a 403. See error codes.

Scopes: what a key may do

In short: a scope is a permission on the key. The endpoint you call checks that your key has the right one, or it returns 403.

The admin "create key" form accepts three scope values:

  • read — read your published content (the v1 read endpoints and webhook polling).
  • write — create, update, and delete posts, projects, courses, talks, experience, education, skills, and certifications (the v1 write endpoints).
  • webhooks — register outbound webhooks.

The newer v2 endpoints check more specific, namespaced scope strings: read:content, read:members, read:analytics, and write:webhooks. The exact scope each endpoint requires is listed beside it in the endpoint reference and in the downloadable OpenAPI file. Give each key only the scopes it needs.

Pagination: reading long lists

In short: v1 lists hand you a cursor to fetch the next page; v2 lists use a page number. Some short lists return everything at once.

v1 lists use a cursor

The v1 list endpoints (posts, projects, courses) return up to limit items (default 20, maximum 100) sorted by sort (one of created, -created, updated, -updated, title, -title; default -created). A cursor is an opaque marker. When more results exist, the response includes next_cursor; pass it back as the cursor query value to get the next page. This is reliable even when two records share a timestamp.

Fetching the next page with a cursor
curl "https://your-site.facetcloud.io/api/v1/posts?limit=20&cursor=CURSOR_FROM_LAST_RESPONSE" \
  -H "Authorization: Bearer fct_live_YOUR_KEY_HERE"

v2 lists use a page number

The v2 list endpoints use page (starting at 1) and per_page (default 50, maximum 200). The response wraps results in an items array and echoes page and per_page.

Simple lists return everything

Experience, education, skills, and certifications return their full list (no paging) inside a data array.

Endpoint reference (every path)

In short: every path you can call, grouped by what it touches, with the scope it needs.

All paths are relative to your site, for example https://your-site.facetcloud.io. A {id} is a record id; a {idOrSlug} is read as a slug by GET and as a record id by PATCH and DELETE.

Profile

The site owner's public profile.

Profile endpoints, with method, path, description, and required scope
MethodPathWhat it doesScope
GET/api/v1/profileGet the owner's public profile.read

Posts

Blog and newsletter posts. You can read them on v1 and v2, and create, edit, and delete them on v1.

Posts endpoints, with method, path, description, and required scope
MethodPathWhat it doesScope
GET/api/v1/postsList published posts (cursor pagination).read
GET/api/v1/posts/{idOrSlug}Get one published post by its slug.read
POST/api/v1/postsCreate a post.write
PATCH/api/v1/posts/{idOrSlug}Update a post by its record id.write
DELETE/api/v1/posts/{idOrSlug}Delete a post by its record id.write
GET/api/v2/postsList posts (page pagination).read:content
GET/api/v2/posts/{id}Get one post by record id.read:content

Projects

Portfolio projects. Same shape as posts: readable on v1 and v2, writable on v1.

Projects endpoints, with method, path, description, and required scope
MethodPathWhat it doesScope
GET/api/v1/projectsList published projects (cursor pagination).read
GET/api/v1/projects/{idOrSlug}Get one published project by its slug.read
POST/api/v1/projectsCreate a project.write
PATCH/api/v1/projects/{idOrSlug}Update a project by its record id.write
DELETE/api/v1/projects/{idOrSlug}Delete a project by its record id.write
GET/api/v2/projectsList projects (page pagination).read:content
GET/api/v2/projects/{id}Get one project by record id.read:content

Courses

Course metadata (titles, prices, descriptions). Lessons are not exposed over the API.

Courses endpoints, with method, path, description, and required scope
MethodPathWhat it doesScope
GET/api/v1/coursesList courses (cursor pagination).read
POST/api/v1/coursesCreate a course.write
PATCH/api/v1/courses/{id}Update a course by its record id.write
DELETE/api/v1/courses/{id}Delete a course by its record id.write
GET/api/v2/coursesList courses (page pagination).read:content

Talks

Talks and speaking engagements.

Talks endpoints, with method, path, description, and required scope
MethodPathWhat it doesScope
GET/api/v1/talksList talks.read
POST/api/v1/talksCreate a talk.write
PATCH/api/v1/talks/{id}Update a talk by its record id.write
DELETE/api/v1/talks/{id}Delete a talk by its record id.write
GET/api/v2/talksList talks (page pagination).read:content

Portfolio

Experience, education, skills, and certifications. These return the full list with no pagination.

Portfolio endpoints, with method, path, description, and required scope
MethodPathWhat it doesScope
GET/api/v1/experienceList work experience.read
POST/api/v1/experienceCreate an experience entry.write
PATCH/api/v1/experience/{id}Update an experience entry by its record id.write
DELETE/api/v1/experience/{id}Delete an experience entry by its record id.write
GET/api/v1/educationList education.read
POST/api/v1/educationCreate an education entry.write
PATCH/api/v1/education/{id}Update an education entry by its record id.write
DELETE/api/v1/education/{id}Delete an education entry by its record id.write
GET/api/v1/skillsList skills.read
POST/api/v1/skillsCreate a skill.write
PATCH/api/v1/skills/{id}Update a skill by its record id.write
DELETE/api/v1/skills/{id}Delete a skill by its record id.write
GET/api/v1/certificationsList certifications.read
POST/api/v1/certificationsCreate a certification.write
PATCH/api/v1/certifications/{id}Update a certification by its record id.write
DELETE/api/v1/certifications/{id}Delete a certification by its record id.write

Members

Member accounts on your site (v2 only).

Members endpoints, with method, path, description, and required scope
MethodPathWhat it doesScope
GET/api/v2/membersList member accounts (page pagination).read:members

Analytics

Your site's analytics (v2 only). The exact fields depend on your plan.

Analytics endpoints, with method, path, description, and required scope
MethodPathWhat it doesScope
GET/api/v2/analyticsGet site analytics.read:analytics

Webhooks

Register where events should be sent, poll for events, and register outbound subscriptions.

Webhooks endpoints, with method, path, description, and required scope
MethodPathWhat it doesScope
GET/api/v1/webhooks/eventsList the event types you can poll for.read
GET/api/v1/webhooks/poll/{event}Poll for events of one type.read
POST/api/v2/webhooksRegister an outbound webhook.write:webhooks

API keys (admin)

Create and manage your keys. These endpoints are called by your own admin panel and need an owner login, not an API key.

API keys (admin) endpoints, with method, path, description, and required scope
MethodPathWhat it doesScope
POST/api/admin/api-keysCreate an API key (full value shown once).owner login
GET/api/admin/api-keysList API keys (prefix only).owner login
PATCH/api/admin/api-keys/{id}Update a key's name, scopes, origins, or expiry.owner login
DELETE/api/admin/api-keys/{id}Revoke (delete) a key.owner login
POST/api/admin/api-keys/{id}/rotateRotate a key's secret — issues a new fct_live_* value (shown once); old secret valid 24 h.owner login

Webhooks: getting notified of events

In short: a webhook is a message Facet sends to a URL you choose whenever something happens, so you do not have to keep asking. Every message is signed so you can trust it.

You have two ways to receive events. Push: register a URL and Facet sends each event to it as it happens. Poll: ask for recent events on a schedule (handy for tools like Zapier, n8n, Make, or Pipedream). Both deliver the same events.

The event catalog

Facet emits these nine events:

The nine webhook events, when each fires, and the main fields in its data object
EventWhen it firesMain data fields
content.createdA post, project, or custom page is created.id, collection, title, slug
content.updatedA post, project, or custom page is updated.id, collection, title, slug
content.deletedA post, project, or custom page is deleted.id, collection, title, slug
content.publishedContent is updated while not a draft. May fire alongside content.updated; de-duplicate on your side.id, collection, title, slug
purchase.completedA one-time purchase is completed.id, collection, buyer_email, content_type, content_id, amount, currency
course.enrolledA learner enrolls in a course.id, collection, course, buyer_email
course.completedA learner finishes a course.id, collection, course, buyer_email, completed_at
testimonial.submittedA testimonial is submitted.id, collection, name, email, company, role
newsletter.subscribeSomeone subscribes to your newsletter.(subscriber details)

What a delivery looks like

Each delivery is an HTTP POST with a JSON body and these headers: X-Facet-Event (the event name), X-Facet-Delivery (a unique id for this delivery), and X-Facet-Signature (the signature, described below). The body looks like this:

Example webhook delivery body
{
  "event": "purchase.completed",
  "timestamp": "2026-01-01T12:00:00Z",
  "data": {
    "id": "rec0123456789abc",
    "collection": "purchases",
    "buyer_email": "[email protected]",
    "content_type": "course",
    "content_id": "rec9876543210xyz",
    "amount": 4900,
    "currency": "usd"
  }
}

Verify the signature

Always check the signature before you trust a delivery. HMAC (Hash-based Message Authentication Code) is a signature computed from the exact request body and your subscription's secret. Facet sends it in the X-Facet-Signature header as sha256= followed by a hex value. Compute the same thing yourself and compare; if they match, the message is authentic and unaltered. Use a constant-time comparison to avoid timing attacks.

Node.js (works in an Express handler):
import crypto from "node:crypto";

// secret = the subscription secret Facet gave you.
// header = the value of the X-Facet-Signature request header.
// rawBody = the exact bytes of the request body (do NOT re-serialise).
function verifyFacetSignature(secret, header, rawBody) {
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", secret).update(rawBody).digest("hex");

  const a = Buffer.from(header ?? "", "utf8");
  const b = Buffer.from(expected, "utf8");
  // Lengths must match before timingSafeEqual, or it throws.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Express: capture the raw body so the bytes are unchanged.
//   app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }));
app.post("/facet-webhook", (req, res) => {
  const ok = verifyFacetSignature(
    process.env.FACET_WEBHOOK_SECRET,
    req.header("X-Facet-Signature"),
    req.rawBody
  );
  if (!ok) return res.status(401).send("bad signature");
  // ...handle req.body...
  res.sendStatus(200);
});
Reproduce the signature on the command line (compare its output to the hex after sha256= in the header):
# Pipe the EXACT raw body to openssl and key it with your secret.
printf '%s' "$RAW_BODY" \
  | openssl dgst -sha256 -hmac "$FACET_WEBHOOK_SECRET"
# -> (stdin)= 3f5b... compare with the X-Facet-Signature header value

Retries and failures (push)

Respond with any 2xx status to acknowledge a delivery. If your endpoint returns a non-2xx status or times out, Facet retries on a back-off schedule: 1 minute, then 5 minutes, 30 minutes, 2 hours, and 12 hours (five retries, so six attempts in total). After 10 consecutive failures the subscription is switched off automatically; re-enable it from the admin panel once your endpoint is healthy. You can also send a test delivery and review delivery history from the admin panel.

Polling instead

To poll, call GET /api/v1/webhooks/poll/{event} with your read-scoped key. By default you get the newest events first; pass the cursor from a previous response to page through them, or pass since (an RFC 3339 timestamp) to replay oldest-first. Each event carries a stable id so you can avoid handling it twice. Polling is capped at 20 requests per minute per key, and events are kept for 7 days.

Polling for purchase events
curl "https://your-site.facetcloud.io/api/v1/webhooks/poll/purchase.completed?limit=50" \
  -H "Authorization: Bearer fct_live_YOUR_KEY_HERE"

Error codes and what they mean

In short: every error is JSON with an error object carrying a code (stable string) and a message (human-readable). The status code tells you the category.

Whenever a request fails, the body is a JSON object: { "error": { "code": "…", "message": "…" } }. Here is what each status means.

HTTP error status codes, their meaning, and an example body
StatusMeaningExample body
400 Bad RequestThe request body or a parameter was malformed.{ "error": { "code": "invalid_request", "message": "invalid request body" } }
401 UnauthorizedThe Authorization header is missing or malformed, or the key is unknown.{ "error": { "code": "unauthorized", "message": "missing or invalid Authorization header" } }
403 ForbiddenThe key is valid but disabled, expired, lacks the required scope, or the origin is not allowed.{ "error": { "code": "insufficient_scope", "message": "insufficient scope" } }
404 Not FoundThe resource does not exist or is not public.{ "error": { "code": "not_found", "message": "not found" } }
429 Too Many RequestsYou hit the rate limit. Read the Retry-After header (seconds).{ "error": { "code": "rate_limited", "message": "too many requests" } }
500 Internal Server ErrorSomething failed on our side, for example a query error.{ "error": { "code": "internal_error", "message": "query failed" } }
503 Service UnavailableWebhook storage is not provisioned on this site yet.{ "error": { "code": "not_provisioned", "message": "webhooks_not_provisioned" } }

Rate limits: how often you can call

In short: each tier sets how many requests per minute you get. Read the response headers to track your remaining budget.

Limits are counted per client IP address. A short burst above the steady rate is allowed (the burst column). Every response carries these headers:

  • X-RateLimit-Limit — the maximum (the burst size).
  • X-RateLimit-Remaining — how many you have left right now.
  • X-RateLimit-Reset — a Unix timestamp (seconds) when the window resets.
  • Retry-After — only on a 429: how many seconds to wait.
Rate-limit tiers with their rate, burst, and which endpoints they apply to
TierRateBurstApplies to
Normal60 requests / minute10All public read and write endpoints (v1 and v2).
Moderate10 requests / minute5Admin key and webhook management endpoints.
Strict5 requests / minute3Sensitive endpoints such as password change (not part of this API).
Poll cap20 polls / minute / keyExtra cap on GET /api/v1/webhooks/poll/{event}, on top of the Normal tier.

When you hit a limit you get a 429 with { "error": { "code": "rate_limited", "message": "too many requests" } }. Wait the number of seconds in Retry-After, then try again.

Need the exact machine-readable contract? The OpenAPI 3.1 file is the source of truth for paths, parameters, schemas, and scopes.