Cookbook
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).
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).
The recipes get the address of the instance and a token from the environment:
export TYPO3_URL='https://example.org'
export TYPO3_TOKEN='<token>'export TYPO3_URL='https://example.org'
export TYPO3_TOKEN='<token>'
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.
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;
}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);
}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);
}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;
}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;
}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;
}
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);
}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);
}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);
}
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);
}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);
}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);
}
Find records#
Use the filters pid, type, language and search. The filter search uses
the label field of the table:
curl -G -H "Authorization: Bearer $TYPO3_TOKEN" \
"$TYPO3_URL/typo3/api/v1/records/tt_content" \
-d pid=3 -d search=Hello -d limit=1curl -G -H "Authorization: Bearer $TYPO3_TOKEN" \
"$TYPO3_URL/typo3/api/v1/records/tt_content" \
-d pid=3 -d search=Hello -d limit=1
{
"items": [
{
"uid": 227,
"pid": 3,
"label": "Hello from the API",
"type": "text",
"hidden": false,
"language": 0
}
],
"next": null
}{
"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.
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.
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>"
}
}'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:
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",
…
}
}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",
…
}
}
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.
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.
Change a single field#
PATCH changes only the given fields and returns the complete record:
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"}}'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.
Upload a file#
The request body is the content of the file. folder and name are query
parameters:
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.txtcurl -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
{
"_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
}
}{
"_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.
Handle errors#
All errors have the same format, so one function can handle them. These responses are from a test instance:
detail400The request contains invalid parameterserrors.401Missing "Authorization" header, App is not available403Missing scope: content:write, No access to workspace 99WWW-Authenticate names a missing scope.404Record not found, Table not found500The action failed, the error is logged with request ID …requestId, which identifies the entry in the log.| 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. |
{
"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"
}
]
}{
"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.
Run the OAuth flow#
An application with many users gets its tokens from the authorization code flow, see OAuth 2.0.
The metadata of the authorization server contains them:
curl "$TYPO3_URL/.well-known/oauth-authorization-server"Create a random code_verifier for each login. Send its SHA-256 hash
with base64url encoding as code_challenge:
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=S256After the approval, TYPO3 redirects the user to the redirect_uri with
code and state. Check state. Then exchange the code:
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"{
"token_type": "Bearer",
"expires_in": 3600,
"access_token": "<access token>",
"refresh_token": "<refresh token>"
}Do this step before the access token expires:
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"The metadata of the authorization server contains them:
curl "$TYPO3_URL/.well-known/oauth-authorization-server"Find the endpoints
The metadata of the authorization server contains them:
curl "$TYPO3_URL/.well-known/oauth-authorization-server"curl "$TYPO3_URL/.well-known/oauth-authorization-server"
Create a random code_verifier for each login. Send its SHA-256 hash
with base64url encoding as code_challenge:
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=S256Send 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:
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=S256https://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
After the approval, TYPO3 redirects the user to the redirect_uri with
code and state. Check state. Then exchange the code:
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"{
"token_type": "Bearer",
"expires_in": 3600,
"access_token": "<access token>",
"refresh_token": "<refresh token>"
}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:
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"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"
{
"token_type": "Bearer",
"expires_in": 3600,
"access_token": "<access token>",
"refresh_token": "<refresh token>"
}{
"token_type": "Bearer",
"expires_in": 3600,
"access_token": "<access token>",
"refresh_token": "<refresh token>"
}
Do this step before the access token expires:
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"Request a new access token
Do this step before the access token expires:
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"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"
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:
curl -H "Authorization: Bearer $TYPO3_TOKEN" \
"$TYPO3_URL/typo3/api/v1/schema" -o openapi.jsoncurl -H "Authorization: Bearer $TYPO3_TOKEN" \
"$TYPO3_URL/typo3/api/v1/schema" -o openapi.json
Generate the client from the document of the installation you work against. Tables and fields depend on its extensions and configuration.
Generate the client from the document of the installation you work against. Tables and fields depend on its extensions and configuration.