The Havenlytics REST API exposes two independent namespaces. hvnlynab/v1 powers the admin experience — plugin settings, the drag-and-drop builders, analytics, meta cleanup, and plugin info — and is locked to administrators. hvnly/v1 powers the Agent Workspace single-page application (SPA) and is driven by roles and portal capabilities rather than manage_options. This reference documents every route that ships in Havenlytics 3.3.1, the permission callback that guards it, the parameters it accepts, and what it returns.
Note: Only the two namespaces below exist. There is no public, unauthenticated REST surface in Havenlytics — every route enforces a permission callback. If a route is not listed on this page, it is not part of the plugin’s REST API.
The two REST namespaces
hvnlynab/v1— admin settings, builders, meta cleanup, analytics, and plugin info. Routes are registered byincludes/Api/Controller.php, which dispatches to the type classes underincludes/Api/Type/. The set of controllers is filterable (see Registering a custom controller), andController::rest_request_filter()ensures the API still resolves under plain (non-pretty) permalinks.hvnly/v1— the Agent Workspace SPA. The namespace is defined byWorkspaceConstants::REST_NAMESPACEand its controllers live underincludes/Workspace/Api/.
Tip: Base URLs are
/wp-json/hvnlynab/v1/…and/wp-json/hvnly/v1/…. Always resolve them fromget_rest_url()or the JSwpApiSettings.rootrather than hard-coding/wp-json/, which can differ on sites using plain permalinks or a custom REST prefix.
Authentication
Admin namespace (hvnlynab/v1)
Nearly every route requires the manage_options capability. Any write (POST, PUT, DELETE) additionally re-verifies a WordPress REST nonce: the request must send a valid X-WP-Nonce header created with wp_create_nonce( 'wp_rest' ). Two routes relax the read capability: GET /price-on-call-texts requires only edit_posts, and the analytics routes evaluate current_user_can( apply_filters( 'hvnly_admin_capability', 'manage_options' ) ), so the required capability is itself filterable.
Workspace namespace (hvnly/v1)
The Workspace uses standard WordPress cookie authentication plus the core wp_rest nonce. The SPA localizes wp_create_nonce( 'wp_rest' ) (see WorkspaceAssets.php) and sends it as X-WP-Nonce on every request. Permission is enforced in layers by the controllers’ permission callbacks:
- Availability —
WorkspaceSettings::is_enabled()andWorkspaceAvailability::is_available(). If the Workspace is turned off, the request fails403 hvnly_workspace_disabled. - Authentication —
is_user_logged_in(). An anonymous request fails401. - Access —
PortalAuthorization::can_access_workspace(). A logged-in user without portal access fails403 hvnly_workspace_forbidden. - Per-item capability — resource controllers add a final gate:
PropertyAccessGatefor property/media operations andcan_manage_inquiries()for inquiries.
MeController is intentionally shallow: it stops at layer 2 (availability + logged-in) so the SPA can bootstrap and discover the current user’s permissions before deciding what to render. No Workspace route requires manage_options; the surface is entirely role- and capability-driven.
Security: Cookie authentication alone is not enough for state-changing REST calls in WordPress — the
X-WP-Nonceheader is what prevents cross-site request forgery. Always send it. See Authentication for the full model.
hvnlynab/v1 — admin, settings & builders
PluginSettingsAPI
Backed by the hvnly_plugin_settings option (includes/Api/Type/Settings/PluginSettingsAPI.php). Every route runs through check_permission, which requires manage_options and, for writes, a valid X-WP-Nonce verified against wp_rest.
| Method & Route | Permission | Params | Returns |
|---|---|---|---|
GET /plugin-settings | manage_options | — | { success, data: { allGroups }, has_defaults } |
POST /plugin-settings/save-all | manage_options + nonce | All groups (JSON body) | Saved settings |
POST /plugin-settings/reset-all | manage_options + nonce | — | Defaults |
POST /plugin-settings/sync-view-type | manage_options + nonce | view_type (GRID|LIST) | Updated value |
POST /plugin-settings/sync-ajax-load-more | manage_options + nonce | ajax_load_more_enabled | Updated value |
GET /plugin-settings/{group} | manage_options | group (path) | Group settings |
POST /plugin-settings/{group} | manage_options + nonce | group + JSON body | Saved group |
POST /plugin-settings/{group}/reset | manage_options + nonce | group | Group defaults |
Valid {group} values are: general, design, search, properties, shortcodes, property-details, map, performance, contact-agent, email, and permalinks. An unrecognized group returns 400 invalid_group.
PluginInfoAPI
| Method & Route | Permission | Params | Returns |
|---|---|---|---|
GET /plugin-info | manage_options (closure) | — | { success, version, name: "Havenlytics", api_namespace } |
PriceOnCallTextAPI
Manages the custom “price on call” labels stored in the hvnly_price_on_call_custom_options option. IDs 1–5 are protected defaults and cannot be deleted.
| Method & Route | Permission | Params | Returns |
|---|---|---|---|
GET /price-on-call-texts | edit_posts | — | List of label options |
POST /price-on-call-texts | manage_options | label (required) | 201 created option |
PUT /price-on-call-texts/{id} | manage_options | id + label | Updated option |
DELETE /price-on-call-texts/{id} | manage_options | id (not 1–5) | Deletion result |
AnalyticsAPI
Every route’s permission is current_user_can( apply_filters( 'hvnly_admin_capability', 'manage_options' ) ). Range arguments are range (default 30d), date_from, and date_to.
| Method & Route | Permission | Params | Returns |
|---|---|---|---|
GET /analytics/overview | hvnly_admin_capability | range, date_from, date_to | Overview metrics |
GET /analytics/charts | hvnly_admin_capability | range, date_from, date_to | Chart series |
GET /analytics/tables | hvnly_admin_capability | range, date_from, date_to | Table datasets |
GET /analytics/export | hvnly_admin_capability | type (default properties) | Export payload |
DnDSections (property page builder)
Persists sections into hvnly_property_builder.sections. All routes require manage_options; writes re-verify the wp_rest nonce.
| Method & Route | Permission | Params | Returns |
|---|---|---|---|
GET /pb-dnd-sections | manage_options | — | Section layout |
POST /pb-dnd-sections | manage_options + nonce | Sections (JSON) | Saved layout |
DELETE /pb-dnd-sections | manage_options + nonce | — | Cleared layout |
POST /pb-dnd-sections/reset-unified | manage_options + nonce | — | Reset layout |
GET /master-base-ids | manage_options | — | Master base IDs |
GET /generate-unique-ids | manage_options | — | Generated IDs |
DnDCardBuilder (property card builder)
Persists into hvnly_property_card.sections and localizes hvnlyApiSettings onto the hvnly-builder script. All routes require manage_options; writes re-verify the wp_rest nonce.
| Method & Route | Permission | Params | Returns |
|---|---|---|---|
GET /pb-card-builder | manage_options | — | Card layout |
POST /pb-card-builder | manage_options + nonce | Card (JSON) | Saved card |
DELETE /pb-card-builder | manage_options + nonce | — | Cleared card |
POST /pb-card-builder/reset | manage_options + nonce | — | Reset card |
MetaCleanup
Diagnostic and maintenance routes for orphaned property meta. All require manage_options.
| Method & Route | Permission | Params | Returns |
|---|---|---|---|
GET /meta-cleanup/orphaned | manage_options | property_id | Orphaned fields |
POST /meta-cleanup/cleanup | manage_options | fields[], property_id | Cleanup result |
POST /meta-cleanup/reset-and-cleanup | manage_options | — | Reset + cleanup result |
GET /meta-cleanup/stats | manage_options | — | Statistics |
POST /meta-cleanup/cleanup-all | manage_options | — | Cleanup result |
GET /meta-cleanup/inventory | manage_options | — | Meta inventory |
POST /meta-cleanup/final-cleanup | manage_options | confirm (scan-only unless true) | Scan or cleanup result |
Warning:
POST /meta-cleanup/final-cleanupis destructive. It runs in scan-only mode unless you sendconfirm=true. Always run a scan first and review the inventory before confirming.
hvnly/v1 — the Agent Workspace SPA
Workspace controllers share the layered permission pattern described under Authentication: availability → logged in → can_access_workspace() → per-item capability. The tables below name the permission callback each route uses.
MeController
| Method & Route | Permission | Params | Returns |
|---|---|---|---|
GET /me | permission_logged_in (availability + is_user_logged_in) | — | { success, data: { user, agent, permissions, verification, workspace } } |
The user object contains id, email, display_name, avatar_url, roles, registration_status, and email_verified.
DashboardController & AnalyticsController
| Method & Route | Permission | Params | Returns |
|---|---|---|---|
GET /dashboard | permission_dashboard | — | Dashboard payload |
GET /analytics | permission_analytics | range (default 30d) | Analytics payload |
PropertiesController
Per-item permissions combine require_workspace_user() with the PropertyAccessGate, so an agent can only act on properties they are allowed to touch.
| Method & Route | Permission | Params | Returns |
|---|---|---|---|
GET /properties/schema | permission_list | — | Field schema |
GET /properties | permission_list | page, per_page, search, status, orderby, order, date_from, date_to | Paginated properties |
POST /properties | permission_create (access → can_create) | Property fields | Created property |
POST /properties/bulk | permission_create | Bulk operation payload | Bulk result |
GET /properties/{id} | permission_edit_item | id | Property |
PUT /properties/{id} | permission_edit_item | id + fields | Updated property |
DELETE /properties/{id} | permission_trash | id | Trashed property |
POST /properties/{id}/restore | permission_restore | id | Restored property |
DELETE /properties/{id}/force | permission_force_delete | id | Permanent deletion |
POST /properties/{id}/duplicate | permission_edit_item | id | Duplicated property |
GET /properties/{id}/preview | permission_edit_item | id | Preview payload |
POST /properties/{id}/submit | access → can_submit | id | Submitted for review |
POST /properties/{id}/publish | access → can_publish | id | Published property |
POST /properties/{id}/status | permission_edit_item | id + status | Status change |
PropertyMediaController
permission_edit here is require_user() plus access->can_edit( id ).
| Method & Route | Permission | Params | Returns |
|---|---|---|---|
GET /properties/{id}/media | permission_edit | id | Media list |
POST /properties/{id}/media | permission_upload | id + file | Uploaded attachment |
PUT /properties/{id}/media/featured | permission_edit | id + attachment | Updated featured image |
PUT /properties/{id}/media/gallery | permission_edit | id + gallery IDs | Updated gallery |
DELETE /properties/{id}/media/{attachment_id} | permission_edit | id, attachment_id | Deletion result |
InquiriesController
Every route requires permission_manage, which enforces can_manage_inquiries().
| Method & Route | Permission | Params | Returns |
|---|---|---|---|
GET /inquiries | permission_manage | page, per_page, status, search | Paginated inquiries |
GET /inquiries/{id} | permission_manage | id | Inquiry |
PATCH /inquiries/{id} | permission_manage | id + fields | Updated inquiry |
POST /inquiries/{id}/reply | permission_manage | id + reply | Reply result |
NotificationsController
| Method & Route | Permission | Params | Returns |
|---|---|---|---|
GET /notifications | permission | — | Notifications list |
PATCH /notifications/read-all | permission | — | Mark-all result |
PATCH /notifications/{id} | permission | id | Updated notification |
ProfileController & SettingsController
| Method & Route | Permission | Params | Returns |
|---|---|---|---|
GET /profile | permission_view | — | Profile payload |
PUT /profile | permission_edit (edit-own-profile) | Profile fields | Updated profile |
GET /settings | permission | — | Workspace settings |
PUT /settings | permission | Settings fields | Updated settings |
Real request & response examples
GET /wp-json/hvnly/v1/me
Authenticated with the WordPress session cookie and an X-WP-Nonce header. This is the first call the SPA makes on boot to discover who the user is and what they can do.
GET /wp-json/hvnly/v1/me HTTP/1.1
Cookie: wordpress_logged_in_...=...
X-WP-Nonce: 8f3c1a9d2b
HTTP/1.1 200 OK
Content-Type: application/json
{
"success": true,
"data": {
"user": {
"id": 42,
"email": "agent@example.com",
"display_name": "Jordan Agent",
"avatar_url": "https://example.com/wp-content/uploads/avatar.jpg",
"roles": ["hvnly_agent"],
"registration_status": "active",
"email_verified": true
},
"agent": { "...": "..." },
"permissions": { "can_access_workspace": true },
"verification": { "satisfied": true, "factors": { "email": { "satisfied": true } } },
"workspace": {
"enabled": true,
"version": "3.3.1",
"namespace": "hvnly/v1",
"role": "hvnly_agent",
"can_access": true,
"registration_status": "active"
}
}
}
An anonymous request returns 401 with the error code hvnly_rest_unauthorized.
POST /wp-json/hvnlynab/v1/plugin-settings/performance
Saving a single settings group. Requires manage_options and a valid X-WP-Nonce.
POST /wp-json/hvnlynab/v1/plugin-settings/performance HTTP/1.1
Content-Type: application/json
X-WP-Nonce: 8f3c1a9d2b
{ "hvnly_cache_enabled": true }
HTTP/1.1 200 OK
{
"success": true,
"data": { "hvnly_cache_enabled": true },
"message": "Performance settings saved successfully"
}
Without a valid capability or nonce the route returns 401/403. An unknown group returns 400 invalid_group.
Calling /me from JavaScript with fetch()
The SPA exposes the nonce and root URL through its localized settings object. Send the nonce on every request and include credentials so the session cookie travels with it.
// hvnlyWorkspace is localized by WorkspaceAssets.php
async function loadCurrentUser() {
const res = await fetch( hvnlyWorkspace.restUrl + 'hvnly/v1/me', {
method: 'GET',
credentials: 'same-origin', // send the auth cookie
headers: {
'X-WP-Nonce': hvnlyWorkspace.nonce, // wp_create_nonce('wp_rest')
'Accept': 'application/json'
}
} );
if ( res.status === 401 ) {
// hvnly_rest_unauthorized — send the user to sign in
return null;
}
const body = await res.json();
return body.data.user;
}
Common mistake: Omitting
credentials: 'same-origin'(or'include') means the browser drops the login cookie and every Workspace call fails401, even with a correct nonce. The nonce authenticates the request; the cookie authenticates the user.
Registering a custom controller (hvnlynab/v1)
The admin namespace builds its route table from a filterable class map. Hook hvnlynab_rest_api_class_map (defined in includes/Api/Controller.php) to add your own controller class alongside the built-in DnDSections, DnDCardBuilder, MetaCleanup, PluginSettingsAPI, PluginInfoAPI, PriceOnCallTextAPI, and AnalyticsAPI.
<?php
/**
* Register a custom controller into the hvnlynab/v1 namespace.
* Your class is instantiated and dispatched by Havenlytics' Controller.
*/
add_filter( 'hvnlynab_rest_api_class_map', function ( array $class_map ) {
// Key is a short identifier; value is your fully-qualified class name.
$class_map['my_integration'] = \My\Plugin\Rest\MyIntegrationController::class;
return $class_map;
} );
Security: Havenlytics does not add a permission callback for you. Every route your controller registers must define its own
permission_callbackthat checks a capability and, for writes, verifies thewp_restnonce — mirror the built-in controllers. Never return__return_truefrom a permission callback.
Best practices
- Resolve URLs dynamically. Use
get_rest_url( null, 'hvnlynab/v1/…' )in PHP and the localized root in JS; never hard-code/wp-json/. - Always send
X-WP-Nonceon writes. Both namespaces re-verify thewp_restnonce for state-changing requests. - Respect the layered Workspace gates. Do not assume a logged-in user can reach a property; the
PropertyAccessGatemay still reject them. - Prefer the group routes for settings.
POST /plugin-settings/{group}is lighter and safer thansave-allwhen you only touch one area. - Use the correct namespace. Admin tooling belongs on
hvnlynab/v1; agent-facing UI belongs onhvnly/v1. They enforce different permission models.
Performance recommendations
- Batch bootstrap. The SPA calls
/meonce and caches the permissions object; do the same in your own integrations rather than re-fetching identity per view. - Page your lists.
GET /propertiesandGET /inquiriesacceptpageandper_page; keepper_pagemodest to avoid heavy queries. - Scan before cleanup.
meta-cleanup/final-cleanupruns a lighter scan whenconfirmis absent — use it to size the job before committing. - Use analytics ranges. Pass a tight
range/date_from/date_toinstead of pulling the widest window.
Security notes
- The admin namespace is
manage_options-gated with nonce re-verification on writes — treat it as administrator-only. - The Workspace namespace is role- and capability-driven; there is no
manage_optionsshortcut into it. - The analytics capability is filterable via
hvnly_admin_capability— audit any filter you add so you do not accidentally widen access. - Every custom controller you register through
hvnlynab_rest_api_class_mapmust enforce its own permissions.
Common mistakes
- Sending
manage_options-level credentials tohvnly/v1and expecting access — the Workspace ignores that capability and checks portal access instead. - Forgetting the nonce on writes and receiving a silent
403. - Deleting a price-on-call option with ID 1–5 — those are protected defaults.
- Posting to an invalid settings group and getting
400 invalid_groupinstead of a save. - Confirming
final-cleanupwithout running the scan first.