Skip to content
W3Bullhorn
W3Bullhorn
Book a demo

Reference

REST API

The plugin serves its administration screens from the w3rp/v1 namespace. Every route in it requires an administrator capability and the wp_rest nonce, and those two checks are fixed by the plugin rather than chosen per route. An add-on cannot register its own route; it describes one and the plugin registers it.

Basics

The namespace

Namespacew3rp/v1
AudienceAdministrators. This is not a public data API
AuthenticationThe WordPress session, plus the wp_rest nonce
AuthorisationAn administrator capability on the current site, checked by the plugin on every route
ArgumentsValidated and size-bounded against a schema before the handler runs
Add-on routesServed under w3rp/v1/admin/ext/{owner}{path}

If you are looking for a way to read job data from outside, this is not it. Jobs are public pages on the site, with structured data, and there are XML feeds for aggregators. The REST namespace exists to run the administration screens.

Design

Why an add-on does not call register_rest_route

This is the part that surprises people, so it is worth the paragraph.

The plugin’s request-authorisation framework is what guarantees that every one of its routes checks a capability, verifies a nonce and validates its arguments. A static gate in the build fails the build for any register_rest_route call made outside that framework. An add-on therefore has no legitimate way to serve a settings screen over REST by itself — and that is not a theoretical concern: two screens in the Pro add-on were built, tested, and left running on nothing for exactly this reason.

The supported way in is a route descriptor. The add-on states what it wants served; the plugin decides how it is protected. There is no parameter for a capability, a nonce, an access policy, a namespace or a rate limit, so none of them can be omitted, weakened, or swapped for a callback that always says yes.

add_action(
    AdminRouteRegistrarInterface::HOOK,
    static function ( AdminRouteRegistrarInterface $routes ): void {
        $route = AdminRoute::create(
            'acme',            // owner slug: namespaces your routes
            '/widgets',        // your path
            'POST',
            $arguments,        // argument schema, plain data
            $handler          // callable( AdminRequest ): mixed
        );

        if ( null !== $route ) {
            $routes->register( $route );
        }
    }
);

AdminRoute

Declaring a route

FactoryAdminRoute::create( $owner, $path, $method, $arguments, $handler, $level = AdminLevel::SITE )
ReturnsThe route, or null when the declaration is refused
Why it was refusedAdminRoute::refusal( $owner, $path, $method, $arguments ) returns the reason
Final addressRead it from $route->rest_route(); do not assemble the string by hand
Handler receivesAn AdminRequest: arguments(), has(), argument(), user_id(), method()
Access levelAdminLevel::SITE is the default. AdminLevel::NETWORK admits network administrators only, and needs contract 1.8.0

Because the route lands under your own owner slug, it can never shadow a plugin route, and two add-ons that choose the same path do not collide. The handler never checks a capability itself: by the time it runs, access has already been decided.

Arguments

An argument schema

Plain data, built through the plugin’s own schema rules, which refuse anything unsafe.

$arguments = array(
    'mapping' => array(
        'type'       => 'object',
        'required'   => true,
        'properties' => array(
            'field' => array(
                'type'      => 'string',
                'format'    => 'text-field',
                'maxLength' => 64,
            ),
            'use'   => array(
                'type' => 'string',
                'enum' => array( 'display', 'filter' ),
            ),
        ),
    ),
);

Every string, array and object needs a size bound. A schema without one is refused with the reason unbounded_schema, because an unbounded argument is how a well-meaning settings screen becomes a denial-of-service surface.

Diagnostics

When a route is refused

Refusals are loud, but they arrive in the debug log rather than on screen.

registration_closedregister() was called after the registration action had finished
duplicate_routeThe same owner, method and path is already registered
invalid_schema:<detail>The schema rules refused the arguments; the detail names the rule
unbounded_schemaA string, array or object has no size bound
invalid_declaration:<detail>Another rule of the authorisation framework refused the route
registration_callback_failedA callback threw. It is isolated: routes registered before it stand, later callbacks did not run

Listen on w3rp/admin_route_rejected while developing. A refused route is simply absent, and an absent route looks exactly like a screen that cannot save.

Put your Bullhorn jobs on your own website

See W3Bullhorn working with your own Bullhorn jobs, on a short call with a specialist who knows Bullhorn and WordPress.

Book a demo How it works