Every browser-to-server call in NiroHelp goes through one namespace,
nirohelp/v1. The base URL is
https://your-site.example/wp-json/nirohelp/v1/. Routes are registered
centrally in Controllers\Common\API; the handlers live in app/API/, one
class per resource.
Authentication — read this first #
Application Passwords do not work on these routes. Almost every
permission_callback in the plugin calls Traits\Auth::has_nonce(), which
requires a valid wp_rest nonce in the X-WP-Nonce header. Application
Password and Basic auth requests carry no such header, so they authenticate
successfully and are then refused with 403 rest_forbidden:
$ curl -u "admin:xxxx xxxx xxxx xxxx" .../wp-json/wp/v2/users/me # 200
$ curl -u "admin:xxxx xxxx xxxx xxxx" .../wp-json/nirohelp/v1/settings/schema # 403
The supported callers are:
- The browser, as a logged-in user. A cookie plus the nonce. The admin SPA
gets both fromapiFetchinspa/lib/api.ts; front-end scripts get the
nonce from theNIROHELP/NIROHELP_DOCSlocalized objects. - Anyone, on the seven
is_publicroutes. The three doc reads, the three
ticket taxonomy lists and ticket creation accept anonymous requests, because
the embed widget inassets/tickets/js/embed.jsruns on other people’s sites
and cannot carry a nonce. Those callbacks are hardened internally instead —
POST /tickets/strips a caller-supplieduser_id, andGET /docs/only
honoursstatusfor someone who can edit docs.
There is one escape hatch, for local development only:
// wp-config.php — NEVER on a production site.
define( 'NIROHELP_SANDBOX', true );
is_sandbox_mode() short-circuits every permission callback in
app/Traits/Auth.php, including the admin-only AI, settings, migration and
dashboard routes. It makes the whole API world-readable and world-writable.
Permission callbacks #
Route tables below cite these by name.
| Callback | Passes when |
|---|---|
is_public |
Always. No nonce, no login. |
has_nonce |
A valid wp_rest nonce is present. Login not required. |
is_guest |
Not logged in, and sandbox mode is off. |
is_member |
Logged in + nonce. |
is_admin |
manage_options + nonce. |
is_agent |
edit_others_tickets or manage_options, + nonce. |
can_edit_docs |
The doc CPT’s edit_pages, or a NiroHelp role holding bare edit_posts, + nonce. |
can_delete_docs |
Same, for delete_posts / delete_pages. |
can_delete_tickets |
delete_others_tickets or manage_options, + nonce. |
can_read_ticket |
edit_others_tickets, manage_options, or being the ticket’s own client, + nonce. |
can_comment_on_ticket |
Same as can_read_ticket. |
can_edit_docs resolves to administrators, Editors and nirohelp_manager;
nirohelp_agent and nirohelp_user both declare edit_posts => false. See
Roles and permissions.
Docs #
| Method | Path | Permission | Notes |
|---|---|---|---|
| GET | /docs/ |
is_public |
status, s, count, formatted. status is honoured only for users who can edit docs; everyone else gets published docs. count is clamped by the nirohelp_docs_api_max_per_page filter (100). |
| GET | /docs/product/{product} |
is_public |
{product} is a term slug. Same s / count / formatted args. |
| POST | /docs/ |
can_edit_docs |
title, description required; slug, status, meta optional. |
| GET | /docs/{id} |
is_public |
|
| PUT | /docs/{id} |
can_edit_docs |
All fields optional except id. |
| DELETE | /docs/{id}/delete |
can_delete_docs |
|
| POST | /docs/{id}/vote |
has_nonce |
Body {"type":"upvote"} or {"type":"downvote"}. Returns the new count. |
Tickets #
| Method | Path | Permission | Notes |
|---|---|---|---|
| GET | /tickets/ |
is_member |
status, client, agent_id, product, reason, urgency, date_from, date_to, search, date_query, per_page (20), page (1). |
| POST | /tickets/ |
is_public |
title and description required. user_id is stripped unless the caller holds edit_others_tickets or manage_options. |
| GET | /tickets/{id} |
can_read_ticket |
|
| PUT | /tickets/{id} |
is_agent |
title, slug, description, status, meta. |
| DELETE | /tickets/{id}/delete |
can_delete_tickets |
|
| GET | /tickets/{id}/comments |
can_read_ticket |
The comments are the private conversation, so this is gated per ticket, not merely on being logged in. |
| POST | /tickets/{id}/comments |
can_comment_on_ticket |
message required. Auto-transitions ticket status. |
| GET | /tickets/products/ |
is_public |
|
| GET | /tickets/reasons/ |
is_public |
|
| GET | /tickets/urgencies/ |
is_public |
The agent on a ticket is changed through meta on PUT /tickets/{id}, which
fires nirohelp-tickets-agent_changed.
Authentication routes #
| Method | Path | Permission | Notes |
|---|---|---|---|
| POST | /login/ |
is_guest |
Drives whichever login method is configured. email required; code on the second step of the OTP flow; name and redirect optional. |
| POST | /wp-login/ |
is_guest |
email + password. Runs wp_authenticate(), so the whole core authenticate filter chain applies. |
| POST | /wp-signup/ |
is_guest |
name + email + password. Gated on nirohelp_can_register_users(). |
See Client login for the three methods and
how they differ.
Options #
| Method | Path | Permission | Notes |
|---|---|---|---|
| GET | /option |
is_admin |
key required. |
| POST | /option |
is_admin |
key + value. |
| DELETE | /option |
is_admin |
key required. |
Do not route settings through /option. It writes a raw option value by
key and so skips Settings::sanitize() — including the two repeater passes
that zero unchecked row checkboxes and drop deleted rows. Use /settings.
Settings #
| Method | Path | Permission | Notes |
|---|---|---|---|
| GET | /settings/schema |
is_admin |
The schema the admin screen renders from. |
| POST | /settings |
is_admin |
settings, an object keyed [tab][section][field]. |
Migration #
Five routes, all is_admin. They start and delete content in bulk and rely on
the REST nonce alone.
| Method | Path | Notes |
|---|---|---|
| GET | /migration/sources |
The importers, grouped, with availability and migrated counts. |
| GET | /migration/status |
Where the current run has got to. Returns the importer’s own summary text verbatim. |
| POST | /migration/start |
Begin an import. |
| POST | /migration/clean |
Delete everything one source produced. |
| POST | /migration/reset |
Clear the run state. |
AI #
All is_admin.
| Method | Path | Notes |
|---|---|---|
| POST | /ai/register |
name, email. Forwards to the service; it mails back a site key. |
| POST | /ai/verify |
site_key required. |
| POST | /ai/resend |
Re-runs register with the stored name and email. |
| POST | /ai/reset |
Unverifies remotely and clears local credentials. |
| GET | /ai/site-key |
The stored site key. 403 until verified. |
| POST | /ai/sync |
Push published docs to the vector store. 300s timeout. |
| GET | /ai/state |
What the AI screens render from: verified, has_site_key, name, email, tickets_enabled. Never returns the key itself. |
| GET | /ai/usage |
Proxies the service’s usage figures. |
| GET | /ai/auto-responder |
Current auto-responder settings. |
| POST | /ai/auto-responder |
delay_amount, delay_unit, threshold required; enabled, author_name, change_status optional. |
| GET | /ai/custom-prompt |
The stored instructions plus the default. |
| POST | /ai/custom-prompt |
custom_prompt. Refuses on an unverified site. |
| GET | /ai/chatbot |
Stored chatbot configuration. |
| POST | /ai/chatbot |
Save it. |
| POST | /ai/chatbot/preview |
Build the embed from posted values without storing them. Backs Preview, View Code and Download. |
| GET | /ai/chatbot/plugin |
The generated single-file plugin. |
| POST | /ai/copilot |
Ask NiroHelp. message required, session_id optional. |
| GET | /ai/logs/{type} |
{type} is chatbot or responder. |
GET /ai/usage and GET /ai/logs/{type} proxy
my.nirosuite.com/wp-json/nirohelp-cloud/v1/{usage,logs}, which do not exist
yet — both 404 today, so the usage panel and the two log screens report the
service’s error rather than data. That is tracked on the service side
(nirohelp-cloud#30), not here.
Dashboard #
All is_admin, all local counts — no remote call, so a slow service cannot
stall the screen.
| Path | Returns |
|---|---|
/dashboard/stats |
Ticket and doc counts, sentiment split, service levels. range in days. |
/dashboard/queues |
Three triage queues: needs attention, needs reply, new. |
/dashboard/ai |
Auto-responder outcomes and handovers. |
/dashboard/chatbot |
Chatbot activity for the window. |
/dashboard/docs |
The four docs panels. |
/dashboard/activity |
Merged ticket, doc and AI-reply events. |
/dashboard/agents |
Per-agent load and medians. |
/dashboard/hot-spots |
Three heatmaps. |
/dashboard/briefing |
The cached daily briefing, written by cron at 06:00. |
A note on args #
register_rest_route‘s args declaration documents a route; it does not
filter the request. Unregistered parameters still reach the callback, and a
missing required one is rejected but an unexpected type is not. Validate
inside the handler.
Worked examples #
Filing a ticket anonymously — the one write that needs no credentials:
curl -H 'Content-Type: application/json' \
-d '{"title":"Login fails","description":"Steps to reproduce...","email":"[email protected]","name":"Alice"}' \
https://your-site.example/wp-json/nirohelp/v1/tickets/
From the front end, where the nonce is already localized:
fetch( '/wp-json/nirohelp/v1/tickets/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': NIROHELP.nonce,
},
body: JSON.stringify( { title: 'Login fails', description: 'Steps...' } ),
} );
From inside the admin SPA, use apiFetch from spa/lib/api.ts — it wires the
nonce and the root URL for you:
import { apiFetch } from '@nirohelp/lib/api';
const stats = await apiFetch< DashboardStats >( { path: '/dashboard/stats' } );
Extending it #
New browser-to-server calls belong here, not on admin-ajax. Register the
route in Controllers\Common\API through Traits\Rest::register_route(), put
the callback in app/API/, and give it a real permission_callback.
A callback placed in Controllers/Admin/ is never loaded during a REST
request — Bootstrap\Initializer skips that whole group — so the route fails
with no obvious cause.
One wp_ajax_* holdout remains, Controllers\Admin\AJAX for doc and topic
reordering. It is tracked for removal, not a precedent.
Was this doc helpful?