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

PropertyDescription
public_keyPublic identifier sent in requests
secret_keySecret verified via bcrypt hash comparison
is_activeKey must be active to authenticate
start_timeIf set, key is rejected before this time (UTC)
expires_timeIf set, key is rejected after this time (UTC)
ip_restrictionComma-separated list of allowed IPs (optional)
permissionAccess level (see Permission Levels below)

Permission Levels

LevelReadCreate/UpdateDeleteDescription
1YesNoNoRead-only
2NoYesNoWrite-only
3YesYesNoRead + Write
4+YesYesYesFull access
Note: Permission level 2 grants write access but blocks read operations (GET requests).

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=ASC

Add a trailing s to the class name for collections.

Pagination Parameters:

ParameterDefaultDescription
page0Page number (0-based)
numperpage3Items per page
sort(none)Database column to sort by
sdirectionASCSort direction: ASC or DESC
Any additional query parameters are passed as filter options to the Multi class. Check the specific Multi class to see which filter keys it accepts.

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=value2

If 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=value2

Fields 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

StatusError TypeMeaning
400AuthenticationErrorMissing headers, invalid key, deleted user
400TransactionErrorObject not found, validation failure, save error, invalid object name
401AuthenticationErrorWrong secret, IP restricted, inactive/expired key
403AuthenticationErrorInsufficient permission for this operation
426SecurityErrorHTTPS required
429RateLimitErrorRate limit exceeded

Rate Limiting

The API enforces two rate limits per IP address:

LimitThresholdWindow
General requests1,000Per hour
Failed auth attempts10Per 15 minutes
When exceeded, the API returns HTTP 429 with a 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.com

Preflight 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-referrer

Action 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": { ... }
}

  • redirect is included when the action would have redirected in the web UI (informational — the API consumer decides what to do with it)
  • data contains any output data from the logic function
Validation error (HTTP 422):
{
    "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

ActionDescriptionSession
registerRegister a new user accountNo
password_reset_1Request password reset emailNo
password_reset_2Set new password via reset codeNo
password_setSet password on first loginNo
password_editChange password (logged in)Yes
change_password_requiredForced password changeYes
contact_preferencesUpdate contact preferencesYes
account_editUpdate profile fieldsYes
address_editUpdate addressYes
phone_numbers_editUpdate phone numbersYes
change_tierChange subscription tierYes
surveySubmit survey responseYes
bookingBook an appointmentYes
cartAdd item to cartYes
cart_clearClear cartYes
event_registerRegister for an eventYes
event_withdrawWithdraw from eventYes
event_waiting_listJoin event waiting listYes
event_sessionsSelect event sessionsYes
event_sessions_courseSelect course sessionsYes
orders_recurring_actionRecurring order actionYes

Action Discovery Endpoint

GET /api/v1/actions

Returns 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 with apk_permission = 1 (read-only CRUD) can call management endpoints; a permission-5 admin's key cannot, regardless of apk_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.

EndpointDescription
healthLiveness probe: {ok: true, version: "…"} — used by JobCommandBuilder::has_api()
statsDisk, memory, load, uptime, PostgreSQL liveness, Joinery version, DB list
versionSystem version, schema version, per-plugin versions
databasesList of PostgreSQL databases accessible to the site
errors/recentLast N error.log lines matching Fatal/Exception/Error (default 20, cap 200)
backups/listFiles in /backups/ with size and date
backups/fetch?path=…Streams a backup file as application/octet-stream (path must be under /backups/)
Discovery: 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.phpGET /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

StatusError TypeMeaning
400AuthenticationErrorMissing headers, invalid key, deleted user
400TransactionErrorObject not found, validation failure, save error, invalid object name
401AuthenticationErrorWrong secret, IP restricted, inactive/expired key
403AuthenticationErrorInsufficient permission for this operation
426SecurityErrorHTTPS required
429RateLimitErrorRate limit exceeded

Action Error Types

StatusError TypeMeaning
404ActionErrorUnknown action name or action not available via API
405ActionErrorWrong HTTP method (actions require POST)
422ActionErrorBusiness logic error (e.g., feature disabled, invalid state)
422ValidationErrorInput 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.