← Back to all posts
August 31, 2026
•

Custom REST API Integration for WordPress: Endpoints, Auth and Caching Done Right

Neatly connected network cables, illustrating custom REST API integration for WordPress

Sooner or later every serious WordPress site has to talk to something that is not WordPress: a mobile app, an ERP, a React front end, a partner’s ordering system, a CRM that needs every new lead within seconds. The built-in endpoints under /wp-json/wp/v2/ get you part of the way, then you hit the wall. You need computed data, write access with proper permissions, webhooks that arrive when you are asleep, and responses fast enough that the other system does not time out. That is custom REST API integration for WordPress, and it is most of what we build on our custom REST API integration service.

This post is for a CTO, product owner or lead developer who has a concrete integration to ship and wants to know what “done right” looks like before scoping it. We cover the three integration directions, how to register endpoints properly, which authentication method fits which client, validation and errors, caching that does not serve stale data, and how we test it all. Code samples are from patterns we ship, simplified for reading.

what custom REST API integration for WordPress actually covers

Integrations fall into three directions, and most real projects need at least two of them:

Direction Example Main risk
Expose WordPress data A React app or mobile app reads products, posts and ACF fields Slow responses, over-exposure of private data
Accept data into WordPress A CRM or payment provider posts a webhook that creates or updates records Unauthenticated writes, duplicate processing
WordPress calls an external API New orders sync to an ERP; leads push to HubSpot Blocking page loads, silent failures, no retry

The first direction is what people usually mean by “the REST API”, and it is the backbone of every headless build, as we show in our step-by-step headless WordPress with React guide. The second and third are where most production incidents happen, because they involve writes, secrets and network calls that can fail.

registering endpoints the right way

Custom endpoints belong in a plugin, not the theme, under your own namespace with a version number. Every route needs a permission_callback (WordPress has warned about missing ones since 5.5) and every argument needs validation and sanitisation. Here is the shape we use for a read endpoint that returns a computed price list:

<?php
add_action( 'rest_api_init', function () {
    register_rest_route( 'swift/v1', '/pricing/(?P<sku>[a-z0-9-]+)', [
        'methods'             => WP_REST_Server::READABLE,
        'callback'            => 'swift_get_pricing',
        'permission_callback' => '__return_true', // public read
        'args'                => [
            'sku' => [
                'required'          => true,
                'validate_callback' => fn( $v ) => (bool) preg_match( '/^[a-z0-9-]{3,40}$/', $v ),
                'sanitize_callback' => 'sanitize_key',
            ],
            'currency' => [
                'default' => 'USD',
                'enum'    => [ 'USD', 'EUR', 'GBP' ],
            ],
        ],
    ] );
} );

function swift_get_pricing( WP_REST_Request $request ) {
    $product = wc_get_product_id_by_sku( $request['sku'] );
    if ( ! $product ) {
        return new WP_Error( 'swift_not_found', 'Unknown SKU', [ 'status' => 404 ] );
    }
    $data = swift_build_price_table( $product, $request['currency'] );
    return rest_ensure_response( $data );
}

Two details matter here. Returning a WP_Error with a status gives the client a proper HTTP code and a JSON body it can parse. And rest_ensure_response() lets you set headers later, which we will use for caching. Once an API grows past four or five routes, move to a class that extends WP_REST_Controller so you get schema, prepare_item_for_response() and consistent collection parameters for free.

authentication: pick the method that fits the client

The right auth method depends on who is calling. Using the wrong one is the most common security finding in the API audits we run.

  • Cookie plus nonce. For JavaScript running on the WordPress site itself. Send X-WP-Nonce from wp_create_nonce( 'wp_rest' ). Useless for anything external.
  • Application Passwords. Built into core since 5.6. Basic auth over HTTPS with a per-application password tied to a real user. Our default for server-to-server calls and headless previews. Create a dedicated user with a minimal custom role, never an administrator.
  • JWT. Use the JWT Authentication for WP REST API plugin when a mobile app logs users in and holds a token. Rotate the secret key and set short expiry.
  • OAuth 2. Only when third parties need delegated access on behalf of your users. Heavier to run; the WP OAuth Server plugin handles the flow.
  • HMAC-signed webhooks. For inbound calls from Stripe, HubSpot, Shopify or your own systems. No user account involved; the caller signs the body with a shared secret.

For inbound webhooks, the permission callback verifies the signature and the callback rejects duplicates using the provider’s event ID:

<?php
function swift_verify_webhook( WP_REST_Request $request ) {
    $secret    = getenv( 'CRM_WEBHOOK_SECRET' ) ?: '';
    $signature = $request->get_header( 'x-signature' );
    $expected  = hash_hmac( 'sha256', $request->get_body(), $secret );
    if ( ! $signature || ! hash_equals( $expected, $signature ) ) {
        return new WP_Error( 'swift_bad_signature', 'Invalid signature', [ 'status' => 401 ] );
    }
    return true;
}

function swift_handle_lead( WP_REST_Request $request ) {
    $event_id = sanitize_text_field( $request['event_id'] );
    if ( get_transient( 'swift_evt_' . $event_id ) ) {
        return rest_ensure_response( [ 'status' => 'duplicate' ] ); // idempotent
    }
    set_transient( 'swift_evt_' . $event_id, 1, DAY_IN_SECONDS );
    as_enqueue_async_action( 'swift_process_lead', [ $request->get_json_params() ] );
    return new WP_REST_Response( [ 'status' => 'queued' ], 202 );
}

Note the 202 Accepted and the hand-off to Action Scheduler. The webhook returns in milliseconds; the actual work (creating a post, calling another API, sending mail) runs in the background where a failure can be retried and logged instead of making the sender give up. WooCommerce ships Action Scheduler; on non-commerce sites install it as a library.

validation, errors and versioning

Define a schema for every route. WordPress uses it to validate request bodies, generate the OPTIONS response that API clients read, and filter output through _fields. Keep error codes stable and namespaced (swift_not_found, swift_rate_limited) because client code will branch on them. When you need to change a response shape, add swift/v2 and keep v1 serving for a documented period rather than editing v1 in place. Also filter rest_authentication_errors to block anonymous access to /wp/v2/users, which otherwise lists every author login on the site.

caching that does not serve stale data

An endpoint that runs six WP_Query calls and joins wp_postmeta three times will take 400 ms or more on a busy site. The fix is layered:

  1. Object cache. Install Redis Object Cache and enable it with wp redis enable. Every get_transient() and wp_cache_get() now hits memory instead of wp_options.
  2. Endpoint-level cache with invalidation. Cache the built response under a key and clear it when the source data changes.
  3. HTTP cache headers. Set Cache-Control on public GET responses so CloudFront, Cloudflare or the host’s edge cache handles repeat traffic without touching PHP.
<?php
function swift_get_pricing( WP_REST_Request $request ) {
    $key  = 'swift_price_' . md5( $request['sku'] . $request['currency'] );
    $data = wp_cache_get( $key, 'swift_api' );
    if ( false === $data ) {
        $data = swift_build_price_table( wc_get_product_id_by_sku( $request['sku'] ), $request['currency'] );
        wp_cache_set( $key, $data, 'swift_api', 15 * MINUTE_IN_SECONDS );
    }
    $response = rest_ensure_response( $data );
    $response->header( 'Cache-Control', 'public, max-age=300, s-maxage=900' );
    return $response;
}

// Bust on product save so editors never wait for expiry.
add_action( 'woocommerce_update_product', function ( $product_id ) {
    $product = wc_get_product( $product_id );
    foreach ( [ 'USD', 'EUR', 'GBP' ] as $cur ) {
        wp_cache_delete( 'swift_price_' . md5( $product->get_sku() . $cur ), 'swift_api' );
    }
} );

Avoid transients for high-volume keys when there is no object cache: each one becomes a row in wp_options, and if a plugin sets them with autoload on, they get loaded on every page view. That single mistake is behind a surprising share of the slow sites we see in our maintenance and support work.

outbound calls without blocking the page

Never call an external API inside save_post or during checkout with no timeout. Use wp_remote_post() with an explicit timeout of 5 to 10 seconds, queue it through Action Scheduler, and log the response code. Replace WP-Cron with a real cron entry (define( 'DISABLE_WP_CRON', true ) plus */5 * * * * wp cron event run --due-now) so queued jobs run on a schedule instead of only when a visitor arrives. For a worked example on the commerce side, see our guide to integrating a third party CRM with WooCommerce.

testing and monitoring the integration

Before anything goes live we exercise every route from the command line with a real Application Password:

# Public read
curl -s "https://example.com/wp-json/swift/v1/pricing/sku-1234?currency=EUR" | jq .

# Authenticated write with an Application Password
curl -s -u "api-bot:xxxx xxxx xxxx xxxx xxxx xxxx" 
  -H "Content-Type: application/json" 
  -X POST "https://example.com/wp-json/swift/v1/leads" 
  -d '{"email":"[email protected]","source":"landing-a"}'

# Discover the schema WordPress generated from your args
curl -s -X OPTIONS "https://example.com/wp-json/swift/v1/pricing/sku-1234" | jq .endpoints[0].args

For regression coverage, PHPUnit with the WordPress test suite lets you build a WP_REST_Request, dispatch it through rest_get_server() and assert on the response without HTTP. In production, hook rest_request_after_callbacks to log route, status and duration to your log stack (CloudWatch Logs if you are on AWS), and alert when the 5xx rate or p95 latency moves. Rate limit public write routes at the edge or with a small transient counter keyed on IP and route.

the mistakes we fix most often in audits

  • Routes with permission_callback set to __return_true on write methods “because the mobile app needs it”.
  • Endpoints that return get_post_meta( $id ) with no field list, leaking internal keys and sometimes personal data.
  • External API calls made synchronously inside page requests, with the default timeout, so a slow partner makes checkout hang.
  • Secrets stored in wp_options in plain text. Put them in environment variables or wp-config.php constants outside the web root.
  • Business logic bolted onto admin-ajax.php, which loads all of the admin bootstrap and cannot be cached.
  • No idempotency on webhooks, so a provider retry creates duplicate orders or leads.

Cleaning these up is usually a one to two week engagement and often improves site speed as a side effect. When the integration touches WooCommerce order data, our WooCommerce CRM integration service covers the commerce-specific hooks and order status mapping.

when to do this yourself vs hire someone

A single read-only endpoint with a permission callback and a transient is a good afternoon’s work for any WordPress developer, and you should do it in-house. Hire when the custom REST API integration for WordPress involves writes from external systems, money, personal data, or an SLA. Those projects need signed webhooks, background queues, retries, logging and a rollback plan, and the cost of getting one of those wrong (duplicate charges, leaked customer data, a partner cutting you off for hammering their API) is far higher than the cost of a senior engineer building it properly the first time.

get your integration scoped

Tell us what needs to talk to what, roughly how much traffic it carries and what happens when it fails, and we will come back within 24 hours with a fixed scope and fixed price in writing for your custom REST API integration for WordPress. You work directly with the engineers who write and test the code. Start the conversation through our contact page.

Share this post

keep reading

Portable external hard drive connected to a laptop, illustrating an automated WordPress backup system
September 22, 2026
•

Automated WordPress Backup System Setup: Offsite, Versioned and Actually Tested

Automated WordPress backup system setup done properly: offsite S3 storage, versioned retention, write-only credentials, hourly store backups, tested restores.

Read more
Page speed test results on a monitor, illustrating Core Web Vitals optimization for WordPress
September 20, 2026
•

Core Web Vitals Optimization for WordPress: LCP, INP and CLS Fixes That Actually Work

Core Web Vitals optimization for WordPress, metric by metric: the LCP, INP and CLS fixes in theme, plugins and server that move…

Read more
Glass cloud icon with data layers above a padlock, illustrating AWS hosting migration for WordPress
September 18, 2026
•

AWS Hosting Migration for WordPress: Lightsail vs EC2 vs Managed Hosting

AWS hosting migration for WordPress compared: Lightsail vs EC2 with RDS vs managed hosting, the migration steps we run, and what breaks…

Read more

want this kind of thinking on your project?

Tell us what you are building and we'll come back with a straight answer on scope, timeline and cost within 24 hours.
start a project