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
Email /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

Quick Links