REST API reference
Complete reference for all WP-PFManagement REST API endpoints. Base URL: /wp-json/pfm/v1/
Authentication
Most endpoints require WordPress authentication via X-WP-Nonce header. Entity CRUD endpoints use a permission resolver that evaluates the current user's roles against the entity's role columns.
Entity & schema management
| Method | Route | Description |
|---|
| GET | /meta/entities | List all entities |
| POST | /meta/entities | Create entity + reconcile table |
| GET | /meta/entities/{slug} | Get single entity |
| PUT | /meta/entities/{slug} | Update entity + reconcile |
| DELETE | /meta/entities/{slug} | Delete entity + drop table |
| POST | /meta/entities/{slug}/dry-run-reconcile | Preview reconcile plan without applying |
| POST | /meta/entities/{slug}/fields | Add a field |
| PATCH | /meta/entities/{slug}/fields/{fieldName} | Patch field config |
| DELETE | /meta/entities/{slug}/fields/{fieldName} | Remove a field |
| POST | /meta/entities/{slug}/fields/reorder | Reorder fields |
| Method | Route | Description |
|---|
| GET | /meta/form-layout/{slug} | Get form layout |
| PUT | /meta/form-layout/{slug} | Save form layout sections |
Auto-numbering
| Method | Route | Description |
|---|
| GET | /meta/entities/{slug}/numbering | Get auto-numbering config |
| PUT | /meta/entities/{slug}/numbering | Set auto-numbering |
| DELETE | /meta/entities/{slug}/numbering | Remove auto-numbering |
| POST | /meta/entities/{slug}/numbering/backfill | Backfill pfm_number for existing rows |
| GET | /meta/numbering | List all numbering sequences |
Navigation
| Method | Route | Description |
|---|
| GET | /meta/navigation | Full apps + modules tree |
| GET | /meta/applications | List applications |
| POST | /meta/applications | Create application |
| PUT | /meta/applications/{slug} | Update application |
| DELETE | /meta/applications/{slug} | Delete application |
| POST | /meta/applications/reorder | Reorder applications |
| GET | /meta/applications/{slug}/modules | List modules |
| POST | /meta/applications/{slug}/modules | Create module |
| PUT | /meta/applications/{slug}/modules/{slug} | Update module |
| DELETE | /meta/applications/{slug}/modules/{slug} | Delete module |
| POST | /meta/applications/{slug}/modules/reorder | Reorder modules |
Templates
| Method | Route | Description |
|---|
| GET | /meta/templates | List available entity templates |
| POST | /meta/templates/{slug}/install | Install template (creates entity + fields + table) |
Records
| Method | Route | Description |
|---|
| GET | /{entity} | List records. Params: filter (EQL), search (free text), limit, offset, sort, fields, include |
| POST | /{entity} | Create record. Anonymous hardening applies (honeypot + rate limit) |
| POST | /{entity}/_bulk | Transactional bulk create ({ "records": [ … ] }, all-or-nothing, max 500) |
| POST | /{entity}/import-csv | CSV import |
| GET | /{entity}/schema | Consumer field catalog (types, relations, options) — read-gated, only readable fields listed |
| GET | /{entity}/{sys_id} | Get single record. include param embeds referenced records |
| PATCH | /{entity}/{sys_id} | Update record (optimistic lock via sys_version) |
| DELETE | /{entity}/{sys_id} | Delete record |
| GET | /{entity}/{sys_id}/related | Inverse-related records grouped by source entity/field |
| POST | /bulk-update | Transactional cross-entity update ({ "updates": [ … ] }, all-or-nothing) |
filter is STRICT EQL: a query that does not parse comes back as 400 pfm_eql_unparseable, never as a successful list with no rows — an integration must be able to tell "your query is wrong" from "nothing matches". Free text belongs in search, which looks through the table's text fields and, when both are sent, narrows inside the filter instead of widening past it.
?include=<field>[,<field>…] deep-loads forward RELATION fields: the target records are returned in an included block keyed by field then by sys_id, so the FK carried on each row joins to its record without an N+1 fetch. Every include honours the same read gates as a direct read (entity ACL, api_exposable, row-visibility, field projection); an unreadable target is silently omitted.
Rate limits
Two ceilings, per caller (per user when signed in, per IP otherwise), counted in one-minute windows. Both answer 429 with a Retry-After header rather than failing silently, and both are bypassed for WP-CLI and cron.
| What | Ceiling | Notes |
|---|
| Reads on admin/metadata routes | 240 / minute | code pfm_rate_limited |
| Writes (POST / PUT / PATCH / DELETE, any route) | 120 / minute | code pfm_write_rate_limited |
The write ceiling is two changes a second sustained, which no person and no screen of ours reaches — the widest burst the product itself produces is a planning save, one record per moved node. It exists so a leaked cookie or a runaway script cannot create rows until the disk fills. If your instance genuinely needs more (a bulk loader of your own, or a limiter already in front of the site), raise or remove it:
add_filter( 'pfm_write_rate_limit_per_minute', fn() => 600 ); // 0 disables it
Anonymous record creation carries its own separate hardening (honeypot + per-entity limit) on top of this.
Response headers, and which ones we control
Every response on this namespace carries X-Request-ID (quote it in a support request), Server-Timing: pfm;dur=<ms> (how long our dispatch took — elapsed time only, no internals), X-Frame-Options, X-Content-Type-Options and Referrer-Policy, plus Vary: Cookie, Origin on user-scoped routes.
X-Powered-By is removed: PHP announces its exact version there by default, and that is precisely what someone probing for a known vulnerability wants.
The Server header is NOT ours. Apache (or nginx) adds it after PHP has finished, so no plugin can remove or change it — verified rather than assumed. If your server announces its version and you would rather it did not, that is a one-line change in YOUR server configuration:
ServerTokens Prod
server_tokens off;
API keys (machine callers)
Integrations authenticate with Authorization: Bearer pfmk_…, resolved to a least-privilege service user. A key carries a { entities, verbs } scope that INTERSECTS the service user's role grants (it never widens them), and it can only reach entities an operator has flagged api_exposable. Keys are schema-manager-gated; the plaintext token is shown once at mint.
| Method | Route | Description |
|---|
| GET | /api-keys | List keys (metadata only, never the token) |
| POST | /api-keys | Mint a key for a service user — returns the plaintext ONCE |
| DELETE | /api-keys/{id} | Revoke a key |
Journal
| Method | Route | Description |
|---|
| GET | /{entity}/{sys_id}/journal | List journal entries |
| POST | /{entity}/{sys_id}/journal | Create journal entry |
| DELETE | /{entity}/{sys_id}/journal/{entry_id} | Delete journal entry |
Events
| Method | Route | Description |
|---|
| GET | /events/catalog | List all registered events |
| POST | /events/catalog | Register a new event |
| GET | /events/{slug} | Get event registry entry |
| PUT | /events/{slug} | Update event |
| DELETE | /events/{slug} | Delete event |
| POST | /events/{slug}/fire | Fire an event with payload |
| GET | /events/log | Query event log |
Actions
| Method | Route | Description |
|---|
| GET | /actions | List all actions |
| POST | /actions | Create action |
| GET | /actions/{id} | Get action |
| PUT | /actions/{id} | Update action |
| DELETE | /actions/{id} | Delete action |
| POST | /actions/{id}/invoke | Invoke action on a record |
| GET | /actions/{entity}/{sys_id} | List visible actions for a record |
Permissions
| Method | Route | Description |
|---|
| GET | /roles | List all roles |
| POST | /roles | Create custom role |
| GET | /roles/{id} | Get role |
| PUT | /roles/{id} | Update role |
| DELETE | /roles/{id} | Delete role |
| GET | /groups | List groups |
| POST | /groups | Create group |
| GET | /groups/{id} | Get group |
| PUT | /groups/{id} | Update group |
| DELETE | /groups/{id} | Delete group |
| PUT | /groups/{id}/users | Replace member set |
| PUT | /groups/{id}/roles | Replace role set |
| GET | /me/perms | Effective permissions for current user |
Business Rules
| Method | Route | Description |
|---|
| GET | /business-rules | List rules |
| POST | /business-rules | Create rule |
| GET | /business-rules/{id} | Get rule |
| PUT | /business-rules/{id} | Update rule |
| DELETE | /business-rules/{id} | Delete rule |
UI Policies
| Method | Route | Description |
|---|
| GET | /ui-policies | List policies |
| POST | /ui-policies | Create policy |
| GET | /ui-policies/{id} | Get policy |
| PUT | /ui-policies/{id} | Update policy |
| DELETE | /ui-policies/{id} | Delete policy |
Row Visibility
| Method | Route | Description |
|---|
| GET | /row-visibility | List row visibility rules |
| POST | /row-visibility | Create rule |
| GET | /row-visibility/{id} | Get rule |
| PUT | /row-visibility/{id} | Update rule |
| DELETE | /row-visibility/{id} | Delete rule |
User Preferences
| Method | Route | Description |
|---|
| GET | /users/me/list-prefs/{entity} | Get per-entity preferences |
| PUT | /users/me/list-prefs/{entity} | Save preferences |
| GET | /users/me/global-prefs | Get global preferences |
| PUT | /users/me/global-prefs | Save global preferences |
Public / Front-end
| Method | Route | Description |
|---|
| GET | /public/nonce | Fresh nonce for cached pages |
| GET | /public/entity/{slug} | Entity definition (visibility-gated) |
Agent & system
| Method | Route | Description |
|---|
| GET | /agent/manifest | Agent manifest for LLM tooling |
| GET | /locales | List platform locales |
| PUT | /locales | Set extra locales |
| GET | /users-map | WP users id → name map |
{
"status": "fail",
"error": "entity_not_found",
"detail": "No entity exists with slug 'tickets'"
}