En egen endpoint är tre saker: en funktion på rest_api_init som anropar register_rest_route(), en permission_callback som avgör vem som får svar, och en callback som returnerar data via rest_ensure_response(). Namnrymden är ditt prefix plus en version, här wpu/v1, så adressen blir /wp-json/wpu/v1/....
Jag bygger ofta den här sortens endpoints när en app eller ett lagersystem ska läsa orderstatus från WooCommerce utan att gå via hela Woo-API:t. Exemplet nedan är den varianten, nedskalad till en route. Koden ligger i ett eget litet plugin, inte i temat, eftersom integrationen ska överleva ett temabyte.
<?php
/**
* Plugin Name: WPU Order Status Endpoint
*/
defined( 'ABSPATH' ) || exit;
// Routes must be registered on rest_api_init, otherwise WordPress logs a _doing_it_wrong.
add_action( 'rest_api_init', 'wpu_register_order_status_route' );
function wpu_register_order_status_route() {
register_rest_route(
'wpu/v1',
'/orderstatus/(?P<id>d+)',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => 'wpu_get_order_status',
// Order data is not public, so the caller needs the shop manager capability.
'permission_callback' => function () {
return current_user_can( 'manage_woocommerce' );
},
'args' => array(
'id' => array(
'type' => 'integer',
'minimum' => 1,
'required' => true,
// validate_callback only runs when it is set explicitly.
'validate_callback' => 'rest_validate_request_arg',
'sanitize_callback' => 'rest_sanitize_request_arg',
),
),
)
);
}
function wpu_get_order_status( WP_REST_Request $request ) {
$order = function_exists( 'wc_get_order' ) ? wc_get_order( $request->get_param( 'id' ) ) : false;
if ( ! $order ) {
// The status key decides the HTTP code. Without it WordPress sends 500.
return new WP_Error( 'wpu_order_not_found', 'Ordern finns inte.', array( 'status' => 404 ) );
}
$modified = $order->get_date_modified();
return rest_ensure_response(
array(
'id' => $order->get_id(),
'status' => $order->get_status(),
'modified' => $modified ? $modified->date( DATE_ATOM ) : null,
)
);
}
Vad delarna gör
Routen är ett reguljärt uttryck. Den namngivna gruppen (?P<id>\d+) blir parametern id, och $request->get_param( 'id' ) hämtar den oavsett om värdet kom från adressen, frågesträngen eller en JSON-kropp. Konstanten WP_REST_Server::READABLE är bara strängen GET; CREATABLE är POST.
Behörigheten avgörs av permission_callback, som körs efter att WordPress identifierat användaren. Returnerar den false får klienten felet rest_forbidden med 401 om ingen är inloggad och 403 om någon är det. För publik data duger 'permission_callback' => '__return_true', men tänk på vad det betyder: vem som helst på internet kan hämta svaret utan att logga in. Lagersaldo du ändå visar i butiken går bra. Orderstatus gör det inte, och därför kräver exemplet manage_woocommerce, den rättighet rollen Butikschef har.
Argumenten under args beskrivs med samma schemanycklar som i kärnans endpoints: type, minimum, maximum, enum, pattern och liknande. Ett värde som inte går igenom ger felet rest_invalid_param med status 400, och ett required som saknas ger rest_missing_callback_param, också 400. Du behöver alltså inte skriva egna felsvar för felaktig indata.
Svaret lindar du in med rest_ensure_response(). Den gör en vanlig array till ett WP_REST_Response med status 200 och låter ett WP_Error passera orört. Vill du svara 201 när något skapats sparar du svaret i en variabel och anropar set_status( 201 ) på det. Fel returnerar du som WP_Error med status i datat, annars blir det 500.
Ett POST-anrop som kräver inloggning
Samma lagersystem brukar också vilja markera ordern som skickad. Routen är densamma men metoden är POST, och argumentet status får bara anta två värden. Här är validate_callback en egen funktion i stället för schemats enum, för att visa formen. Den får värdet, request-objektet och parameterns namn; de två sista kan du utelämna som här.
<?php
/**
* Plugin Name: WPU Order Status Update Endpoint
*/
defined( 'ABSPATH' ) || exit;
add_action( 'rest_api_init', 'wpu_register_order_status_update_route' );
function wpu_register_order_status_update_route() {
register_rest_route(
'wpu/v1',
'/orderstatus/(?P<id>d+)',
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => 'wpu_update_order_status',
'permission_callback' => function () {
return current_user_can( 'manage_woocommerce' );
},
'args' => array(
'id' => array(
'type' => 'integer',
'minimum' => 1,
'required' => true,
'validate_callback' => 'rest_validate_request_arg',
'sanitize_callback' => 'rest_sanitize_request_arg',
),
'status' => array(
'type' => 'string',
'required' => true,
// The warehouse may only move orders between these two states.
'validate_callback' => function ( $value ) {
return in_array( $value, array( 'processing', 'completed' ), true );
},
'sanitize_callback' => 'sanitize_key',
),
),
)
);
}
function wpu_update_order_status( WP_REST_Request $request ) {
$order = function_exists( 'wc_get_order' ) ? wc_get_order( $request->get_param( 'id' ) ) : false;
if ( ! $order ) {
return new WP_Error( 'wpu_order_not_found', 'Ordern finns inte.', array( 'status' => 404 ) );
}
$order->update_status( $request->get_param( 'status' ), 'Uppdaterad via lagersystemet.' );
return rest_ensure_response(
array(
'id' => $order->get_id(),
'status' => $order->get_status(),
)
);
}
Själva inloggningen behöver du inte koda. WordPress har två inbyggda sätt, och permission_callback ser bara resultatet. Inifrån sajten, alltså JavaScript som körs för en inloggad användare, skickar du en nonce skapad med wp_create_nonce( 'wp_rest' ) i headern X-WP-Nonce. Utan nonce nollställer rest_cookie_check_errors() användaren, och anropet blir anonymt trots att cookien finns. Utifrån, från ett lagersystem eller en app, använder du ett applikationslösenord. Det skapar du under Användare, Redigera användare, och skickar med Basic Auth över HTTPS. Jag ger integrationen en egen användare med rollen Butikschef, så att nyckeln kan dras in utan att röra någon människas konto.
Testa med curl och i webbläsaren
Börja med /wp-json/ i webbläsaren. Under namespaces ska wpu/v1 finnas, och /wp-json/wpu/v1 listar routen med metoder och argument. Finns den inte där har registreringen inte körts, oftast för att pluginet är inaktivt eller för att anropet ligger utanför rest_api_init.
Sedan anropar du endpointen utan inloggning och förväntar dig 401:
curl -i https://butiken.se/wp-json/wpu/v1/orderstatus/1234
Med applikationslösenordet ska GET ge 200 och POST byta status:
curl --user "lager:xxxx xxxx xxxx xxxx xxxx xxxx" https://butiken.se/wp-json/wpu/v1/orderstatus/1234
curl --user "lager:xxxx xxxx xxxx xxxx xxxx xxxx" -X POST -d status=completed https://butiken.se/wp-json/wpu/v1/orderstatus/1234
Prova också status=refunded och orderstatus/0 och kontrollera att svaret är 400 med rest_invalid_param. I webbläsaren fungerar skyddade endpoints bara med nonce, så dem testar jag via curl. Publika endpoints med __return_true kan du öppna direkt.
Fallgropar
Det vanligaste felet jag ser är att register_rest_route() anropas direkt i pluginfilen. Då loggas ett _doing_it_wrong och routen saknas i /wp-json/. Allt ska ligga i funktionen på rest_api_init.
Det näst vanligaste är ett WP_Error utan status. Klienten får 500, loggen är tom, och felsökningen tar en förmiddag innan någon ser att det var ett 404 hela tiden.
Saknar sajten permalänkar finns inte /wp-json/. Adressen blir då /?rest_route=/wpu/v1/orderstatus/1234, och rest_url() returnerar rätt variant åt dig. Och validerar du aldrig id passerar orderstatus/0 till callbacken, så sätt minimum och validate_callback från början även om det känns överflödigt.
Vanliga frågor
Varför får jag 401 eller 403 fast jag är inloggad i webbläsaren?
Utan nonce nollställer WordPress användaren och behandlar anropet som anonymt. Skicka nonce i headern X-WP-Nonce eller som parametern _wpnonce, skapad med wp_create_nonce( ’wp_rest’ ). 401 betyder att ingen användare kändes igen, 403 att användaren saknar rättigheten.
Måste jag ha permission_callback på en publik endpoint?
Ja. Saknas den loggar WordPress ett _doing_it_wrong sedan version 5.5. För publik data sätter du den till __return_true, men bara om svaret verkligen får läsas av vem som helst på internet.
Varför svarar min endpoint 500 när jag returnerar ett WP_Error?
Statuskoden hämtas ur felets data under nyckeln status. Saknas den skickar WordPress 500. Skriv array( ’status’ => 404 ) som tredje argument till WP_Error.
Kan jag ha GET och POST på samma adress?
Ja. Antingen skickar du en lista med flera endpoint-arrayer till register_rest_route i ett anrop, eller så registrerar du routen två gånger som i exemplen här. WordPress slår ihop dem så länge du inte skickar override som true.
Hur ser jag att endpointen är registrerad?
Öppna /wp-json/ i webbläsaren och leta efter wpu/v1 under namespaces. Adressen /wp-json/wpu/v1 listar alla routes i namnrymden med metoder och argument.
Källor
- Adding Custom Endpoints, REST API Handbook
- Authentication (cookie, nonce, applikationslösenord), REST API Handbook
- register_rest_route(), Code Reference
- WP_REST_Request, Code Reference
- rest_ensure_response(), Code Reference
- Schema och argumentvalidering (rest_validate_value_from_schema), REST API Handbook
