Recipe: Custom REST Endpoint
Goal: Register a REST endpoint in your own namespace (my-addon/v1) that reads Eventonomy data through contracts, enforces the same authorization rules, and returns the canonical list envelope so existing clients handle it without extra mapping.
Seams Used
| Seam | Purpose |
|---|---|
rest_api_init |
Standard WP hook to register routes. |
evnm() + Eventonomy\Contracts\* |
Resolve repositories without concrete-class imports. |
evnm_user_can_manage_event() |
Object-level permission check (filterable). |
Canonical list envelope { items, total, page, per_page, pages, has_more } |
Return format - clients and blocks handle it without extra mapping. |
Example - Attendance Sheet Endpoint
This endpoint returns a going-attendee list for a given event. Access is restricted to users who can manage the event's RSVPs.
add_action( 'rest_api_init', function (): void {
register_rest_route(
'my-addon/v1',
'/events/(?P<id>\d+)/attendance-sheet',
[
'methods' => 'GET',
'permission_callback' => function ( \WP_REST_Request $req ): bool|\WP_Error {
$event = evnm( \Eventonomy\Contracts\EventRepositoryInterface::class )
->get( (int) $req['id'] );
if ( ! $event ) {
return new \WP_Error(
'evnm_not_found',
__( 'Event not found.', 'my-addon' ),
[ 'status' => 404 ]
);
}
return evnm_user_can_manage_event(
get_current_user_id(),
$event,
'manage_rsvps'
)
? true
: new \WP_Error(
'evnm_forbidden',
__( 'You cannot manage this event.', 'my-addon' ),
[ 'status' => 403 ]
);
},
'callback' => function ( \WP_REST_Request $req ): \WP_REST_Response {
$repo = evnm( \Eventonomy\Contracts\RsvpRepositoryInterface::class );
$result = $repo->query( [
'event_id' => (int) $req['id'],
'status' => 'going',
'per_page' => (int) ( $req->get_param( 'per_page' ) ?? 100 ),
'page' => (int) ( $req->get_param( 'page' ) ?? 1 ),
] );
// Map to the output shape your client needs.
$attendees = array_map(
fn( array $rsvp ) => [
'name' => $rsvp['guest_name'],
'guests' => $rsvp['guests_count'],
'checked_in' => 'checked_in' === $rsvp['status'],
],
$result['items'] ?? []
);
// Return the canonical list envelope.
return rest_ensure_response( [
'items' => $attendees,
'total' => $result['total'],
'page' => $result['page'],
'per_page' => $result['per_page'],
'pages' => $result['pages'],
'has_more' => $result['has_more'],
] );
},
'args' => [
'id' => [ 'validate_callback' => 'is_numeric' ],
'per_page' => [ 'default' => 100, 'sanitize_callback' => 'absint' ],
'page' => [ 'default' => 1, 'sanitize_callback' => 'absint' ],
],
]
);
} );
Key Rules
1. Permission callbacks must return WP_Error, not false.
Returning false from a permission callback causes WP to send a generic 401/403 with no useful message. Return a typed WP_Error with the right HTTP status so clients can display a meaningful error.
2. Use the canonical envelope shape.
The shape { items, total, page, per_page, pages, has_more } is what Eventonomy's own blocks and frontend code expect. Reuse it so your endpoint integrates without extra client-side mapping.
3. Never query the database directly.
Always resolve via evnm( SomeRepositoryInterface::class )->query( $args ). This keeps your code decoupled from table layout changes and respects the evnm_{resource}_query_args filter.
4. Prefer the evnm_rest_prepare_* filter for extra fields on existing resources.
If you need to add a single field to event, RSVP, or order objects in the standard responses - not a new endpoint - use evnm_rest_prepare_event (§2.4 in docs/EXTENDING.md). Reserve custom endpoints for truly separate resource shapes.
Testing with WP-CLI
wp eval '
$req = new WP_REST_Request( "GET", "/my-addon/v1/events/1/attendance-sheet" );
wp_set_current_user( 1 );
$res = rest_get_server()->dispatch( $req );
print_r( $res->get_data() );
'
Verify It Worked
curl -s -u admin:password \
"https://yoursite.com/wp-json/my-addon/v1/events/1/attendance-sheet" \
| jq '.items | length'
Confirm the count matches the Going RSVP count for that event.
Related
docs/REST-API.md- the fulleventonomy/v1endpoint reference and envelope contract.docs/EXTENDING.md§4 - REST as an extension surface.- REST API Reference
- Recipes Index