Skip to content

Jetonomy REST API Reference

Generated by reading every register_rest_route() call in the Jetonomy (free) source in includes/api/*.php, and from Jetonomy Pro’s includes/extensions/*/class-*.php. This is the human-readable companion to openapi.json — the OpenAPI 3.1 file is the authoritative, machine-readable contract (exact schemas, every parameter’s type/enum/default). Load openapi.json into Swagger UI or Redoc for an interactive browser. First generated at 1.8.0 and extended since; the documented route surface is current to 1.9.4.

Namespace Base URL Used by
jetonomy/v1 https://your-site.com/wp-json/jetonomy/v1 Free core, and most Pro extensions (Private Messaging, Analytics, Polls, Reactions, Custom Fields, Custom Badges, Webhooks, White Label, Web Push, AI, Advanced Moderation, Email Digest, SEO Pro, Reply By Email, Anonymous Posting, and the public GET /announcements/active read)
jetonomy-pro/v1 https://your-site.com/wp-json/jetonomy-pro/v1 Pro’s Attachments extension (/attachments*), and the Site Announcements admin/management routes (/site-announcements, /site-announcements/{id})

The jetonomy-pro/v1 split is not documented anywhere in the plugin’s own manifest notes — it was found by reading includes/extensions/attachments/class-rest.php and includes/extensions/site-announcements/class-extension.php directly. Every other Pro extension registers into the shared jetonomy/v1 namespace.

Jetonomy supports two authentication modes, both handled by \Jetonomy\API\REST_Auth::auth_mutation() (free) and its lazy Pro wrapper Extension::rest_auth_mutation():

The browser holds the WordPress logged_in session cookie. Every mutating request (POST/PATCH/DELETE) additionally requires an X-WP-Nonce header carrying a valid wp_rest nonce:

X-WP-Nonce: <nonce>

Get one from wp_create_nonce('wp_rest') server-side (localized into the page on load), or refresh it client-side via:

GET /wp-json/jetonomy/v1/auth/nonce

A missing/invalid nonce on a cookie-authenticated request returns 403 rest_cookie_invalid_nonce. Cookie-authenticated requests are detected by the presence of $_COOKIE and the absence of an Authorization header — see the next section.

2. Application Passwords (the mobile app + external integrations)

Section titled “2. Application Passwords (the mobile app + external integrations)”

WordPress core Application Passwords sent as HTTP Basic auth:

Authorization: Basic base64(username:application_password)

Header-authenticated requests skip the cookie-nonce check entirely (Application Passwords authenticate per-request and don’t carry a WP session). They still go through the same login, ban/silence, and pending-email-verification gates as cookie auth — REST_Auth::auth_mutation() is the single choke point for both.

On every mutation route, auth_mutation() also enforces:

  • Banned accounts are rejected (403 jetonomy_user_banned) — closes an Application Password bypass that existed before 1.6.0, where a banned member’s authenticate filter never ran for header-authed requests.
  • Pending-email-verification accounts are rejected (403 jetonomy_pending_verification) unless the route explicitly opts out (allow_unverified) — currently only DELETE /users/me (a member must always be able to delete an account they created, verified or not).

Independent of the above, Settings → General → Community Access toggles the whole community between public (default — anyone can read) and private (every read requires login). This is enforced by \Jetonomy\Visibility::rest_check() and wraps almost every GET route’s permission_callback. In private mode, a logged-out GET returns 401 community_private instead of the resource. /auth/* and most /moderation/*//admin/* routes are exempt (they have their own gates and must never lock users out).

POST /auth/login, /auth/register, /auth/lost-password, /auth/resend-verification, and DELETE /users/me are all IP-bucketed via a shared Base_Controller::check_rate_limit() transient (login: 5/min; register: 5/min; lost-password & resend-verification: 3/5min; delete-account: 5/hour). Exceeded limits return 429 jetonomy_rate_limited.

Two shapes appear across the API, both bounded (no unbounded SELECT *):

  • Offset-based (limit + offset, most list endpoints): response body is { "data": [...], "meta": { "count", "has_more", "cursor_next", "total", "offset" } }. X-WP-Total and X-WP-TotalPages headers are set on most collection routes.
  • Cursor-based (after/before on posts/replies/messages): the base get_collection_params() schema declares after/before alongside legacy offset — cursor is preferred for large datasets since OFFSET on a 100k-row table degrades linearly.

Default limit is usually 20 (search defaults 20, capped 50; leaderboards default 20, capped 100; media default 24). Always check the specific endpoint’s args schema in openapi.json for the real min/max/default.

Every error is a standard WP_Error-shaped JSON body:

{ "code": "jetonomy_forbidden", "message": "You do not have permission to perform this action.", "data": { "status": 403 } }

Sub-communities. POST /spaces requires manage_options OR jetonomy_create_spaces OR a WP role the admin opted into (Settings → General → frontend_space_creation_roles). Space-level writes (PATCH/DELETE on a space, member-role changes, invites) require the caller to hold the admin per-space role (independent of WP capabilities) or manage_options.

GET /spaces List (visibility-filtered)
POST /spaces Create
GET /spaces/{id} Get
PATCH /spaces/{id} Update (space admin)
DELETE /spaces/{id} Delete (space admin)
GET /spaces/{id}/members List members
POST /spaces/{id}/members Join
GET /spaces/{id}/privileged-members Admins/moderators (public read)
GET /spaces/{id}/join-requests Pending requests (space mod)
POST /spaces/{id}/join-requests/{request_id}/approve Approve
POST /spaces/{id}/join-requests/{request_id}/deny Deny
PATCH /spaces/{id}/members/{user_id} Change role (space admin)
DELETE /spaces/{id}/members/{user_id} Leave/remove
POST /spaces/{id}/invite Generate invite link (space admin)
GET /invite/{token} Accept invite
GET /spaces/{id}/access-rules List access rules (jetonomy_manage_spaces)
POST /spaces/{id}/access-rules Create an access rule (jetonomy_manage_spaces)
DELETE /access-rules/{id} Delete an access rule (jetonomy_manage_spaces)

Access rules decide who may enter a space. Until 1.9.4 they were configurable only from wp-admin, so an API-driven or app-based space-admin flow could not touch them.

rule_type is one of membership, role, capability, trust_level, logged_in, everyone. grants is read, participate or full, and space_role (viewer, member, moderator, admin) is the roster role a matching member receives.

A restrictive rule on a PUBLIC space switches that space to Private. Public spaces are always readable, and the permission engine stops non-members on private spaces before rules are consulted — so a membership / role / capability / trust_level rule attached to a public space would silently do nothing. Rather than accept a gate that does not gate, the space is made private and the response says so via made_private. The wp-admin path has always behaved this way; both surfaces now share one implementation (AccessRule::enforce_gate_on_public_space()).

Terminal window
curl -X POST https://your-site.com/wp-json/jetonomy/v1/spaces/42/access-rules \
-H "Content-Type: application/json" -H "X-WP-Nonce: $NONCE" --cookie "$COOKIES" \
-d '{"rule_type":"trust_level","rule_value":"2","grants":"participate","space_role":"member"}'
{
"data": { "id": 7, "space_id": 42, "rule_type": "trust_level", "rule_value": "2", "grants": "participate", "space_role": "member", "priority": 0 },
"made_private": true,
"message": "Access rule added. This space was switched to Private so the rule can restrict access — a rule cannot gate a public space."
}

Example — create a space:

Terminal window
curl -X POST https://your-site.com/wp-json/jetonomy/v1/spaces \
-H "Content-Type: application/json" -H "X-WP-Nonce: $NONCE" --cookie "$COOKIES" \
-d '{"title":"Product Feedback","type":"ideas","visibility":"public","join_policy":"open"}'
{ "id": 42, "title": "Product Feedback", "slug": "product-feedback", "type": "ideas", "visibility": "public", "join_policy": "open", "member_count": 1, "post_count": 0, "is_member": true, "viewer_role": "admin", "..." : "..." }

Topics/questions/ideas/status-updates, always nested under a space for create/list. 1.8.0 adds an attachments array to every post payload (free, whether or not Pro is active — see below).

GET /spaces/{space_id}/posts List (space-scoped, sort=latest|popular|oldest|newest|unanswered)
POST /spaces/{space_id}/posts Create
GET /posts/drafts My drafts
GET /posts/{id} Get
PATCH /posts/{id} Update / publish a draft
DELETE /posts/{id} Trash
POST /posts/{id}/close Close/reopen to new replies
POST /posts/{id}/pin Pin/unpin (capped per space)
POST /posts/{id}/move Move to another space
POST /posts/{id}/merge Merge into another post
POST /posts/{id}/idea-status Set roadmap status (Ideas spaces only)
POST /posts/{id}/vote Vote
DELETE /posts/{id}/vote Remove my vote
GET /link-preview OG metadata for a URL

Example response fragment (GET /posts/{id}), showing the 1.8.0 attachments shape:

{
"id": 501, "title": "How do I configure SSO?", "type": "question", "status": "publish",
"content": "<p>...</p>", "viewer_vote": 0, "is_bookmarked": false,
"attachments": [
{ "id": 88, "link_id": 12, "url": "https://.../screenshot.png", "thumb": "https://.../screenshot-300x200.png",
"mime": "image/png", "name": "screenshot.png", "size": 84213, "type": "image", "ext": "PNG", "is_image": true }
]
}

Threaded, up to 3 levels via parent_id, nested under a post for create/list.

GET /posts/{post_id}/replies List (sort=oldest|newest|best)
POST /posts/{post_id}/replies Create
PATCH /replies/{id} Update (no GET on this route)
DELETE /replies/{id} Trash
POST /replies/{id}/accept Mark accepted answer (Q&A spaces)
DELETE /replies/{id}/accept Un-accept
POST /replies/{id}/split Split into a new post (moderator)
POST /replies/{id}/vote Vote
DELETE /replies/{id}/vote Remove my vote
GET /users/me My full profile
PATCH /users/me Update my profile
DELETE /users/me Delete my account (Apple 5.1.1(v) / GDPR Art. 17)
GET /users/{id} Public profile by ID
GET /users/by-login/{login} Public profile by username
GET /users/{id}/posts A user's public posts
GET /users/suggest @mention typeahead (login required)

DELETE /users/me example:

Terminal window
curl -X DELETE https://your-site.com/wp-json/jetonomy/v1/users/me \
-H "Content-Type: application/json" -H "X-WP-Nonce: $NONCE" --cookie "$COOKIES" \
-d '{"password":"correct horse battery staple","confirm":"DELETE","delete_content":false}'
{ "deleted": true, "user_id": 88, "content_policy": "anonymized" }

By default content is anonymized (author reassigned to a 0/tombstone; existing replies still render, showing [deleted]). Pass delete_content: true to hard-delete every post/reply the member authored instead.

GET /users/me/blocks List who I've blocked
POST /users/me/blocks Block a user ({user_id})
DELETE /users/me/blocks/{user_id} Unblock (idempotent)
POST /auth/login Sign in (rate-limited)
POST /auth/register Create an account (rate-limited, anti-spam)
POST /auth/lost-password Request a reset email (rate-limited)
GET /auth/verify-email Consume an email verification link
GET /auth/nonce Fresh wp_rest nonce for this session
POST /auth/resend-verification Resend the verification link (rate-limited)
GET /notifications List (filter=all|unread|mentions|replies|votes|badges)
GET /notifications/unread-count Unread count (cached 15s)
POST /notifications/mark-all-read Mark all read
PATCH /notifications/{id} Mark one read
DELETE /notifications/{id} Delete one (scoped to me)
POST /notifications/bulk Bulk mark-read/delete ({action, ids})
GET /subscriptions List mine (now includes title/slug/exists of the target, 1.8.0)
POST /subscriptions Subscribe to a space or post ({object_type, object_id, via})
DELETE /subscriptions/{id} Unsubscribe
GET /search?q=...&type=post|reply|space|tag|all&sort=relevance|newest|votes&limit=&offset=

MySQL FULLTEXT boolean-mode search. type=all returns {posts, spaces, tags} grouped, with limit/offset applying to the posts group only. 1.7.1 added real limit/offset pagination (previously hardcoded to 20/10 per type) plus X-WP-Total/X-WP-TotalPages headers.

GET/POST /categories List (nested) / create (jetonomy_manage_categories)
GET/PATCH/DELETE /categories/{id} Get / update / delete (jetonomy_manage_categories)
GET /tags List post tags
POST /tags Create a tag (jetonomy_manage_settings)
PATCH/DELETE /tags/{id} Rename/re-slug, or delete and detach from every post
GET /leaderboards Reputation ranking (period=all|month|week)
GET/POST /bookmarks List mine / toggle a bookmark
DELETE /bookmarks/{post_id} Remove
GET /moderation/queue Pending+spam queue (jetonomy_moderate)
POST /moderation/approve/{type}/{id} Approve
POST /moderation/spam/{type}/{id} Mark spam (reputation penalty)
POST /moderation/trash/{type}/{id} Trash
GET/POST /flags List (mod) / report content
GET /moderation/flags List flags for moderation
POST /moderation/flags/{id}/resolve Resolve (valid trashes + rewards reporter)
POST /moderation/bulk Bulk approve/spam/trash
GET /posts/{id}/flags Flags on one post
GET/POST /moderation/ban List / issue ban-silence-spaceban
DELETE /moderation/ban/{id} Lift a restriction
GET /spaces/{id}/moderation/flags Space-scoped flag queue (space mod)
POST /spaces/{id}/moderation/flags/{flag_id}/resolve
POST /spaces/{id}/moderation/{approve|spam|trash}/{type}/{obj_id}

Banning has three built-in integrity guards: nobody can restrict themselves, nobody can restrict a site administrator, and a moderator (not an admin) cannot restrict another moderator.

POST /media Upload an image (multipart, upload_files|jetonomy_upload_media|jetonomy_create_posts|jetonomy_create_replies)
GET /media List community media library uploads (jetonomy_manage_settings)
GET /updates Poll for activity since a timestamp (scope=global|space|post)
GET /oembed oEmbed 1.0 JSON for a forum thread URL (Slack/X/Discord unfurl)
POST /admin/recount Rebuild counters (manage_options)
POST /admin/users/trust-level Bulk-set trust level (manage_options)
GET /app/config Public branding + Pro feature flags (pre-login theming)
GET /feed Global cross-space home feed (sort=hot|new|top)

GET/POST /conversations List mine / start a new one
GET/PATCH /conversations/{id} Get / mute
GET/POST /conversations/{id}/messages List / send (cursor: before)
GET /conversations/unread-count Cached 30s
POST /conversations/{id}/mute Toggle mute
POST /conversations/{id}/archive Toggle archive
POST /conversations/{id}/leave Leave (group)
POST /conversations/{id}/block Block the other side (direct)
GET /messaging/recipient-suggestions Typeahead (shared-space scoped)

Requires trust level ≥ 1 to send (site admins/moderators bypass). A new direct message additionally requires the sender and recipient to share at least one space, and neither side may have blocked the other.

GET/POST /posts/{id}/reactions Get counts / toggle ({emoji})
GET/POST /replies/{id}/reactions Get counts / toggle

Single-reaction-per-user model — re-adding a different emoji swaps the prior one. POST requires jetonomy_vote.

GET/POST /posts/{post_id}/poll Get / create ({question, type, options[], closes_at})
PATCH /polls/{id} Close/reopen (poll creator or moderator)
POST /polls/{id}/vote Cast/switch vote
DELETE /polls/{id}/vote Remove my vote

One poll per post (409 on a second create). Multiple-choice POST /polls/{id}/vote treats option_ids as the desired final set (idempotent reconcile), not a per-option toggle.

GET/POST /fields List (public) / create (manage_options)
PATCH/DELETE /fields/{id} Update / deactivate (manage_options)
GET/PATCH /posts/{id}/fields Read (public) / write (author/mod)
GET /users/{id}/fields Read
PATCH /users/me/fields Write my own

Field types: text, textarea, number, email, url, select, checkbox, radio, date. Contexts: post, profile, space. Write endpoints accept a raw JSON object of {slug: value} and validate/sanitize per field type; invalid values are reported per-field in a 400 without blocking the rest of the save.

GET/POST /badges List active (paginated) / create (manage_options)
GET/PATCH/DELETE /badges/{id} Get (with earned_count) / update / deactivate
GET /users/{id}/badges A user's earned badges
POST/DELETE /badges/{id}/award Manual award / revoke ({user_id})

Auto-award runs on an event-driven queue (post/reply/vote/reputation/trust-level changes) plus a 6-hour cron sweep. Criteria: 8 metrics (post_count, reply_count, reputation, trust_level, vote_received, days_active, accepted_answers, spaces_joined) × 5 operators × all/any match mode.

GET/POST /webhooks List / create (manage_options)
PATCH/DELETE /webhooks/{id} Update / delete
POST /webhooks/{id}/test Send a test delivery
GET /webhooks/{id}/deliveries Paginated delivery log (30-day retention)

13 event slugs: post.created/updated/deleted, reply.created/updated/deleted, user.registered, user.trust_level_changed, vote.cast, flag.created/resolved, space.member_joined/left. Payloads are HMAC-signed (X-Jetonomy-Signature: sha256=...), event name in X-Jetonomy-Event. Auto-disabled after 5 consecutive delivery failures.

GET/PATCH /settings/white-label Branding (manage_options)
GET /jetonomy/v1/announcements/active Public/app read of active pins
GET /jetonomy-pro/v1/site-announcements Admin: list pinned post IDs (manage_options)
POST /jetonomy-pro/v1/site-announcements/{id} Admin: pin a post (capped at 5)
DELETE /jetonomy-pro/v1/site-announcements/{id} Admin: unpin

Note the namespace split — the public read stays on the shared jetonomy/v1 namespace; the admin write surface is on jetonomy-pro/v1.

GET /push/vapid-key Public VAPID key
GET /push/service-worker.js Service worker script
POST/DELETE /push/subscribe Browser PushSubscription
POST/DELETE /push/register-device Native (Expo) push token
GET /ai/usage Usage stats (manage_options)
GET /ai/usage/summary By-provider summary (manage_options)
POST /ai/suggest-reply Suggest a reply draft ({post_id}, logged in + can read the post)
GET/POST /moderation/rules List / create rules (manage_options)
PATCH/DELETE /moderation/rules/{id} Update / delete
GET /moderation/rules/{id}/stats Trigger stats
GET/PATCH /users/me/digest-preferences My frequency + subscribed types
POST /admin/digest/test Send a test digest to me (manage_options)
GET /admin/digest/stats Send statistics (manage_options)
GET/PATCH /spaces/{id}/seo Per-space meta title/description/OG image (space admin)
POST /reply-by-email/inbound Inbound email webhook — signature/token-verified, not a WP capability
POST /anonymous/reveal Reveal the real author of an anonymous post/reply (manage_options, audit-logged)
POST /attachments Link an uploaded WP media item to a post/reply
DELETE /attachments/{id} Detach a link (id = link_id, not the WP media ID)
GET /attachments/{id}/download Force-download (Content-Disposition, public read following the parent's visibility)
GET /attachments/batch Batch read for an admin content-view page

The attachment link storage and REST payload injection are free as of 1.7.1/1.8.0 — Jetonomy\Models\Attachment and the attachments array on Post/Reply responses work with or without Pro. Pro’s attachments extension adds the upload composer UI, size/type policy, and the /jetonomy-pro/v1/attachments* link/detach/download management routes.


  • Post and reply REST payloads now always carry an attachments array (free).
  • POST /users/me/blocks, DELETE /users/me/blocks/{user_id} — block/report a member.
  • DELETE /users/me — in-app account deletion.
  • Object cache is invalidated immediately on space/profile/membership writes.
  • GET /subscriptions now returns title/slug/exists for each subscribed target.
  • GET /search gained real limit/offset pagination and X-WP-Total/X-WP-TotalPages headers (previously hardcoded per-type limits).

See openapi.json for the exact parameter/response contract of every route above.