Recipe: Custom REST Endpoint
Recipe: Custom REST Endpoint
Section titled “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
Section titled “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. Note: a repository’s own query() returns only { items, total, pages, has_more }; you supply page/per_page yourself. |
Example: Attendance Sheet Endpoint
Section titled “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 );
$page = (int) ( $req->get_param( 'page' ) ?? 1 ); $per_page = (int) ( $req->get_param( 'per_page' ) ?? 100 );
$result = $repo->query( [ 'event_id' => (int) $req['id'], 'status' => 'going', 'per_page' => $per_page, 'page' => $page, ] );
// Map to the output shape your client needs. There is NO 'checked_in' // RSVP status - check-in is recorded in the `checked_in_at` column, so // derive the boolean from whether that timestamp is set. $attendees = array_map( fn( array $rsvp ) => [ 'name' => $rsvp['guest_name'], 'guests' => $rsvp['guests_count'], 'checked_in' => ! empty( $rsvp['checked_in_at'] ), ], $result['items'] ?? [] );
// Repository::query() returns ONLY { items, total, pages, has_more }. // page/per_page are NOT in its output - echo back the request values // (Eventonomy's own controllers add them the same way, in respond_list()). return rest_ensure_response( [ 'items' => $attendees, 'total' => $result['total'], 'page' => $page, 'per_page' => $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
Section titled “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, and know where the keys come from.
The shape { items, total, page, per_page, pages, has_more } is what Eventonomy’s own blocks and frontend code expect. But a repository’s query() only returns { items, total, pages, has_more }; page and per_page (and next_cursor on cursor endpoints) are added by the internal respond_list() helper, which third-party endpoints can’t reach. So echo back the request’s own page/per_page to complete the envelope, exactly as the example above does.
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
Section titled “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
Section titled “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
Section titled “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

