Skip to content

REST requests

Request method

Depending on the HTTP method used, different actions are possible on the same resource. Example:

Action Description
GET /content/objects/2/version/3 Fetches data about version #3 of content item #2
PATCH /content/objects/2/version/3 Updates the version #3 draft of content item #2
DELETE /content/objects/2/version/3 Deletes the (draft or archived) version #3 from content item #2
COPY /content/objects/2/version/3 Creates a new draft version of content item #2 from its version #3
PUBLISH /content/objects/2/version/3 Promotes the version #3 of content item #2 from draft to published
OPTIONS /content/objects/2/version/3 Lists all the methods usable with this resource, the 5 ones above

The following list of available methods gives an overview of the kind of action a method triggers on a resource, if available. For method action details per resource, see the REST API reference.

HTTP method Status Description Safe
OPTIONS Standard List available methods Yes
GET Standard Collect data Yes
HEAD Standard Check existence Yes
POST Standard Create an item No
PATCH Custom Update an item No
COPY Custom Duplicate an item No
MOVE Custom Move an item No
SWAP Custom Swap two Locations No
PUBLISH Custom Publish an item No
DELETE Standard Remove an item No

Caution with custom HTTP methods

Using custom HTTP methods can cause issues with several HTTP proxies, network firewall/security solutions and simpler web servers. To avoid issues with this, REST API allows you to set these using the HTTP header X-HTTP-Method-Override alongside the standard POST method instead of using a custom HTTP method. For example: X-HTTP-Method-Override: PUBLISH

If applicable, both methods are always mentioned in the specifications.

Unsafe methods require a CSRF token if session-based authentication is used.

OPTIONS method

Any REST API URI responds to an OPTIONS request.

The response contains an Allow header, which lists the methods accepted by the resource.

1
curl -IX OPTIONS https://api.example.com/api/ibexa/v2/content/objects/1
1
2
OPTIONS /content/objects/1 HTTP/1.1
Host: api.example.com
1
2
HTTP/1.1 200 OK
Allow: PATCH,GET,DELETE,COPY
1
curl -IX OPTIONS https://api.example.com/api/ibexa/v2/content/locations/1/2
1
2
OPTIONS /content/locations/1/2 HTTP/1.1
Host: api.example.com
1
2
HTTP/1.1 200 OK
Allow: GET,PATCH,DELETE,COPY,MOVE,SWAP

Request headers

You can use the following HTTP headers with a REST request:

  • Accept describing the desired response type and format
  • Content-Type describing the payload type and format
  • X-Siteaccess specifying the target SiteAccess
  • X-HTTP-Method-Override allowing to pass a custom method while using POST method as previously seen in HTTP method
  • Destination specifying where to move an item
  • X-Expected-User specifying the user needed for the request execution

Other headers related to authentication methods can be found in REST API authentication.

SiteAccess

To specify a SiteAccess when communicating with the REST API, provide a custom X-Siteaccess header. Otherwise, the default SiteAccess is used.

The following example shows what could be a SiteAccess called restapi dedicated to REST API accesses:

1
2
3
4
GET / HTTP/1.1
Host: api.example.com
Accept: application/vnd.ibexa.api.Root+json
X-Siteaccess: restapi

One of the principles of REST is that the same resource (such as content item, Location, content type) should be unique. It allows caching your REST API using a reverse proxy such as Varnish. If the same resource is available in multiple locations, cache purging is noticeably more complex. This is why SiteAccess matching with REST isn't enabled at URL level (or domain).

Media types

On top of methods, HTTP request headers allow you to personalize the request's behavior. On every resource, you can use the Accept header to indicate which format you want to communicate in, JSON or XML. This header is also used to specify the response type you want the server to send when multiple types are available.

  • Accept: application/vnd.ibexa.api.Content+xml to get Content (full data, Fields included) as XML
  • Accept: application/vnd.ibexa.api.ContentInfo+json to get ContentInfo (metadata only) as JSON

Media types are also used with the Content-Type header to characterize a request body or a response body. See Creating content with binary attachments below. Also see Creating session examples.

If the resource only returns one media type, it's also possible to skip it and to just specify the format using application/xml or application/json.

A response indicates hrefs to related resources and their media types.

Destination

The Destination request header is the request counterpart of the Location response header. It's used for a COPY, MOVE or SWAP operation to indicate where the resource should be moved, copied to or swapped with by using the ID of the parent or target Location.

Examples of such requests are:

Expected user

The X-Expected-User header specifies the user needed for the request execution. With this header, if the current username on server side isn't equal to X-Expected-User value, a 401 Unauthorized error is returned. Without this header, the request is executed with the current user who might be unexpected (like the Anonymous user if a previous authentication has expired) and an ambiguous response might be returned as a success not informing about a wrong user.

For example, it prevents a Content request to be executed with Anonymous user in the case of an expired authentication, and the response being a 200 OK but missing content items due to access rights difference with the expected user.

Request body

You can pass some short scalar parameters in the URIs or as GET parameters, but other resources need heavier structured payloads passed in the request body, in particular the ones to create (POST) or update (PATCH) items. In the REST API reference, request payload examples are given when needed.

One example is the creation of an authentication session.

When creating a content item, a special payload is needed if the ContentType has some Image or BinaryFile Fields as files need to be attached. See the example of a script uploading images below.

When searching for content items (or Locations), the query grammar is also particular. See the Search section below.

Creating content with binary attachments

The example below is a command-line script to upload images. It's based on the Symfony HttpClient.

This script:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
<?php declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use Symfony\Component\HttpClient\HttpClient;
use Symfony\Contracts\HttpClient\Exception as HttpException;

if ($argc < 2) {
    // Print script usage
    echo "Usage: php {$argv[0]} <FILE_PATH> [<IMAGE_NAME>]\n";
    exit(1);
}

if (!is_file($argv[1])) {
    echo "{$argv[1]} doesn't exist or is not a file.\n";
    exit(2);
}

// URL to Ibexa DXP installation and its REST API
$host = 'api.example.com';
$scheme = 'https';
$api = '/api/ibexa/v2';
$baseUrl = "{$scheme}://{$host}{$api}";

// User credentials
$username = 'admin';
$password = 'publish';

// Targets
$contentTypeId = 5; // "Image"
$parentLocationPath = '1/43/51'; // "Media > Images"
$sectionId = 3; // "Media"

$fileName = basename($argv[1]);
$fileSize = filesize($argv[1]);
$fileContent = base64_encode(file_get_contents($argv[1]));
$name = $argv[2] ?? $fileName;

// Request payload
$data = <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<ContentCreate>
  <ContentType href="$api/content/types/$contentTypeId" />
  <mainLanguageCode>eng-GB</mainLanguageCode>
  <LocationCreate>
    <ParentLocation href="$api/content/locations/$parentLocationPath" />
    <sortField>PATH</sortField>
    <sortOrder>ASC</sortOrder>
  </LocationCreate>
  <Section href="$api/content/sections/$sectionId" />
  <fields>
    <field>
      <fieldDefinitionIdentifier>name</fieldDefinitionIdentifier>
      <fieldValue>$name</fieldValue>
    </field>
    <field>
      <fieldDefinitionIdentifier>caption</fieldDefinitionIdentifier>
      <fieldValue>
        <value key="xml"><![CDATA[<section xmlns="http://ibexa.co/namespaces/ezpublish5/xhtml5/edit"><h1>$name</h1></section>]]></value>
      </fieldValue>
    </field>
    <field>
      <fieldDefinitionIdentifier>image</fieldDefinitionIdentifier>
      <fieldValue>
        <value key="fileName">$fileName</value>
        <value key="fileSize">$fileSize</value>
        <value key="data"><![CDATA[$fileContent]]></value>
      </fieldValue>
    </field>
  </fields>
</ContentCreate>
XML;

$client = HttpClient::createForBaseUri($baseUrl, [
    'auth_basic' => [$username, $password],
]);
$doc = new DOMDocument();

try {
    $response = $client->request('POST', "$baseUrl/content/objects", [
        'headers' => [
            'Content-Type: application/vnd.ibexa.api.ContentCreate+xml',
            'Accept: application/vnd.ibexa.api.ContentInfo+xml',
        ],
        'body' => $data,
    ]);
} catch (HttpException\TransportExceptionInterface $exception) {
    echo "Client error: {$exception->getMessage()}\n";
    exit(3);
}

if (201 !== $responseCode = $response->getStatusCode()) {
    if (!empty($response->getContent(false)) && $doc->loadXML($response->getContent(false)) && 'ErrorMessage' === $doc->firstChild->nodeName) {
        echo "Server error: {$doc->getElementsByTagName('errorCode')->item(0)->nodeValue} {$doc->getElementsByTagName('errorMessage')->item(0)->nodeValue}\n";
        echo "\t{$doc->getElementsByTagName('errorDescription')->item(0)->nodeValue}\n";
        exit(4);
    }
    $responseHeaders = $response->getInfo('response_headers');
    $error = $responseHeaders[0] ?? $responseCode;
    echo "Server error: $error\n";
    exit(5);
}

$doc->loadXML($response->getContent());

if ('Content' !== $doc->firstChild->nodeName || !$doc->firstChild->hasAttribute('id')) {
    echo "Response error: Unexpected response structure\n";
    exit(6);
}

$contentId = $doc->firstChild->getAttribute('id');

try {
    $response = $client->request('PUBLISH', "$baseUrl/content/objects/$contentId/versions/1", [
        'headers' => [
            'Accept: application/xml',
        ],
    ]);
} catch (HttpException\TransportExceptionInterface $exception) {
    echo "Client error: {$exception->getMessage()}\n";
    exit(7);
}

if (204 !== $responseCode = $response->getStatusCode()) {
    if (!empty($response->getContent(false)) && $doc->loadXML($response->getContent(false)) && 'ErrorMessage' === $doc->firstChild->nodeName) {
        echo "Server error: {$doc->getElementsByTagName('errorCode')->item(0)->nodeValue} {{$doc->getElementsByTagName('errorMessage')->item(0)->nodeValue}\n";
        echo "\t{$doc->getElementsByTagName('errorDescription')->item(0)->nodeValue}\n";
        exit(8);
    }
    $responseHeaders = $response->getInfo('response_headers');
    $error = $responseHeaders[0] ?? $responseCode;
    echo "Server error: $error\n";
    exit(9);
}

echo "Success: Image content item created with ID $contentId and published.\n";

exit(0);
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
<?php declare(strict_types=1);

require __DIR__ . '/vendor/autoload.php';

use Symfony\Component\HttpClient\HttpClient;
use Symfony\Contracts\HttpClient\Exception as HttpException;

if ($argc < 2) {
    // Print script usage
    echo "Usage: php {$argv[0]} <FILE_PATH> [<IMAGE_NAME>]\n";
    exit(1);
}

if (!is_file($argv[1])) {
    echo "{$argv[1]} doesn't exist or is not a file.\n";
    exit(2);
}

// URL to Ibexa DXP installation and its REST API
$host = 'api.example.com';
$scheme = 'https';
$api = '/api/ibexa/v2';
$baseUrl = "{$scheme}://{$host}{$api}";

// User credentials
$username = 'admin';
$password = 'publish';

// Targets
$contentTypeId = 5; // "Image"
$parentLocationPath = '1/43/51'; // "Media > Images"
$sectionId = 3; // "Media"

// Request payload
$data = [
    'ContentCreate' => [
        'ContentType' => [
            '_href' => "$api/content/types/$contentTypeId",
        ],
        'mainLanguageCode' => 'eng-GB',
        'LocationCreate' => [
            'ParentLocation' => [
                '_href' => "$api/content/locations/$parentLocationPath",
            ],
            'sortField' => 'PATH',
            'sortOrder' => 'ASC',
        ],
        'Section' => [
            '_href' => "$api/content/sections/$sectionId",
        ],
        'fields' => [
            'field' => [
                [
                    'fieldDefinitionIdentifier' => 'name',
                    'fieldValue' => $argv[2] ?? basename($argv[1]),
                ],
                [
                    'fieldDefinitionIdentifier' => 'image',
                    'fieldValue' => [
                        // Original file name
                        'fileName' => basename($argv[1]),
                        // File size in bytes
                        'fileSize' => filesize($argv[1]),
                        // File content must be encoded as Base64
                        'data' => base64_encode(file_get_contents($argv[1])),
                    ],
                ],
            ],
        ],
    ],
];

$client = HttpClient::createForBaseUri($baseUrl, [
    'auth_basic' => [$username, $password],
]);

try {
    $response = $client->request('POST', "$baseUrl/content/objects", [
        'headers' => [
            'Content-Type: application/vnd.ibexa.api.ContentCreate+json',
            'Accept: application/vnd.ibexa.api.ContentInfo+json',
        ],
        'json' => $data,
    ]);
} catch (HttpException\TransportExceptionInterface $exception) {
    echo "Client error: {$exception->getMessage()}\n";
    exit(3);
}

if (201 !== $responseCode = $response->getStatusCode()) {
    try {
        $response = $response->toArray(false);
        if (array_key_exists('ErrorMessage', $response)) {
            echo "Server error: {$response['ErrorMessage']['errorCode']} {$response['ErrorMessage']['errorMessage']}\n";
            echo "\t{$response['ErrorMessage']['errorDescription']}\n";
            exit(4);
        }
    } catch (HttpException\DecodingExceptionInterface $exception) {
    }
    $responseHeaders = $response->getInfo('response_headers');
    $error = $responseHeaders[0] ?? $responseCode;
    echo "Server error: $error\n";
    exit(5);
}

$response = $response->toArray();

if (!(array_key_exists('Content', $response) && array_key_exists('_id', $response['Content']))) {
    echo "Response error: Unexpected response structure\n";
    exit(6);
}

$contentId = $response['Content']['_id'];

try {
    $response = $client->request('PUBLISH', "$baseUrl/content/objects/$contentId/versions/1", [
        'headers' => [
            'Accept: application/json',
        ],
    ]);
} catch (HttpException\TransportExceptionInterface $exception) {
    echo "Client error: {$exception->getMessage()}\n";
    exit(7);
}

if (204 !== $responseCode = $response->getStatusCode()) {
    try {
        $response = $response->toArray(false);
        if (array_key_exists('ErrorMessage', $response)) {
            echo "Server error: {$response['ErrorMessage']['errorCode']} {$response['ErrorMessage']['errorMessage']}\n";
            echo "\t{$response['ErrorMessage']['errorDescription']}\n";
            exit(8);
        }
    } catch (HttpException\DecodingExceptionInterface $exception) {
    }
    $responseHeaders = $response->getInfo('response_headers');
    $error = $responseHeaders[0] ?? $responseCode;
    echo "Server error: $error\n";
    exit(9);
}

echo "Success: Image content item created with ID $contentId and published.\n";

exit(0);

Search (/views)

The /views route allows you to search in the repository. It works similarly to its PHP API counterpart.

The model allows combining criteria using the logical operators AND, OR and NOT.

Most Search Criteria are available in REST API. The suffix Criterion is added when used with REST API.

Most Sort Clauses are available too. They require no additional prefix or suffix.

The search request has a Content-Type: application/vnd.ibexa.api.ViewInput+xml or +json header to specify the format of its body's payload. The root node is <ViewInput> and it has two mandatory children: <identifier> and <Query>.

You can add version=1.1 to the Content-Type header to support the distinction between ContentQuery and LocationQuery instead of just Query which implicitly looks only for content items.

The following examples search for article and news typed content items everywhere or for content items of all types directly under Location 123. All those content items must be in the standard Section.

1
2
POST /views HTTP/1.1
Content-Type: application/vnd.ibexa.api.ViewInput+xml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8"?>
<ViewInput>
  <identifier>test</identifier>
  <Query>
    <Filter>
        <AND>
            <OR>
                <ContentTypeIdentifierCriterion>article</ContentTypeIdentifierCriterion>
                <ContentTypeIdentifierCriterion>news</ContentTypeIdentifierCriterion>
                <ParentLocationIdCriterion>123</ParentLocationIdCriterion>
            </OR>
            <SectionIdentifierCriterion>standard</SectionIdentifierCriterion>
        </AND>
    </Filter>
    <limit>10</limit>
    <offset>0</offset>
    <SortClauses>
      <ContentName>ascending</ContentName>
    </SortClauses>
  </ContentQuery>
</ViewInput>
1
2
POST /views HTTP/1.1
Content-Type: application/vnd.ibexa.api.ViewInput+xml; version=1.1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8"?>
<ViewInput>
  <identifier>test</identifier>
  <ContentQuery>
    <Filter>
        <AND>
            <OR>
                <ContentTypeIdentifierCriterion>article</ContentTypeIdentifierCriterion>
                <ContentTypeIdentifierCriterion>news</ContentTypeIdentifierCriterion>
                <ParentLocationIdCriterion>123</ParentLocationIdCriterion>
            </OR>
            <SectionIdentifierCriterion>standard</SectionIdentifierCriterion>
        </AND>
    </Filter>
    <limit>10</limit>
    <offset>0</offset>
    <SortClauses>
      <ContentName>ascending</ContentName>
    </SortClauses>
  </ContentQuery>
</ViewInput>
1
2
POST /views HTTP/1.1
Content-Type: application/vnd.ibexa.api.ViewInput+json
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
  "ViewInput": {
    "identifier": "test",
    "Query": {
      "Filter": {
        "AND": {
          "OR": {
            "ContentTypeIdentifierCriterion": [
              "article",
              "news"
            ],
            "ParentLocationIdCriterion": 123
          },
          "SectionIdentifierCriterion": "standard"
        }
      },
      "limit": "10",
      "offset": "0",
      "SortClauses": { "ContentName": "ascending" }
    }
  }
}
1
2
POST /views HTTP/1.1
Content-Type: application/vnd.ibexa.api.ViewInput+json; version=1.1
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
  "ViewInput": {
    "identifier": "test",
    "ContentQuery": {
      "Filter": {
        "AND": {
          "OR": {
            "ContentTypeIdentifierCriterion": [
              "article",
              "news"
            ],
            "ParentLocationIdCriterion": 123
          },
          "SectionIdentifierCriterion": "standard"
        }
      },
      "limit": "10",
      "offset": "0",
      "SortClauses": { "ContentName": "ascending" }
    }
  }
}

Note

In JSON, the structure for ContentTypeIdentifierCriterion with multiple values has a slightly different format as keys must be unique. In JSON, if there is only one item in SortClauses, it can be passed directly without an array to wrap it.

You can omit logical operators. If Criteria are of mixed types, they're wrapped in an implicit AND. If they're of the same type, they're wrapped in an implicit OR.

For example, the AND operator from previous example's Filter could be removed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<Filter>
    <AND>
        <OR>
            <ContentTypeIdentifierCriterion>article</ContentTypeIdentifierCriterion>
            <ContentTypeIdentifierCriterion>news</ContentTypeIdentifierCriterion>
            <ParentLocationIdCriterion>123</ParentLocationIdCriterion>
        </OR>
        <SectionIdentifierCriterion>standard</SectionIdentifierCriterion>
    </AND>
</Filter>
1
2
3
4
5
6
7
8
<Filter>
    <OR>
        <ContentTypeIdentifierCriterion>article</ContentTypeIdentifierCriterion>
        <ContentTypeIdentifierCriterion>news</ContentTypeIdentifierCriterion>
        <ParentLocationIdCriterion>123</ParentLocationIdCriterion>
    </OR>
    <SectionIdentifierCriterion>standard</SectionIdentifierCriterion>
</Filter>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
"Filter": {
  "AND": {
    "OR": {
       "ContentTypeIdentifierCriterion": [
        "article",
        "news"
      ],
      "ParentLocationIdCriterion": 123
    },
    "SectionIdentifierCriterion": "standard"
  }
},
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
"Filter": {
  "OR": {
     "ContentTypeIdentifierCriterion": [
      "article",
      "news"
    ],
    "ParentLocationIdCriterion": 123
  },
  "SectionIdentifierCriterion": "standard"
},