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.

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?

Scroll to Top