Twenty’s primary API is a GraphQL API that provides flexible, efficient access to your CRM data.
Overview
The GraphQL API offers:
Flexible queries - Request exactly the data you need
Type safety - Strongly typed schema with introspection
Real-time updates - GraphQL subscriptions for live data
Batching - Efficient data loading with DataLoader
Two endpoints - Core API (data) and Metadata API (schema)
API Endpoints
Core API (Production)
Core API (Self-Hosted)
Metadata API
Local Development
https://api.twenty.com/graphql
Authentication
Include your API key in the Authorization header:
Authorization: Bearer YOUR_API_KEY
Generate an API key at Settings → API & Webhooks in your Twenty workspace.
GraphQL Playground
Explore the API interactively:
Navigate to http://localhost:3000/graphql (for local development)
Add authorization header in the playground
Use the built-in documentation explorer
Try example queries
The GraphQL playground is only available in development mode for security reasons.
Core API
The Core API provides access to workspace data.
Querying Records
Find Many Records
query GetPeople {
people ( filter : {
email : { contains : "@example.com" }
}, orderBy : {
createdAt : DESC
}, limit : 10 ) {
edges {
node {
id
firstName
lastName
email
jobTitle
company {
id
name
}
createdAt
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
Find One Record
query GetPerson {
person ( id : "a1b2c3d4-e5f6-7890-abcd-ef1234567890" ) {
id
firstName
lastName
email
phone
activities {
edges {
node {
id
title
type
completedAt
}
}
}
}
}
Creating Records
Create One
mutation CreatePerson {
createPerson ( data : {
firstName : "John"
lastName : "Doe"
email : "[email protected] "
jobTitle : "Software Engineer"
company : {
connect : "company-id"
}
}) {
id
firstName
lastName
email
createdAt
}
}
Create Many
mutation CreateMultiplePeople {
createPeople ( data : [
{
firstName : "John"
lastName : "Doe"
email : "[email protected] "
},
{
firstName : "Jane"
lastName : "Smith"
email : "[email protected] "
}
]) {
id
firstName
email
}
}
Updating Records
Update One
mutation UpdatePerson {
updatePerson (
id : "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
data : {
jobTitle : "Senior Software Engineer"
phone : "+1-555-0100"
}
) {
id
jobTitle
phone
updatedAt
}
}
Update Many
mutation UpdateMultiplePeople {
updatePeople (
filter : {
company : { id : { eq : "company-id" } }
}
data : {
jobTitle : "Team Member"
}
) {
id
jobTitle
}
}
Deleting Records
mutation DeletePerson {
deletePerson ( id : "a1b2c3d4-e5f6-7890-abcd-ef1234567890" ) {
id
}
}
Delete operations are soft deletes by default. Records are marked as deleted but not permanently removed.
Advanced Queries
Filtering
Supported filter operators:
Contains substring (case-insensitive)
Complex Filter Example
query FilteredPeople {
people ( filter : {
and : [
{ email : { isNot : null } }
{
or : [
{ jobTitle : { contains : "engineer" } }
{ jobTitle : { contains : "developer" } }
]
}
{ createdAt : { gte : "2024-01-01T00:00:00Z" } }
]
}) {
edges {
node {
id
firstName
lastName
jobTitle
}
}
}
}
Sorting
query SortedCompanies {
companies ( orderBy : [
{ employees : DESC }
{ name : ASC }
]) {
edges {
node {
id
name
employees
}
}
}
}
Twenty uses cursor-based pagination:
query PaginatedPeople ( $cursor : String ) {
people ( first : 10 , after : $cursor ) {
edges {
node {
id
firstName
lastName
}
cursor
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
Fetch next page:
query NextPage {
people ( first : 10 , after : "cursor-from-previous-page" ) {
edges {
node {
id
firstName
}
}
}
}
Aggregations
query OpportunityStats {
opportunitiesAggregate {
count
sum {
amount
}
avg {
amount
}
min {
amount
}
max {
amount
}
}
}
Relations
Query related data:
query PersonWithRelations {
person ( id : "person-id" ) {
id
firstName
lastName
# Related company
company {
id
name
website
}
# Related activities
activities {
edges {
node {
id
title
type
completedAt
}
}
}
# Related opportunities
opportunities {
edges {
node {
id
name
amount
stage
}
}
}
}
}
The Metadata API manages your workspace schema.
Get Objects
query GetObjects {
objects {
edges {
node {
id
nameSingular
namePlural
labelSingular
labelPlural
description
icon
isCustom
isActive
}
}
}
}
Get Fields
query GetFieldsForObject {
fields ( filter : {
objectMetadataId : { eq : "object-id" }
}) {
edges {
node {
id
name
label
type
description
isNullable
defaultValue
}
}
}
}
Create Custom Object
mutation CreateObject {
createObject ( input : {
nameSingular : "project"
namePlural : "projects"
labelSingular : "Project"
labelPlural : "Projects"
description : "Customer projects"
icon : "folder"
}) {
id
nameSingular
namePlural
}
}
Create Custom Field
mutation CreateField {
createField ( input : {
objectMetadataId : "object-id"
name : "budget"
label : "Budget"
type : CURRENCY
description : "Project budget"
}) {
id
name
label
type
}
}
Subscriptions
Receive real-time updates:
subscription OnPersonCreated {
personCreated {
id
firstName
lastName
email
}
}
Using Subscriptions
import { ApolloClient , InMemoryCache , split , HttpLink } from '@apollo/client' ;
import { GraphQLWsLink } from '@apollo/client/link/subscriptions' ;
import { getMainDefinition } from '@apollo/client/utilities' ;
import { createClient } from 'graphql-ws' ;
const httpLink = new HttpLink ({
uri: 'https://api.twenty.com/graphql' ,
headers: {
authorization: `Bearer ${ API_KEY } ` ,
},
});
const wsLink = new GraphQLWsLink (
createClient ({
url: 'wss://api.twenty.com/graphql' ,
connectionParams: {
authorization: `Bearer ${ API_KEY } ` ,
},
})
);
const splitLink = split (
({ query }) => {
const definition = getMainDefinition ( query );
return (
definition . kind === 'OperationDefinition' &&
definition . operation === 'subscription'
);
},
wsLink ,
httpLink
);
const client = new ApolloClient ({
link: splitLink ,
cache: new InMemoryCache (),
});
// Subscribe to events
client . subscribe ({
query: gql `
subscription OnPersonCreated {
personCreated {
id
firstName
email
}
}
` ,
}). subscribe ({
next ( data ) {
console . log ( 'New person created:' , data );
},
error ( err ) {
console . error ( 'Subscription error:' , err );
},
});
Using with cURL
Query the API using cURL:
curl https://api.twenty.com/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"query": "query { people { edges { node { id firstName lastName } } } }"
}'
With variables:
curl https://api.twenty.com/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"query": "query GetPerson($id: UUID!) { person(id: $id) { id firstName } }",
"variables": { "id": "person-id" }
}'
Error Handling
{
"errors" : [
{
"message" : "Field 'email' is required" ,
"extensions" : {
"code" : "BAD_USER_INPUT" ,
"field" : "email"
},
"path" : [ "createPerson" ]
}
],
"data" : null
}
Common Error Codes
UNAUTHENTICATED - Invalid or missing API key
FORBIDDEN - Insufficient permissions
BAD_USER_INPUT - Invalid input data
NOT_FOUND - Resource doesn’t exist
INTERNAL_SERVER_ERROR - Server error
Handling Errors
const { ApolloClient , gql } = require ( '@apollo/client' );
async function createPerson ( data ) {
try {
const result = await client . mutate ({
mutation: gql `
mutation CreatePerson($data: PersonInput!) {
createPerson(data: $data) {
id
firstName
}
}
` ,
variables: { data },
});
return result . data . createPerson ;
} catch ( error ) {
if ( error . graphQLErrors ) {
error . graphQLErrors . forEach (({ message , extensions }) => {
console . error ( `GraphQL Error: ${ message } ` );
console . error ( `Code: ${ extensions . code } ` );
});
}
if ( error . networkError ) {
console . error ( 'Network Error:' , error . networkError );
}
throw error ;
}
}
Schema Introspection
Get Schema
query GetSchema {
__schema {
types {
name
kind
description
fields {
name
type {
name
kind
}
}
}
}
}
query GetPersonType {
__type ( name : "Person" ) {
name
fields {
name
type {
name
kind
}
description
}
}
}
Rate Limiting
The API implements rate limiting to ensure fair usage:
Default limit - 100 requests per minute per API key
Configurable - Can be adjusted via API_RATE_LIMITING_* env vars
Headers - Rate limit info in response headers
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1709546460
Handle Rate Limits
async function makeRequest ( query , variables ) {
try {
return await client . query ({ query , variables });
} catch ( error ) {
if ( error . extensions ?. code === 'RATE_LIMIT_EXCEEDED' ) {
const resetTime = error . extensions . resetAt ;
const waitTime = resetTime - Date . now ();
console . log ( `Rate limited. Waiting ${ waitTime } ms` );
await new Promise ( resolve => setTimeout ( resolve , waitTime ));
// Retry request
return await client . query ({ query , variables });
}
throw error ;
}
}
Best Practices
Request Only Needed Fields Reduce payload size by selecting only required fields
Use Variables Always use variables instead of string interpolation
Batch Requests Use batching for multiple operations
Cache Responses Cache query results when appropriate
Query Optimization
# Good: Only request needed fields
query OptimizedQuery {
people {
edges {
node {
id
firstName
email
}
}
}
}
# Bad: Request all fields
query UnoptimizedQuery {
people {
edges {
node {
id
firstName
lastName
email
phone
jobTitle
# ... many more fields
}
}
}
}
Use Fragments
fragment PersonFields on Person {
id
firstName
lastName
email
company {
id
name
}
}
query GetPeople {
people {
edges {
node {
... PersonFields
}
}
}
}
query GetPerson ( $id : UUID ! ) {
person ( id : $id ) {
... PersonFields
phone
activities {
edges {
node {
id
title
}
}
}
}
}
Code Generation
Generate TypeScript types from your schema:
Using GraphQL Code Generator
npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations
schema :
- https://api.twenty.com/graphql :
headers :
Authorization : Bearer ${TWENTY_API_KEY}
generates :
./src/generated/graphql.ts :
plugins :
- typescript
- typescript-operations
Use Generated Types
import { GetPeopleQuery , CreatePersonMutation } from './generated/graphql' ;
const people : GetPeopleQuery = await client . query ({
query: GET_PEOPLE_QUERY ,
});
Next Steps
REST API Simpler REST alternative
JavaScript SDK Use the Twenty SDK
Authentication API authentication guide