REST API reference

Home » Developers » REST API reference

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 from apiFetch in spa/lib/api.ts; front-end scripts get the

    nonce from the NIROHELP / NIROHELP_DOCS localized objects.
  • Anyone, on the seven is_public routes. The three doc reads, the three

    ticket taxonomy lists and ticket creation accept anonymous requests, because

    the embed widget in assets/tickets/js/embed.js runs on other people’s sites

    and cannot carry a nonce. Those callbacks are hardened internally instead —

    POST /tickets/ strips a caller-supplied user_id, and GET /docs/ only

    honours status for 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.

CallbackPasses when
is_publicAlways. No nonce, no login.
has_nonceA valid wp_rest nonce is present. Login not required.
is_guestNot logged in, and sandbox mode is off.
is_memberLogged in + nonce.
is_adminmanage_options + nonce.
is_agentedit_others_tickets or manage_options, + nonce.
can_edit_docsThe doc CPT’s edit_pages, or a NiroHelp role holding bare edit_posts, + nonce.
can_delete_docsSame, for delete_posts / delete_pages.
can_delete_ticketsdelete_others_tickets or manage_options, + nonce.
can_read_ticketedit_others_tickets, manage_options, or being the ticket’s own client, + nonce.
can_comment_on_ticketSame 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

MethodPathPermissionNotes
GET/docs/is_publicstatus, 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_docstitle, description required; slug, status, meta optional.
GET/docs/{id}is_public
PUT/docs/{id}can_edit_docsAll fields optional except id.
DELETE/docs/{id}/deletecan_delete_docs
POST/docs/{id}/votehas_nonceBody {"type":"upvote"} or {"type":"downvote"}. Returns the new count.

Tickets

MethodPathPermissionNotes
GET/tickets/is_memberstatus, client, agent_id, product, reason, urgency, date_from, date_to, search, date_query, per_page (20), page (1).
POST/tickets/is_publictitle 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_agenttitle, slug, description, status, meta.
DELETE/tickets/{id}/deletecan_delete_tickets
GET/tickets/{id}/commentscan_read_ticketThe comments are the private conversation, so this is gated per ticket, not merely on being logged in.
POST/tickets/{id}/commentscan_comment_on_ticketmessage 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

MethodPathPermissionNotes
POST/login/is_guestDrives whichever login method is configured. email required; code on the second step of the OTP flow; name and redirect optional.
POST/wp-login/is_guestemail + password. Runs wp_authenticate(), so the whole core authenticate filter chain applies.
POST/wp-signup/is_guestname + email + password. Gated on nirohelp_can_register_users().

See Client login for the three methods and
how they differ.

Options

MethodPathPermissionNotes
GET/optionis_adminkey required.
POST/optionis_adminkey + value.
DELETE/optionis_adminkey 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

MethodPathPermissionNotes
GET/settings/schemais_adminThe schema the admin screen renders from.
POST/settingsis_adminsettings, 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.

MethodPathNotes
GET/migration/sourcesThe importers, grouped, with availability and migrated counts.
GET/migration/statusWhere the current run has got to. Returns the importer’s own summary text verbatim.
POST/migration/startBegin an import.
POST/migration/cleanDelete everything one source produced.
POST/migration/resetClear the run state.

AI

All is_admin.

MethodPathNotes
POST/ai/registername, email. Forwards to the service; it mails back a site key.
POST/ai/verifysite_key required.
POST/ai/resendRe-runs register with the stored name and email.
POST/ai/resetUnverifies remotely and clears local credentials.
GET/ai/site-keyThe stored site key. 403 until verified.
POST/ai/syncPush published docs to the vector store. 300s timeout.
GET/ai/stateWhat the AI screens render from: verified, has_site_key, name, email, tickets_enabled. Never returns the key itself.
GET/ai/usageProxies the service’s usage figures.
GET/ai/auto-responderCurrent auto-responder settings.
POST/ai/auto-responderdelay_amount, delay_unit, threshold required; enabled, author_name, change_status optional.
GET/ai/custom-promptThe stored instructions plus the default.
POST/ai/custom-promptcustom_prompt. Refuses on an unverified site.
GET/ai/chatbotStored chatbot configuration.
POST/ai/chatbotSave it.
POST/ai/chatbot/previewBuild the embed from posted values without storing them. Backs Preview, View Code and Download.
GET/ai/chatbot/pluginThe generated single-file plugin.
POST/ai/copilotAsk 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.

PathReturns
/dashboard/statsTicket and doc counts, sentiment split, service levels. range in days.
/dashboard/queuesThree triage queues: needs attention, needs reply, new.
/dashboard/aiAuto-responder outcomes and handovers.
/dashboard/chatbotChatbot activity for the window.
/dashboard/docsThe four docs panels.
/dashboard/activityMerged ticket, doc and AI-reply events.
/dashboard/agentsPer-agent load and medians.
/dashboard/hot-spotsThree heatmaps.
/dashboard/briefingThe 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?

Scroll to Top