From redaxo-api-addon
Guides calling REDAXO FriendsOfRedaxo/api REST endpoints with Bearer auth, route tables for articles/categories/slices/modules/templates/languages/media/users, slice POST schema, OpenAPI spec, and 401/404/500 diagnostics. Covers trailing-slash quirks, auth messages, LIMIT bug, name-filter bug, and yrewrite cache invalidation.
npx claudepluginhub friendsofredaxo/claude-marketplace --plugin redaxo-api-addonThis skill uses the workspace's default tool permissions.
The `api` addon (Repo: `FriendsOfRedaxo/api`, Vendor-NS: `FriendsOfRedaxo\Api`) exposes a RESTful API for a REDAXO backend. Bearer-token auth, Symfony `UrlMatcher` routing, mounted at `/api/...` via `YREWRITE_PREPARE`.
Guides Next.js Cache Components and Partial Prerendering (PPR): 'use cache' directives, cacheLife(), cacheTag(), revalidateTag() for caching, invalidation, static/dynamic optimization. Auto-activates on cacheComponents: true.
Processes PDFs: extracts text/tables/images, merges/splits/rotates pages, adds watermarks, creates/fills forms, encrypts/decrypts, OCRs scans. Activates on PDF mentions or output requests.
Share bugs, ideas, or general feedback.
api Addon – Calling the APIThe api addon (Repo: FriendsOfRedaxo/api, Vendor-NS: FriendsOfRedaxo\Api) exposes a RESTful API for a REDAXO backend. Bearer-token auth, Symfony UrlMatcher routing, mounted at /api/... via YREWRITE_PREPARE.
Authorization: Bearer <token>BearerAuth — token-based frontend API (default for most routes)BackendUser — uses the REDAXO backend session (cookie); for routes that should only be called from the backend (e.g. backend media picker)AuthorizationApache strips Authorization in some configurations. If the token is correct but the server returns Authorization failed, add to .htaccess:
RewriteCond %{HTTP:Authorization} .
RewriteRule ^ - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
Authoritative list lives in the addon's README.md. Most-used routes:
| Method | Path | Scope (= route name) |
|---|---|---|
| GET | /api/structure/articles | structure/articles/list |
| POST | /api/structure/articles/ | structure/articles/add |
| DELETE | /api/structure/articles/{id} | structure/articles/delete |
| POST | /api/structure/articles/{id}/slices | structure/articles/slices/add |
| POST | /api/structure/categories/ | structure/categories/add |
| DELETE | /api/structure/categories/{id} | structure/categories/delete |
| GET/POST | /api/templates | templates/list / templates/add |
| GET/POST | /api/modules | modules/list / modules/add |
| GET/POST | /api/system/clangs | system/clangs/list / system/clangs/add |
| GET | /api/users | users/list |
| GET | /api/media | media/list |
| GET | /api/media/categories | media/category/list |
The addon registers structure/articles/list (GET) on 'structure/articles' (no slash) and structure/articles/add (POST) on 'structure/articles/' (with slash). Symfony's UrlMatcher is strict, so:
/api/structure/articles/api/structure/articles/The wrong variant returns {"error":"Route with method not found or no access"} (HTTP 401). That's not an auth problem — the matcher exception is wrapped generically into 401 by RouteCollection::handle(). Other endpoints have similar inconsistencies; trust the body schema in the source code over a first-guess URL.
| Response | Meaning |
|---|---|
{"error":"Authorization failed"} | Token is valid, scope is missing |
{"error":"Route with method not found or no access"} | Matcher found no route (or controller threw) |
For "Route with method not found": check path / method / trailing slash against the source in src/addons/api/lib/RoutePackage/*.php first, then suspect the token.
Authorization failed → extend the token's scope list.Route with method not found or no access → check path / method / trailing-slash against the source code, then tail var/log/system.log for controller PHP errors.structure/articles list — broken name filterIn Structure::handleArticleList:
if (null !== $Query['filter']['name']) { // default is '' not null → branch always runs
$SqlQueryWhere[':name'] = 'id LIKE :name'; // should be 'name LIKE'
$SqlParameters[':name'] = '%' . $Query['filter']['id'] . '%'; // 'id' doesn't exist in filter
}
Consequences:
Undefined array key "id" on every list callid LIKE '%%' — matches everything but the filter is broken'' === $Query['filter']['name'] instead, then LIKE %name% on nameLIMIT bindingrex_sql::factory()->getArray($sql, $params) binds parameters as strings by default. MySQL doesn't accept LIMIT 'x', 'y' in all configurations (especially with PDO::ATTR_EMULATE_PREPARES=false). The endpoint then fails with "Route with method not found" (= swallowed exception) even though it's an SQL error.
Workaround: cast (int)$start and (int)$per_page and inline them into the SQL string instead of using placeholders.
RouteCollection::handle() calls JSON_PRETTY_PRINT for the response, then rex_response::setStatus() sets headers. Some configurations emit bytes earlier → log warnings (not functionally broken).
A newly-created article/category may return 404 on the frontend even with status=1:
var/cache/addon/yrewrite/...rex_article_service::addArticle() triggers the right EPs, but yrewrite doesn't always regenerate its path cache (especially for new root categories)rex_yrewrite::generatePathFile([])The article add schema knows only base fields (name, category_id, priority, status, template_id). Custom metainfos (art_*, cat_*) are not accepted. Workaround: set them via a separate backend step or write SQL directly. PUT/PATCH on articles is in the README as planned but not implemented.
POST /api/media is marked ❌ in the README. Workaround: place media manually (or via migration) in the media pool, then reference filenames via media1..media10 when creating slices.
POST /api/structure/articles/{id}/slices body:
{
"module_id": <int, required>,
"clang_id": <int, required>,
"ctype_id": <int, default 1>,
"value1": "...", "value2": "...", ... "value19": "...",
"media1": "...", ... "media10": "...",
"medialist1":"...", ... "medialist10":"...",
"link1": "...", ... "link10": "...",
"linklist1":"...", ... "linklist10":"..."
}
One slice = one module. Which value* / media* slot maps to which editor field is defined in src/modules/<name> [id]/input.php as REX_INPUT_VALUE[n] / REX_MEDIA[id=n].
The addon validates before insert:
rex_article::get(...))rex_clang::getAllIds())rex_module)rex_template::hasModule(...)) — otherwise 404. If this check fails, look at the template's ctype/module assignment in the backend.Available in the backend at ?page=api/openapi. Generated from the route definitions via OpenAPIConfig. External consumers (Postman, OpenAPI generator): the JSON spec is only viewable when authenticated via backend login — Swagger UI renders client-side.
POST /api/structure/articles without the trailing slash – 401 "Route with method not found". The matcher cares.structure/article/add vs. structure/articles/add) – 401 "Authorization failed".Authorization header missing on Apache – add the RewriteRule above.