API Reference

10 min readUpdated 27 Apr 2026

VeloCMS exposes two API surfaces: the PocketBase tenant API (standard PocketBase REST endpoints at your POCKETBASE_URL) and the VeloCMS webhook API (/api/stripe/webhook and /api/member-webhook/[tenantSlug]). This reference covers both, with a focus on the auth model, webhook signing, and the three endpoints you'll actually hit when building integrations.

Collections overview

CollectionPurposeAuth required
postsBlog posts — title, content_html/json, slug, status, tagsAdmin or member (visibility-gated)
mediaUploaded files — filename, url, mime_type, size, dimensionsAdmin
site_settingsBlog name, description, theme, integrations configAdmin
blog_membersReader accounts — email, tier, stripe_customer_idAdmin or self
categoriesPost categories — name, slug, descriptionAdmin
tenantsTenant registry (multi-mode only) — slug, owner_idAdmin

Auth model

VeloCMS has two separate auth systems that must never be confused (P-022). Admin auth is for blog owners — credentials live in PocketBase's built-in _superusers collection and the session is stored in a pb_auth cookie. Member (reader) auth is for subscribers — stored in blog_members and the session uses a pb_member_auth cookie. An admin can read everything. A member can only read public posts plus their own member record.

Authenticate as admin
# Get an admin auth token
curl -X POST '$POCKETBASE_URL/api/admins/auth-with-password' \
  -H 'Content-Type: application/json' \
  -d '{"identity":"[email protected]","password":"yourpassword"}'

# Response includes a token field — use it as a Bearer header:
# Authorization: Bearer <token>
bash
Authenticate as member
# Member (reader) login — against blog_members collection
curl -X POST '$POCKETBASE_URL/api/collections/blog_members/auth-with-password' \
  -H 'Content-Type: application/json' \
  -d '{"identity":"[email protected]","password":"memberpassword"}'
bash

Listing posts

The posts collection supports PocketBase's standard query syntax for filtering, sorting, and pagination. Published public posts are readable without auth:

List published posts
curl '$POCKETBASE_URL/api/collections/posts/records?filter=(status="published")&sort=-published_at&page=1&perPage=20'
bash

Creating a post

Creating a post requires admin auth. The content_json field holds the TipTap JSON AST; content_html is the rendered HTML for display. You can set either or both — if you only set content_html, the editor won't be able to reopen the post for editing.

Create a post
curl -X POST '$POCKETBASE_URL/api/collections/posts/records' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <admin-token>' \
  -d '{
    "title": "My first post",
    "slug": "my-first-post",
    "content_html": "<p>Hello, world.</p>",
    "status": "draft",
    "visibility": "public",
    "tags": ["intro"],
    "tenant_id": "<your-tenant-id>"
  }'
bash

Uploading media

Media uploads go to the media collection as multipart/form-data. VeloCMS backend stores the file in Cloudflare R2 and saves the public URL in the url field:

Upload a file
curl -X POST '$POCKETBASE_URL/api/collections/media/records' \
  -H 'Authorization: Bearer <admin-token>' \
  -F 'file=@/path/to/image.jpg' \
  -F 'tenant_id=<your-tenant-id>'
bash

Webhook signing — HMAC SHA-256

Both webhook endpoints (/api/stripe/webhook and /api/member-webhook/[tenantSlug]) verify incoming payloads using HMAC-SHA256 signatures. This prevents replay attacks and ensures the payload wasn't tampered with in transit.

For Stripe webhooks, the signature lives in the Stripe-Signature header and is validated using your STRIPE_WEBHOOK_SECRET (the one from the Stripe Dashboard endpoint, not the CLI). For the member-webhook endpoint, the signature is in an X-VeloCMS-Signature header and uses a per-tenant HMAC secret stored in site_settings.member_webhook_secret.

Verify member-webhook signature (example)
import { createHmac } from "crypto";

function verifyMemberWebhook(
  rawBody: string,
  signature: string,
  secret: string
): boolean {
  const expected = createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");
  return `sha256=${expected}` === signature;
}
typescript

Member-webhook payload

The /api/member-webhook/[tenantSlug] endpoint receives events from Stripe when a reader's subscription changes. The payload shape varies by event type:

Event typePayload fieldsAction
member.signupemail, tier, stripeCustomerIdCreate blog_members record
member.upgradedemail, tier, previousTierUpdate blog_members.tier
member.cancelledemail, effectiveDateDowngrade to free tier on effectiveDate
member.payment_failedemail, attemptCountNotify member, restrict on 3rd failure

HMAC unsubscribe links

Newsletter unsubscribe links include an HMAC token that proves the link was generated by VeloCMS for a specific email address — preventing one subscriber from unsubscribing another. The token is generated server-side and validated on the /member/unsubscribe route:

Generate unsubscribe token (server-side)
import { createHmac } from "crypto";

function generateUnsubscribeToken(email: string, secret: string): string {
  return createHmac("sha256", secret).update(email).digest("hex");
}

// Link format: /member/unsubscribe?email=<encoded>&token=<hmac>
typescript