Skip to the content
Documentation
Help Start
Reading in:
  • German: 9,059 of 9,059 pages
About the help in other languages
All help pages

REST API v1 and v2: one extension

This guide explains how Exponential 6.0.15 serves its REST content API: version 1 and version 2 come from one extension, ezprestapi (1.2.6 or later). It shows which URL answers at which version, how a client…

Download as PDF

Address: https://doc.exponential.earth/exphelp/viewer/guides/rest-api-v1-and-v2.md

On this page

REST API v1 and v2: one extension

This guide explains how Exponential 6.0.15 serves its REST content API: version 1 and version 2 come from one extension, ezprestapi (1.2.6 or later). It shows which URL answers at which version, how a client authenticates, how to call the API with curl, which settings drive it, and how to move an installation off the deprecated ezprestapiprovider extension.

It is written for two readers. Developers who write clients or extend the API read sections 1 to 5. Site administrators who install, upgrade or check an installation read sections 5 to 8.

Every statement below was checked on the demonstration server (Apache with PHP-FPM, and Velocity) on 15 October 2026. Host names, identifiers and secrets are replaced by documentation values (example.com, <client id>, expk_...).

Guides · Related: Personal API keys · ezprestapi · ezprestapiprovider (deprecated) · Remote services and apps

In short

  • One extension, one activation: ActiveExtensions[]=ezprestapi. It registers one provider class, ezp7xRestApiProvider, for the prefixes ezp and ezpl, with one route table and one controller, ezp7xRestContentController.
  • Reads answer at v1 and v2 (/api/ezp/v1/..., /api/ezp/v2/...); writes (create, update, delete) answer at v2 only.
  • Every read and write checks the user's policies (content/read, content/create, content/edit, content/remove), whatever the request was authenticated with. The check is the kernel's expRestContentPermission and needs Exponential 6.0.15.
  • Authentication: an OAuth2 token from POST /api/auth/v1/oauth/token (grants authorization_code and refresh_token), or a personal API key sent as Authorization: Bearer expk_....
  • ezprestapiprovider is deprecated. Its final release, 6.0.4, is a compatibility shim on top of ezprestapi. Activate ezprestapi and remove ezprestapiprovider from ActiveExtensions (section 6).
  • Nothing changes for clients: the v1 and v2 answers are byte for byte the same as before, except that OutputFormat=xhtml fills renderedOutput again.

Contents

  1. What serves what
  2. The routes
  3. Authentication
  4. Examples with curl
  5. Configuration
  6. Migrating from ezprestapiprovider
  7. Activation combinations, before and since
  8. Known issue: listAtom
  9. Migrations

1. What serves what

PartWhere it livesWhat it does
REST frameworkthe kernel (kernel/private/rest/), settings/rest.iniprefix filter, routing, versions, authentication, answer cache, output renderers
Auth provider ezpRestAuthProviderthe kernel, prefix auththe OAuth2 token endpoint and the authentication routes
Content provider ezp7xRestApiProviderezprestapi, prefixes ezp and ezplthe content routes of v1 and v2
Content controller ezp7xRestContentControllerezprestapievery content read and write, with the permission checks
Create and delete backendnxc_powercontentcreates and removes content for the controller
Permission helper expRestContentPermissionthe kernel (6.0.15)asks the user's policies the way the content module does
ezprestapiprovider 6.0.4deprecated shimezpRestApiProvider and ezpRestContentController extend the ezprestapi classes; maps ezp and ezpl to ezp7xRestApiProvider

ezpl is an alias prefix of ezp: the same routes, the same answers.

2. The routes

All paths below are relative to /api/ezp/<version> (or /api/ezpl/<version>).

MethodPathv1v2What it does
GET/content/node/<id>yesyesone node (ResponseGroups=Metadata,Locations,Fields, OutputFormat=xhtml)
GET/content/node/<id>/list(/offset/<n>)(/limit/<n>)(/sort/<key>(/asc|desc))yesyesthe children
GET/content/node/<id>/childrenCountyesyesthe number of children
GET/content/node/<id>/fieldsyesyesevery field of the node's object
GET/content/node/<id>/field/<identifier>yesyesone field
GET/content/object/<id>, .../fields, .../field/<identifier>yesyesthe same by object id
GET/content/node/<id>/listAtomyesyesAtom feed of the children; answers 500, see section 8
POST/content/node/createnoyescreate and publish below parentNodeID
POST/content/node/<id>noyesupdate: checks content/edit, then answers 501 (not implemented yet)
DELETE or POST/content/node/delete/<id>noyesremove the node's object with all its locations and what is below them

A write sent to v1 does not exist there and is refused. The links inside a v1 answer stay in v1, so existing clients keep following them.

The answers and the policies each call checks are described in Personal API keys, section 3 and Permissions of the REST interface. In short: 200, 201 for a create, 400 for a missing or unknown field, 401 without a valid token or key, 403 access_denied when the user may not, 404 for an unknown node.

3. Authentication

rest.ini [Authentication] RequireAuthentication=enabled is the default: every content route needs a user. Two credentials are accepted.

3.1 OAuth2 tokens

For an application that lets other users sign in and act for them.

  1. An administrator registers the application at Setup > oAuth admin (/oauthadmin/list). It gets a client identifier, a secret and the redirect address of the application.
  2. The application sends the user to the authorize page of the site: https://example.com/oauth/authorize?client_id=<client id>&redirect_uri=<redirect uri>&response_type=code&state=<random>, with PKCE also &code_challenge=<challenge>&code_challenge_method=S256. The user signs in and allows the application; the site redirects back with code=<code> and the state of the request, unchanged (the application checks it). The user needs the policy OAuth / Authorize (oauth/authorize); without it the site redirects back with error=access_denied (upgrade note).
  3. The application exchanges the code for a token at the token endpoint, POST /api/auth/v1/oauth/token, with grant_type=authorization_code, client_id, client_secret, code and redirect_uri, plus code_verifier when it sent a challenge. A public client (stored without a secret, such as a mobile app) sends no client_secret and must use PKCE (PKCE and state).
  4. It sends the token with every request (Authorization: Bearer <token> or Authorization: OAuth <token>; the oauth_token parameter of a POST form body as well, unless rest.ini [OAuthSettings] AllowTokenInBody=disabled; the oauth_token query parameter only with AllowTokenInQuery=enabled, off by default).
  5. When the token has expired (rest.ini [OAuthSettings] TokenTTL, one hour by default), the application asks for a new one with grant_type=refresh_token and refresh_token=<refresh token>. A refresh token is good for RefreshTokenTTL (30 days by default, 0 = no expiry) from the refresh that issued it.

A token, a code or a refresh token stops working as soon as its user is disabled or locked by failed logins (OAuth token hardening).

The token endpoint supports the grants authorization_code and refresh_token.

A page in a browser on another host (a single page application) can call the API only when CORS is switched on for its origin, rest.ini [CorsSettings]; it is off by default (CORS for the REST API).

3.2 Personal API keys

For a script of the user's own. The user makes a key on the site (My account > API access) and sends it as Authorization: Bearer expk_.... The request runs as the key's owner, limited to the key's scopes, and never beyond the owner's policies. The whole story is in Personal API keys.

4. Examples with curl

export EXP_API_KEY='expk_...'     # a personal API key; an OAuth token works the same way with "OAuth <token>"

# one node, v1 (what existing clients call)
curl -s -H "Authorization: Bearer $EXP_API_KEY" https://example.com/api/ezp/v1/content/node/2

# the first 20 children with their fields, v1 (the call of the mobile app)
curl -s -H "Authorization: Bearer $EXP_API_KEY" \
     "https://example.com/api/ezp/v1/content/node/<node id>/list/offset/0/limit/20?ResponseGroups=Fields"

# the same node at v2, with the rendered XHTML of its view
curl -s -H "Authorization: Bearer $EXP_API_KEY" \
     "https://example.com/api/ezp/v2/content/node/2?OutputFormat=xhtml"

# one field by object id
curl -s -H "Authorization: Bearer $EXP_API_KEY" https://example.com/api/ezp/v2/content/object/<object id>/field/title

# create a folder below a node (v2 only)
curl -s -H "Authorization: Bearer $EXP_API_KEY" https://example.com/api/ezp/v2/content/node/create \
     -d parentNodeID=<parent node id> -d classIdentifier=folder -d languageLocale=eng-US -d name='From a script'
# {"message":"Created","objectId":<object id>,"nodeId":<node id>}

# remove it again (v2 only)
curl -s -X DELETE -H "Authorization: Bearer $EXP_API_KEY" https://example.com/api/ezp/v2/content/node/delete/<node id>
# {"message":"Removed","nodeId":<node id>,"objectId":<object id>}

Exchanging an authorization code for an OAuth token:

curl -s https://example.com/api/auth/v1/oauth/token \
     -d grant_type=authorization_code -d client_id='<client id>' -d client_secret='<client secret>' \
     -d code='<code>' -d redirect_uri='<redirect uri>'

Keep keys, secrets and tokens in environment variables or a secret store, never in a script, a repository or shell history on a shared machine.

5. Configuration

The kernel's settings/rest.ini holds the defaults; ezprestapi adds its provider and output section in extension/ezprestapi/settings/rest.ini.append.php. Override in settings/override/rest.ini.append.php.

[System]
# the path below which the REST interface answers
ApiPrefix=/api

[ApiProvider]
# set by ezprestapi (and, identically, by the ezprestapiprovider 6.0.4 shim)
ProviderClass[ezp]=ezp7xRestApiProvider
ProviderClass[ezpl]=ezp7xRestApiProvider

[Authentication]
RequireAuthentication=enabled
AuthenticationStyle=ezpRestOauthAuthenticationStyle
#AuthenticationStyle=ezpRestBasicAuthStyle
RequireHTTPS=disabled
DefaultUserID=

[OAuthSettings]
TokenTTL=3600
RefreshTokenTTL=2592000
AllowTokenInQuery=disabled
AllowTokenInBody=enabled

# the template of OutputFormat=xhtml for the controller of ezprestapi (ezprestapi 1.2.6, kernel 6.0.15)
[ezp7xRestContentController_viewContent_OutputSettings]
Template=rest_pagelayout.tpl
SettingMeaning
[System] ApiPrefixthe URL path the REST interface answers below (/api)
[ApiProvider] ProviderClass[<prefix>]the provider class of a prefix: /api/<prefix>/<version>/.... ezp and ezpl are ezp7xRestApiProvider; auth is the kernel's ezpRestAuthProvider
[Authentication] RequireAuthenticationdisabled lets every request run as DefaultUserID (or the anonymous user); the permission checks still apply
[Authentication] AuthenticationStyleOAuth (default) or HTTP basic authentication; personal API keys are accepted by both
[Authentication] RequireHTTPSrefuse plain HTTP
[<Controller>_<action>_OutputSettings] Templatethe page layout template of the XHTML renderer for that controller action
[ApiKeySettings]the personal API keys, see Personal API keys, section 13

The output section is named after the controller. Up to ezprestapi 1.2.5 only [ezpRestContentController_viewContent_OutputSettings] existed, the name of the old v1 controller, so a read with OutputFormat=xhtml through ezp7xRestContentController found no template and answered an empty renderedOutput. 1.2.6 and the kernel of 6.0.15 add the section for ezp7xRestContentController.

After a change to rest.ini, clear the INI cache (php bin/php/ezcache.php --clear-tag=ini --allow-root-user), reload PHP-FPM and restart Velocity.

6. Migrating from ezprestapiprovider

ezprestapiprovider 6.0.4 (15 October 2026) is its final release. It keeps old installations working as a shim: ezpRestApiProvider extends ezp7xRestApiProvider, ezpRestContentController extends ezp7xRestContentController, its rest.ini maps ezp and ezpl to ezp7xRestApiProvider, and its Composer package requires se7enxweb/ezprestapi ~1.2.6. It no longer ships the five classes it duplicated (ezpRestAtomController, ezpRestAtomView, ezpRestAtomDecorator, ezpRestContentModel, ezpRestApiViewController); ezprestapi ships them.

To move an existing installation:

  1. Update the packages: se7enxweb/ezprestapi to 1.2.6 or later (and se7enxweb/ezprestapiprovider to 6.0.4 while it is still installed). The Exponential 6.0.15 root composer.json requires se7enxweb/ezprestapi ~1.2.6 and no longer requires ezprestapiprovider.

  2. In settings/override/site.ini.append.php, keep or add the one line and remove the other:

    [ExtensionSettings]
    ActiveExtensions[]=ezprestapi
    # remove: ActiveExtensions[]=ezprestapiprovider
  3. Clear the INI cache, regenerate the extension autoloads, and reload the web runtime:

    php bin/php/ezcache.php --clear-tag=ini --allow-root-user
    php bin/php/ezpgenerateautoloads.php -e
    systemctl reload <your PHP-FPM service>
    ./console exp:velocity restart --allow-root-user     # when Velocity serves the site
  4. Check: curl -s -H "Authorization: Bearer $EXP_API_KEY" https://example.com/api/ezp/v1/content/node/2 and the same at /api/ezp/v2/ answer 200.

Things that keep working:

  • A rest.ini override that says ProviderClass[ezp]=ezpRestApiProvider works through the shim while ezprestapiprovider is installed. Change it to ezp7xRestApiProvider.
  • Code that extends ezpRestContentController works while the shim is installed. Extend ezp7xRestContentController instead.
  • Nothing changes for OAuth clients, their tokens, personal API keys or the v1 URLs.

New installations: the setup wizard (eZStepCreateSites::restApiExtension() in kernel/setup/steps/ezstep_create_sites.php) activates ezprestapi, or ezprestapiprovider only when ezprestapi is not on disk, never both. The installers of the legacy site packages (ezwebin_site, ezflow_site, ezdemo_site and their _clean variants) activate ezprestapi from the 6.0.15 packages of 15 October 2026 on; the sevenx and cjw site packages already did.

7. Activation combinations, before and since

Both extensions set rest.ini [ApiProvider] ProviderClass[ezp]. Extension settings are prepended per ActiveExtensions entry, so the extension listed earlier in ActiveExtensions is read last and its value wins.

Before (ezprestapi up to 1.2.5, ezprestapiprovider up to 6.0.3):

ActiveProviderClass[ezp]Result
ezprestapi aloneezp7xRestApiProviderv1 reads and v2 reads and writes, all checked
both, ezprestapi listed firstezp7xRestApiProviderthe same; the provider extension contributed nothing
both, ezprestapiprovider listed firstezpRestApiProviderevery /api/ezp/v2/... answered 404, create and delete too; v1 reads ran in ezpRestContentController without the permission checks; ezpl still served v2 through ezprestapi
ezprestapiprovider alone (as the setup wizard used to activate it)ezpRestApiProviderv1 reads only, unchecked; v2 404; the prefix ezpl did not exist

With both installed, the five duplicate class names also made the autoload generator warn and keep the copies of ezprestapi.

Since (ezprestapi 1.2.6, ezprestapiprovider 6.0.4):

ActiveProviderClass[ezp] and [ezpl]Result
ezprestapi aloneezp7xRestApiProviderv1 and v2, every content route in ezp7xRestContentController, all checked
both, in either orderezp7xRestApiProviderthe same
ezprestapiprovider alone (with ezprestapi installed as its dependency)ezp7xRestApiProviderthe same

This was checked by building the route table for each combination. The answers were compared with fixtures of 33 URLs recorded on Apache and on Velocity before and after the change: the mobile app's GET /api/ezp/v1/content/node/<id>/list/offset/0/limit/20?ResponseGroups=Fields and every other v1 and v2 read are byte for byte identical. The only difference is OutputFormat=xhtml, whose renderedOutput is filled again.

8. Known issue: listAtom

GET /content/node/<id>/listAtom answers 500 at v1 and v2, before and after this change. Two things are missing: the route's URL generation, and the feed view handler of the Zeta Components MVC feed tie-in (ezcMvcFeedViewHandler), a package the distribution does not install and that is not among the expcomponents forks. Use the list route, or the site's RSS export, until it is fixed.

Demo mode. On a public demo (environment variable EXP_DEMO_MODE=1) the web services refuse to change or reset the password of an administrator: expaccount.reset, expaccount.changePassword and expuser.setPassword answer HTTP 403 with the reason, and expaccount.forgotRequest sends no key and gives the same answer as for any address. Demo mode.

9. Migrations

The state of the migrations and their runs, read only, under /api/migration/v1 (the kernel's expMigrationRestProvider, rest.ini [ApiProvider] ProviderClass[migration]):

MethodPathWhat it answers
GET/api/migration/v1/statussummary (pending, failed, orphans, total, ready), environment, engine, lock, every migration with its state (migrations), orphans, warnings, errors
GET/api/migration/v1/runs?limit=20&offset=0the latest runs, newest first (at most 100 at once)
GET/api/migration/v1/runs/<run id>one run and the migrations it wrote; 404 for an unknown run
GET/api/migration/v1/snapshot?compare=classes,roles&subtree=<node remote id>&limit=500fingerprints (SHA-1) of the classes, roles, sections, object state groups, languages, settings of settings/override (secrets left out) and a content subtree, never a value: what exp:migration:generate --from-diff=<address> of another installation compares with (generators); 400 for an unknown kind

The user needs the policy migration/read (403 access_denied without it). A personal API key needs the scope migration:read ("Read the state of the migrations"); an OAuth token reads as its user. The answers are never kept in the REST application cache. Nothing runs or is undone through the API: a run and a rollback stay with ./console exp:migration:migrate and Setup > Migrations. The same data is in GraphQL as migrationStatus, migrations, migrationRuns and migrationRun (the GraphQL API).

curl -s -H "Authorization: Bearer expk_..." https://www.example.com/api/migration/v1/status
curl -s -H "Authorization: Bearer expk_..." "https://www.example.com/api/migration/v1/runs?limit=5"

Become an Exponential Partner

Partners share their information, provide funding, contribute back and develop the products with us, and that is how Exponential grows. A partner website and dashboard are planned for mid-2027: please become a partner today to speed up our growth rate and land new features faster.

Something wrong on this page? Report an error

Documentation license

This documentation is free documentation, licensed under the GNU Free Documentation License 1.3 or later.

Exponential Copyright © 1998-2026 Exponential Foundation and 7x