Extend the REST API
Jetonomy's REST API lives at jetonomy/v1. This page covers three extension points: adding fields to an existing response via the jetonomy_rest_prepare_* filters, registering a new endpoint in the same namespace, and hooking into the content moderation intercept jetonomy_check_content.
Source references:
- Response filters:
includes/api/class-posts-controller.php:1140,class-replies-controller.php:782,class-spaces-controller.php:1135,class-users-controller.php:250,class-notifications-controller.php:323 - Auth helper:
includes/api/class-rest-auth.php - Moderation intercept:
includes/api/class-posts-controller.php:510,class-replies-controller.php:292 - Base controller:
includes/api/class-base-controller.php
Adding a field to an existing response
Each core object type exposes a filter on the prepared response array immediately before it is returned to the client. Hook in, add your key, return the array.
Available prepare filters
| Filter | Object type | Source |
|---|---|---|
jetonomy_rest_prepare_post |
Post row | class-posts-controller.php:1140 |
jetonomy_rest_prepare_reply |
Reply row | class-replies-controller.php:782 |
jetonomy_rest_prepare_space |
Space row | class-spaces-controller.php:1135 |
jetonomy_rest_prepare_user |
WP_User |
class-users-controller.php:250 |
jetonomy_rest_prepare_notification |
Notification row | class-notifications-controller.php:323 |
Parameters (all five filters share this shape):
| Parameter | Type | Description |
|---|---|---|
$data |
array |
The prepared response data array - modify and return this |
$object |
object |
The raw model row (or WP_User for the user filter) |
$request |
WP_REST_Request|null |
The originating request; null in non-request contexts |
Returns: array - the modified data array.
Example: append a gamification score to post responses
add_filter( 'jetonomy_rest_prepare_post', function ( array $data, object $post, $request ): array {
// Add a field from an external gamification plugin.
$data['wb_gam_score'] = (int) WB_Gam\Scores::for_post( (int) $post->id );
return $data;
}, 10, 3 );
Example: append custom field values to replies
add_filter( 'jetonomy_rest_prepare_reply', function ( array $data, object $reply, $request ): array {
// Attach a "sentiment" label computed by a text-analysis service.
$data['sentiment'] = get_post_meta( (int) $reply->id, 'my_sentiment', true ) ?: 'neutral';
return $data;
}, 10, 3 );
Example: add a badge count to user responses
add_filter( 'jetonomy_rest_prepare_user', function ( array $data, \WP_User $user, $request ): array {
$data['badge_count'] = (int) My_Badges::count_for_user( $user->ID );
return $data;
}, 10, 3 );
The frontend JavaScript reads response data via
window.jetonomyRest.restFetch. Fields you add here are available inresult.datawithout any further wiring.
Registering a new jetonomy/v1 route
Companion plugins can register additional endpoints inside the jetonomy/v1 namespace on rest_api_init. This keeps all community API calls under one namespace and nonce context.
Permission callbacks
Every mutation route (POST, PUT, PATCH, DELETE) must use \Jetonomy\API\REST_Auth::auth_mutation() as its permission_callback. Do not use raw closures, is_user_logged_in(), or current_user_can() - the audit script enforces this rule.
For read-only routes, use \Jetonomy\Visibility::rest_check (respects the Private Community setting) or '__return_true' for fully public data.
Important for companion plugins:
REST_Authis defined in the free plugin. If your plugin may be active before Jetonomy, resolve the class lazily inside the callback rather than in the registration call. Eager static calls at route-registration time can fatal the REST API if the free plugin loads later.
Basic read endpoint
add_action( 'rest_api_init', function () {
register_rest_route(
'jetonomy/v1',
'/my-plugin/stats',
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => 'my_plugin_stats_handler',
'permission_callback' => array( \Jetonomy\Visibility::class, 'rest_check' ),
)
);
} );
function my_plugin_stats_handler( \WP_REST_Request $request ): \WP_REST_Response {
return rest_ensure_response( array(
'total_posts' => My_Plugin::count_posts(),
) );
}
Mutation endpoint with auth
add_action( 'rest_api_init', function () {
register_rest_route(
'jetonomy/v1',
'/my-plugin/notes/(?P<post_id>\d+)',
array(
array(
'methods' => \WP_REST_Server::CREATABLE,
'callback' => 'my_plugin_save_note',
// Lazy resolution - safe if Jetonomy loads after this plugin.
'permission_callback' => function ( \WP_REST_Request $r ) {
if ( ! class_exists( \Jetonomy\API\REST_Auth::class ) ) {
return new \WP_Error( 'jetonomy_not_loaded', 'Jetonomy is required.', array( 'status' => 503 ) );
}
return call_user_func( \Jetonomy\API\REST_Auth::auth_mutation( 'read' ) );
},
'args' => array(
'post_id' => array( 'type' => 'integer', 'required' => true, 'minimum' => 1 ),
'note' => array( 'type' => 'string', 'required' => true, 'sanitize_callback' => 'sanitize_textarea_field' ),
),
),
)
);
} );
function my_plugin_save_note( \WP_REST_Request $request ): \WP_REST_Response {
$post_id = (int) $request->get_param( 'post_id' );
$note = (string) $request->get_param( 'note' );
$user_id = get_current_user_id();
My_Plugin::save_note( $post_id, $user_id, $note );
return rest_ensure_response( array( 'saved' => true ) );
}
The endpoint is then callable from window.jetonomyRest.restFetch:
const result = await window.jetonomyRest.restFetch(
'/my-plugin/notes/' + postId,
{ method: 'POST', body: { note: 'Great post' } }
);
Content moderation intercept: jetonomy_check_content
The jetonomy_check_content filter fires inside the post and reply controllers immediately before a new item is saved, giving you a chance to route it to a moderation state. It is the correct place to hook an AI spam detector, a profanity filter, or a word-list checker.
Parameters
| Parameter | Type | Description |
|---|---|---|
$action |
string|null |
The current action; null on entry (pass-through). Return a value to override. |
$data |
array |
The content data. For posts: ['title' => string, 'content' => string, ...]. For replies: ['content' => string, ...]. |
$space_id |
int |
The space the content is being posted to |
$user_id |
int |
The author's WP user ID |
Returns: one of:
| Return value | Effect |
|---|---|
null |
No action - content saves normally |
'hold' |
Status set to pending; moves to the moderation queue |
'spam' |
Status set to spam |
'flag' |
Content publishes but a Flag record is created; it surfaces in the moderation queue for review |
'block' |
Request returns a 400 error to the author; content is not saved |
Source: includes/api/class-posts-controller.php:510, class-replies-controller.php:292
Example: hold posts from new users
add_filter( 'jetonomy_check_content', function ( $action, array $data, int $space_id, int $user_id ) {
if ( ! is_null( $action ) ) {
return $action; // Respect a decision already made (e.g. by a higher-priority listener).
}
// Hold any post from a user registered less than 24 hours ago.
$user = get_userdata( $user_id );
if ( $user && ( time() - strtotime( $user->user_registered ) ) < DAY_IN_SECONDS ) {
return 'hold';
}
return null;
}, 10, 4 );
Example: spam-detect via an external API
add_filter( 'jetonomy_check_content', function ( $action, array $data, int $space_id, int $user_id ) {
if ( ! is_null( $action ) ) {
return $action;
}
$content = ( $data['title'] ?? '' ) . ' ' . ( $data['content'] ?? '' );
$verdict = My_Spam_Service::check( $content, $user_id );
if ( 'spam' === $verdict ) {
return 'spam';
}
if ( 'suspicious' === $verdict ) {
return 'hold';
}
return null;
}, 15, 4 ); // Priority 15 so it runs after trust-level checks (priority 10).
Example: hard-block prohibited phrases
add_filter( 'jetonomy_check_content', function ( $action, array $data, int $space_id, int $user_id ) {
if ( 'block' === $action ) {
return $action; // Already blocked by another listener.
}
$text = strtolower( ( $data['title'] ?? '' ) . ' ' . ( $data['content'] ?? '' ) );
$prohibited = array( 'buy cheap', 'click here to win', 'guaranteed income' );
foreach ( $prohibited as $phrase ) {
if ( str_contains( $text, $phrase ) ) {
return 'block';
}
}
return $action;
}, 10, 4 );
Note: the
jetonomy_check_contentfilter described here differs from thejetonomy_check_contentfilter in Hooks Reference §Filter Hooks which returnstrue|WP_Error. That filter is the older surface; this filter returns a string action and is the one used by the controllers for moderation routing.
What's next
- Hooks Reference - action and filter hooks fired around every lifecycle event
- Extend the Frontend - call your new endpoint from JavaScript
- Adapters - replace the email, search, or real-time adapter with a custom implementation