Skip to content
Reactions
Reactions
Universal reactions for MODX 3 — likes, GitHub-style sets, tops and trending on any object
  1. Extras
  2. Reactions
  3. REST API

REST API

Base URL:

text
https://your-site/assets/components/reactions/api.php

Entry point file: assets/components/reactions/api.php (loads bootstrap and calls Reactions\Api\Router).

Routing

The action comes from the action parameter or PATH_INFO:

http
GET  /assets/components/reactions/api.php?action=counts
GET  /assets/components/reactions/api.php/counts
POST /assets/components/reactions/api.php?action=react
GET  /assets/components/reactions/api.php?action=admin/types

Nested admin resources: action=admin/types, admin/sets, admin/bans, admin/stats.

The HTTP method comes from REQUEST_METHOD. Clients without DELETE can emulate it:

  • POST + body field _method=DELETE, or
  • POST + $_POST['_method']=DELETE.

Response format

All responses are JSON (JSON_UNESCAPED_UNICODE). The success field is always present.

Success (top-level fields depend on the endpoint):

json
{
  "success": true,
  "data": { },
  "csrf": "…"
}

Error:

json
{
  "success": false,
  "error": "Human-readable description",
  "code": "error_code"
}

Error codes and HTTP statuses

codeHTTPWhen
validation_error400Missing required fields (class_key, object_id, type…)
bad_request400Empty action
not_found404Unknown action / object / type / set
method_not_allowed405Wrong HTTP method for the endpoint
forbidden403CSRF/Origin/nonce, ban, bot, type outside set/full_types, cancelled by plugin
auth_required401Admin without reactions_manage. auth_only strategy for a guest
rate_limit429Exceeded reactions_rate_limit
internal_error500Uncaught exception (details in error.log)
save_failed500Admin: failed to save the object

Public endpoint overview

MethodactionDescriptionCSRF
GETcsrfIssue a CSRF tokenno
GETcountsCounters + current visitor reactionsno
POSTreactAdd / toggle a reactionyes
DELETEreactRemove a reactionyes
GETtopTop by likes / rating / totalno
GETtrendingTop by trending_scoreno
GETlatestFeed of latest reactionsno

GET endpoints are cached on a CDN only if you configure that yourself. The counts response depends on cookie/session (user_reaction) — do not cache it publicly.


GET csrf

Returns a token bound to the PHP session ($_SESSION['reactions_csrf']). A new call overwrites the previous token.

bash
curl -c cookies.txt \
  "https://example.com/assets/components/reactions/api.php?action=csrf"

Response:

json
{
  "success": true,
  "csrf": "a1b2c3d4e5f6…"
}

Then pass the session cookie (-b cookies.txt) on every request that needs the same CSRF.

The JS widget makes this request itself when data-csrf is empty.


GET counts

Counters for an object and the list of reaction types for the current visitor (by fingerprint / user_id).

ParameterRequiredDescription
class_keyyesObject class (modResource, msProduct, TicketComment…)
object_idyesObject ID (> 0)
contextnoContext. Default web
bash
curl -b cookies.txt \
  "https://example.com/assets/components/reactions/api.php?action=counts&class_key=modResource&object_id=1&context=web"

Response:

json
{
  "success": true,
  "data": {
    "class_key": "modResource",
    "object_id": 1,
    "context": "web",
    "counts": {
      "like": 42,
      "dislike": 3,
      "love": 7
    },
    "total": 52,
    "user_reaction": ["like"]
  }
}
  • counts — associative array of type name → number.
  • total — sum of all types in the aggregate.
  • user_reaction — array of type names already set by the current visitor (may be empty or hold several names when multi is allowed).

POST react

Add a reaction. A repeated POST of the same type usually removes it (toggle). In exclusive mode (updown or !allow_multiple) another type replaces the previous one (action=changed).

Body (JSON)

FieldRequiredDescription
csrfyesToken from GET csrf
nonceyesOne-time string (32 hex recommended). TTL 300 s, at most ~50 active in the session
class_keyyesObject class
object_idyesID
typeyesType name (like, love, fire…)
contextnoDefault web
setnoSet key for type validation. Empty → reactions_default_set

Server checks (order)

  1. Origin / Referer matches the host from site_url (fail-closed: no headers → 403).
  2. CSRF matches the session.
  3. Nonce has not been used yet.
  4. Identity + anti-bot + ban + rate limit.
  5. Object exists: a short class_key (e.g. msProduct) resolves to FQCN / STI, then a check by object_id.
  6. Type is active and belongs to set set.
  7. For set=full: type is in reactions_full_types when that setting is not empty.
  8. OnBeforeReaction (can cancel).

Example

bash
CSRF=$(curl -s -c cookies.txt -b cookies.txt \
  "https://example.com/assets/components/reactions/api.php?action=csrf" \
  | jq -r '.csrf')

NONCE=$(openssl rand -hex 16)

curl -s -b cookies.txt -c cookies.txt \
  -X POST "https://example.com/assets/components/reactions/api.php?action=react" \
  -H "Content-Type: application/json" \
  -H "Origin: https://example.com" \
  -d "{
    \"csrf\": \"$CSRF\",
    \"nonce\": \"$NONCE\",
    \"class_key\": \"modResource\",
    \"object_id\": 1,
    \"type\": \"like\",
    \"context\": \"web\",
    \"set\": \"github\"
  }"

Response:

json
{
  "success": true,
  "data": {
    "action": "added",
    "counts": { "like": 43, "dislike": 3 },
    "total": 46,
    "user_reaction": ["like"],
    "type": "like"
  }
}
actionMeaning
addedNew reaction
removedRepeated click / removal
changedType change in exclusive mode

Set full and type subsets

json
{ "set": "full", "type": "beer", ... }

If reactions_full_types=like,love,fire and the client sends type=beer403 forbidden, even when the type exists in the DB set full.

The snippet parameter &types= narrows UI only (buttons / data-types). The API for set=full also filters by reactions_full_types. A type outside the UI but allowed by the API can still be sent via a manual POST. For a hard ban use OnBeforeReaction or narrow the set in the manager/CLI.


DELETE react

Remove the given type. Body fields match POST (csrf, nonce, class_key, object_id, type, context, set).

bash
curl -s -b cookies.txt \
  -X DELETE "https://example.com/assets/components/reactions/api.php?action=react" \
  -H "Content-Type: application/json" \
  -H "Origin: https://example.com" \
  -d "{
    \"csrf\": \"$CSRF\",
    \"nonce\": \"$NONCE\",
    \"class_key\": \"modResource\",
    \"object_id\": 1,
    \"type\": \"like\",
    \"context\": \"web\",
    \"set\": \"github\"
  }"

In the response action is usually removed. After removal the package still fires events, recalculates the aggregate, and (when enabled) the webhook. Author notification is not sent for DELETE.


GET top

Top objects by aggregates (or a recount over raw reactions for a period when periodall).

ParameterDefaultDescription
class_keymodResourceObject class
sortlikeslikes, rating, total
periodallday, week, month, year, all
context(empty)Context filter. Empty means all
limit201–100
offset0Offset
bash
curl "https://example.com/assets/components/reactions/api.php?action=top&class_key=modResource&sort=likes&period=week&limit=10"

Response:

json
{
  "success": true,
  "data": {
    "items": [
      {
        "class_key": "modResource",
        "object_id": 42,
        "context": "web",
        "counts": { "like": 120, "dislike": 5 },
        "total": 125,
        "likes": 120,
        "dislikes": 5,
        "rating": 115,
        "trending_score": 2.079181
      }
    ],
    "limit": 10,
    "offset": 0,
    "total": 10
  }
}

Here data.total is the number of items on the current result page (not the full table count).


Sort by trending_score (Reddit hot formula). The period parameter does not change the window for trending — the package uses the current score in the aggregate.

ParameterDefaultDescription
class_keymodResourceClass
context(empty)Filter
limit201–100
offset0Offset
bash
curl "https://example.com/assets/components/reactions/api.php?action=trending&class_key=msProduct&limit=5"

Response shape matches top (list of items with metrics).


GET latest

Feed of latest reactions (raw rows, not aggregates). The public response does not include user_id.

ParameterDescription
class_keyOptional filter
contextOptional filter
limit1–100, default 20
offsetOffset
bash
curl "https://example.com/assets/components/reactions/api.php?action=latest&class_key=modResource&limit=10"

Response:

json
{
  "success": true,
  "data": {
    "items": [
      {
        "id": 501,
        "class_key": "modResource",
        "object_id": 42,
        "context": "web",
        "type": "like",
        "emoji": "👍",
        "created_at": 1710000000
      }
    ],
    "limit": 10,
    "offset": 0,
    "total": 10
  }
}

created_at is a Unix timestamp. total is the current page size.


Admin API

Access

Requires an authenticated manager user (or the front end with the same session) with the reactions_manage policy.

Without permission → 401 / auth_required.

Admin mutations (POST/DELETE) also require Origin + CSRF + nonce, same as react (see guardMutation).

Resources

MethodactionDescription
GET/POST/DELETEadmin/typesReaction types
GET/POST/DELETEadmin/setsSets and type bindings
GET/POST/DELETEadmin/bansIP / user bans
GETadmin/statsSummary

GET admin/types

bash
curl -b mgr-cookies.txt \
  "https://example.com/assets/components/reactions/api.php?action=admin/types"
json
{
  "success": true,
  "data": {
    "items": [
      {
        "id": 1,
        "name": "like",
        "emoji": "👍",
        "icon": null,
        "ordering": 10,
        "active": true
      }
    ]
  }
}

POST admin/types

Create or update. Fields: id (for update), name, emoji, icon, ordering, active.

bash
curl -b mgr-cookies.txt \
  -X POST "https://example.com/assets/components/reactions/api.php?action=admin/types" \
  -H "Content-Type: application/json" \
  -H "Origin: https://example.com" \
  -d '{
    "csrf": "'"$CSRF"'",
    "nonce": "'"$NONCE"'",
    "name": "favorite",
    "emoji": "⭐",
    "ordering": 90,
    "active": true
  }'

Update:

json
{ "id": 9, "emoji": "🌟", "active": false, "csrf": "…", "nonce": "…" }

DELETE admin/types

bash
curl -b mgr-cookies.txt \
  -X DELETE "https://example.com/assets/components/reactions/api.php?action=admin/types" \
  -H "Content-Type: application/json" \
  -H "Origin: https://example.com" \
  -d '{"id":9,"csrf":"…","nonce":"…"}'

You can also pass id as a query parameter.

GET admin/sets

List of sets with bound types.

POST admin/sets

Fields: id, key, title, exclusive, active, types (array of names or IDs).

bash
curl -b mgr-cookies.txt \
  -X POST "https://example.com/assets/components/reactions/api.php?action=admin/sets" \
  -H "Content-Type: application/json" \
  -H "Origin: https://example.com" \
  -d '{
    "csrf": "'"$CSRF"'",
    "nonce": "'"$NONCE"'",
    "key": "social",
    "title": "Social",
    "exclusive": false,
    "active": true,
    "types": ["like", "love", "funny"]
  }'

exclusive=true — mutually exclusive types (like updown). Multi on the front still needs reactions_allow_multiple=Yes.

DELETE admin/sets

Body: { "id": 3, "csrf": "…", "nonce": "…" }.

GET / POST / DELETE admin/bans

Ban by IP (hashed on the server) or user_id.

bash
curl -b mgr-cookies.txt \
  -X POST "https://example.com/assets/components/reactions/api.php?action=admin/bans" \
  -H "Content-Type: application/json" \
  -H "Origin: https://example.com" \
  -d '{
    "csrf": "'"$CSRF"'",
    "nonce": "'"$NONCE"'",
    "ip": "203.0.113.10",
    "reason": "spam",
    "expires_at": 1711000000
  }'

Or: { "user_id": 42, "reason": "abuse", "csrf": "…", "nonce": "…" }.

expires_at is a Unix timestamp. Omit it for a permanent ban.

GET admin/stats

bash
curl -b mgr-cookies.txt \
  "https://example.com/assets/components/reactions/api.php?action=admin/stats"
json
{
  "success": true,
  "data": {
    "totals": {
      "reactions": 1500,
      "aggregates": 320,
      "types": 8,
      "bans": 2,
      "today": 45
    },
    "top_liked": [
      {
        "class_key": "modResource",
        "object_id": 12,
        "context": "web",
        "likes": 90,
        "total": 100,
        "trending_score": 1.5
      }
    ],
    "top_trending": [ ]
  }
}

today — reactions with created_at from the start of the current server day. Tops — up to 10 rows.


Security

MechanismDetails
CSRFSession token. Required for POST/DELETE (public react and admin mutations)
NonceOne-time, TTL 300 s. Replay protection
OriginCompare host of Origin or Referer with host of site_url. No headers → reject
Rate limitreactions_rate_limit / reactions_rate_limit_window per fingerprint
Botsreactions_block_bots + User-Agent detector
BansReactionBan table by IP-hash / user_id
PermissionsAdmin only with reactions_manage

Widget requests use credentials: 'same-origin' — the session cookie is sent automatically.

For CDN-hosted reactions.js, set the API explicitly (data-api or window.Reactions.config.api on the same origin as the site).


  • JS widget — how the client calls the API
  • Events — hooks around react / unreact
  • Webhooks — outbound HTTP after a change
  • CLI — the same admin work without HTTP