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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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.
Since 1.6.0, REST_Auth::auth_mutation() (and the Pro wrapper rest_auth_mutation()) also hard-rejects banned users (jetonomy_user_banned) and users still pending email verification (jetonomy_pending_verification) with a 403, before the capability check runs. This holds even for Application-Password sessions. Extension authors inherit this account-status protection automatically by using the standard callback - there is nothing extra to wire.
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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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 or search adapter with a custom implementation

