If you're building a WordPress plugin that pulls listings, units or reports from the AppFolio API, and every call comes back 403 Forbidden, your credentials may be fine. The problem may be one word in a header you never set.

AppFolio blocks any request whose User-Agent contains the word "WordPress." WordPress puts that word in every outgoing request by default. So a plugin that doesn't override it fails on every single call.

Here's how we found it, the tests that prove it, and the fix.

What it looked like

We were building a vacancy sync for a property management client. It pulled the unit directory from AppFolio's Reports API into their WordPress site every hour.

One day the sync started failing. Every request returned 403. The obvious suspects:

  • The API credentials had been rotated or revoked
  • AppFolio had changed the endpoint
  • A firewall on the hosting side was blocking outbound calls

None of them were it. The clue came from a different tool. We had a small Python script that used the same credentials against the same endpoint, and it was getting 200 responses at the same moment WordPress was getting 403s.

Same credentials, same endpoint, same minute, different result. That rules out credentials and endpoint changes, and points to something about how the request was made.

The test

We sent the same authenticated request to the Reports API (/api/v2/reports/unit_directory.json) and changed only the User-Agent header:

User-Agent sentResponse
mysite-leasing-assistant/1.0200 OK
curl/8.0200 OK
WordPress/6.7.1; https://example.com403 Forbidden
mysite-vacancy-sync/1.0 (WordPress plugin)403 Forbidden
WordPress403 Forbidden

The fourth row matters most. Even a custom User-Agent gets blocked if "WordPress" appears anywhere in it, including a polite note like "(WordPress plugin)."

Why WordPress triggers it

WordPress's HTTP functions (wp_remote_get, wp_remote_post and the rest) send a default User-Agent that looks like this:

WordPress/6.7.1; https://yoursite.com

Most plugin developers never touch it, so every AppFolio call from a typical plugin carries the blocked word.

We don't know why AppFolio has this rule. Our guess is a bot filter, since many automated scanners and spam tools run on WordPress. It applies at AppFolio's edge server, before your credentials are even checked.

How to tell this is your problem

Three signs, from quickest to check:

  1. The 403 body is an HTML page, not JSON. A real API auth error from AppFolio comes back as JSON. A generic HTML "403 Forbidden" page means the request was stopped before it reached the API.
  2. The same request works from curl or Postman. Run it outside WordPress with the same credentials. If it works there, the credentials are fine.
  3. Your code doesn't set a User-Agent. Search your plugin for user-agent. If nothing comes up, you're sending the WordPress default.

The fix

Set an explicit User-Agent on every AppFolio request, and keep the word "WordPress" out of it:

$response = wp_remote_get( $url, array(
    'user-agent' => 'mysite-vacancy-sync/1.0',
    'timeout'    => 20,
    'headers'    => array(
        'Authorization' => 'Basic ' . base64_encode( $client_id . ':' . $client_secret ),
    ),
) );

Then log what comes back while you're testing, so the next failure is obvious:

$code = wp_remote_retrieve_response_code( $response );
if ( 200 !== $code ) {
    error_log( 'AppFolio ' . $code . ': ' . substr( wp_remote_retrieve_body( $response ), 0, 200 ) );
}

If the logged body starts with <html>, you're being blocked at the edge, not rejected by the API.

Where this applies

  • Affected: the AppFolio Reports API (/api/v2/reports/...) when called from WordPress or any tool using a WordPress-style User-Agent.
  • Not affected: AppFolio's public listings pages, which accept normal browser User-Agents.

We tested this in August 2026. AppFolio could change the rule at any time, so if your plugin works today, keep the explicit User-Agent anyway. It costs nothing and protects you if the rule comes back.

The bigger lesson

When an API fails in one tool and works in another with the same credentials, stop checking the credentials. Compare the two requests header by header. The difference is almost always something the failing tool added without telling you.