Eventonomy

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 full eventonomy/v1 endpoint reference and envelope contract.
  • docs/EXTENDING.md §4 - REST as an extension surface.
  • REST API Reference
  • Recipes Index