Joomla Web Services API
|
This content was generated with the assistance of AI and should be verified against the official Joomla documentation before being relied on in production. This sub-section’s Joomla bibliography lists the reference material consulted while preparing these pages. |
Since Joomla 4.0 every Joomla site ships a second front door next to the website and the administrator: the API
application. It answers HTTP requests under /api/index.php/v1/... with machine-readable JSON, so that a mobile app,
a static-site generator, a build script or another server can read and change the site’s content without scraping
HTML or driving the administrator forms. The same endpoints are what a headless front end uses (see
Headless Joomla) and what Joomla.org uses to trigger Automated Core Updates on a
site (see Updating Joomla and Automated Core Updates).
This page explains the application, the request lifecycle, the JSON:API format, authentication with API tokens, the
core endpoints, filtering and pagination, and the everyday operations on articles with curl. Adding endpoints for your
own component is covered in
Extending the Web Services API.
It was written against Joomla 6.2 (current; the 6.2.1 source tree was read, PHP 8.3 or newer) and checked against the
Joomla Developer Manual and the source on 2026-10-30. Joomla 5.4 receives security fixes only, and 4.x and 3.x are end
of life; the Web Services API does not exist in Joomla 3, and differences in 4.x and 5.x are mentioned where they
matter. The examples use the fictitious Tallpine Hiking Club (package pkg_tallpine, component com_tallpine,
plugin plg_webservices_tallpine) at https://tallpine.example.
What the API is for, and who uses it
-
A site builder or administrator decides who may use the API, issues tokens and enables or disables the authentication and
webservicesplugins. -
A front-end developer reads articles, categories, tags and media from a separate application (Astro, Next.js, a mobile app) and sometimes writes back.
-
A developer of integrations automates content imports, publishes from a CI job or synchronises users.
-
An extension developer exposes the records of a component through the same mechanism.
The Joomla Developer Manual page "Web Services" is the official starting point, but it is short: it shows
create, read, update and delete requests for articles (as Joomla Framework HTTP and PHP cURL code) and it does not document
response formats, error codes or pagination, and its headings still say "Joomla 4.x". The wiki page
J4.x:Joomla_Core_APIs on docs.joomla.org lists the endpoints, but that wiki is a legacy resource written for 4.x; use it
only as an index, and check an endpoint against the source tree or against your own site. The authoritative list of
routes is the code of the webservices plugins in plugins/webservices/* of the Joomla source tree. This page
describes what those plugins and the application register in 6.2.
The API application
Joomla has four applications: site, administrator, console and API. The API application is the PHP class
Joomla\CMS\Application\ApiApplication (client id 3), started by api/index.php. It reuses the same container, the
same configuration, the same extensions and the same database as the website; it differs in what it does with a
request:
| Aspect | API application behaviour |
|---|---|
Entry point |
|
Router |
|
Document |
|
Authentication |
Plugins of the |
Components |
Each API-enabled component has an |
The base URL is therefore https://tallpine.example/api/index.php/v1. The v1 segment is a version prefix owned by the
route definitions; every core route starts with it. The API application also needs the site’s secret in
configuration.php, because tokens are checked against it (see Authentication: API tokens).
If the site is served from the public folder introduced in Joomla 5.0, the API entry point belongs to the public
folder as well; see Installation and the public folder. A reverse
proxy or CDN must pass the Authorization and X-Joomla-Token headers and must not cache API responses that depend on
them; see Web server, reverse proxy and HTTPS.
Source: Joomla Manual: Web Services
The request lifecycle
ApiApplication::doExecute() runs three steps: initialiseApp(), route() and dispatch(). The route() step is the
one that differs from the other applications and is where authentication happens:
In words:
-
The application imports all enabled plugins of the
webservicesgroup and firesonBeforeApiRoutewith the router. Each plugin registers the routes of one component withcreateCRUDRoutes()or individualRouteobjects. A component whosewebservicesplugin is disabled has no API. -
ApiRouter::parseApiRoute()finds the first route whose method and path pattern match. No match is a404(RouteNotFoundException). -
Content negotiation uses the
Acceptheader. By default onlyapplication/vnd.api+jsonis offered (a route may offer others through aformatvariable). If nothing matches, the application throwsNotAcceptableand answers406. The negotiated type decides the document and view type:application/vnd.api+jsonmaps to thejsonapiformat andapplication/jsontojson. -
After
onAfterApiRoute, any route that is not markedpubliccallslogin()silently with the actioncore.login.api. Theapi-authenticationplugins run (see Authentication: API tokens); if no plugin authenticates the user, or the user lacks the permission, the application throwsAuthenticationFailedand the response is401. -
dispatch()loads the component of the route throughComponentHelper::renderComponent(). The router has setcontrollerandtaskin the input, socom_content'sarticlescontroller runsdisplayList,displayItem,add,editordelete. -
The controller prepares the model state (filters, pagination), checks access and calls the view. The view (
JsonApiView) serialises items with a serializer and the document renders them as JSON.
The respond() step adds the header X-Powered-By: JoomlaAPI/1.0 and, when enabled, the CORS headers (see
CORS for browser clients).
Source: Joomla source: ApiApplication
JSON:API format
Joomla follows the JSON:API specification, using the tobscure/json-api library. The required
request header is Accept: application/vnd.api+json. Requests that carry a body use Content-Type: application/json;
the controllers read the raw JSON body and an optional data key. A list response looks like this (shortened):
{
"links": {
"self": "https://tallpine.example/api/index.php/v1/content/articles",
"next": "https://tallpine.example/api/index.php/v1/content/articles?page%5Boffset%5D=20",
"last": "https://tallpine.example/api/index.php/v1/content/articles?page%5Boffset%5D=60"
},
"data": [
{
"type": "articles",
"id": "17",
"attributes": {
"id": 17,
"title": "Ridge walk to Lake Tallpine",
"alias": "ridge-walk-to-lake-tallpine",
"state": 1,
"language": "*",
"created": "2026-09-14 09:12:00",
"modified": "2026-10-02 18:40:11",
"distance-km": "12.5"
},
"relationships": {
"category": { "data": { "type": "categories", "id": "8" } },
"created_by": { "data": { "type": "users", "id": "42" } }
}
}
],
"meta": { "total-pages": 4 }
}
Source: JSON:API specification
Points to know:
-
Each resource has
type,id,attributesand, for articles,relationships(category,created_by,tags; single items alsomodified_by, andlanguageAssociationswhen multilingual is enabled). -
Custom fields appear as attributes named after the field (
distance-km,elevation-gain,difficulty,seasonof the Tallpine articles), using the field’s API value when its plugin provides one. -
The
linksobject carriesself, and for listsfirst,previous,nextandlast. Themetaobject hastotal-pages. There is nototal-itemscount; compute the totals from the number of pages if you need them. -
The article
textattribute is the introtext and the fulltext joined by a space, and the content plugins have run on it (onContentPrepare) before it is serialised. Articleimagesare resolved to absolute URLs. -
The
fields[type]=a,bandinclude=parameters of the JSON:API specification are not generally supported. Joomla decides which attributes and relationships to render in the view (fieldsToRenderItem,fieldsToRenderList,relationship) and lets plugins change that with theonApiGetFieldsevent (see Extending the Web Services API). Do not expect sparse fieldsets to reduce the payload.
Errors are JSON:API error documents with an HTTP status: 401 for failed authentication, 403 when the user may log in
to the API but not perform the action, 404 for an unknown route or record, 406 for a bad Accept header, 409 for
a record that must be trashed before deletion; invalid data and save failures use the status of the exception raised
(for example 422 for invalid parameters) and unexpected failures give 500. Error bodies of the JSON:API handlers have
an errors array whose entries contain title (the message) and code (the status); the 404 and 409 bodies of a delete
are plain JSON objects with status, code and message.
Authentication: API tokens
Anonymous access exists only for routes explicitly marked public (for example the Joomla Update notification routes
used by Automated Core Updates, which carry their own signature). Everything else needs an identity. Joomla ships two
plugins in the api-authentication group:
| Plugin (name in the Plugin Manager) | Fresh 6.2 install | Credential |
|---|---|---|
API Authentication - Web Services Joomla Token ( |
enabled |
An API token in |
API Authentication - Web Services Basic Auth ( |
disabled |
The username and password of a Joomla user sent by HTTP Basic authentication. |
A second plugin manages the tokens themselves: User - Joomla API Token (plg_user_token, group user, enabled by
default). It adds the Joomla API Token tab to the user edit form and to the user’s own profile. Without it nobody can
create a token, and the token authentication plugin has nothing to check.
Creating a token
-
In the administrator open System → Plugins and check that User - Joomla API Token and API Authentication - Web Services Joomla Token are enabled. Open User - Joomla API Token and review Allowed User Groups. The default is only the Super Users group (id 8). Add a group to let its members use tokens; clearing the field allows every group.
-
Give the user (or the group) the permission Web Services Login (
core.login.api) in System → Global Configuration → Permissions, or on the group’s row in Users → Groups. The permission is inherited like all global permissions, so allow it on the group that holds your API accounts and nowhere else. The official Manual says an API token is used with the permissions of a Super User or of an account that hascore.login.apiandcore.login.site; the token plugin itself checks the user’s group and thecore.login.apiaction when it callslogin(). -
Edit the user and open the Joomla API Token tab. If the user has no token yet, save the profile once: the plugin generates 32 random bytes and stores them. After saving, reopen the tab to see the Token field with a Copy button. The Active switch temporarily disables the token, and Reset (set it to Yes and save) replaces it with a new one.
-
Store the token in a secret manager or in an environment variable of the job that calls the API, never in the page source, a repository or a front-end bundle.
A user cannot see someone else’s token. An administrator with permission can enable, disable or reset another user’s token, for example when a leak is suspected; that is the incident-response path.
What the token is, and what is stored
The database does not contain the token you copy. The #__user_profiles table holds two rows for the user, keyed
joomlatoken.token and joomlatoken.enabled. The first row is the seed: the 32 random bytes, base64-encoded. The
displayed token is computed from the seed and the site’s secret when the form is rendered:
token = base64( algorithm ":" userId ":" HMAC(algorithm, base64decode(seed), siteSecret) )
The algorithm is sha256 by default (sha512 is also accepted). On each request the token authentication plugin decodes
the token, checks the algorithm, rebuilds the HMAC from the stored seed and the secret of configuration.php, and
compares the two values with hash_equals(). It also checks that the token is active, that the user belongs to an
allowed group, and that the user is not blocked, unactivated or marked "require password reset". All conditions are
evaluated before the plugin decides, so the response time does not reveal which one failed.
Consequences:
-
A database leak does not reveal usable tokens without the site
secret; keep both protected, and never commitconfiguration.php. -
Changing the
secretinconfiguration.phpinvalidates every token because the HMAC changes. The tokens look unchanged in the user profiles but stop working until users copy the new value. A site restored from a backup onto a newconfiguration.phphas the same effect. -
An empty
secretmakes the plugin refuse every token. -
Resetting a user’s token changes the seed and so invalidates the old value at once.
-
A token is the user’s identity: every ACL rule, workflow and check-in applies to that user.
Sending the token
The plugin looks first for Authorization: Bearer <token> (the word Bearer is case-sensitive) and then for the
X-Joomla-Token header, which is the form used in the Manual and most examples. Both carry the same value. The
.htaccess distributed with Joomla contains the rewrite that copies the Authorization header to
HTTP_AUTHORIZATION for PHP FastCGI; without that rule Apache drops the header and only X-Joomla-Token works. If you
use another header mechanism, check that the web server passes the header to PHP.
# The token lives in the environment of the shell or CI job, not in the command.
export JOOMLA_API_TOKEN="$(cat /run/secrets/tallpine_api_token)"
export BASE="https://tallpine.example/api/index.php/v1"
curl --silent --show-error \
--header "Accept: application/vnd.api+json" \
--header "X-Joomla-Token: ${JOOMLA_API_TOKEN}" \
"${BASE}/content/articles?page%5Blimit%5D=1"
Source: Joomla Manual: Web Services
The Manual advises against environment variables for secrets and suggests a password manager or secret vault; in a CI
job use the platform’s secret store, which exposes the value to the process as an environment variable only for the
duration of the job, and keep it out of shell history and logs (set +x).
Basic Auth is disabled by default
The API Authentication - Web Services Basic Auth plugin lets a client authenticate with a username and password
through the standard Authorization: Basic ... header (read by PHP as PHP_AUTH_USER and PHP_AUTH_PW). It is installed
but disabled on a fresh 6.2 site, while the token plugin is enabled. Keep it that way:
-
The request carries a reusable password on every call; the API login is a silent login, so there is no interactive step where a second factor could be asked for.
-
Passwords end up in client configuration, logs and proxies.
-
A token can be disabled or reset without changing the user’s password and does not give access to the administrator.
The user still needs the core.login.api permission when Basic Auth is used. Enable the plugin only for a trusted
environment where tokens are impossible, and only over HTTPS. Older tutorials and books enable Basic Auth first because
it is shorter to demonstrate; Developing Extensions for Joomla! 5 (2023) describes tokens, and the 4.x-era material that
tells you to switch the Basic Auth plugin on should be read with this default in mind.
Permissions on top of authentication
Authentication proves who the caller is; the controllers still apply the ACL of the component. For com_content:
-
listing and reading follow the visibility the user would have in the administrator list (the model applies the access-level and category rules of the user’s groups);
-
POSTrequirescore.createon the component or the target category, viaallowAdd(); -
PATCHrequirescore.manageon the component, andcore.editon the record orcore.edit.ownon a record the user created, viaallowEdit(); -
DELETErequires the matching delete permission and fails with409when the record cannot be deleted (for example it is not trashed).
Give an integration account the smallest role that works, for instance a custom API Publisher group that may create articles in the Trails category and cannot reach the administrator. See Users, groups, access levels and ACL for creating groups and actions, and Security hardening, MFA and passkeys for the rest of the hardening checklist.
The core endpoints
Each core webservices plugin registers the routes of one component. All routes are below /api/index.php/v1.
createCRUDRoutes() produces five routes: GET list, GET item (/:id, digits only), POST add, PATCH edit
(/:id) and DELETE (/:id). Note that updates use PATCH, not PUT; the router only registers the verbs listed.
| Plugin | Endpoints | Notes |
|---|---|---|
|
|
Articles are the most complete resource: filters, custom fields, tags and Schema.org data. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The items endpoints filter on |
|
|
|
|
|
|
|
|
Filters: |
|
|
|
|
|
Reads and writes Global Configuration and component options. Treat it as administrator-level. |
|
|
List and read adapters and files; create, change and delete files; see Media. |
|
|
Private messages. |
|
|
|
|
|
Personal data, so keep it restricted. |
|
|
|
|
|
The endpoints used by Automated Core Updates (5.4/6.0 and later). See Updating Joomla and Automated Core Updates. |
The set of endpoints depends on the installed version and on which webservices plugins are enabled (the Menus,
Redirect, Templates, Messages, Privacy, Languages, Config, Plugins, Installer and Joomla Update
plugins are all separate extensions). To list what a site really offers, open System → Plugins, filter by the
webservices type, and read the route registration in each plugin class. A site without a plugin has no route and
answers 404. A custom component’s endpoints are added the same way by
plg_webservices_tallpine; see Extending the Web Services API.
The security consequence is that the API is a second administration interface. An account that may call config,
plugins, users or extensions is effectively an administrator. Disable the webservices plugins that your
integrations do not use; this removes the endpoints entirely.
Reading articles
The articles list endpoint is GET /v1/content/articles. It accepts these filters (all optional), taken from the
ArticlesController:
| Parameter | Meaning |
|---|---|
|
User id of the author ( |
|
Category id. |
|
Free-text search of the article list. |
|
Publication state: |
|
|
|
Tag id. |
|
Language tag such as |
|
Only articles modified on or after, or before, the given date and time ( |
|
User id that has the article checked out. |
|
Workflow stage id, when workflows are enabled. |
|
A valid list column (for example |
Square brackets in a query string must be percent-encoded for strict clients; curl with --globoff accepts them as
they are. Joomla itself writes %5B and %5D in the links it returns.
# Published articles of the category "Alpine" (id 8) modified since 1 October, newest first.
curl --silent --globoff \
--header "Accept: application/vnd.api+json" \
--header "X-Joomla-Token: ${JOOMLA_API_TOKEN}" \
"${BASE}/content/articles?filter[category]=8&filter[state]=1&filter[modified_start]=2026-10-01%2000:00:00&list[ordering]=a.modified&list[direction]=desc"
Source: Joomla Manual: Web Services
The modified_start and modified_end filters compare against the stored modification time in UTC. For a nightly
export, remember the largest modified value of the previous run and ask for everything after it; the trashed and
unpublished articles are returned only if you ask for their state, so a synchroniser must query the states it cares
about. Deleted records disappear without a trace, so a mirror needs a periodic full comparison as well.
A single article is GET /v1/content/articles/17. A missing id gives 404.
Pagination
Lists use offset pagination with the JSON:API parameters page[offset] and page[limit]. The default page size is
20 items (itemsPerPage of ApiController), and the view defaults to offset=0, limit=20. Each response includes:
-
links.self, andlinks.firstandlinks.previouswhen the offset is greater than zero, -
links.nextandlinks.lastwhen more pages exist, -
meta.total-pages.
An offset greater than the total number of items gives 404 instead of an empty page. Follow the next link until it
disappears, or compute the offsets from total-pages. Do not hard-code a large limit hoping to fetch everything: a
large limit loads and serialises every item (including their fulltext and custom fields) in one request.
# Walk all published articles, 50 at a time, following the "next" link.
url="${BASE}/content/articles?filter[state]=1&page[limit]=50"
while [ -n "$url" ]; do
page="$(curl --silent --globoff \
--header "Accept: application/vnd.api+json" \
--header "X-Joomla-Token: ${JOOMLA_API_TOKEN}" "$url")"
echo "$page" | jq -r '.data[] | "\(.id)\t\(.attributes.title)"'
url="$(echo "$page" | jq -r '.links.next // empty')"
done
Source: JSON:API: Pagination
Creating, updating and deleting articles
The write operations take the article’s form fields as a JSON object. The controller validates the data with the same
form (com_content's article form) and the same model as the administrator, so required fields, filters and access
rules are identical. catid, title, alias (optional), articletext (or introtext and fulltext), state,
access, language and the custom fields are accepted; tags is an array of tag ids. Custom fields are sent as
top-level properties named after the field, and the controller moves them into com_fields for the model.
Create
curl --silent --show-error --request POST \
--header "Accept: application/vnd.api+json" \
--header "Content-Type: application/json" \
--header "X-Joomla-Token: ${JOOMLA_API_TOKEN}" \
--data '{
"title": "Ridge walk to Lake Tallpine",
"alias": "ridge-walk-to-lake-tallpine",
"catid": 8,
"articletext": "<p>A 12.5 km loop with 640 m of ascent.</p>",
"state": 1,
"access": 1,
"language": "*",
"tags": [3, 4],
"distance-km": "12.5",
"elevation-gain": "640",
"difficulty": "moderate"
}' \
"${BASE}/content/articles"
Source: Joomla Manual: Web Services
On success the response is the JSON:API document of the new article (the controller saves the record, checks it in and
displays it), with its generated id. Clients should read that id rather than guess it. The Manual’s article example
uses the Uncategorized category, which has id 2 on a default install; real sites use their own category ids, here
8 for Trails › Alpine. The Manual also says that the article text must be split into introtext and fulltext
when you want a "Read more" break; the combined articletext property puts the break where the <hr id="system-readmore">
is in the HTML. If the article has a featured or publishing window, send featured, publish_up and publish_down.
Typical failures are a missing catid or title (422 with the validation message), a category the user may not
create in (403), a duplicated alias in the category (422/500 with the save error), an invalid access or
language, and a custom field value that does not match the field type. The error message tells which; at most three
validation messages are returned.
Update with PATCH
PATCH sends only the properties that change. For the properties you leave out, the controller loads the stored record
and merges its values before validation, so omitting state does not unpublish the article. Tags are kept when tags
is not sent.
curl --silent --show-error --request PATCH \
--header "Accept: application/vnd.api+json" \
--header "Content-Type: application/json" \
--header "X-Joomla-Token: ${JOOMLA_API_TOKEN}" \
--data '{ "title": "Ridge walk to Lake Tallpine (reopened)", "difficulty": "easy" }' \
"${BASE}/content/articles/17"
Source: Joomla Manual: Web Services
The edit checks the record out and in again, so an article that another user holds checked out fails with a
check-in/check-out error. The code also keeps stored UTC dates unchanged when the request does not mention them. There is no PUT route.
Delete
# Joomla refuses to delete a published article (409); trash it first.
curl --silent --request PATCH \
--header "Accept: application/vnd.api+json" --header "Content-Type: application/json" \
--header "X-Joomla-Token: ${JOOMLA_API_TOKEN}" \
--data '{ "state": -2 }' "${BASE}/content/articles/17"
curl --silent --request DELETE --include \
--header "Accept: application/vnd.api+json" \
--header "X-Joomla-Token: ${JOOMLA_API_TOKEN}" \
"${BASE}/content/articles/17"
Source: Joomla Manual: Web Services
A successful delete answers 204 No Content with no body; 404 means the id does not exist and 409 means the
record must be trashed first. The 404 and 409 bodies of a delete are plain JSON objects with status, code and
message, not JSON:API error documents. Deletion is permanent, so give integration accounts the core.delete
permission only where it is intended.
The same call from PHP
The Manual shows both forms. With the Joomla Framework HTTP package (joomla/http, installed with Joomla) the request
from a PHP script on another machine looks like this; the token comes from the environment.
<?php
use Joomla\Http\HttpFactory;
use Joomla\Uri\Uri;
$token = getenv('JOOMLA_API_TOKEN') ?: throw new RuntimeException('JOOMLA_API_TOKEN is not set');
$base = 'https://tallpine.example/api/index.php/v1';
$http = (new HttpFactory())->getHttp([], 'curl');
$response = $http->get(
new Uri($base . '/content/articles?filter[state]=1&page[limit]=5'),
[
'Accept' => 'application/vnd.api+json',
'X-Joomla-Token' => $token,
]
);
$document = json_decode((string) $response->getBody(), true, 512, JSON_THROW_ON_ERROR);
foreach ($document['data'] as $article) {
echo $article['id'], ' ', $article['attributes']['title'], PHP_EOL;
}
Source: Joomla Manual: Web Services
Other resources by example
Categories, tags and custom fields
Categories are GET /v1/content/categories (filters search and state) and are created with the same JSON shape as
the administrator form (title, extension set to com_content, parent_id). Tags are at /v1/tags. The definition of
custom fields is at /v1/fields/content/articles and of their groups at /v1/fields/groups/content/articles, which is
how a client can discover that Tallpine articles have distance-km and difficulty. Creating a field this way needs
the same permissions as the administrator field form. The values themselves travel inside the article. See
Custom fields and Categories and tags.
# What custom fields do Tallpine articles have?
curl --silent \
--header "Accept: application/vnd.api+json" \
--header "X-Joomla-Token: ${JOOMLA_API_TOKEN}" \
"${BASE}/fields/content/articles" | jq -r '.data[].attributes | "\(.name)\t\(.type)"'
Users
GET /v1/users lists users and accepts filter[search], filter[state] (blocked or not), filter[active]
(activated or not) and filter[groupid]. POST /v1/users creates one with name, username, email, password,
groups. Read the response carefully: it contains personal data. An integration that creates members should not be able to
add itself to the Super Users group; the ACL checks of the users component apply, but the safest design is an account that
cannot reach /v1/users at all.
Menus, modules, plugins, languages and configuration
# Site menu items of the main menu.
curl --silent --globoff \
--header "Accept: application/vnd.api+json" --header "X-Joomla-Token: ${JOOMLA_API_TOKEN}" \
"${BASE}/menus/site/items?filter[menutype]=mainmenu"
# Installed content languages (English and German on the Tallpine site).
curl --silent \
--header "Accept: application/vnd.api+json" --header "X-Joomla-Token: ${JOOMLA_API_TOKEN}" \
"${BASE}/languages/content"
# Is the Tallpine webservices plugin enabled?
curl --silent --globoff \
--header "Accept: application/vnd.api+json" --header "X-Joomla-Token: ${JOOMLA_API_TOKEN}" \
"${BASE}/plugins?filter[element]=tallpine&filter[type]=webservices"
# Read the application configuration (needs administrator-level permission).
curl --silent \
--header "Accept: application/vnd.api+json" --header "X-Joomla-Token: ${JOOMLA_API_TOKEN}" \
"${BASE}/config/application"
The config endpoints return and change what System → Global Configuration shows, including database and mail
settings. Restrict them to administrators and never expose them to a front-end token.
Media
GET /v1/media/adapters lists the storage adapters (by default the local images folder as local-images).
GET /v1/media/files lists the files of an adapter; GET /v1/media/files/local-images:/trails/ridge.jpg returns one. The path
query parameter and the filter[search] filter narrow the result, and url=true returns absolute URLs; a recursive
search is requested with search_recursive. To upload, POST a JSON body with path (the destination, including the
adapter prefix and extension) and content (the file, base64-encoded); PATCH renames or replaces; DELETE removes.
# Upload a trail photo to images/trails/ridge.jpg
B64="$(base64 < ridge.jpg | tr -d '\n')"
curl --silent --show-error --request POST \
--header "Accept: application/vnd.api+json" \
--header "Content-Type: application/json" \
--header "X-Joomla-Token: ${JOOMLA_API_TOKEN}" \
--data "{\"path\": \"local-images:/trails/ridge.jpg\", \"content\": \"${B64}\"}" \
"${BASE}/media/files"
The same restrictions apply as in the Media Manager: the allowed extensions and MIME types, the maximum size and the
image-check options of Media → Options decide what the upload accepts, and the user needs core.create on com_media.
See Media Manager. The JSON body is limited by PHP’s post_max_size, and base64
adds about a third to the size.
Common errors and pitfalls
| Symptom | Usual cause |
|---|---|
|
The token plugin or User - Joomla API Token is disabled; the token of this user is inactive; the user is not in an
allowed user group; the user lacks Web Services Login ( |
|
The Basic Auth plugin is disabled (the default). |
|
Authenticated, but the user may not perform the action on the component or category. Check the permission for the
specific action, not |
|
The |
|
The |
HTML or a redirect to the login page instead of JSON |
The request went to the website, not to the API entry point, or an offline site or a security plugin intercepted it. |
|
There is no |
|
Validation failed (required fields, alias in use, invalid access level, custom field type) or the record is checked out by someone else. |
CORS errors in the browser console |
CORS is off or the allowed headers do not include |
Tokens stopped working after a restore or migration |
The new site has a different |
Missing custom fields in the response |
The field is unpublished, belongs to another category assignment, or the user may not view it. |
Debugging tips: add --include to curl to see status and headers; the response header X-Powered-By: JoomlaAPI/1.0
tells you the request reached the API application; and enabling System → Global Configuration → Server → Error
Reporting and the debug options shows more detail in the logs. Do not leave the debug mode on in production, because
the error documents can then contain traces. See
Error handling and legacy APIs.
CORS for browser clients
A browser page on another origin (for example https://trails.tallpine.example, built with Next.js or React) can call
the API only when the API answers the CORS preflight. System → Global Configuration → Web Services has a switch and
three fields:
| Setting | Behaviour in 6.2 |
|---|---|
Enable CORS ( |
Off by default. When on, the API adds CORS headers and answers |
|
Default |
|
Default |
|
Empty means the methods registered for the matched route. |
Use exact origins in production. Better still, call the API from your server code (Next.js route handlers, Astro build steps, server components) so that the token never reaches the browser; a token embedded in client-side code is public. The general mechanism of CORS is explained in CORS, and the front-end frameworks in Next.js and React. If you need user-bound access from an external application instead of a shared integration token, see OAuth; Joomla core has no OAuth server, so that would be a separate extension or a proxy in front of the API. The full headless architecture is in Headless Joomla.
Security checklist
-
Keep Basic Auth disabled; use tokens only, over HTTPS only.
-
One token per integration, on a dedicated user whose group has only
core.login.apiand the specific content permissions. Restrict Allowed User Groups of the token plugin to the groups that need tokens. -
Never put a token in front-end code, a repository, a URL query string or a log. Rotate by using Reset and update the consumer.
-
Treat the
secretofconfiguration.phpas part of the credential. -
Disable the
webservicesplugins you do not need (config,plugins,users,installer,privacy). -
Restrict CORS to the exact origins of your front ends. Consider rate limiting and an IP allow list for
/api/at the web server or proxy; Joomla does not ship API rate limiting. -
Do not return the API to the public cache: responses depend on the token, and a shared cache must key on it or skip these paths.
-
Review the API calls in the server access logs, and use the Action Logs for changes made by the API user (they are recorded as that user). See Privacy and action logs.
Corrections to older material
-
Older tutorials and the 4.x wiki pages assume Basic Auth for simplicity; in current installations only the token plugin is enabled, and Basic Auth is a deliberate opt-in.
-
Joomla 4 material often shows
X-Joomla-Tokenas the only way;Authorization: Beareralso works on 6.2. -
Developing Extensions for Joomla! 5 (2023) is a good guide to tokens and
createCRUDRoutes(). Its webservices plugin example extendsCMSPluginwithout namespaces and registers routes inonBeforeApiRoute(&$router). Since 5.0 the event classBeforeApiRouteEventis passed and the plugin implementsSubscriberInterface, as the core plugins in 6.2 do (see Plugin development and events and Extending the Web Services API). -
The J4.x wiki page
Joomla_Core_APIsis not maintained for 6.x; the Developer Manual page that replaces it is still short, and its headings, links and one of its examples are out of step with the code. -
There is no pagination
metawith a total item count and no support for the general JSON:APIfieldsandincludeparameters, in contrast to what a generic JSON:API client library expects.