Contents
REST API - Plugixa CRM
Everything the admin screens do, they do through this API. There is no private back channel, which is why it is worth documenting: if the app can do it, so can you.
Namespace
/wp-json/plugixa-crm/v1/
Around 234 routes across 55 controllers. Every path below takes that prefix.
Entities
Each registered entity gets the same CRUD surface:
GET /accounts list, with search, filters, sorting and paging
POST /accounts create
GET /accounts/{id} read one
PUT /accounts/{id} update
DELETE /accounts/{id} trash
POST /accounts/{id}/restore
DELETE /accounts/{id}/purge
| Entity | Base |
|---|---|
| Account | /accounts |
| Contact | /contacts |
| Lead | /leads |
| Opportunity | /opportunities |
| Case | /cases |
| Task, Meeting, Call | /tasks, /meetings, /calls |
/emails |
|
| Campaign, target list, mass email | /campaigns, /target_lists, /mass_emails |
| Knowledge base article | /kb_articles |
| Document | /library_documents |
| Team | /teams |
Trash and purge are separate, matching the behaviour of the screens: DELETE
soft-deletes, and purging is its own call.
Routes that answer a question
| Route | Answers |
|---|---|
GET /bootstrap |
Everything the app needs to start |
GET /settings, POST /settings |
Read and save settings |
GET /search |
Global search |
GET /meta/entities |
What entities exist, and their fields |
GET /dashboard/stats, /dashboard/layout |
The dashboard’s figures and arrangement |
GET /filters/{entity} |
What that entity can be filtered by |
POST /mass-action/{entity} |
A bulk write, server-side |
GET /export/{entity} |
A list as CSV or Excel |
GET /duplicates/{entity}, POST /merge/{entity} |
Find and merge duplicates |
GET /audit/{entity}/{id} |
A record’s history |
GET /personal-data/{entity}/{id} |
Everything held about somebody |
GET /favourites/{entity} |
Starred records |
POST /leads/{id}/convert |
One-step conversion |
POST /opportunities/{id}/stage, GET /opportunities/board |
Move a deal, and the kanban |
GET /availability, /free-busy, /working-calendars |
Who is free when |
GET /reports, POST /reports/{id}/run, GET /reports/forecast |
Reporting |
GET /stream/{entity}/{id}, POST .../follow |
The feed and its followers |
GET /notifications, /notifications/read |
The bell |
GET /roles, /teams, /crm-access, /record-access/{entity}/{id} |
The access model |
GET /imports, POST /imports/{id}/run, /imports/{id}/revert |
Importing |
GET /webhooks, POST /webhooks/{id}/test |
Webhooks |
GET /integrations, PUT /integrations/{id}/enabled |
Connectors |
GET /portal/* |
The customer portal’s own surface |
POST /pdf/print/{id} |
Print a record |
Premium connectors add their own: /form-maps, /google-calendar/*,
/microsoft-365/*, /sms/{entity}/{id}, /woo-customers/{id}. An absent
connector has no routes - the free build answers rest_no_route, because the
code is not there rather than because a check refused it.
Authentication and permissions
Standard WordPress: a logged-in session with a nonce, or application passwords for a script.
Every route answers through one permission method, and that method is handed
the record, not just the action. That is what lets own and team scopes
exist at all, and it is why the
front-end shortcode needed no
authorisation code of its own.
Three consequences:
- A list you may not read comes back empty, not partial.
- Field-level rules apply to the response, so a field you may not read is absent from the JSON rather than filtered out in the browser.
GET /filters/{entity}is answered after the permission check, so somebody who may not read an entity cannot learn which fields it has by asking what it can be filtered by.
The five public routes
Exactly five routes are reachable without signing in. Each has a written justification in the source, and none of them can read anything:
| Route | Why |
|---|---|
POST /lead-capture/{id} |
A web-to-lead submission. Create-only, field allow-list, honeypot, per-IP rate limit, optional CAPTCHA |
GET /lead-capture/confirm/{token} |
Double opt-in, on a 64-character token |
GET /campaign/open/{token} |
The open pixel |
GET /campaign/click/{token} |
Click tracking |
GET /campaign/unsubscribe/{token} |
Unsubscribe |
The lead-capture route accepts JSON or form-encoded, so the no-JavaScript
<form method="post"> fallback works against the same endpoint.
Note what is not in that list: the public knowledge base has no REST route at all. It is rendered on the server, so there is nothing public to defend.
List conventions
Every list route takes the same parameters: a search term, filters, a sort field
and direction, a page and a page size, and view=trash to see deleted records.
Page size defaults to the Rows per page setting.
Errors
Ordinary WordPress error shapes with a code you can branch on: 422 for a
validation problem naming the field, 409 for a conflict, 403 for a refusal,
404 for absent - including a route whose module is not installed.
Extending it
plugixa_crm_register_rest_routes fires with the namespace and the container, so
your own routes can sit in the same namespace and reuse the same permission model.
See Hooks.
Troubleshooting
| Symptom | Usual cause |
|---|---|
rest_no_route |
The connector is absent. Free has five fewer. |
401 from a script |
Use an application password, or send a nonce. |
| A list is empty for one user and full for another | Permission scoping. That is the feature. |
| A field is missing from the JSON | Field-level rules. It is removed server-side. |
403 on a record visible in a list |
A role scopes edit or delete more narrowly than read. |
| A deleted record still exists | DELETE trashes. Purge is a separate call. |
| The filter list is shorter than expected | It is answered after the permission check. |
| A public route returns 404 | Check the token. They expire. |
What to do next
- React to events: Hooks.
- Who may call what: Roles, Teams and Permissions.
- What the API exposes about people: Privacy and Data.