API Reference
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
| Collection | Purpose | Auth required |
|---|---|---|
| posts | Blog posts — title, content_html/json, slug, status, tags | Admin or member (visibility-gated) |
| media | Uploaded files — filename, url, mime_type, size, dimensions | Admin |
| site_settings | Blog name, description, theme, integrations config | Admin |
| blog_members | Reader accounts — email, tier, stripe_customer_id | Admin or self |
| categories | Post categories — name, slug, description | Admin |
| tenants | Tenant registry (multi-mode only) — slug, owner_id | Admin |
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.
# 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># 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"}'Listing posts
The posts collection supports PocketBase's standard query syntax for filtering, sorting, and pagination. Published public posts are readable without auth:
curl '$POCKETBASE_URL/api/collections/posts/records?filter=(status="published")&sort=-published_at&page=1&perPage=20'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.
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>"
}'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:
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>'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.
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;
}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 type | Payload fields | Action |
|---|---|---|
| member.signup | email, tier, stripeCustomerId | Create blog_members record |
| member.upgraded | email, tier, previousTier | Update blog_members.tier |
| member.cancelled | email, effectiveDate | Downgrade to free tier on effectiveDate |
| member.payment_failed | email, attemptCount | Notify 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:
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>