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: Bearerheader. Lifetime is controlled byJWT_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/loginto 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), andschema_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.
Navigation
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).