Application REST API

ContentGrid Applications expose a REST API that can be used to programmatically access the metadata and content stored in your ContententGrid application.

The API documentation gives examples using curl, but various tools or libraries can be used for making calls to the API.

Authentication

Authenticate to the ContentGrid Application using OpenID Connect

API Usage

Basic usage of the REST API

HAL Usage

Advanced usage of the REST API using HAL and HAL-FORMS as hypertext formats

Error handling

Handling of REST API errors using Problem Details

Subsections of Application REST API

Authentication

An access token is required to access a ContentGrid Application.

Access tokens are always obtained through OpenID Connect (OIDC). The authentication token contains user identity information and the IAM attributes assigned to the user.

OIDC clients

To identify the application that performs the authentication, an OIDC client needs to be registered in the Organization’s IAM Realm. An Organization can have multiple IAM Realms; ensure you are using the same Realm as the one used by the Application.

An OIDC client can be configured in the “Clients” tab of the IAM Realm.

Setting up an OIDC connection requires 3 configuration parameters that can be obtained from the OIDC client page:

  1. Issuer URI
  2. Client ID
  3. Client Secret

OIDC configuration can be discovered from the Issuer URI using OIDC Discovery. If OIDC discovery is not supported by your software, you can obtain the authorization and token endpoints manually from the discovery document, but this is not recommended.

Service account authentication

When using service account authentication, the access token is issued with its own privileges instead of with the privileges of a user.

To authenticate as a service account, you need to use the Client credentials grant.

By default, no IAM attributes are assigned to a service account. These need to be set up if the applicable permission policies require it to provide access.

curl https://auth.$REGION.contentgrid.cloud/realms/$REALM/protocol/openid-connect/token    \
  -u $CLIENT_ID:$CLIENT_SECRET       \
  -d grant_type=client_credentials

User authentication

When using user authentication, the access token is issued with the privileges of the user.

To use user authentication, you need to use the Authorization code grant. This flow will necessary include a pass through the user’s web browser as part of the flow to obtain an access token.

Supplying credentials to the application

The access token is supplied to the application by using bearer authorization.

Other methods of supplying access tokens (like an access_token query parameter or form-encoded body parameter) are not supported.

curl -i https://$APP_ID.$REGION.contentgrid.cloud/   \
    -H "Authorization: Bearer $TOKEN"
GET / HTTP/1.1
Authorization: Bearer $TOKEN

API Usage

ContentGrid applications expose a REST API using JSON as a response format. All JSON responses use HAL as a hypertext format, linking to related resources. Actions that can be taken on resources are specified using the HAL-FORMS format.

Before using the API, you need to authenticate and obtain an access token.

This documentation covers the basics of using the Application REST API, advanced usage with HAL is described in HAL Usage.

Because the specific API is generated from the configured data model, the examples will use an example datamodel.

OpenAPI spec

An OpenAPI spec for your application’s datamodel is available from the ContentGrid Console. Every Release has its own associated OpenAPI spec, which you can view or download.

For deployed applications, a copy of the OpenAPI spec is also available on /openapi.yaml (after authentication).

Resource types

A ContentGrid application can be configured with kinds of entities that have attributes and relations.

A single instance of such an entity is generically referred to as an entity-item. Multiple entity-items of the same kind together form an entity-collection.

These two form the main resource types in a ContentGrid application. To support these resource types, a couple of additional resource types exist. Their relation to the main resource types is explained in the Resource Types reference.

Basic operations

This section explains all the basic operations (create, read, update, delete) for the resource types.

The operations are explained in their minimal form, without any request modifiers applied.

Tip

You can follow along with the examples by creating an application with the Example datamodel set up. Define the following variables to make the examples easily runnable.

REGION="<Region where the application is deployed>"
APP_ID="<ID of the application>"
REALM="<Internal reference of your IAM Realm>"
CLIENT_ID="<IAM Service account Client ID>"
CLIENT_SECRET="<IAM Service account Client Secret>"

You will need to obtain an access token. This can be done easily with an IAM service account. The Authentication documentation contains more details on this.

TOKEN="$(curl -Ss https://auth.$REGION.contentgrid.cloud/realms/$REALM/protocol/openid-connect/token    \
 -u $CLIENT_ID:$CLIENT_SECRET       \
 -d grant_type=client_credentials | jq .access_token -r)"

An access token is valid for 5 minutes; you can obtain a new token by re-running the command above.

Next, set up some entities that will be used by the examples

INVOICE_ID="$(curl -Ss https://$APP_ID.$REGION.contentgrid.cloud/invoices   \
    -F "total_amount=15.95"   \
    -F "received=2024-07-15"   \
    -F "pay_before=2024-08-14"   \
    -F "document=dummy-invoice;filename=invoice.txt;type=text/plain"   \
    -H "Authorization: Bearer $TOKEN"   \
    | jq .id -r
)"
INVOICE2_ID="$(curl -Ss https://$APP_ID.$REGION.contentgrid.cloud/invoices   \
    -F "total_amount=123.4"   \
    -F "received=2020-01-01"   \
    -F "pay_before=2020-02-01"   \
    -H "Authorization: Bearer $TOKEN"   \
    | jq .id -r
)"
SUPPLIER_ID="$(curl -Ss https://$APP_ID.$REGION.contentgrid.cloud/suppliers   \
    -F "name=Test supplier"   \
    -F "telephone=test"   \
    -H "Authorization: Bearer $TOKEN"   \
    | jq .id -r
)"

entity-item operations

An entity-item resource is exposed on the path /<entity-name-plural>/{id} (e.g. /invoices/{id} for the invoice entity type). {id} is a placeholder for the actual ID of the entity-item.

The following operations are available:

Method Content-Type Description
GET application/hal+json Read the entity-item with the specified id
PUT application/json Replace the entity-item data with the data supplied in the request body
PATCH application/json Update the entity-item data, modifying only the fields supplied in the request body
DELETE N/A Remove the entity-item
Note

The difference between PUT and PATCH is how attributes that are not present in the request body are handled:

  • PUT: It is written to the entity-item as null, removing the stored value.
  • PATCH: It is skipped, the stored value is unchanged.

This rule also applies to attributes of type content. Omitting the field will remove the content.

Update entity-item using PUT/PATCH

To show the difference between updating an entity-item with PUT or PATCH, we first show the existing state

{
  "id": "3211be1d-1ed1-4850-8ea6-3fa3218031f6",
  "received": "2024-07-15",
  "document": {
     "size": 123456,
     "mimetype": "application/pdf",
     "filename": "example-invoice.pdf"
  },
  "pay_before": "2024-08-14",
  "total_amount": 15.95,
  "_links": {
    [...]
  },
  "_templates": {
    [...]
  }
}
curl -i -X PUT https://$APP_ID.$REGION.contentgrid.cloud/invoices/$INVOICE_ID   \
    --json "{\"pay_before\": \"2024-08-31\", \"received\": \"2024-07-15\", \"total_amount\": 15.95}"   \
    -H "Authorization: Bearer $TOKEN"
PUT /invoices/$INVOICE_ID HTTP/1.1
Authorization: Bearer $TOKEN
Content-Type: application/json

{"pay_before": "2024-08-31", "received": "2024-07-15", "total_amount": 15.95}
HTTP/1.1 204 No Content

Note that document was not present in the request body, and has now been set to null, removing the document.

curl -i https://$APP_ID.$REGION.contentgrid.cloud/invoices/$INVOICE_ID   \
    -H "Authorization: Bearer $TOKEN"
GET /invoices/$INVOICE_ID HTTP/1.1
Authorization: Bearer $TOKEN
HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": "3211be1d-1ed1-4850-8ea6-3fa3218031f6",
  "received": "2024-07-15",
  "document": null,
  "pay_before": "2024-08-31",
  "total_amount": 15.95,
  "_links": {
    [...]
  },
  "_templates": {
    [...]
  }
}
curl -i -X PATCH https://$APP_ID.$REGION.contentgrid.cloud/invoices/$INVOICE_ID   \
    --json "{\"pay_before\": \"2024-08-31\", \"received\": \"2024-07-15\", \"total_amount\": 15.95}"   \
    -H "Authorization: Bearer $TOKEN"
PATCH /invoices/$INVOICE_ID HTTP/1.1
Authorization: Bearer $TOKEN
Content-Type: application/json

{"pay_before": "2024-08-31", "received": "2024-07-15", "total_amount": 15.95}
HTTP/1.1 204 No Content

Fields that are absent in the request body are not modified from their original values.

curl -i https://$APP_ID.$REGION.contentgrid.cloud/invoices/$INVOICE_ID   \
    -H "Authorization: Bearer $TOKEN"
GET /invoices/$INVOICE_ID HTTP/1.1
Authorization: Bearer $TOKEN
HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": "3211be1d-1ed1-4850-8ea6-3fa3218031f6",
  "received": "2024-07-15",
  "document": {
     "size": 123456,
     "mimetype": "application/pdf",
     "filename": "example-invoice.pdf"
  },
  "pay_before": "2024-08-31",
  "total_amount": 15.95,
  "_links": {
    [...]
  },
  "_templates": {
    [...]
  }
}

entity-collection operations

The entity-collection resource is exposed on the path /<entity-name-plural> (e.g. /invoices for the invoice entity type).

The following operations are available:

Method Content-Type Description
GET application/hal+json Read the entity-collection, applying the specified filters and pagination
POST application/json Create a new entity-item. The response will be the created entity-item resource
POST application/x-www-form-urlencoded Create a new entity-item with a form submission. The response will be the created entity-item resource
POST multipart/form-data Create a new entity-item, and immediately upload content as well. The response will be the created entity-item resource
Create an entity using JSON
curl -i https://$APP_ID.$REGION.contentgrid.cloud/invoices   \
    --json "{\"received\": \"2024-07-15\", \"total_amount\": 15.95, \"pay_before\": \"2024-08-14\"}"   \
    -H "Authorization: Bearer $TOKEN"
POST /invoices HTTP/1.1
Authorization: Bearer $TOKEN
Content-Type: application/json

{"received": "2024-07-15", "total_amount": 15.95, "pay_before": "2024-08-14"}
HTTP/1.1 201 Created
Location: /invoices/3211be1d-1ed1-4850-8ea6-3fa3218031f6
Content-Type: application/prs.hal-forms+json

{
  "id": "3211be1d-1ed1-4850-8ea6-3fa3218031f6",
  "received": "2024-07-15",
  "document": null,
  "pay_before": "2024-08-14",
  "total_amount": 15.95,
  "_links": {
    [...]
  },
  "_templates": {
    [...]
  }
}
Create an entity with content using multipart/form-data
curl -i https://$APP_ID.$REGION.contentgrid.cloud/invoices   \
    -F received=2024-07-15   \
    -F total_amount=15.95   \
    -F pay_before=2024-08-14   \
    -F document=@example-invoice.pdf   \
    -H "Authorization: Bearer $TOKEN"
POST /invoices HTTP/1.1
Authorization: Bearer $TOKEN
Content-Type: multipart/form-data;boundary="delimiter123"

--delimiter123
Content-Disposition: form-data; name="received"

2024-07-15
--delimiter123
Content-Disposition: form-data; name="total_amount"

15.95
--delimiter123
Content-Disposition: form-data; name="pay_before"

2024-08-14
--delimiter123
Content-Disposition: form-data; name="document"; filename="example-invoice.pdf"

....Omitted contents of file example-invoice.pdf....
--delimiter123--
HTTP/1.1 201 Created
Location: /invoices/3211be1d-1ed1-4850-8ea6-3fa3218031f6
Content-Type: application/prs.hal-forms+json

{
  "id": "3211be1d-1ed1-4850-8ea6-3fa3218031f6",
  "received": "2024-07-15",
  "document": {
     "size": 123456,
     "mimetype": "application/pdf",
     "filename": "example-invoice.pdf"
  },
  "pay_before": "2024-08-14",
  "total_amount": 15.95,
  "_links": {
    [...]
  },
  "_templates": {
    [...]
  }
}

Collection query parameters

An entity-collection has 3 query parameters that control the collection itself:

Query Parameter Type Description
_size integer, 1-1000 or absent Page size, the number of entity-item resources that will be returned on a single page
_cursor string or absent Cursor for the page to retrieve. Absent to retrieve the first page
_sort string or absent Sort the collection by an attribute. Parameter can be repeated multiple times to sort on multiple attributes

Pagination

By default, every page contains 20 entity-items. This can be changed with the _size query parameter.

When there are multiple pages, the meta information will contain prev_cursor and/or next_cursor fields. A previous or next page can be retrieved by setting the _cursor query parameter to the value in the prev_cursor or next_cursor field.

Note

A cursor is not a fancy page number.

  • Cursors are opaque. A client should not parse or attempt to modify a cursor; they are only meaningful to the server.
  • Cursors are unique for every entity-collection. They can not be reused in a different context or used with different filter parameters.
  • Cursors are ephemeral. They should not be stored permanently to use at a later point in time, and they do not permanently identify a certain page
Example of navigating through pages with a cursor

curl -i https://$APP_ID.$REGION.contentgrid.cloud/invoices   \
    -H "Authorization: Bearer $TOKEN"
GET /invoices HTTP/1.1
Authorization: Bearer $TOKEN
HTTP/1.1 200 OK
Content-Type: application/prs.hal-forms+json

{
    "_embedded": {
        [...]
    },
    "page": {
        "size": 20,
        "next_cursor": "1wskufc1",
        "total_items_estimate": 48,
        "total_items_exact": 48
    }
}
Navigate to the next page. Note that a prev_cursor appears, which can be used to navigate to the previous page
curl -i https://$APP_ID.$REGION.contentgrid.cloud/invoices?_cursor=1wskufc1   \
    -H "Authorization: Bearer $TOKEN"
GET /invoices?_cursor=1wskufc1 HTTP/1.1
Authorization: Bearer $TOKEN
HTTP/1.1 200 OK
Content-Type: application/prs.hal-forms+json

{
    "_embedded": {
        [...]
    },
    "page": {
        "size": 20,
        "prev_cursor": "0dtcbvz0",
        "next_cursor": "0qk2h5e2",
        "total_items_estimate": 48,
        "total_items_exact": 48
    }
}

Navigate again to the next page. Note that this is the final page, and next_cursor is absent, indicating that there are no further pages.

curl -i https://$APP_ID.$REGION.contentgrid.cloud/invoices?_cursor=0qk2h5e2   \
    -H "Authorization: Bearer $TOKEN"
GET /invoices?_cursor=0qk2h5e2 HTTP/1.1
Authorization: Bearer $TOKEN
HTTP/1.1 200 OK
Content-Type: application/prs.hal-forms+json

{
    "_embedded": {
        [...]
    },
    "page": {
        "size": 20,
        "prev_cursor": "1wskufc1",
        "total_items_estimate": 48,
        "total_items_exact": 48
    }
}

Sorting

By default, the collection is not sorted. The collection can be sorted by using the _sort query parameter.

The _sort query parameter can be repeated multiple times to sort on multiple attributes.

The _sort value is composed of the attribute name, followed by ,asc or ,desc to sort ascending or descending.

Note that not all attributes support sorting. The OpenAPI spec lists the valid values for the _sort query parameter.

Example of collection sorting on multiple attributes

First sort ascending on received, then descending on total_amount

curl -i https://$APP_ID.$REGION.contentgrid.cloud/invoices?_sort=received,asc\&_sort=total_amount,desc   \
    -H "Authorization: Bearer $TOKEN"
GET /invoices?_sort=received,asc&_sort=total_amount,desc HTTP/1.1
Authorization: Bearer $TOKEN

Filtering

Search filters can be applied to the entity-collection. By default no filters are applied.

All filters are application-specific, and are mapped directly to query parameters. The OpenAPI spec lists all the valid filters as query parameters.

Note

Using a query parameter that is not a known search filter or one of the special query parameters will be ignored and have no effect.

Multiple search filters can be combined. Different search filters are AND’ed together; repeating the same search filter multiple times will perform an OR on the different values of that filter.

Example of collection filtering with multiple search filters

Show invoices with total_amount equal to 15.95

curl -i https://$APP_ID.$REGION.contentgrid.cloud/invoices?total_amount=15.95   \
    -H "Authorization: Bearer $TOKEN"
GET /invoices?total_amount=15.95 HTTP/1.1
Authorization: Bearer $TOKEN
Show invoices with total_amount equal to 15.95 OR 123.4
curl -i https://$APP_ID.$REGION.contentgrid.cloud/invoices?total_amount=19.95\&total_amount=123.4   \
    -H "Authorization: Bearer $TOKEN"
GET /invoices?total_amount=19.95&total_amount=123.4 HTTP/1.1
Authorization: Bearer $TOKEN
Show invoices with total_amount equal to 15.95 AND pay_before equal to 2024-08-14
curl -i https://$APP_ID.$REGION.contentgrid.cloud/invoices?total_amount=15.95\&pay_before=2024-08-14   \
    -H "Authorization: Bearer $TOKEN"
GET /invoices?total_amount=15.95&pay_before=2024-08-14 HTTP/1.1
Authorization: Bearer $TOKEN
Show invoices with (total_amount equal to 15.95 OR 123.4) AND pay_before equal to 2024-08-14
curl -i https://$APP_ID.$REGION.contentgrid.cloud/invoices?total_amount=15.95\&total_amount=123.4\&pay_before=2024-08-14   \
    -H "Authorization: Bearer $TOKEN"
GET /invoices?total_amount=15.95&total_amount=123.4&pay_before=2024-08-14 HTTP/1.1
Authorization: Bearer $TOKEN

relation operations

An entity can have relations to a different entity. These are exposed on the path /<entity-name-plural>/{id}/<relation-name>. (e.g. /invoices/{id}/supplier for the invoice entity type and a relation supplier)

Depending on the relation type, the available operations and responses are different.

to-one relation operations

These operations are applicable to one-to-one and many-to-one relations.

Method Content-Type Description
GET N/A Read the relation link. Redirects to the entity-item that the relation refers to
PUT text/uri-list Update the relation link. Requires a single URL to the entity-item that the relation should refer to
DELETE N/A Remove the relation link. No entity-items are deleted, only the link between the two is severed
Reading and updating a to-one relation

Initially, no supplier is linked to the invoice, and fetching the relation results in a 404 Not Found error.

curl -i https://$APP_ID.$REGION.contentgrid.cloud/invoices/$INVOICE_ID/supplier   \
    -H "Authorization: Bearer $TOKEN"
GET /invoices/$INVOICE_ID/supplier HTTP/1.1
Authorization: Bearer $TOKEN
HTTP/1.1 404 Not Found

Setting a relation requires sending the URL of the created entity-item in the body.

curl -i -X PUT https://$APP_ID.$REGION.contentgrid.cloud/invoices/$INVOICE_ID/supplier   \
    --data-binary "https://$APP_ID.$REGION.contentgrid.cloud/suppliers/$SUPPLIER_ID"   \
    -H 'Content-Type: text/uri-list'   \
    -H "Authorization: Bearer $TOKEN"
PUT /invoices/$INVOICE_ID/supplier HTTP/1.1
Authorization: Bearer $TOKEN
Content-Type: text/uri-list

https://$APP_ID.$REGION.contentgrid.cloud/suppliers/$SUPPLIER_ID
HTTP/1.1 204 No Content

Reading the relation afterwards will result in a redirect to the specific item that was linked.

curl -i https://$APP_ID.$REGION.contentgrid.cloud/invoices/$INVOICE_ID/supplier   \
    -H "Authorization: Bearer $TOKEN"
GET /invoices/$INVOICE_ID/supplier HTTP/1.1
Authorization: Bearer $TOKEN
HTTP/1.1 302 Found
Location: /suppliers/$SUPPLIER_ID

to-many relation operations

These operations are applicable to one-to-many and many-to-many relations.

Method Content-Type Description
GET application/json Read the relation collection. Redirects to the entity-collection containing all entity-items that the relation refers to
POST text/uri-list Adds entity-items to the relation. Accepts one or more URLs to the entity-items that should be added to the relation collection
DELETE N/A Clear the relation collection. No entity-items are deleted, only the relation collection is emptied.
Reading and updating a to-many relation

In this example, we will add 2 invoices to a supplier.

curl -i -X POST https://$APP_ID.$REGION.contentgrid.cloud/suppliers/$SUPPLIER_ID/invoices   \
    --data-binary "https://$APP_ID.$REGION.contentgrid.cloud/invoices/$INVOICE_ID
https://$APP_ID.$REGION.contentgrid.cloud/invoices/$INVOICE2_ID"   \
    -H 'Content-Type: text/uri-list'   \
    -H "Authorization: Bearer $TOKEN"
POST /suppliers/$SUPPLIER_ID/invoices HTTP/1.1
Authorization: Bearer $TOKEN
Content-Type: text/uri-list

https://$APP_ID.$REGION.contentgrid.cloud/invoices/$INVOICE_ID
https://$APP_ID.$REGION.contentgrid.cloud/invoices/$INVOICE2_ID
HTTP/1.1 204 No Content

Reading the relation will result in a redirect to a collection containing all items that are linked. Note that the query parameter used in the redirect is subject to change and is not part of the public API.

curl -i https://$APP_ID.$REGION.contentgrid.cloud/suppliers/$SUPPLIER_ID/invoices   \
    -H "Authorization: Bearer $TOKEN"
GET /suppliers/$SUPPLIER_ID/invoices HTTP/1.1
Authorization: Bearer $TOKEN
HTTP/1.1 302 Found
Location: /invoices?_internal_supplier=$SUPPLIER_ID

relation-item operations

A to-many relation also has a way to reference an individual item inside the relation; the relation-item resource. These are exposed on the path /<entity-name-plural>/{id}/<relation-name>/{itemId} (e.g. /suppliers/{id}/invoices/{itemId} for the supplier entity type and a relation invoices).

Method Description
GET Read the relation collection item. Redirects to the entity-item if it is part of the relation collection
DELETE Remove the relation collection item from the relation collection. No entity-items are deleted, only the link between the two is severed
Removing an item from a to-many relation

This will remove a single entity-item from the relation

curl -i -X DELETE https://$APP_ID.$REGION.contentgrid.cloud/suppliers/$SUPPLIER_ID/invoices/$INVOICE_ID   \
    -H "Authorization: Bearer $TOKEN"
DELETE /suppliers/$SUPPLIER_ID/invoices/$INVOICE_ID HTTP/1.1
Authorization: Bearer $TOKEN
HTTP/1.1 204 No Content

After the entity-item is removed from the relations, further attempts to remove the same item will result in a 404 Not Found error

curl -i -X DELETE https://$APP_ID.$REGION.contentgrid.cloud/suppliers/$SUPPLIER_ID/invoices/$INVOICE_ID   \
    -H "Authorization: Bearer $TOKEN"
DELETE /suppliers/$SUPPLIER_ID/invoices/$INVOICE_ID HTTP/1.1
Authorization: Bearer $TOKEN
HTTP/1.1 404 Not Found

entity-content operations

An entity-content resource is exposed on the path /<entity-name-plural>/{id}/<attribute-name> (e.g. /invoices/{id}/document for the invoice entity type and a content attribute document). {id} is a placeholder for the actual ID of the entity-item.

The following operations are available:

Method Content-Type Description
GET any Retrieve stored file. If no file is currently stored, responds with HTTP 404 Not Found
PUT any Overwrite file with contents of request body
PUT multipart/form-data Overwrite file with contents of the file form-field
DELETE N/A Remove stored file

For the GET and PUT with arbitrary Content-Type; the filename is provided in the Content-Disposition header. If no filename is provided during content upload, the filename field of the content attribute will be set to null

Read and write content

Read content of an invoice with id $INVOICE_ID

curl -i https://$APP_ID.$REGION.contentgrid.cloud/invoices/$INVOICE_ID/document   \
    -H "Authorization: Bearer $TOKEN"
GET /invoices/$INVOICE_ID/document HTTP/1.1
Authorization: Bearer $TOKEN
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: attachment;filename="example-invoice.pdf"

....Raw PDF body omitted....
Overwrite content of an invoice using direct upload, setting the filename as well.
curl -i -X PUT https://$APP_ID.$REGION.contentgrid.cloud/invoices/$INVOICE_ID/document   \
    --data-binary "@example-invoice.pdf"   \
    -H 'Content-Disposition: attachment;filename="example-invoice.pdf"'   \
    -H 'Content-Type: application/pdf'   \
    -H "Authorization: Bearer $TOKEN"
PUT /invoices/$INVOICE_ID/document HTTP/1.1
Authorization: Bearer $TOKEN
Content-Disposition: attachment;filename="example-invoice.pdf"
Content-Type: application/pdf

....Omitted contents of file example-invoice.pdf....
HTTP/1.1 204 No Content
Overwrite content of an invoice using multipart upload
curl -i -X PUT https://$APP_ID.$REGION.contentgrid.cloud/invoices/$INVOICE_ID/document   \
    -F file=@example-invoice.pdf   \
    -H "Authorization: Bearer $TOKEN"
PUT /invoices/$INVOICE_ID/document HTTP/1.1
Authorization: Bearer $TOKEN
Content-Type: multipart/form-data;boundary="delimiter123"

--delimiter123
Content-Disposition: form-data; name="file"; filename="example-invoice.pdf"

....Omitted contents of file example-invoice.pdf....
--delimiter123--
HTTP/1.1 204 No Content

Request modifiers

The basic operations explained above can be extended with additional functionality. Additional functionality is layered on top of the basic operations using HTTP headers.

Conditional requests

Conditional requests allow to check a precondition before applying the request to the target resource. They are an implementation of RFC9110 Conditional Requests.

The primary usage of conditional requests is to prevent the “lost update” problem, where one system overwrites the changes of another system that made a write between the read and write of the first system.

Comparing without/with conditional requests
Without conditional request With conditional request
sequenceDiagram
    autonumber
    participant a as System A
    participant s as ContentGrid API
    participant b as System B

    a ->>+ s: GET item
    s -->>- a: 200 OK<br>ETag: "abc"<br>original data

    b ->>+ s: PUT item<br>new data
    s -->>- b: 204 No Content<br>ETag: "def"

    a ->>+ s: PUT item<br>other data
    s -->>- a: 204 No Content<br>ETag: "mno"
    note over a, s: "new data" was accidentally<br>overwritten by "other data"<br>without System A having seen it
sequenceDiagram
    autonumber
    participant a as System A
    participant s as ContentGrid API
    participant b as System B

    a ->>+ s: GET item
    s -->>- a: 200 OK<br>ETag: "abc"<br>original data

    b ->>+ s: PUT item<br>new data
    s -->>- b: 204 No Content<br>ETag: "def"

    a ->>+ s: PUT item<br>If-Match: "abc"<br>other data
    rect red
    s -->>- a: 419 Precondition Failed
    note over a, s : request was rejected<br>System A did not accidentally<br>overwrite the "new data"
    end

Conditional requests can be used on the resources that have an ETag response header, which are these:

Name URL
entity-item /<entity-name-plural>/{id}
to-one relation /<entity-name-plural>/{id}/<relation-name>
entity-content /<entity-name-plural>/{id}/<attribute-name>

To perform conditional requests, a previously obtained ETag value has to be placed in the If-Match or If-None-Match headers. Note that the quotes are part of the ETag value.

The If-Modified-Since and If-Unmodified-Since headers are not supported, because no Last-Modified response header is present, and they are less precise.

Using a conditional request for updating

In this example, we’re going to move the pay_before date one day earlier.

curl -i -X GET https://$APP_ID.$REGION.contentgrid.cloud/invoices/$INVOICE_ID   \
    -H "Authorization: Bearer $TOKEN"
GET /invoices/$INVOICE_ID HTTP/1.1
Authorization: Bearer $TOKEN
HTTP/1.1 200 OK
ETag: "1e0k4j9"
Content-Type: application/hal+json

{
  "id": "019be613-baa4-7644-9438-e252be3bbe84",
  "received": "2024-07-15",
  "document": {
    "filename": "invoice.txt",
    "length": 13,
    "mimetype": "text/plain"
  },
  "pay_before": "2024-08-14",
  "total_amount": 15.95,
  "_links": {
    [...]
  }
}

After retrieving the invoice response, we take note of the ETag header, and place if in the If-Match header for our next request. Then we perform a PATCH request to update only the pay_before attribute.

Between our requests…

To execute this example, you will need to change the If-Match header to the correct value yourself.

curl -i -X PATCH https://$APP_ID.$REGION.contentgrid.cloud/invoices/$INVOICE_ID   \
    --json "{\"pay_before\": \"2024-08-13\"}"   \
    -H 'If-Match: "1e0k4j9"'   \
    -H "Authorization: Bearer $TOKEN"
PATCH /invoices/$INVOICE_ID HTTP/1.1
Authorization: Bearer $TOKEN
If-Match: "1e0k4j9"
Content-Type: application/json

{"pay_before": "2024-08-13"}
HTTP/1.1 204 No Content
ETag: "i4xcj6"

The response to the modification also contains the new ETag value for the updated version of the object.

curl -i -X PATCH https://$APP_ID.$REGION.contentgrid.cloud/invoices/$INVOICE_ID   \
    --json "{\"pay_before\": \"2024-08-13\"}"   \
    -H 'If-Match: "1e0k4j9"'   \
    -H "Authorization: Bearer $TOKEN"
PATCH /invoices/$INVOICE_ID HTTP/1.1
Authorization: Bearer $TOKEN
If-Match: "1e0k4j9"
Content-Type: application/json

{"pay_before": "2024-08-13"}
HTTP/1.1 412 Precondition Failed
Content-Type: application/problem+json

{
  "type": "https://contentgrid.cloud/problems/unsatisfied-version",
  "title": "Object has changed",
  "detail": "Requested version constraint 'is any of [exactly '1e0k4j9']' can not be satisfied (actual version exactly '1r061qt')",
  "status": 412,
  "actual_version": "1r061qt"
}

More details about the error response format can be found in the error handling section.

Range requests

Range requests allow retrieving a partial representation of the entity-content resource. They are an implementation of RFC9110 Range Requests.

Range requests are only supported on the entity-content resource (/<entity-name-plural>/{id}/<attribute-name>), as indicated by the Accept-Ranges header.

You usually don’t use range requests directly, but some tools that work with potentially large files (like PDF viewers or download managers) can make use of them.

Warning

When composing a file together from multiple parts, it is critical to check that the ETags of all parts are identical.

Otherwise, a concurrent change of the content will result in the composed file being corrupted.

Using a range request to retrieve a part of a resource

In this example, the first 4 bytes are fetched separately.

curl -i https://$APP_ID.$REGION.contentgrid.cloud/invoices/$INVOICE_ID/document   \
    -H 'Range: bytes=0-3'   \
    -H "Authorization: Bearer $TOKEN"
GET /invoices/$INVOICE_ID/document HTTP/1.1
Authorization: Bearer $TOKEN
Range: bytes=0-3
HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Range: bytes 0-3/13
ETag: "8xhtyfikrvnts4izydc95vfcn"
Content-Length: 4
Content-Type: text/plain

dumm

After the initial fetch, If-Match is used to ensure that the rest of the same file is fetched.

curl -i https://$APP_ID.$REGION.contentgrid.cloud/invoices/$INVOICE_ID/document   \
    -H 'If-Match: "8xhtyfikrvnts4izydc95vfcn"'   \
    -H 'Range: bytes=4-'   \
    -H "Authorization: Bearer $TOKEN"
GET /invoices/$INVOICE_ID/document HTTP/1.1
Authorization: Bearer $TOKEN
If-Match: "8xhtyfikrvnts4izydc95vfcn"
Range: bytes=4-
HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Range: bytes 4-12/13
ETag: "8xhtyfikrvnts4izydc95vfcn"
Content-Length: 9
Content-Type: text/plain

y-invoice

HAL Usage

ContentGrid applications expose a REST API using JSON as a response format. All JSON responses use HAL as a hypertext format, linking to related resources. Actions that can be taken on resources are specified using the HAL-FORMS format.

Before using the API, you need to authenticate and obtain an access token.

This guide explains how HAL features make the API discoverable and how to leverage HAL features to build robust integrations.

Basic usage of the API, where no automatic adaptations to the data model are necessary is described in API Usage.

When to use HAL?

Using HAL is only recommended when building integrations with ContentGrid that have to dynamically discover the datamodel and adjust to it.

It is particularly useful for systems that integrate with multiple distinct ContentGrid applications, which will have different datamodels.

When building such a dynamic system, you want it to automatically discover the datamodel and automatically adjust to it when it changes.

HAL provides several benefits for these systems:

  • Discoverability: Navigate the API by following links instead of constructing URLs manually
  • Flexibility: Discover available operations dynamically instead of hardcoding them
  • Evolvability: Adapt to data model changes without breaking your integration
  • Self-documentation: The API tells you what actions are possible on each resource

HAL Basics

HAL and HAL-FORMS are built on top of JSON. They add 3 special fields to the resource objects:

  • _links: Contains links to related resources
  • _embedded: Directy embeds related resources
  • _templates: Describes operations that can be performed on this resource (HAL-FORMS)
Invoice resource with HAL and HAL-FORMS fields
{
  "id": "3211be1d-1ed1-4850-8ea6-3fa3218031f6",
  "received": "2024-07-15",
  "document": null,
  "pay_before": "2024-08-14",
  "total_amount": 15.95,
  "_links": {
    "self": {
      "href": "https://app.contentgrid.example/invoices/3211be1d-1ed1-4850-8ea6-3fa3218031f6"
    },
    "cg:relation": [
      {
        "href": "https://app.contentgrid.example/invoices/3211be1d-1ed1-4850-8ea6-3fa3218031f6/supplier",
        "title": "Supplier",
        "name": "supplier"
      }
    ],
    "cg:content": [
      {
        "href": "https://app.contentgrid.example/invoices/3211be1d-1ed1-4850-8ea6-3fa3218031f6/document",
        "title": "Document",
        "name": "document"
      }
    ],
    "curies": [
      {
        "href": "https://contentgrid.cloud/rels/contentgrid/{rel}",
        "name": "cg",
        "templated": true
      }
    ]
  },
  "_templates": {
    "default": {
      "method": "PUT",
      "contentType": "application/json",
      "properties": [
        {
          "name": "received",
          "prompt": "Received",
          "required": true,
          "type": "datetime"
        },
        {
          "name": "pay_before",
          "prompt": "Pay before",
          "required": true,
          "type": "datetime"
        },
        {
          "name": "total_amount",
          "prompt": "Total amount",
          "required": true,
          "type": "number"
        }
      ]
    },
    "delete": {
      "method": "DELETE",
      "properties": []
    },
    "set-supplier": {
      "method": "PUT",
      "contentType": "text/uri-list",
      "properties": [
        {
          "name": "supplier",
          "type": "url"
        }
      ],
      "target": "https://app.contentgrid.example/invoices/3211be1d-1ed1-4850-8ea6-3fa3218031f6/supplier"
    },
    "clear-supplier": {
      "method": "DELETE",
      "properties": [],
      "target": "https://app.contentgrid.example/invoices/3211be1d-1ed1-4850-8ea6-3fa3218031f6/supplier"
    }
  }
}

Links connect resources together. Instead of constructing URLs yourself, you use the provided link to go to the related resource.

Links express a relation between the resource they appear on (the link context), and the link target. The link relation type (RFC8288 Sec 3.3) determines the kind of relation between the link context and the link target.

Next to the standard link relation types, ContentGrid defines extension link relation types.

CURIEs

CURIEs (Compact URIs) are a shorthand notation for extension link relation types. They are used as defined in the HAL specification. Before comparing a link relation, a CURIE must be expanded into the full URI.

See the Link Relation Types reference for the formal definition of CURIE expansion and examples.

Working with Embedded Resources

Instead of linking to a related resource, it is also possible to directly embed it in the main resource. This is of only possible when the resource to be embedded is also a HAL resource.

In the ContentGrid application, this is mostly used for the entity-collection resource, where the page is the main resource, and all items on the page are embedded resources.

Link Relation Types work the same for embedded resources as they do for links.

An embedded resource should have a self link. The link attributes on the self-link of the embedded resource can be considered as the link attributes that would be present on a normal link to the embedded resource.

A HAL resource with embedded resources
{
  "_embedded": {
    "item": [
      {
        "id": "62282aba-13bc-11f1-9a7d-ebcebd0bcf11",
        [...]
        "_links": {
          "self": {
            "href": "/invoices/62282aba-13bc-11f1-9a7d-ebcebd0bcf11",
            "title": "Invoice 62282aba-13bc-11f1-9a7d-ebcebd0bcf11"
          },
          [...]
        }
      }
    ]
  },
  [...]
}

The equivalent using only links would be

{
  "_links": {
    "item": [
      {
        "href": "/invoices/62282aba-13bc-11f1-9a7d-ebcebd0bcf11",
        "title": "Invoice 62282aba-13bc-11f1-9a7d-ebcebd0bcf11"
      },
    ]
  },
  [...]
}

Working with HAL-FORMS Templates

Where HAL links describe related resources, HAL-FORMS templates describe the operations available on the resource.

HAL-FORMS are described in their own specification. ContentGrid adds additional functionality to the base specification, described in HAL-FORMS extensions.

The keys in _templates tell you what forms are available.

ContentGrid standardizes the following keys:

Resource Key Description
entity-item default Update the resource
entity-item delete Delete the resource
entity-item set-<relation> Update a to-one relation
entity-item add-<relation> Add items to a to-many relation
entity-item clear-<relation> Remove all items from a relation
entity-profile create-form Create a new item of the type described by the profile
entity-profile search Search within the collection of the type described by the profile

HAL-FORMS Extensions

ContentGrid extends the HAL-FORMS specification with support for nested JSON objects, additional body encodings (multipart/form-data and text/uri-list), and extended options element behavior.

See the HAL-FORMS Extensions reference for the full specification.

The entity-collection resource provides HAL links that can be used pagination.

Pagination uses the standard IANA-registered link relation types for their purpose:

  • first: First page of results (may be absent if you’re already on the first page)
  • prev: Previous page of results (absent when on the first page)
  • next: Next page of results (absent when on the last page)
  • self: Current page

To navigate through pages, follow the next link until it’s absent. The cursors in these links are opaque values managed by the server - don’t try to parse or construct them yourself.

Discovering the API Structure

Start at the entities-root resource to discover the entire API.

Example
curl -i https://$APP_ID.$REGION.contentgrid.cloud/   \
    -H "Authorization: Bearer $TOKEN"
GET / HTTP/1.1
Authorization: Bearer $TOKEN
HTTP/1.1 200 OK
Content-Type: application/prs.hal-forms+json

{
  "_links": {
    "self": {
      "href": "https://app.contentgrid.example/"
    },
    "profile": {
      "href": "https://app.contentgrid.example/profile"
    },
    "cg:entity": [
      {
        "href": "https://app.contentgrid.example/invoices",
        "title": "Invoices",
        "name": "invoice"
      },
      {
        "href": "https://app.contentgrid.example/suppliers",
        "title": "Suppliers",
        "name": "supplier"
      }
    ],
    "curies": [
      {
        "href": "https://contentgrid.cloud/rels/contentgrid/{rel}",
        "name": "cg",
        "templated": true
      }
    ]
  }
}

The root resource contains cg:entity links to all entity collections in your data model. Use these to discover what entities are available without hardcoding entity names.

Alternatively, follow the profile link to arrive at the profile-root, and follow the cg:entity links there to arrive at the entity-profiles for every entity in your data model.

Using Entity Profiles

Entity profiles provide machine-readable metadata about entity types in a ContentGrid application. They describe the attributes, relations, constraints, and available operations for each entity type.

The entity-profile supports content negotiation between two formats:

Accept Header Format
application/prs.hal-forms+json HAL-FORMS format with embedded resources and templates
application/schema+json JSON Schema format (JSON Schema 2020-12)

Profiles are linked from:

HAL-FORMS Format

The HAL-FORMS profile format is the richest representation. It describes the full model of an entity type, its attributes and relations, and provides search and create-form templates.

The full HAL-FORMS entity profile format is documented in the Entity Profiles: HAL-FORMS Format reference.

JSON Schema Format

Request a profile with Accept: application/schema+json to receive a JSON Schema (2020-12) representation of the entity type.

In this format:

  • Attributes are mapped to properties with their corresponding JSON Schema types
  • Relations are represented as properties with "format": "uri"
  • Content attributes use "$ref": "#/$defs/content", with sub-attribute properties defined in $defs.content
  • Required attributes are listed in the required array
Entity profile in JSON Schema format
curl -i https://$APP_ID.$REGION.contentgrid.cloud/profile/invoices   \
    -H 'Accept: application/schema+json'   \
    -H "Authorization: Bearer $TOKEN"
GET /profile/invoices HTTP/1.1
Authorization: Bearer $TOKEN
Accept: application/schema+json
HTTP/1.1 200 OK
Content-Type: application/schema+json

{
  "title": "Invoice",
  "$defs": {
    "content": {
      "type": "object",
      "properties": {
        "filename": {
          "title": "Filename",
          "readOnly": false,
          "type": "string"
        },
        "length": {
          "title": "Size",
          "description": "File size in bytes",
          "readOnly": true,
          "type": "integer"
        },
        "mimetype": {
          "title": "Mimetype",
          "description": "Technical indicator of the type of the file",
          "readOnly": false,
          "type": "string"
        }
      }
    }
  },
  "properties": {
    "pay_before": {
      "title": "Pay before",
      "readOnly": false,
      "type": "string",
      "format": "date"
    },
    "total_amount": {
      "title": "Total amount",
      "readOnly": false,
      "type": "number"
    },
    "document": {
      "title": "Document",
      "readOnly": false,
      "$ref": "#/$defs/content"
    },
    "supplier": {
      "title": "Supplier",
      "readOnly": false,
      "type": "string",
      "format": "uri"
    },
    "received": {
      "title": "Received",
      "readOnly": false,
      "type": "string",
      "format": "date"
    },
    "id": {
      "title": "id",
      "readOnly": true,
      "type": "string",
      "format": "uuid"
    }
  },
  "required": [
    "received",
    "pay_before",
    "total_amount"
  ],
  "type": "object",
  "$schema": "https://json-schema.org/draft/2020-12/schema"
}

Error handling

The ContentGrid REST API uses the standard HTTP status codes to signal errors.

RFC9457 Problem Details are used to provide additional information about the error condition.

HTTP Status Codes

The ContentGrid Application API uses standard HTTP status codes to indicate the success or failure of API requests.

These status codes provide high-level information about what happened with your request, and are generically described in RFC9110.

Successful Responses (2xx)

  • 200 OK: The request succeeded. The response body contains the requested resource or data.
  • 201 Created: A new resource was successfully created. The Location header contains the URL of the newly created resource.
  • 204 No Content: The request succeeded but there is no response body. This is typically returned after successful updates or deletions.

Redirection Responses (3xx)

  • 302 Found: The resource is temporarily available at a different URI. The Location header contains the URI where the resource can be found. This is commonly used when accessing relations.

Client Error Responses (4xx)

  • 400 Bad Request: The request is malformed or contains invalid data. The response body may contain a problem detail explaining what went wrong if the request is sufficiently syntactically valid to be able to be processed.
  • 401 Unauthorized: The request lacks valid authentication credentials.
  • 403 Forbidden: The server understood the request but refuses to authorize it. This occurs when you don’t have permission to perform the requested operation.
  • 404 Not Found: The requested resource does not exist. This could be an entity that doesn’t exist, an empty relation, or an unknown endpoint.
  • 409 Conflict: The request conflicts with the current state of the resource. This occurs with unique constraint violations or when trying to delete an entity that is still referenced by required relations.
  • 412 Precondition Failed: The condition specified in the request headers (If-Match or If-None-Match) was not met. This typically occurs when using optimistic locking and the entity has been modified by another request.
  • 415 Unsupported Media Type: The Content-Type header specifies a media type that the server doesn’t support for this endpoint.

Server Error Responses (5xx)

  • 500 Internal Server Error: An unexpected error occurred on the server. Contact support if this persists.

Problem Details (RFC 9457)

When an error occurs (4xx or 5xx status codes), the API returns a standardized error response using the RFC9457 Problem Details format. This provides a consistent, machine-readable way to communicate error information.

Problem Detail Structure

All error responses have Content-Type: application/problem+json and include these standard fields:

  • type (string): A URI identifying the problem type (e.g., https://contentgrid.cloud/problems/not-found/entity-item)
  • title (string): A short, human-readable summary of the problem type
  • detail (string): A human-readable explanation specific to this occurrence of the problem
  • status (number): The HTTP status code for this problem
  • Additional properties: Problem-specific fields that provide more context
Example
curl -i -X PATCH https://$APP_ID.$REGION.contentgrid.cloud/invoices/$INVOICE_ID   \
    --json "{\"pay_before\": \"2024-08-13\"}"   \
    -H 'If-Match: "outdated-version"'   \
    -H "Authorization: Bearer $TOKEN"
PATCH /invoices/$INVOICE_ID HTTP/1.1
Authorization: Bearer $TOKEN
If-Match: "outdated-version"
Content-Type: application/json

{"pay_before": "2024-08-13"}
HTTP/1.1 412 Precondition Failed
Content-Type: application/problem+json

{
  "type": "https://contentgrid.cloud/problems/unsatisfied-version",
  "title": "Object has changed",
  "detail": "Requested version constraint 'is any of [exactly 'outdated-version']' can not be satisfied (actual version exactly '1r061qt')",
  "status": 412,
  "actual_version": "1r061qt"
}

Problem Types Reference

For a complete catalog of all problem types returned by the ContentGrid API, see the Problem Types reference.