Skip to main content
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

https://api.twenty.com/graphql

Authentication

Include your API key in the Authorization header:
Authorization: Bearer YOUR_API_KEY
Generate an API key at SettingsAPI & Webhooks in your Twenty workspace.

GraphQL Playground

Explore the API interactively:
  1. Navigate to http://localhost:3000/graphql (for local development)
  2. Add authorization header in the playground
  3. Use the built-in documentation explorer
  4. 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:
eq
any
Equals
neq
any
Not equals
in
array
In array
contains
string
Contains substring (case-insensitive)
startsWith
string
Starts with string
endsWith
string
Ends with string
gt
number | date
Greater than
gte
number | date
Greater than or equal
lt
number | date
Less than
lte
number | date
Less than or equal
is
null
Is null
isNot
null
Is not null

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
      }
    }
  }
}

Pagination

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
        }
      }
    }
  }
}

Metadata API

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

Error Response Format

{
  "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
        }
      }
    }
  }
}

Get Type Information

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
codegen.yml
schema:
  - https://api.twenty.com/graphql:
      headers:
        Authorization: Bearer ${TWENTY_API_KEY}
generates:
  ./src/generated/graphql.ts:
    plugins:
      - typescript
      - typescript-operations
npx graphql-codegen

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

Webhooks

Set up webhooks

Build docs developers (and LLMs) love