Improve REST API Docs (#13931)

Various improvements to the REST API docs as we often get questions
(misconceptions on how to use filters, how to use with LLMs, etc.)
This commit is contained in:
Félix Malfait
2025-08-15 10:49:00 +02:00
committed by GitHub
parent 0c33dcc16a
commit d0bcf8a871
6 changed files with 136 additions and 58 deletions
@@ -46,7 +46,7 @@ export const SettingsApiKeys = () => {
<StyledContainer>
<Section>
<H2Title
title={t`Playground`}
title={t`Documentation`}
description={t`Try our REST or GraphQL API playgrounds.`}
/>
<StyledSettingsApiPlaygroundCoverImage />
@@ -84,7 +84,12 @@ export class OpenApiService {
`${request.protocol}://${request.get('host')}`,
);
const schema = baseSchema('core', baseUrl);
const tokenFromQuery = request.query.token;
const schema = baseSchema(
'core',
baseUrl,
typeof tokenFromQuery === 'string' ? tokenFromQuery : undefined,
);
const workspace = await this.getWorkspaceFromRequest(request);
@@ -171,7 +176,12 @@ export class OpenApiService {
`${request.protocol}://${request.get('host')}`,
);
const schema = baseSchema('metadata', baseUrl);
const tokenFromQuery = request.query.token;
const schema = baseSchema(
'metadata',
baseUrl,
typeof tokenFromQuery === 'string' ? tokenFromQuery : undefined,
);
const workspace = await this.getWorkspaceFromRequest(request);
@@ -1,9 +1,5 @@
import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { DEFAULT_CONJUNCTION } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/add-default-conjunction.utils';
import { FilterComparators } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-base-filter.utils';
import { Conjunctions } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter.utils';
import { DEFAULT_ORDER_DIRECTION } from 'src/engine/api/rest/input-factories/order-by-input.factory';
import {
computeDepthParameters,
computeEndingBeforeParameters,
@@ -36,12 +32,8 @@ describe('computeParameters', () => {
expect(computeOrderByParameters()).toEqual({
name: 'order_by',
in: 'query',
description: `Sorts objects returned.
Should have the following shape: **field_name_1,field_name_2[DIRECTION_2],...**
Available directions are **${Object.values(OrderByDirection).join(
'**, **',
)}**.
Default direction is **${DEFAULT_ORDER_DIRECTION}**`,
description: `Format: **field_name_1,field_name_2[DIRECTION_2]
Refer to the filter section at the top of the page for more details.`,
required: false,
schema: {
type: 'string',
@@ -65,9 +57,9 @@ describe('computeParameters', () => {
name: 'depth',
in: 'query',
description: `Determines the level of nested related objects to include in the response.
- 0: Returns only the primary object's information.
- 1: Returns the primary object along with its directly related objects (with no additional nesting for related objects).
- 2: Returns the primary object, its directly related objects, and the related objects of those related objects.`,
- 0: Primary object only
- 1: Primary object + direct relations
- 2: Primary object + direct relations + nested relations`,
required: false,
schema: {
type: 'integer',
@@ -82,19 +74,8 @@ describe('computeParameters', () => {
expect(computeFilterParameters()).toEqual({
name: 'filter',
in: 'query',
description: `Filters objects returned.
Should have the following shape: **field_1[COMPARATOR]:value_1,field_2[COMPARATOR]:value_2...
To filter on composite type fields use **field.subField[COMPARATOR]:value_1
**
Available comparators are **${Object.values(FilterComparators).join(
'**, **',
)}**.
You can create more complex filters using conjunctions **${Object.values(
Conjunctions,
).join('**, **')}**.
Default root conjunction is **${DEFAULT_CONJUNCTION}**.
To filter **null** values use **field[is]:NULL** or **field[is]:NOT_NULL**
To filter using **boolean** values use **field[eq]:true** or **field[eq]:false**`,
description: `Format: field[COMPARATOR]:value,field2[COMPARATOR]:value2
Refer to the filter section at the top of the page for more details.`,
required: false,
schema: {
type: 'string',
@@ -7,12 +7,118 @@ export const API_Version = 'v0.1';
export const baseSchema = (
schemaName: 'core' | 'metadata',
serverUrl: string,
token?: string,
): OpenAPIV3_1.Document => {
return {
openapi: '3.1.1',
info: {
title: 'Twenty Api',
description: `This is a **Twenty REST/API** playground based on the **OpenAPI 3.1 specification**.`,
description: `Use this page to explore and call the **REST API**.
## Authentication
Send a Bearer token with each request:
\`\`\`http
Authorization: Bearer <token>
\`\`\`
Example cURL:
\`\`\`bash
curl -H 'Authorization: Bearer <token>' <server>/rest/core/companies
\`\`\`
Tokens can be generated in Settings → Playground and are workspace-scoped.
## Filters
Use the \`filter\` query parameter to narrow results.
- Format: \`field[COMPARATOR]:value\`
- Multiple conditions: \`field1[eq]:1,field2[gte]:10\` (root conjunction is AND)
- Composite fields: \`field.subField[COMPARATOR]:value\`
- Common comparators: \`eq\`, \`neq\`, \`in\`, \`containsAny\`, \`is\`, \`gt\`, \`gte\`, \`lt\`, \`lte\`, \`startsWith\`, \`like\`, \`ilike\`
Examples:
\`\`\`text
filter=status[eq]:"open"
filter=createdAt[gte]:"2024-01-01"
filter=owner.name[ilike]:"%smith%"
filter=id[in]:["id-1","id-2"]
filter=deletedAt[is]:NULL
filter=isActive[eq]:true
\`\`\`
Advanced (optional): \`and(...)\`, \`or(...)\`, \`not(...)\` (\`not\` wraps one condition)
\`\`\`text
filter=or(status[eq]:"open",assigneeId[is]:NULL)
\`\`\`
Notes: Strings and dates are quoted; numbers are not.
## Pagination and ordering
All list endpoints use cursor-based pagination.
- Use **limit** to cap page size (default: 60, max: 60).
- Use **starting_after** to fetch the next page (forward).
- Use **ending_before** to fetch the previous page (backward).
- Responses include **pageInfo** with \`hasNextPage\`, \`startCursor\`, and \`endCursor\`.
Examples:
\`\`\`bash
# First page
curl -H 'Authorization: Bearer <token>' \\
'<server>/rest/core/companies?limit=60'
# Next page
curl -H 'Authorization: Bearer <token>' \\
'<server>/rest/core/companies?limit=60&starting_after=<endCursorFromPreviousPage>'
# Previous page
curl -H 'Authorization: Bearer <token>' \\
'<server>/rest/core/companies?limit=60&ending_before=<startCursorFromCurrentPage>'
\`\`\`
You can combine pagination with filters and ordering.
Ordering with \`order_by\`:
- Shape: \`field1,field2[DIRECTION2]\`
- Directions: AscNullsFirst, AscNullsLast, DescNullsFirst, DescNullsLast
- Default per-field direction: AscNullsFirst
Examples:
\`\`\`text
order_by=createdAt
order_by=id[AscNullsFirst],createdAt[DescNullsLast]
\`\`\`
## Usage with LLMs
You can use AI to generate code based on the OpenAPI schema with the following URLs:
\`\`\`text
Core: ${serverUrl}/rest/open-api/core?token=${token ?? '<your_token>'}
Metadata: ${serverUrl}/rest/open-api/metadata?token=${token ?? '<your_token>'}
\`\`\`
Quick prompt example (Cursor or any agent):
\`\`\`text
Here is an OpenAPI schema for the Twenty REST API:\n${serverUrl}/rest/open-api/core?token=${token ?? '<your_token>'}
Use it to list companies created after 2024-01-01, ordered by createdAt desc, and include only 20 results.
\`\`\`
Notes:
- Treat the token like a secret; prefer a short-lived Playground token.
- Most editors can fetch and process the schema even if it's large.
`,
termsOfService:
'https://github.com/twentyhq/twenty?tab=coc-ov-file#readme',
contact: {
@@ -2,11 +2,6 @@ import { type OpenAPIV3_1 } from 'openapi-types';
import { OrderByDirection } from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
import { DEFAULT_CONJUNCTION } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/add-default-conjunction.utils';
import { FilterComparators } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-base-filter.utils';
import { Conjunctions } from 'src/engine/api/rest/core/query-builder/utils/filter-utils/parse-filter.utils';
import { DEFAULT_ORDER_DIRECTION } from 'src/engine/api/rest/input-factories/order-by-input.factory';
export const computeLimitParameters = (
fromMetadata = false,
): OpenAPIV3_1.ParameterObject => {
@@ -28,12 +23,8 @@ export const computeOrderByParameters = (): OpenAPIV3_1.ParameterObject => {
return {
name: 'order_by',
in: 'query',
description: `Sorts objects returned.
Should have the following shape: **field_name_1,field_name_2[DIRECTION_2],...**
Available directions are **${Object.values(OrderByDirection).join(
'**, **',
)}**.
Default direction is **${DEFAULT_ORDER_DIRECTION}**`,
description: `Format: **field_name_1,field_name_2[DIRECTION_2]
Refer to the filter section at the top of the page for more details.`,
required: false,
schema: {
type: 'string',
@@ -56,9 +47,9 @@ export const computeDepthParameters = (): OpenAPIV3_1.ParameterObject => {
name: 'depth',
in: 'query',
description: `Determines the level of nested related objects to include in the response.
- 0: Returns only the primary object's information.
- 1: Returns the primary object along with its directly related objects (with no additional nesting for related objects).
- 2: Returns the primary object, its directly related objects, and the related objects of those related objects.`,
- 0: Primary object only
- 1: Primary object + direct relations
- 2: Primary object + direct relations + nested relations`,
required: false,
schema: {
type: 'integer',
@@ -72,20 +63,8 @@ export const computeFilterParameters = (): OpenAPIV3_1.ParameterObject => {
return {
name: 'filter',
in: 'query',
description: `Filters objects returned.
Should have the following shape: **field_1[COMPARATOR]:value_1,field_2[COMPARATOR]:value_2...
To filter on composite type fields use **field.subField[COMPARATOR]:value_1
**
Available comparators are **${Object.values(FilterComparators).join(
'**, **',
)}**.
You can create more complex filters using conjunctions **${Object.values(
Conjunctions,
).join('**, **')}**.
Default root conjunction is **${DEFAULT_CONJUNCTION}**.
To filter **null** values use **field[is]:NULL** or **field[is]:NOT_NULL**
To filter using **boolean** values use **field[eq]:true** or **field[eq]:false**`,
description: `Format: field[COMPARATOR]:value,field2[COMPARATOR]:value2
Refer to the filter section at the top of the page for more details.`,
required: false,
schema: {
type: 'string',
@@ -164,6 +164,8 @@ export const CodeEditor = ({
onValidate?.(markers);
}}
options={{
formatOnPaste: true,
formatOnType: true,
overviewRulerLanes: 0,
scrollbar: {
vertical: 'hidden',