Skip to main content

Any WordPress site

WordPress describes its own API. Every site publishes a REST index listing each route, the methods it answers and the arguments it takes, including the routes its plugins add. A WordPress connection is built from that index: you give the site's address, choose which parts of the API to expose, review what that produces, and connect.

There is no catalog manifest to wait for, so this works for core WordPress (posts, pages, media, users), for any plugin that registers REST routes, and for your own code. A curated bbPress connector, with a short, hand-reviewed set of actions, is not in the gateway yet, so connect a bbPress forum this way for now: its bbpress/v1 namespace shows up in discovery like any other plugin's. Events Manager has one: pick Events Manager from the catalog for a hand-reviewed set of its REST actions.

What you need​

A WordPress application password, sent as HTTP Basic auth. It is built into WordPress 5.6 and later and needs the site to be served over https://.

  1. Decide which WordPress user the agent should act as. WordPress checks that user's permissions on every call, so its role is the real ceiling on what any agent can do here. A dedicated account with the lowest role that covers the job is clearer and safer than your own.
  2. Sign in as that user, go to Users → Profile, and under Application Passwords add one named permaura.
  3. Copy the password WordPress shows. It is displayed once; the spaces in it are ignored.

Add the connection​

In the console, open Access → Connections → Add connection and choose Connect your own. Enter the site's address, the one you open it at in a browser, and choose Discover.

Your gateway looks for the site's REST API at /wp-json/, then at /?rest_route=/ for sites without pretty permalinks. A site installed in a subdirectory, such as https://example.com/support, is found at that address rather than at the domain's root. The gateway only accepts an API that the site you named serves itself: if the index reports a REST root on another host, discovery refuses it and names that host, so you can enter its address instead. A bare www. difference is allowed.

Discovery shows the site's name, the REST root it found, and the site's namespaces: wp/v2 is WordPress core, and each plugin with an API adds its own, such as bbpress/v1. Tick the ones you want. You then see every capability they produce, split into what is enabled when you connect and what needs your approval, along with any routes that could not be turned into a capability and the fields that will be redacted. Give the connection a name, enter the username and application password, and connect.

The capability list you reviewed is what gets stored. From gateway 1.3.56 the gateway creates the connection only against the review it matches: if the site's index changes between your review and connecting, it refuses and you review the site again. Nothing re-reads the site when an agent makes a call, so a plugin update that adds a route cannot give an agent a new action on its own. See Re-sync for how to pick up changes.

What the agent can do​

Each route and method becomes one capability, named after the namespace and the route's fixed segments, then a verb:

RouteCapability
GET /wp/v2/postswp.posts.list
GET /wp/v2/posts/{id}wp.posts.get
POST /wp/v2/postswp.posts.create
POST /wp/v2/posts/{id}wp.posts.update
DELETE /wp/v2/posts/{id}wp.posts.delete

Some plugins put several actions behind one route and let the caller name the action. bbPress, for example, has a single topics/{id}/actions/{action} route that closes, sticks, trashes or merges a topic. Those are split into one capability per action, bbpress.topics.actions.close, bbpress.topics.actions.trash and so on, because closing a thread and trashing it carry very different risk and should not share one switch.

What the capability doesRiskWhen you connect
Reads (GET)MediumOn
A read whose route names exportHighOff
Creates and updatesHighOff
DeletesCriticalOff, and asks for approval on every call
A write whose route names refund, charge, capture, payout or transferCriticalOff, and asks for approval on every call
Anything, reads included, whose route below its namespace has trash, delete, destroy, purge, remove, flush, reset, wipe, clear, empty, revoke, deactivate, disable, uninstall, cancel, invalidate, logout, disconnect, unsubscribe, regenerate or trigger as a word or at a word boundaryCriticalOff, and asks for approval on every call
Any write whose route or action names spam, merge, split, ban or block, even inside a longer wordCriticalOff, and asks for approval on every call
Any write under users, roles, plugins, themes, settings or application passwordsCriticalOff, and asks for approval on every call

The word match for writes deliberately errs on the safe side, so unspam and writes to wp/v2/blocks are gated as critical too. From gateway 1.3.56 the operation words in the sixth row apply to reads as well: a plugin's GET route named purge or clear-cache changes something whatever method it is sent with. Matching ignores case and splits at punctuation and separators. An operation can also start or end a segment, so resetCache, resetcache, hardReset and deleted are gated; interior text such as presets and swiper is not, and a namespace that happens to contain one does not escalate every route under it. Reading wp/v2/blocks stays an ordinary read: block names a thing there, not an operation. Turn on the ones you need from the connection page, and they still answer to your grants and policies.

Everything in this table comes from the route's method and the words in its path, both of which the site writes, so they can only make a capability riskier: nothing the site says makes one safer than its method. A route argument named _method, which WordPress reads as the method to run instead, is never imported. If a rule here gates something you know is safe, the gateway's owner can lower it: see Running a capability without its approval.

The Abilities API​

WordPress 6.9 added the Abilities API, where plugins register named abilities with an input schema and annotations that say whether an ability only reads, or is destructive. When a site exposes it, discovery offers an Abilities API option.

WordPress lists abilities only to a signed-in user, so they are read after the connection is created, with the application password you just sealed. Each ability becomes a capability named abilities.<name> that takes a single input parameter:

  • Any ability is high and off, and asks for approval on every call once you turn it on.
  • An ability marked destructive, or whose name contains one of the risky words above or refund, charge, capture, payout, transfer or drop, is critical.
  • An ability marked read-only is called with GET, because WordPress runs read-only abilities that way, and otherwise keeps the posture above.

An annotation can make a capability riskier, never safer: the site writes its own annotations, and WordPress runs whatever an ability's code does on the method its annotation asks for, so read-only changes how the call is sent and nothing about how it is gated. Every ability added this way arrives switched off. The console lists what was added, and you turn on the ones you want from the connection page.

Before gateway 1.3.56 an ability marked read-only was a medium read that ran without an approval once turned on. A gateway upgraded from an earlier version raises every such ability it stored, and every stored GET route the operation words now cover, once when it starts: each becomes high or critical and is switched off, and the audit log records the change as permaura.capability.posture.floor. Turn back on, or lower, the ones you rely on.

Abilities Permaura has reviewed​

Some plugins' abilities have been reviewed and their levels compiled into the gateway, the way the catalog is. An ability with one of those names, registered with exactly the annotations it was reviewed with, arrives at the level its reviewed entry gives instead of the floor above: a read is a medium read that is on when you connect and asks for no approval, creates and updates are high and off, deletes are critical and ask every time, and anything that reaches a customer or moves money asks every time too. The gateway checks that the ability is still the one that was reviewed: the same annotations, and an input schema that takes no field the review did not see. If either has changed, by a plugin update or by anything else on the site, the ability gets the floor instead, which means an approval on every call, until it is reviewed again. A site cannot claim a reviewed level by borrowing a name.

Reviewed so far:

  • Events Manager (7.3 and later): every events-manager/ ability. Taking, changing, canceling or deleting a booking asks for approval on every call.
  • Events Manager Pro (3.8.1 and later): every events-manager-pro/ ability, including coupons, transactions, attendance, RSVPs and booking forms. Coupon changes, RSVP changes and changing the default form ask for approval on every call.

A reviewed level is the starting point, not a ceiling: your policies, grants and the user's WordPress role still decide what runs. The check runs when the gateway reads the site's abilities, which is when you connect and when you re-sync. A connection keeps the level it was stored with between those, so re-sync after updating a plugin.

Running a capability without its approval​

The posture a site's annotations or route names give a capability is the floor the site cannot lower. The gateway's owner can, one capability at a time: on the connection page, Run without approval lowers a capability to medium risk (or leaves it lower) with no approval of its own, and Restore original posture restores the risk and approval setting it had before the owner lowered it. The confirmation shows the reduction before it is submitted. Only the owner of the workspace can do this, and from off the gateway's own machine, with an approval device paired, the change waits for that device to sign it, like any other change that loosens access. The audit log records the old posture and the new.

The setting is yours, not the site's, so a re-sync keeps it while the site still describes the ability or route the same way. If the method changes, or an annotation makes it riskier (marked destructive, say), the re-sync drops the setting and shows the capability as changed and switched off. Remember what you are trusting: WordPress runs whatever the ability's code does, whatever its annotation says.

Re-sync​

The connection page has a Source card showing the site, the REST root and the namespaces the connection was built from. Re-sync reads the site again and shows what changed before anything is applied:

  • New capabilities are added switched off, reads included.
  • Changed capabilities are updated and switched off so you can review them again.
  • Removed capabilities are kept and flagged, never deleted, so a policy that names one fails loudly at review rather than quietly at call time.

On a site with an application password sealed, re-sync can include the Abilities API too.

Redaction​

On top of the gateway's own list of credential fields, every response from a WordPress connection has these fields blanked before an agent sees it: email, user_email, author_email, author_ip, author_user_agent, ip and ip_address. That matters most for context=edit, which returns more of each record, including user email addresses, to a user allowed to edit them.

Good to know​

  • Edits are sent as POST. WordPress lists edits as POST, PUT, PATCH, and many shared hosts refuse the last two, so the connection always uses POST. Deletes use DELETE, which some hosts block as well; a 405 or 403 on a delete usually means the host, not WordPress.
  • The address is pinned for good. If the site moves, changes host, or switches its permalink setting, add the connection again.
  • A path value has to fit its route. From gateway 1.3.51 each value an agent puts in the path is checked against the pattern the site's route gives that parameter, and a slash is accepted only where that pattern allows one, such as a plugin id like akismet/akismet on the plugin routes, which is then sent as a path separator. A slash anywhere else is refused, encoded or not, and so is a value with a slash that another route on the site would also match. From gateway 1.3.56 that last check covers every value, with a slash or without one: WordPress serves the first route that matches, so on a site with both /acme/v1/export and a generic /acme/v1/{report}, the generic capability refuses export rather than reaching the export route you left switched off. A route whose pattern the gateway cannot compile as written is read as the widest pattern it could mean (its lookaheads dropped), and counts as a match only when even that cannot be read. A capability stored before gateway 1.3.51 has no pattern recorded, so it refuses a slash in every path value until a re-sync records one. The console's Re-sync does not count that as a change, so when nothing else changed it shows the connection as up to date with nothing to apply. Send POST /v1/connections/:id/resync with the operator token and without dry_run instead, which records the patterns while keeping each capability's switch and applies any other change it finds as Re-sync does, or add the connection again. Before 1.3.51 a slash was sent encoded, as akismet%2Fakismet, which some Apache setups refuse under pretty permalinks.
  • Security plugins can hide the REST index. If discovery finds nothing on a site you know runs WordPress, check whether a plugin restricts /wp-json/ to signed-in users.
  • Large sites produce long lists. wp/v2 alone is well over a hundred capabilities. Import only the namespaces an agent needs; you can add another later with a new connection.