REST API Reference
Generated from the Jetonomy 1.8.0 (free) source by reading every register_rest_route() call 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.
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
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)
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)
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)
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
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
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
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
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
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 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
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", "..." : "..." }
Posts
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 }
]
}
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 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
Users
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:
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)
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)
Auth
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)
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 read
PATCH /notifications/{id} Mark one read
DELETE /notifications/{id} Delete one (scoped to me)
POST /notifications/bulk Bulk mark-read/delete ({action, ids})
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} Unsubscribe
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
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 (GET only — no write routes)
GET /leaderboards Reputation ranking (period=all|month|week)
GET/POST /bookmarks List mine / toggle a bookmark
DELETE /bookmarks/{post_id} Remove
Moderation
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.
Media
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
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)
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)
Private Messaging
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.
Reactions
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.
Polls
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.
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 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.
Custom Badges
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.
Webhooks
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.
White Label
GET/PATCH /settings/white-label Branding (manage_options)
Site Announcements
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.
Web Push
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
AI
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)
Advanced Moderation
GET/POST /moderation/rules List / create rules (manage_options)
PATCH/DELETE /moderation/rules/{id} Update / delete
GET /moderation/rules/{id}/stats Trigger stats
Email Digest
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)
SEO Pro
GET/PATCH /spaces/{id}/seo Per-space meta title/description/OG image (space admin)
Reply By Email
POST /reply-by-email/inbound Inbound email webhook — signature/token-verified, not a WP capability
Anonymous Posting
POST /anonymous/reveal Reveal the real author of an anonymous post/reply (manage_options, audit-logged)
Attachments — jetonomy-pro/v1 namespace
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.
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.