Skip to content

User Manual

This guide explains how to authenticate with the Snackbox API and interact with its resources. The full endpoint reference is in the api/openapi.yaml on GitLab.

Authentication

Snackbox uses JWT (JSON Web Token) bearer authentication. Obtain a token by logging in, then include it in every subsequent request via the Authorization header.

Token lifecycle

Snackbox uses a two-token model:

  • Access token - a short-lived JWT included in every API request via the Authorization: Bearer header. Lifetime is controlled by JWT_EXPIRY (default 15 m).
  • Refresh token - a long-lived opaque token stored in the database. Used only to obtain a new access token when the current one expires. Lifetime is controlled by JWT_REFRESH_EXPIRY (default 7 days).

Login

curl -s -X POST http://localhost:8080/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "admin@example.com", "password": "your-password"}' \
  | jq .

Response:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh_token": "a3f1c2d4e5...",
  "expires_at": "2024-01-02T15:04:05Z",
  "refresh_expires_at": "2024-01-09T15:04:05Z"
}

Store both tokens. Pass the access token with every authenticated request:

TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/v1/posts

Refresh

When the access token expires, use the refresh token to obtain a new pair without re-entering credentials:

curl -s -X POST http://localhost:8080/api/v1/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{"refresh_token": "a3f1c2d4e5..."}' \
  | jq .

Returns a new access_token, refresh_token, expires_at, and refresh_expires_at. The old refresh token is immediately invalidated (token rotation). Store the new refresh token for the next refresh. Use refresh_expires_at to proactively re-authenticate before the refresh token expires rather than waiting for a 401.

Logout

curl -s -X POST http://localhost:8080/api/v1/auth/logout \
  -H "Content-Type: application/json" \
  -d '{"refresh_token": "a3f1c2d4e5..."}'

Returns 204 No Content. The refresh token is deleted from the database, invalidating the session. No Authorization header is required - the refresh token itself is the credential. Submitting an already-invalidated or unknown token returns 204 (idempotent).

Logout all sessions

To invalidate every active session at once (e.g. after a password change or suspected compromise), use logout-all. Requires a valid access token.

curl -s -X POST http://localhost:8080/api/v1/auth/logout-all \
  -H "Authorization: Bearer $TOKEN"

Returns 204 No Content. All refresh tokens for the authenticated user are deleted. Existing access tokens remain valid until they expire naturally - shorten JWT_EXPIRY if immediate invalidation is required.

Change password

Any authenticated user can change their own password without admin intervention:

curl -s -X POST http://localhost:8080/api/v1/auth/change-password \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"current_password": "old-password", "new_password": "new-password"}' \
  -w "%{http_code}"

Returns 204 No Content on success. Returns 401 if current_password is wrong. The new_password must be between 8 and 72 characters.

Note: A successful password change immediately invalidates all active sessions for your account, including the one used to make this request. You must re-authenticate via POST /auth/login to obtain new tokens.

This endpoint is rate-limited per IP address (same limit as login). Exceeding the limit returns 429 Too Many Requests with a Retry-After header indicating how many seconds to wait before retrying.

Roles

Every user account is assigned exactly one role. The role determines which endpoints are accessible.

Role What it can do
admin Full access - all resources, user management, site settings
author Create, edit, and delete their own pages and posts; upload media files. Cannot delete media (admin only), manage users, settings, or other authors' content
editor Create, edit, and delete any page or post regardless of authorship; upload media files. Cannot delete media (admin only), manage users, settings, navigation, or social accounts
member Authenticate and read own profile; no write capabilities in 1.x

The member role is a forward-compatible identity for registered users. Members have a persistent account but no content creation rights in 1.x. Their permissions will expand in 2.0.0 (comments, newsletter subscriptions).

Resources

All paginated list endpoints accept ?page=<n>&limit=<n> query parameters (default: page=1, limit=20, max limit=100) and return the total item count in the X-Total-Count response header.

Health

Check whether the server is running. No authentication required.

curl http://localhost:8080/health

Version

Returns version information. No authentication required, but the response varies by caller:

  • Unauthenticated callers (and non-admin authenticated callers) receive only the release version string.
  • Authenticated admins additionally receive commit (first 7 characters of the VCS revision hash), dirty (whether the binary was built from an uncommitted working tree), and schema_version (the latest applied migration filename).
# Unauthenticated  -  version only
curl http://localhost:8080/version

# Authenticated admin  -  full BuildInfo
curl -H "Authorization: Bearer $ADMIN_TOKEN" http://localhost:8080/version

Unauthenticated response:

{
  "version": "1.0.0"
}

Authenticated admin response:

{
  "version": "1.0.0",
  "commit": "abc1234",
  "dirty": false,
  "schema_version": "001_init.sql"
}

Tags

Tags categorize posts and pages.

# List all tags (supports ?page=1&limit=20)
curl "http://localhost:8080/api/v1/tags?page=1&limit=10"

# Get a single tag by numeric ID
curl http://localhost:8080/api/v1/tags/1

# Get a single tag by slug
curl http://localhost:8080/api/v1/tags/guides

# Create a tag (requires admin role)
curl -X POST http://localhost:8080/api/v1/tags \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "guides", "slug": "guides", "image_url": "https://example.com/guides.png"}'

# Update a tag (requires admin role)
curl -X PUT http://localhost:8080/api/v1/tags/1 \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "tutorials", "slug": "tutorials", "image_url": null}'

# Delete a tag (requires admin role)
curl -X DELETE http://localhost:8080/api/v1/tags/1 \
  -H "Authorization: Bearer $TOKEN"

The optional image_url field accepts an absolute URI and is intended for a representative cover image for the tag (e.g. displayed as a category banner). Pass null to clear it.

Pages

Pages are static content entries (e.g. "About", "Contact").

The list endpoint returns all published pages. Unpublished pages return 404 on single-item endpoints (GET /pages/{id}). Authenticated admin, editor, and author users additionally see draft (unpublished) content in list responses.

The published_at field records when the page was first published. It is cleared (null) when the page is unpublished, and re-set on the next publish.

# List all pages
curl http://localhost:8080/api/v1/pages

# Get a single page
curl http://localhost:8080/api/v1/pages/1

# Create a page (requires admin, author, or editor role)
curl -X POST http://localhost:8080/api/v1/pages \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "About",
    "slug": "about",
    "excerpt": "Learn more about our team.",
    "content": "Welcome to our site.",
    "title_image": "https://example.com/about-hero.png",
    "published": true,
    "author_ids": [1]
  }'

# Update a page (requires admin, author, or editor role)
curl -X PUT http://localhost:8080/api/v1/pages/1 \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title": "About Us", "slug": "about", "excerpt": "Our story.", "content": "...", "published": true, "author_ids": [1, 2]}'

# Delete a page (requires admin, author, or editor role)
curl -X DELETE http://localhost:8080/api/v1/pages/1 \
  -H "Authorization: Bearer $TOKEN"

Scheduled publishing

Set publish_at to an ISO 8601 UTC timestamp to schedule a page for future publication. Pages with a future publish_at return 404 on public detail endpoints until the timestamp passes.

# Create a scheduled page
curl -X POST http://localhost:8080/api/v1/pages \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Coming Soon",
    "slug": "coming-soon",
    "content": "Stay tuned.",
    "published": false,
    "publish_at": "2025-12-31T23:59:59Z"
  }'

Posts

Posts are regular content entries (e.g. blog posts, articles).

The list endpoint returns all published posts. Unpublished posts return 404 on single-item endpoints (GET /posts/{id}). Authenticated admin, editor, and author users additionally see draft (unpublished) content in list responses.

The published_at field records when the post was first published. It is cleared (null) when the post is unpublished, and re-set on the next publish.

# List all posts (supports ?page=1&limit=20)
curl "http://localhost:8080/api/v1/posts?page=1&limit=10"

# Get a single post
curl http://localhost:8080/api/v1/posts/1

# Create a post (requires admin, author, or editor role)
curl -X POST http://localhost:8080/api/v1/posts \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "My First Post",
    "slug": "my-first-post",
    "excerpt": "A short summary shown in listings.",
    "content": "Hello, world.",
    "title_image": "https://example.com/hero.png",
    "published": true,
    "tag_ids": [1, 2],
    "author_ids": [1]
  }'

# Update a post (requires admin, author, or editor role)
curl -X PUT http://localhost:8080/api/v1/posts/1 \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title": "Updated Title", "slug": "my-first-post", "content": "...", "published": true, "tag_ids": []}'

# Delete a post (requires admin, author, or editor role)
curl -X DELETE http://localhost:8080/api/v1/posts/1 \
  -H "Authorization: Bearer $TOKEN"

Scheduled publishing

Set publish_at to an ISO 8601 UTC timestamp to schedule a post for future publication. Content with a future publish_at is hidden from public list and detail endpoints until the timestamp passes.

# Create a scheduled post  -  hidden until 2025-12-31 23:59:59 UTC
curl -X POST http://localhost:8080/api/v1/posts \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "New Year Post",
    "slug": "new-year-post",
    "content": "Happy new year!",
    "published": false,
    "publish_at": "2025-12-31T23:59:59Z"
  }'

To publish due content, run the snackbox publish-due CLI command or wire it into a cron job - see the CLI reference in the Administrator Manual.

Media

Upload and retrieve binary files (images, documents, audio, video, etc.).

# Upload a file (requires admin or author role)
curl -X POST http://localhost:8080/api/v1/media \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@photo.jpg"

# List all media (public  -  no authentication required)
curl http://localhost:8080/api/v1/media

# Get metadata for a single media item (public  -  no authentication required)
curl http://localhost:8080/api/v1/media/1

# Delete a media item (requires admin role)
curl -X DELETE http://localhost:8080/api/v1/media/1 \
  -H "Authorization: Bearer $TOKEN"

The default maximum upload size is 32 MB. This limit is configurable by the administrator via MAX_UPLOAD_SIZE.

My Profile

Any authenticated user can view and update their own profile through the self-service endpoints, regardless of role. No admin privileges are required.

# Get your own profile
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/v1/me

# Update your name or email
curl -X PUT http://localhost:8080/api/v1/me \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Jane Doe", "email": "jane@example.com"}'

# To change your password, use the dedicated endpoint (requires current password)
curl -X POST http://localhost:8080/api/v1/auth/change-password \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"current_password": "old-password", "new_password": "new-password"}'

The role field is ignored - users cannot change their own role via this endpoint. To change a user's role, an admin must use PUT /api/v1/users/{id}. Sending a password field to PUT /me returns a 422 validation error; use POST /api/v1/auth/change-password instead.

Users

User management is restricted to administrators.

# List all users (admin only)
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/v1/users

# Get a single user (admin only)
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/v1/users/1

# Create a user (admin only)
curl -X POST http://localhost:8080/api/v1/users \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Jane Doe",
    "email": "jane@example.com",
    "password": "secure-password",
    "role": "author"
  }'

# Update a user (admin only)
curl -X PUT http://localhost:8080/api/v1/users/2 \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Jane Doe", "email": "jane@example.com", "role": "author"}'

# Delete a user (admin only)
curl -X DELETE http://localhost:8080/api/v1/users/2 \
  -H "Authorization: Bearer $TOKEN"

See the Roles reference in the Administrator Manual for a full description of each role and its permissions.

Settings

Site-wide configuration is a public read, admin write singleton resource.

# Get current settings (public  -  no authentication required)
curl http://localhost:8080/api/v1/settings

# Update settings (admin only)
curl -X PUT http://localhost:8080/api/v1/settings \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "My Site",
    "description": "A minimal headless CMS",
    "slogan": "Simple and fast",
    "logo_url": "https://example.com/logo.png",
    "icon_url": "https://example.com/icon.png",
    "brand_color": "#ff6600",
    "language": "en",
    "theme": "default",
    "announcement_text": "**Welcome** to my site!",
    "announcement_active": true
  }'

All fields are optional. language must be a valid BCP 47 tag (e.g. en, en-US, zh-Hans) and defaults to "en" when omitted. brand_color must be a CSS hex color (#fff or #ff6600). logo_url and icon_url must be absolute http or https URLs. announcement_text accepts Markdown and is capped at 500 characters.

Social Accounts

Site social profile links (Mastodon, GitHub, LinkedIn, etc.) that frontends can use to render "follow us" links.

# List all social accounts (public  -  no authentication required)
curl http://localhost:8080/api/v1/settings/social-accounts

# Add a social account (admin only)
curl -X POST http://localhost:8080/api/v1/settings/social-accounts \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "GitHub", "url": "https://github.com/example"}'

# Delete a social account (admin only)
curl -X DELETE http://localhost:8080/api/v1/settings/social-accounts/1 \
  -H "Authorization: Bearer $TOKEN"

name is a free-form label (e.g. "Mastodon", "LinkedIn"). url must be a valid http or https URL.

Site navigation menus. Returns all items ordered by type (main first, then secondary), then by order within each type.

# List all navigation items (public  -  no authentication required)
curl http://localhost:8080/api/v1/navigation

# Create a navigation item (admin only)
curl -X POST http://localhost:8080/api/v1/navigation \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "Home",
    "url": "/",
    "type": "main",
    "order": 1
  }'

# Update a navigation item (admin only)
curl -X PUT http://localhost:8080/api/v1/navigation/1 \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"label": "Home", "url": "/", "type": "main", "order": 0}'

# Delete a navigation item (admin only)
curl -X DELETE http://localhost:8080/api/v1/navigation/1 \
  -H "Authorization: Bearer $TOKEN"

type must be main or secondary. url is an arbitrary string - absolute URLs, relative paths, and anchors are all valid. order is a non-negative integer controlling the sort order within each type group.

Rate Limiting

The login, token-refresh, and change-password endpoints (POST /api/v1/auth/login, POST /api/v1/auth/refresh, and POST /api/v1/auth/change-password) are rate-limited per IP address. The default limit is 10 requests per second. Exceeding the limit returns 429 Too Many Requests with a Retry-After header indicating how many seconds to wait before retrying. Contact your administrator if you need the limit adjusted (AUTH_RATE_LIMIT config variable).