Scanner says core files were modified? Open the diff. After wp2shell, that is often leftover access.

How to read it

WordPress REST API and external API security

Call external APIs from WordPress safely and harden the WordPress REST API: auth, permissions, rate limits, key storage, and what Security Ninja does and does not cover.

Topics Beginner guides Hardening & checklists

Updated Published

WordPress REST API and external API security Open larger image: WordPress REST API and external API security

APIs let WordPress talk to other services: weather, payments, CRMs, maps, stock data. Done well, your site shows live data without hand-editing pages. Done badly, you leak API keys, hammer third-party quotas, or ship a custom REST route that exposes admin data to strangers.

This guide covers outbound integrations (WordPress calls Stripe) and inbound REST security (other apps call your WordPress). Both matter for a typical agency build.

WordPress API integration

Two APIs, two directions

DirectionWhat it isCommon mistake
OutboundWordPress PHP calls api.stripe.com or similarKeys in the theme; no caching; trusting JSON blindly
Inbound (REST)Clients call /wp-json/ on your siteCustom routes with permission_callback => true
Admin AJAXLegacy admin-ajax.php handlersMissing nonces and capability checks

The WordPress REST API is how apps talk to WordPress. wp_remote_get() is how WordPress talks out. Many sites use both.

WordPress REST API security basics

Public read endpoints for posts are by design. Risk clusters in custom plugin routes:

  1. Authentication: Application Passwords, OAuth plugins, or cookie auth for logged-in users. Do not invent weak token schemes.
  2. Permission callbacks: Every register_rest_route() needs a real permission_callback, not __return_true for sensitive data.
  3. Capabilities: Match current_user_can() to the data returned. Subscriber-only data should not leak to anonymous callers.
  4. Rate limiting: Pair with firewall or host WAF when endpoints get hammered.
  5. Discovery: /wp-json/ lists routes. Hiding the index is optional; fixing permissions is mandatory.

Audit custom plugins and headless front ends after major updates. A route that was “internal only” often becomes public when someone forgets the callback.

Calling an external API from PHP

Prefer the WordPress HTTP API over raw curl unless you have a specific need.

$response = wp_remote_get(
	'https://api.example.com/v1/status',
	array(
		'timeout' => 15,
		'headers' => array(
			'Authorization' => 'Bearer ' . EXAMPLE_API_TOKEN,
		),
	)
);

if ( is_wp_error( $response ) ) {
	return;
}

$code = wp_remote_retrieve_response_code( $response );
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );

wp_remote_post() follows the same pattern for writes. Put reusable logic in a small custom plugin, not only in the theme’s functions.php, so the integration survives theme switches.

Walkthrough: shortcode that shows remote status

Goal: show a short status string from https://api.example.com/v1/status without exposing the API token to browsers.

  1. Store the secret in wp-config.php (or the host env), not in the database options screen:
define( 'EXAMPLE_API_TOKEN', 'replace-me' );
  1. Fetch on the server, cache the result, and fail closed:
function example_get_remote_status() {
	$cached = get_transient( 'example_api_status' );
	if ( false !== $cached ) {
		return $cached;
	}

	$response = wp_remote_get(
		'https://api.example.com/v1/status',
		array(
			'timeout' => 15,
			'headers' => array(
				'Authorization' => 'Bearer ' . EXAMPLE_API_TOKEN,
			),
		)
	);

	if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
		return '';
	}

	$data = json_decode( wp_remote_retrieve_body( $response ), true );
	if ( ! is_array( $data ) || empty( $data['status'] ) ) {
		return '';
	}

	$status = sanitize_text_field( $data['status'] );
	set_transient( 'example_api_status', $status, 5 * MINUTE_IN_SECONDS );
	return $status;
}
  1. Print with escaping in a shortcode:
add_shortcode(
	'example_status',
	function () {
		$status = example_get_remote_status();
		if ( '' === $status ) {
			return '<p>Status unavailable.</p>';
		}
		return '<p>Status: ' . esc_html( $status ) . '</p>';
	}
);
  1. Optional REST wrapper if JavaScript needs the value: register a custom route that calls the same helper and returns JSON. Still keep EXAMPLE_API_TOKEN only in PHP. Check capabilities if the data is not public.

  2. Operate it: log failures, watch provider rate limits, rotate the token when people leave, and delete the plugin when the integration dies.

Application Passwords and automation

WordPress Application Passwords let scripts authenticate to REST without sharing the main admin password. Use them for:

  • Staging sync tools
  • Headless front ends
  • Internal automation with scoped accounts

Give each integration its own user with the minimum role. Revoke the application password when the tool is removed. Pair with 2FA on human admin accounts. Guide: login security.

Plugins vs custom code

No-code API plugins (WPGetAPI-style tools) help non-developers map endpoints in wp-admin. Custom code gives you control, tests, and clearer security review.

Either way:

  1. Read the provider’s auth and rate-limit docs
  2. Store credentials outside the theme repo when you can
  3. Cache responses that do not need to be live every page view
  4. Validate and sanitize anything you print

Secure integration habits

Protect keys. Do not hardcode secrets in public repos or paste them into page content. wp-config.php constants, environment variables, or a secrets store beat committing tokens to Git.

Least privilege. Use scoped API keys. Revoke keys you no longer need. See least privilege.

Validate responses. Never trust remote JSON blindly. Check HTTP status codes. Escape output when rendering HTML (XSS lives in bad prints).

Rate limit and cache. Call APIs on cron or on demand with transients, not on every anonymous page hit, unless the provider and your host can take it.

Error handling. Failed calls should degrade gracefully, not white-screen the site.

Log without leaking. When debugging, do not log full API responses that contain PII or tokens.

Where WP Security Ninja fits

WP Security Ninja helps with site security posture: tests, vulnerability scanning, Cloud Firewall, malware tools, and login hardening. It is not an API connector builder and it does not “auto-integrate” Stripe or weather feeds for you.

Use Security Ninja to keep the WordPress install harder to abuse while your integration code (or a dedicated API plugin) handles the external calls. Broader baseline: WordPress security checklist, plugin risks.

Quick start checklist

  1. Pick the API and confirm auth, HTTPS endpoints, and quotas
  2. Audit custom REST routes for permission callbacks
  3. Decide plugin UI vs custom plugin code
  4. Store the key securely; never expose private tokens to the browser
  5. Fetch with wp_remote_get / wp_remote_post, parse JSON, handle errors
  6. Cache, display safely, and monitor failures in logs
  7. Re-test after WordPress and plugin updates

APIs make WordPress more useful. Treat every key like a password, every REST route like a public door unless proven otherwise, and every remote payload like untrusted input.

Found this useful? Share it.

Frequently asked questions

Is the WordPress REST API secure by default? +

It exposes structured endpoints with permission callbacks. Public read routes are intentional. Risk rises when plugins register custom routes with weak permission checks or expose user data without authentication.

Where should I store API keys in WordPress? +

Outside the theme and public repos: wp-config.php constants, environment variables, or a host secrets store. Never paste private keys into posts, options screens, or JavaScript sent to browsers.

Does Security Ninja secure my Stripe or weather API integration? +

No. Security Ninja hardens the WordPress site (tests, vulnerabilities, firewall, malware, login). Your integration code or API plugin must handle keys, validation, and outbound calls safely.

Larger screenshot

Enlarged image