Skip to main content
Skip to main content

Developers Doc

REST API: The Complete Developer Reference

12 mins read 6 Views 3+

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 by includes/Api/Controller.php, which dispatches to the type classes under includes/Api/Type/. The set of controllers is filterable (see Registering a custom controller), and Controller::rest_request_filter() ensures the API still resolves under plain (non-pretty) permalinks.
  • hvnly/v1 — the Agent Workspace SPA. The namespace is defined by WorkspaceConstants::REST_NAMESPACE and its controllers live under includes/Workspace/Api/.

Tip: Base URLs are /wp-json/hvnlynab/v1/… and /wp-json/hvnly/v1/…. Always resolve them from get_rest_url() or the JS wpApiSettings.root rather 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:

  1. Availability — WorkspaceSettings::is_enabled() and WorkspaceAvailability::is_available(). If the Workspace is turned off, the request fails 403 hvnly_workspace_disabled.
  2. Authentication — is_user_logged_in(). An anonymous request fails 401.
  3. Access — PortalAuthorization::can_access_workspace(). A logged-in user without portal access fails 403 hvnly_workspace_forbidden.
  4. Per-item capability — resource controllers add a final gate: PropertyAccessGate for property/media operations and can_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-Nonce header 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 & RoutePermissionParamsReturns
GET /plugin-settingsmanage_options{ success, data: { allGroups }, has_defaults }
POST /plugin-settings/save-allmanage_options + nonceAll groups (JSON body)Saved settings
POST /plugin-settings/reset-allmanage_options + nonceDefaults
POST /plugin-settings/sync-view-typemanage_options + nonceview_type (GRID|LIST)Updated value
POST /plugin-settings/sync-ajax-load-moremanage_options + nonceajax_load_more_enabledUpdated value
GET /plugin-settings/{group}manage_optionsgroup (path)Group settings
POST /plugin-settings/{group}manage_options + noncegroup + JSON bodySaved group
POST /plugin-settings/{group}/resetmanage_options + noncegroupGroup defaults

Valid {group} values are: generaldesignsearchpropertiesshortcodesproperty-detailsmapperformancecontact-agentemail, and permalinks. An unrecognized group returns 400 invalid_group.

PluginInfoAPI

Method & RoutePermissionParamsReturns
GET /plugin-infomanage_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 15 are protected defaults and cannot be deleted.

Method & RoutePermissionParamsReturns
GET /price-on-call-textsedit_postsList of label options
POST /price-on-call-textsmanage_optionslabel (required)201 created option
PUT /price-on-call-texts/{id}manage_optionsid + labelUpdated option
DELETE /price-on-call-texts/{id}manage_optionsid (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 & RoutePermissionParamsReturns
GET /analytics/overviewhvnly_admin_capabilityrangedate_fromdate_toOverview metrics
GET /analytics/chartshvnly_admin_capabilityrangedate_fromdate_toChart series
GET /analytics/tableshvnly_admin_capabilityrangedate_fromdate_toTable datasets
GET /analytics/exporthvnly_admin_capabilitytype (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 & RoutePermissionParamsReturns
GET /pb-dnd-sectionsmanage_optionsSection layout
POST /pb-dnd-sectionsmanage_options + nonceSections (JSON)Saved layout
DELETE /pb-dnd-sectionsmanage_options + nonceCleared layout
POST /pb-dnd-sections/reset-unifiedmanage_options + nonceReset layout
GET /master-base-idsmanage_optionsMaster base IDs
GET /generate-unique-idsmanage_optionsGenerated 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 & RoutePermissionParamsReturns
GET /pb-card-buildermanage_optionsCard layout
POST /pb-card-buildermanage_options + nonceCard (JSON)Saved card
DELETE /pb-card-buildermanage_options + nonceCleared card
POST /pb-card-builder/resetmanage_options + nonceReset card

MetaCleanup

Diagnostic and maintenance routes for orphaned property meta. All require manage_options.

Method & RoutePermissionParamsReturns
GET /meta-cleanup/orphanedmanage_optionsproperty_idOrphaned fields
POST /meta-cleanup/cleanupmanage_optionsfields[]property_idCleanup result
POST /meta-cleanup/reset-and-cleanupmanage_optionsReset + cleanup result
GET /meta-cleanup/statsmanage_optionsStatistics
POST /meta-cleanup/cleanup-allmanage_optionsCleanup result
GET /meta-cleanup/inventorymanage_optionsMeta inventory
POST /meta-cleanup/final-cleanupmanage_optionsconfirm (scan-only unless true)Scan or cleanup result

Warning: POST /meta-cleanup/final-cleanup is destructive. It runs in scan-only mode unless you send confirm=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 & RoutePermissionParamsReturns
GET /mepermission_logged_in (availability + is_user_logged_in){ success, data: { user, agent, permissions, verification, workspace } }

The user object contains idemaildisplay_nameavatar_urlrolesregistration_status, and email_verified.

DashboardController & AnalyticsController

Method & RoutePermissionParamsReturns
GET /dashboardpermission_dashboardDashboard payload
GET /analyticspermission_analyticsrange (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 & RoutePermissionParamsReturns
GET /properties/schemapermission_listField schema
GET /propertiespermission_listpageper_pagesearchstatusorderbyorderdate_fromdate_toPaginated properties
POST /propertiespermission_create (access → can_create)Property fieldsCreated property
POST /properties/bulkpermission_createBulk operation payloadBulk result
GET /properties/{id}permission_edit_itemidProperty
PUT /properties/{id}permission_edit_itemid + fieldsUpdated property
DELETE /properties/{id}permission_trashidTrashed property
POST /properties/{id}/restorepermission_restoreidRestored property
DELETE /properties/{id}/forcepermission_force_deleteidPermanent deletion
POST /properties/{id}/duplicatepermission_edit_itemidDuplicated property
GET /properties/{id}/previewpermission_edit_itemidPreview payload
POST /properties/{id}/submitaccess → can_submitidSubmitted for review
POST /properties/{id}/publishaccess → can_publishidPublished property
POST /properties/{id}/statuspermission_edit_itemid + statusStatus change

PropertyMediaController

permission_edit here is require_user() plus access->can_edit( id ).

Method & RoutePermissionParamsReturns
GET /properties/{id}/mediapermission_editidMedia list
POST /properties/{id}/mediapermission_uploadid + fileUploaded attachment
PUT /properties/{id}/media/featuredpermission_editid + attachmentUpdated featured image
PUT /properties/{id}/media/gallerypermission_editid + gallery IDsUpdated gallery
DELETE /properties/{id}/media/{attachment_id}permission_editidattachment_idDeletion result

InquiriesController

Every route requires permission_manage, which enforces can_manage_inquiries().

Method & RoutePermissionParamsReturns
GET /inquiriespermission_managepageper_pagestatussearchPaginated inquiries
GET /inquiries/{id}permission_manageidInquiry
PATCH /inquiries/{id}permission_manageid + fieldsUpdated inquiry
POST /inquiries/{id}/replypermission_manageid + replyReply result

NotificationsController

Method & RoutePermissionParamsReturns
GET /notificationspermissionNotifications list
PATCH /notifications/read-allpermissionMark-all result
PATCH /notifications/{id}permissionidUpdated notification

ProfileController & SettingsController

Method & RoutePermissionParamsReturns
GET /profilepermission_viewProfile payload
PUT /profilepermission_edit (edit-own-profile)Profile fieldsUpdated profile
GET /settingspermissionWorkspace settings
PUT /settingspermissionSettings fieldsUpdated 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 fails 401, 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 DnDSectionsDnDCardBuilderMetaCleanupPluginSettingsAPIPluginInfoAPIPriceOnCallTextAPI, 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_callback that checks a capability and, for writes, verifies the wp_rest nonce — mirror the built-in controllers. Never return __return_true from 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-Nonce on writes. Both namespaces re-verify the wp_rest nonce for state-changing requests.
  • Respect the layered Workspace gates. Do not assume a logged-in user can reach a property; the PropertyAccessGate may still reject them.
  • Prefer the group routes for settings. POST /plugin-settings/{group} is lighter and safer than save-all when you only touch one area.
  • Use the correct namespace. Admin tooling belongs on hvnlynab/v1; agent-facing UI belongs on hvnly/v1. They enforce different permission models.

Performance recommendations

  • Batch bootstrap. The SPA calls /me once and caches the permissions object; do the same in your own integrations rather than re-fetching identity per view.
  • Page your lists. GET /properties and GET /inquiries accept page and per_page; keep per_page modest to avoid heavy queries.
  • Scan before cleanup. meta-cleanup/final-cleanup runs a lighter scan when confirm is absent — use it to size the job before committing.
  • Use analytics ranges. Pass a tight range / date_from / date_to instead 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_options shortcut 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_map must enforce its own permissions.

Common mistakes

  • Sending manage_options-level credentials to hvnly/v1 and 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_group instead of a save.
  • Confirming final-cleanup without running the scan first.

Need more help?

Can't find what you're looking for? Our team and community are here to help you ship faster.