REST API Documentation
Overview
The Joinery platform provides a REST API for programmatic access to data and operations.
- Base URL:
https://{site-domain}/api/v1/ - Format: JSON (all requests and responses)
- Methods: GET (read), POST (create), PUT (update), DELETE (soft delete)
- HTTPS Required: All requests must use HTTPS
Authentication
All API requests require two custom headers:
public_key: {your_public_key}
secret_key: {your_secret_key}Obtaining API Keys
API keys are created by an administrator via Admin > API Keys. Each key is associated with a user account. The key inherits that user's identity for object-level authorization checks.
Key Properties
| Property | Description |
|---|---|
public_key | Public identifier sent in requests |
secret_key | Secret verified via bcrypt hash comparison |
is_active | Key must be active to authenticate |
start_time | If set, key is rejected before this time (UTC) |
expires_time | If set, key is rejected after this time (UTC) |
ip_restriction | Comma-separated list of allowed IPs (optional) |
permission | Access level (see Permission Levels below) |
Permission Levels
| Level | Read | Create/Update | Delete | Description |
|---|---|---|---|---|
| 1 | Yes | No | No | Read-only |
| 2 | No | Yes | No | Write-only |
| 3 | Yes | Yes | No | Read + Write |
| 4+ | Yes | Yes | Yes | Full access |
CRUD Endpoints
Read Single Object
GET /api/v1/{ClassName}/{id}Example: GET /api/v1/User/123
Response:
{
"api_version": "1.0",
"success_message": "User found.",
"data": {
"usr_user_id": 123,
"usr_first_name": "Jane",
"usr_last_name": "Doe",
"usr_email": "[email protected]"
}
}List Objects (Collection)
GET /api/v1/{ClassName}s?page=0&numperpage=10&sort=field&sdirection=ASCAdd a trailing s to the class name for collections.
Pagination Parameters:
| Parameter | Default | Description |
|---|---|---|
page | 0 | Page number (0-based) |
numperpage | 3 | Items per page |
sort | (none) | Database column to sort by |
sdirection | ASC | Sort direction: ASC or DESC |
Example: GET /api/v1/Users?page=0&numperpage=20&sort=usr_id&sdirection=DESC
Response:
{
"api_version": "1.0",
"success_message": "",
"num_results": 100,
"page": 0,
"numperpage": 20,
"data": [ ... ]
}Create Object
POST /api/v1/{ClassName}
Content-Type: application/x-www-form-urlencoded
field1=value1&field2=value2If the model has a CreateNew() static method, it is called first. Otherwise, a new object is created and fields are set from the POST body.
Response:
{
"api_version": "1.0",
"success_message": "New User successful.",
"data": { ... }
}Update Object
PUT /api/v1/{ClassName}/{id}?field1=value1&field2=value2Fields to update are passed as query string parameters.
Response:
{
"api_version": "1.0",
"success_message": "User update successful.",
"data": { ... }
}Soft Delete Object
DELETE /api/v1/{ClassName}/{id}Sets the delete timestamp on the object. Does not permanently remove data.
Response:
{
"api_version": "1.0",
"success_message": "Deletion successful.",
"data": { ... }
}Available Models
Any SystemBase model class is available via the API. Class names are case-sensitive and use PascalCase.
Common models include: User, Product, Event, EventRegistrant, EventSession, Order, OrderItem, Group, GroupMember, Post, Page, Email, Message, File, CouponCode, SubscriptionTier, Location, Video, Comment, Survey, SurveyAnswer, Question, QuestionOption, MailingList, MailingListRegistrant.
Error Handling
Error Response Format
{
"api_version": "1.0",
"errortype": "AuthenticationError",
"error": "Error: description of what went wrong",
"data": ""
}Error Types and HTTP Status Codes
| Status | Error Type | Meaning |
|---|---|---|
| 400 | AuthenticationError | Missing headers, invalid key, deleted user |
| 400 | TransactionError | Object not found, validation failure, save error, invalid object name |
| 401 | AuthenticationError | Wrong secret, IP restricted, inactive/expired key |
| 403 | AuthenticationError | Insufficient permission for this operation |
| 426 | SecurityError | HTTPS required |
| 429 | RateLimitError | Rate limit exceeded |
Rate Limiting
The API enforces two rate limits per IP address:
| Limit | Threshold | Window |
|---|---|---|
| General requests | 1,000 | Per hour |
| Failed auth attempts | 10 | Per 15 minutes |
RateLimitError. Wait for the time window to pass before retrying.HTTPS Requirement
All API requests must use HTTPS. Requests over plain HTTP are rejected with HTTP 426 (Upgrade Required).
This can be disabled for development by setting api_require_https to false in the site settings.
CORS
CORS is disabled by default. To enable it, set api_allowed_origins in site settings to a comma-separated list of allowed origins:
https://example.com,https://app.example.comPreflight OPTIONS requests are handled automatically when CORS is configured.
Security Headers
All API responses include:
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block
Referrer-Policy: no-referrerAction Endpoints
Actions execute multi-step business logic (registration, event signup, payments, etc.) rather than raw CRUD operations. All logic functions that have been opted in via a companion _api() function are available.
Making a Logic Function Available via API
Add a companion function to your logic file:
// In logic/your_action_logic.php
function your_action_logic_api() {
return [
'requires_session' => true, // default: true
'description' => 'What this action does',
];
}That's it — no registry file or mapping needed.
Action Request Format
POST /api/v1/action/{action_name}
Content-Type: application/json
public_key: {key}
secret_key: {key}
{ "field": "value", ... }Actions require API key write permission (level 2+).
Action Response Formats
Success (HTTP 200):
{
"api_version": "1.0",
"success_message": "Action 'register' completed successfully.",
"redirect": "/page/register-thanks",
"data": { ... }
}redirectis included when the action would have redirected in the web UI (informational — the API consumer decides what to do with it)datacontains any output data from the logic function
{
"api_version": "1.0",
"errortype": "ValidationError",
"error": "Please correct the errors below",
"validation_errors": {
"field_name": "Error message for this field"
},
"data": {}
}Action error (HTTP 422):
{
"api_version": "1.0",
"errortype": "ActionError",
"error": "This feature is turned off",
"data": {}
}Available Actions
| Action | Description | Session |
|---|---|---|
register | Register a new user account | No |
password_reset_1 | Request password reset email | No |
password_reset_2 | Set new password via reset code | No |
password_set | Set password on first login | No |
password_edit | Change password (logged in) | Yes |
change_password_required | Forced password change | Yes |
contact_preferences | Update contact preferences | Yes |
account_edit | Update profile fields | Yes |
address_edit | Update address | Yes |
phone_numbers_edit | Update phone numbers | Yes |
change_tier | Change subscription tier | Yes |
survey | Submit survey response | Yes |
booking | Book an appointment | Yes |
cart | Add item to cart | Yes |
cart_clear | Clear cart | Yes |
event_register | Register for an event | Yes |
event_withdraw | Withdraw from event | Yes |
event_waiting_list | Join event waiting list | Yes |
event_sessions | Select event sessions | Yes |
event_sessions_course | Select course sessions | Yes |
orders_recurring_action | Recurring order action | Yes |
Action Discovery Endpoint
GET /api/v1/actionsReturns a list of all available actions with descriptions. Useful for API consumers to programmatically determine what actions are available.
Response:
{
"api_version": "1.0",
"success_message": "Available actions",
"data": {
"register": {
"description": "Register a new user account",
"requires_session": false
},
"event_register": {
"description": "Register for an event",
"requires_session": true
}
}
}Management API (Read-Only)
The /api/v1/management/* namespace is a separate read-only surface used by the server_manager control plane to observe managed nodes (stats, version, backup files, error log). It is not part of the public CRUD API: endpoints don't map to SystemBase models and have their own convention.
Authorization
Management endpoints reuse the existing stg_api_keys table unchanged. The gate is user-level, not key-level:
- The API key's owning user must have
usr_permission >= 10(superadmin). apk_permission(1–4 CRUD gradient) is NOT the gate here — it is orthogonal to the management check. A superadmin's key withapk_permission = 1(read-only CRUD) can call management endpoints; a permission-5 admin's key cannot, regardless ofapk_permission.- All other existing auth checks (active, not expired, IP restriction, bcrypt secret) apply unchanged — management dispatch only happens after
apiv1.php's full auth chain has passed.
Endpoints
All under /api/v1/management/, all GET, all return the standard success envelope except backups/fetch which streams a binary file.
| Endpoint | Description |
|---|---|
health | Liveness probe: {ok: true, version: "…"} — used by JobCommandBuilder::has_api() |
stats | Disk, memory, load, uptime, PostgreSQL liveness, Joinery version, DB list |
version | System version, schema version, per-plugin versions |
databases | List of PostgreSQL databases accessible to the site |
errors/recent | Last N error.log lines matching Fatal/Exception/Error (default 20, cap 200) |
backups/list | Files in /backups/ with size and date |
backups/fetch?path=… | Streams a backup file as application/octet-stream (path must be under /backups/) |
GET /api/v1/management lists every endpoint with its method and description. Parallels /api/v1/actions.Adding a management endpoint
Convention-based, mirrors the action-endpoints layout. A single file defines two functions:
// includes/management_api/my_thing_handler.php
function my_thing_handler($request) {
return ['value' => 42]; // non-null array → router wraps with api_success()
}
function my_thing_handler_api() {
return [
'method' => 'GET',
'description' => 'What this endpoint does',
];
}$request is an associative array: method, path, query ($_GET), body (decoded JSON for non-GET), headers. Handlers should use $request rather than touching $_GET/$_POST directly. For streaming endpoints (backups/fetch), write bytes yourself and return null — the router will not append an envelope.
Nested paths mirror subdirectories: includes/management_api/backups/list_handler.php → GET /api/v1/management/backups/list → function backups_list_handler().
Writes? Not here.
The management API is permanently read-only. Mutating operations (backups, restores, upgrades, installs, deletions) stay on SSH — SSH is the more deliberate transport for state changes, and a compromised read-only key cannot do damage. If you find yourself wanting to add a write endpoint, extend SSH instead.
Error Types
CRUD Error Types
| Status | Error Type | Meaning |
|---|---|---|
| 400 | AuthenticationError | Missing headers, invalid key, deleted user |
| 400 | TransactionError | Object not found, validation failure, save error, invalid object name |
| 401 | AuthenticationError | Wrong secret, IP restricted, inactive/expired key |
| 403 | AuthenticationError | Insufficient permission for this operation |
| 426 | SecurityError | HTTPS required |
| 429 | RateLimitError | Rate limit exceeded |
Action Error Types
| Status | Error Type | Meaning |
|---|---|---|
| 404 | ActionError | Unknown action name or action not available via API |
| 405 | ActionError | Wrong HTTP method (actions require POST) |
| 422 | ActionError | Business logic error (e.g., feature disabled, invalid state) |
| 422 | ValidationError | Input validation failed — check validation_errors for field-level detail |
Request Logging
All API requests are logged for audit purposes. Logs include: feature, action, IP address, user ID, success/failure, HTTP status code, and response time. Secret keys and request bodies are never logged.
Logs are retained for a configurable period (default: 90 days) and automatically cleaned up by a scheduled task.