---
title: "Cookbook"
description: "Recipes for the HTTP API with real requests and responses: pagination, creating and updating records, uploads, error handling and the OAuth flow."
canonical: cookbook.html
navigation-title: "Cookbook"
---

<a id="usage-examples"></a>

<a id="cookbook"></a>

# Cookbook

- [Read all records of a table](#read-all-records-of-a-table)
- [Find records](#find-records)
- [Create a content element](#create-a-content-element)
- [Change a single field](#change-a-single-field)
- [Upload a file](#upload-a-file)
- [Handle errors](#handle-errors)
- [Run the OAuth flow](#run-the-oauth-flow)
- [Generate a client](#generate-a-client)

> [!NOTE]
> We recorded the responses on this page on TYPO3 14.3.10 with the content of
> EXT:introduction. The mark `…` shows a shortened response. The fields of
> your records depend on the TCA of your installation, see
> [API documentation (OpenAPI)](../usage/open-api.md#usage-openapi).

The recipes get the address of the instance and a token from the environment:

```bash
export TYPO3_URL='https://example.org'
export TYPO3_TOKEN='<token>'
```

<a id="usage-examples-pagination"></a>

<a id="read-all-records-of-a-table"></a>

## Read all records of a table

A list returns not more than `limit` records. The maximum value of `limit` is
\200. If `next` is not `null`, send its value as `cursor` to get the next page.
Without `language`, the list contains the records of the default language and
of all languages. Set `language` to get translations.

**PHP**

```php
function allRecords(string $table, int $limit = 100): Generator
{
    $base = getenv('TYPO3_URL') . '/typo3/api/v1';
    $context = stream_context_create(['http' => [
        'header' => 'Authorization: Bearer ' . getenv('TYPO3_TOKEN'),
        'ignore_errors' => true,
    ]]);
    $query = ['limit' => $limit];
    do {
        $url = "$base/records/$table?" . http_build_query($query);
        $body = file_get_contents($url, false, $context);
        $page = json_decode($body, true, flags: JSON_THROW_ON_ERROR);
        if (isset($page['status'])) {
            $message = $page['status'] . ' ' . $page['detail'];
            throw new RuntimeException($message);
        }
        foreach ($page['items'] as $record) {
            yield $record;
        }
        $query['cursor'] = $page['next'];
    } while ($page['next'] !== null);
}

foreach (allRecords('pages') as $page) {
    echo $page['uid'], ' ', $page['label'], PHP_EOL;
}
```

**JavaScript**

```javascript
const base = process.env.TYPO3_URL + '/typo3/api/v1';
const headers = { Authorization: `Bearer ${process.env.TYPO3_TOKEN}` };

async function* allRecords(table, limit = 100) {
    let cursor = null;
    do {
        const query = new URLSearchParams({ limit });
        if (cursor) query.set('cursor', cursor);
        const url = `${base}/records/${table}?${query}`;
        const response = await fetch(url, { headers });
        if (!response.ok) {
            const problem = await response.json();
            throw new Error(`${problem.status} ${problem.detail}`);
        }
        const page = await response.json();
        yield* page.items;
        cursor = page.next;
    } while (cursor);
}

for await (const page of allRecords('pages')) {
    console.log(page.uid, page.label);
}
```

**TypeScript**

```typescript
interface RecordReference {
    uid: number;
    pid: number;
    label: string;
    type: string | null;
    hidden: boolean | null;
    language: number | null;
}

interface RecordList {
    items: RecordReference[];
    next: string | null;
}

const base = `${process.env.TYPO3_URL}/typo3/api/v1`;
const headers = { Authorization: `Bearer ${process.env.TYPO3_TOKEN}` };

async function* allRecords(
    table: string,
    limit = 100,
): AsyncGenerator<RecordReference> {
    let cursor: string | null = null;
    do {
        const query = new URLSearchParams({ limit: String(limit) });
        if (cursor) query.set('cursor', cursor);
        const url = `${base}/records/${table}?${query}`;
        const response = await fetch(url, { headers });
        if (!response.ok) {
            const problem = await response.json();
            throw new Error(`${problem.status} ${problem.detail}`);
        }
        const page = (await response.json()) as RecordList;
        yield* page.items;
        cursor = page.next;
    } while (cursor);
}

for await (const page of allRecords('pages')) {
    console.log(page.uid, page.label);
}
```

<a id="usage-examples-filter"></a>

<a id="find-records"></a>

## Find records

Use the filters `pid`, `type`, `language` and `search`. The filter `search` uses
the label field of the table:

```bash
curl -G -H "Authorization: Bearer $TYPO3_TOKEN" \
    "$TYPO3_URL/typo3/api/v1/records/tt_content" \
    -d pid=3 -d search=Hello -d limit=1
```

Response

```json
{
    "items": [
        {
            "uid": 227,
            "pid": 3,
            "label": "Hello from the API",
            "type": "text",
            "hidden": false,
            "language": 0
        }
    ],
    "next": null
}
```

A list contains only these fields. To get the complete record, use
`GET /typo3/api/v1/records/tt_content/227`.

<a id="usage-examples-create"></a>

<a id="create-a-content-element"></a>

## Create a content element

The path contains the table and the page. `_schema` must match the table.
This action needs the scopes `content:read` and `content:write`.

```bash
curl -i -X POST "$TYPO3_URL/typo3/api/v1/records/tt_content/3" \
    -H "Authorization: Bearer $TYPO3_TOKEN" \
    -H 'Content-Type: application/json' \
    -d '{
        "record": {
            "_schema": "tt_content",
            "CType": "text",
            "header": "Hello from the API",
            "bodytext": "<p>Created by an import.</p>"
        }
    }'
```

The status of the response is `201`. The `Location` header contains the URL of
the new record. The body contains the complete record:

Response (shortened)

```text
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Location: https://example.org/typo3/api/v1/records/tt_content/227

{
    "record": {
        "_schema": "tt_content",
        "uid": 227,
        "pid": 3,
        "CType": "text",
        "header": "Hello from the API",
        …
    }
}
```

> [!NOTE]
> The target must be a page. This action cannot create records with `pid` \0.
> The status of the response is then `400`, with the detail
> `Target page not found`.

<a id="usage-examples-update"></a>

<a id="change-a-single-field"></a>

## Change a single field

`PATCH` changes only the given fields and returns the complete record:

```bash
curl -X PATCH "$TYPO3_URL/typo3/api/v1/records/tt_content/227" \
    -H "Authorization: Bearer $TYPO3_TOKEN" \
    -H 'Content-Type: application/json' \
    -d '{"record": {"_schema": "tt_content", "header": "New header"}}'
```

`DELETE` on the same address returns the status `204`. After this, a request for
the record returns the status `404`.

<a id="usage-examples-upload"></a>

<a id="upload-a-file"></a>

## Upload a file

The request body is the content of the file. `folder` and `name` are query
parameters:

```bash
curl -X POST "$TYPO3_URL/typo3/api/v1/files?folder=1:/&name=report.txt" \
    -H "Authorization: Bearer $TYPO3_TOKEN" \
    -H 'Content-Type: text/plain' \
    --data-binary @report.txt
```

Response with status 201 (shortened)

```json
{
    "_schema": "sys_file",
    "uid": 81,
    "identifier": "1:/report.txt",
    "folder": "1:/",
    "name": "report.txt",
    "mimeType": "text/plain",
    "size": 5,
    …
    "url": "https://example.org/fileadmin/report.txt",
    "metadata": {"_schema": "sys_file_metadata", "uid": 81},
    "permissions": {
        "read": true,
        "write": true,
        "rename": true,
        "delete": true
    }
}
```

This action needs the scopes `content:read` and `content:write`.
`GET /typo3/api/v1/storages` lists the file storages and their entry folders
(`mounts`). Use it to find the identifier for `folder`. See [File and folder actions](../usage/files.md#usage-files).

<a id="usage-examples-errors"></a>

<a id="handle-errors"></a>

## Handle errors

All errors have the same format, so one function can handle them. These
responses are from a test instance:

| Status | Example `detail` | What to do |
| --- | --- | --- |
| `400` | `The request contains invalid parameters` | Do not repeat the request. Fix the values listed in `errors`. |
| `401` | `Missing "Authorization" header`, `App is not available` | Check the token. The app may be disabled or its secret changed. |
| `403` | `Missing scope: content:write`, `No access to workspace 99` | Ask an administrator for the scope or the permission. The header `WWW-Authenticate` names a missing scope. |
| `404` | `Record not found`, `Table not found` | Check the URL. For an app, the action may also not be offered. |
| `500` | `The action failed, the error is logged with request ID …` | Report the `requestId`, which identifies the entry in the log. |

Status 400, with one entry per problem

```json
{
    "type": "https://typo3.org/go/exception/CMS/1793870606",
    "title": "Bad Request",
    "status": 400,
    "detail": "The request contains invalid parameters",
    "errors": [
        {
            "parameter": "limit",
            "detail": "String value found, but an integer is required"
        }
    ]
}
```

For values in the request body, an entry has a `pointer`, for example
`#/record/sectionIndex`, not a `parameter`.

<a id="usage-examples-oauth"></a>

<a id="run-the-oauth-flow"></a>

## Run the OAuth flow

An application with many users gets its tokens from the authorization code
flow, see [OAuth 2.0](../usage/authentication.md#usage-oauth).

1. **Find the endpoints**
   The metadata of the authorization server contains them:

   ```bash
   curl "$TYPO3_URL/.well-known/oauth-authorization-server"
   ```
2. **Send the user to the authorization endpoint**
   Create a random `code_verifier` for each login. Send its SHA-256 hash
   with base64url encoding as `code_challenge`:

   ```text
   https://example.org/typo3/oauth/authorize
       ?response_type=code
       &client_id=<identifier of the app>
       &redirect_uri=https://app.example/callback
       &scope=content:read
       &state=<random value>
       &code_challenge=<hash of code_verifier>
       &code_challenge_method=S256
   ```
3. **Exchange the code on your server**
   After the approval, TYPO3 redirects the user to the `redirect_uri` with
   `code` and `state`. Check `state`. Then exchange the code:

   ```bash
   curl -X POST "$TYPO3_URL/typo3/api/v1/oauth/token" \
       -u '<identifier of the app>:<client secret>' \
       -d grant_type=authorization_code \
       -d code="$CODE" \
       -d redirect_uri=https://app.example/callback \
       -d code_verifier="$CODE_VERIFIER"
   ```

   Response

   ```json
   {
       "token_type": "Bearer",
       "expires_in": 3600,
       "access_token": "<access token>",
       "refresh_token": "<refresh token>"
   }
   ```
4. **Request a new access token**
   Do this step before the access token expires:

   ```bash
   curl -X POST "$TYPO3_URL/typo3/api/v1/oauth/token" \
       -u '<identifier of the app>:<client secret>' \
       -d grant_type=refresh_token \
       -d refresh_token="$REFRESH_TOKEN"
   ```

<a id="usage-examples-clients"></a>

<a id="generate-a-client"></a>

## Generate a client

`GET /typo3/api/v1/schema` returns an OpenAPI 3.1 document, which describes the
record types of your installation. Save it and pass it to the client generator
of your choice:

```bash
curl -H "Authorization: Bearer $TYPO3_TOKEN" \
    "$TYPO3_URL/typo3/api/v1/schema" -o openapi.json
```

> [!TIP]
> Generate the client from the document of the installation you work against.
> Tables and fields depend on its extensions and configuration.
