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.
Base URLs
Section titled “Base URLs”| 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.
Authentication
Section titled “Authentication”Jetonomy supports two authentication modes, both handled by \Jetonomy\API\REST_Auth::auth_mutation() (free) and its lazy Pro wrapper Extension::rest_auth_mutation():
1. Cookie + nonce (the web app)
Section titled “1. Cookie + nonce (the web app)”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/nonceA 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.
Account-status gates (both modes)
Section titled “Account-status gates (both modes)”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’sauthenticatefilter 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 onlyDELETE /users/me(a member must always be able to delete an account they created, verified or not).
Public / private community mode
Section titled “Public / private community mode”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).
Rate limiting
Section titled “Rate limiting”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.
Pagination
Section titled “Pagination”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-TotalandX-WP-TotalPagesheaders are set on most collection routes. - Cursor-based (
after/beforeon posts/replies/messages): the baseget_collection_params()schema declaresafter/beforealongside legacyoffset— cursor is preferred for large datasets sinceOFFSETon 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.
Error shape
Section titled “Error shape”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 } }Resources
Section titled “Resources”Spaces
Section titled “Spaces”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 CreateGET /spaces/{id} GetPATCH /spaces/{id} Update (space admin)DELETE /spaces/{id} Delete (space admin)GET /spaces/{id}/members List membersPOST /spaces/{id}/members JoinGET /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 ApprovePOST /spaces/{id}/join-requests/{request_id}/deny DenyPATCH /spaces/{id}/members/{user_id} Change role (space admin)DELETE /spaces/{id}/members/{user_id} Leave/removePOST /spaces/{id}/invite Generate invite link (space admin)GET /invite/{token} Accept inviteGET /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 (1.9.4)
Section titled “Access rules (1.9.4)”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()).
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:
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 CreateGET /posts/drafts My draftsGET /posts/{id} GetPATCH /posts/{id} Update / publish a draftDELETE /posts/{id} TrashPOST /posts/{id}/close Close/reopen to new repliesPOST /posts/{id}/pin Pin/unpin (capped per space)POST /posts/{id}/move Move to another spacePOST /posts/{id}/merge Merge into another postPOST /posts/{id}/idea-status Set roadmap status (Ideas spaces only)POST /posts/{id}/vote VoteDELETE /posts/{id}/vote Remove my voteGET /link-preview OG metadata for a URLExample 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 } ]}Replies
Section titled “Replies”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 CreatePATCH /replies/{id} Update (no GET on this route)DELETE /replies/{id} TrashPOST /replies/{id}/accept Mark accepted answer (Q&A spaces)DELETE /replies/{id}/accept Un-acceptPOST /replies/{id}/split Split into a new post (moderator)POST /replies/{id}/vote VoteDELETE /replies/{id}/vote Remove my voteGET /users/me My full profilePATCH /users/me Update my profileDELETE /users/me Delete my account (Apple 5.1.1(v) / GDPR Art. 17)GET /users/{id} Public profile by IDGET /users/by-login/{login} Public profile by usernameGET /users/{id}/posts A user's public postsGET /users/suggest @mention typeahead (login required)DELETE /users/me example:
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.
Blocks (member-to-member blocking)
Section titled “Blocks (member-to-member blocking)”GET /users/me/blocks List who I've blockedPOST /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 linkGET /auth/nonce Fresh wp_rest nonce for this sessionPOST /auth/resend-verification Resend the verification link (rate-limited)Notifications
Section titled “Notifications”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 readPATCH /notifications/{id} Mark one readDELETE /notifications/{id} Delete one (scoped to me)POST /notifications/bulk Bulk mark-read/delete ({action, ids})Subscriptions
Section titled “Subscriptions”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} UnsubscribeSearch
Section titled “Search”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.
Categories, Tags, Leaderboards, Bookmarks
Section titled “Categories, Tags, Leaderboards, Bookmarks”GET/POST /categories List (nested) / create (jetonomy_manage_categories)GET/PATCH/DELETE /categories/{id} Get / update / delete (jetonomy_manage_categories)GET /tags List post tagsPOST /tags Create a tag (jetonomy_manage_settings)PATCH/DELETE /tags/{id} Rename/re-slug, or delete and detach from every postGET /leaderboards Reputation ranking (period=all|month|week)GET/POST /bookmarks List mine / toggle a bookmarkDELETE /bookmarks/{post_id} RemoveModeration
Section titled “Moderation”GET /moderation/queue Pending+spam queue (jetonomy_moderate)POST /moderation/approve/{type}/{id} ApprovePOST /moderation/spam/{type}/{id} Mark spam (reputation penalty)POST /moderation/trash/{type}/{id} TrashGET/POST /flags List (mod) / report contentGET /moderation/flags List flags for moderationPOST /moderation/flags/{id}/resolve Resolve (valid trashes + rewards reporter)POST /moderation/bulk Bulk approve/spam/trashGET /posts/{id}/flags Flags on one postGET/POST /moderation/ban List / issue ban-silence-spacebanDELETE /moderation/ban/{id} Lift a restrictionGET /spaces/{id}/moderation/flags Space-scoped flag queue (space mod)POST /spaces/{id}/moderation/flags/{flag_id}/resolvePOST /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)Updates, OEmbed, Admin
Section titled “Updates, OEmbed, Admin”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)App / Feed (mobile app surface)
Section titled “App / Feed (mobile app surface)”GET /app/config Public branding + Pro feature flags (pre-login theming)GET /feed Global cross-space home feed (sort=hot|new|top)Pro Resources (jetonomy/v1 unless noted)
Section titled “Pro Resources (jetonomy/v1 unless noted)”Private Messaging
Section titled “Private Messaging”GET/POST /conversations List mine / start a new oneGET/PATCH /conversations/{id} Get / muteGET/POST /conversations/{id}/messages List / send (cursor: before)GET /conversations/unread-count Cached 30sPOST /conversations/{id}/mute Toggle mutePOST /conversations/{id}/archive Toggle archivePOST /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.
Reactions
Section titled “Reactions”GET/POST /posts/{id}/reactions Get counts / toggle ({emoji})GET/POST /replies/{id}/reactions Get counts / toggleSingle-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 voteDELETE /polls/{id}/vote Remove my voteOne 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.
Custom Fields
Section titled “Custom Fields”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 ReadPATCH /users/me/fields Write my ownField 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.
Custom Badges
Section titled “Custom Badges”GET/POST /badges List active (paginated) / create (manage_options)GET/PATCH/DELETE /badges/{id} Get (with earned_count) / update / deactivateGET /users/{id}/badges A user's earned badgesPOST/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.
Webhooks
Section titled “Webhooks”GET/POST /webhooks List / create (manage_options)PATCH/DELETE /webhooks/{id} Update / deletePOST /webhooks/{id}/test Send a test deliveryGET /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.
White Label
Section titled “White Label”GET/PATCH /settings/white-label Branding (manage_options)Site Announcements
Section titled “Site Announcements”GET /jetonomy/v1/announcements/active Public/app read of active pinsGET /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: unpinNote the namespace split — the public read stays on the shared jetonomy/v1 namespace; the admin write surface is on jetonomy-pro/v1.
Web Push
Section titled “Web Push”GET /push/vapid-key Public VAPID keyGET /push/service-worker.js Service worker scriptPOST/DELETE /push/subscribe Browser PushSubscriptionPOST/DELETE /push/register-device Native (Expo) push tokenGET /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)Advanced Moderation
Section titled “Advanced Moderation”GET/POST /moderation/rules List / create rules (manage_options)PATCH/DELETE /moderation/rules/{id} Update / deleteGET /moderation/rules/{id}/stats Trigger statsEmail Digest
Section titled “Email Digest”GET/PATCH /users/me/digest-preferences My frequency + subscribed typesPOST /admin/digest/test Send a test digest to me (manage_options)GET /admin/digest/stats Send statistics (manage_options)SEO Pro
Section titled “SEO Pro”GET/PATCH /spaces/{id}/seo Per-space meta title/description/OG image (space admin)Reply By Email
Section titled “Reply By Email”POST /reply-by-email/inbound Inbound email webhook — signature/token-verified, not a WP capabilityAnonymous Posting
Section titled “Anonymous Posting”POST /anonymous/reveal Reveal the real author of an anonymous post/reply (manage_options, audit-logged)Attachments — jetonomy-pro/v1 namespace
Section titled “Attachments — jetonomy-pro/v1 namespace”POST /attachments Link an uploaded WP media item to a post/replyDELETE /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 pageThe 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.
Changelog note (1.8.0)
Section titled “Changelog note (1.8.0)”- Post and reply REST payloads now always carry an
attachmentsarray (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 /subscriptionsnow returnstitle/slug/existsfor each subscribed target.GET /searchgained reallimit/offsetpagination andX-WP-Total/X-WP-TotalPagesheaders (previously hardcoded per-type limits).
See openapi.json for the exact parameter/response contract of every route above.

