Skip to main content
Secure your API requests with Twenty’s authentication system supporting API keys, OAuth tokens, and JWT.

Authentication Methods

Twenty supports multiple authentication methods:

API Keys

Best for server-to-server integrations and scripts

OAuth Tokens

For user-authorized third-party applications

JWT Tokens

Session-based authentication for web applications

Personal Access Tokens

Long-lived tokens for personal use

API Keys

API keys provide programmatic access to your workspace.

Creating API Keys

  1. Navigate to SettingsAPI & Webhooks
  2. Click Create API Key
  3. Give it a descriptive name
  4. Copy the key immediately (it’s only shown once)
API keys grant full access to your workspace. Store them securely and never commit them to version control.

Using API Keys

Include the API key in the Authorization header:
curl https://api.twenty.com/graphql \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "{ people { edges { node { id firstName } } } }"}'

API Key Best Practices

Environment Variables

Store API keys in environment variables, never hardcode

Separate Keys

Use different keys for development, staging, and production

Rotate Regularly

Rotate keys periodically and when team members leave

Least Privilege

Use workspace permissions to limit API key access

Revoking API Keys

  1. Go to SettingsAPI & Webhooks
  2. Find the key to revoke
  3. Click Delete
  4. Confirm deletion
Revoking an API key immediately invalidates it. Any applications using that key will receive 401 errors.

OAuth Authentication

For applications that act on behalf of users.

OAuth Flow

Step 1: Authorization Request

Redirect user to Twenty’s authorization endpoint:
https://api.twenty.com/auth/authorize?
  client_id=YOUR_CLIENT_ID&
  redirect_uri=https://your-app.com/callback&
  response_type=code&
  scope=read:people write:people
client_id
string
required
Your OAuth application’s client ID
redirect_uri
string
required
URL to redirect to after authorization
response_type
string
required
Must be code for authorization code flow
scope
string
Space-separated list of permissions requested
state
string
Optional state parameter for CSRF protection

Step 2: Handle Callback

Twenty redirects to your callback URL with an authorization code:
https://your-app.com/callback?code=AUTH_CODE&state=STATE_VALUE

Step 3: Exchange Code for Token

curl -X POST https://api.twenty.com/auth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "code": "AUTH_CODE",
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET",
    "redirect_uri": "https://your-app.com/callback"
  }'
Response:
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 1800
}

Step 4: Use Access Token

curl https://api.twenty.com/graphql \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query": "{ people { edges { node { id firstName } } } }"}'

Refresh Tokens

Access tokens expire after 30 minutes. Use refresh token to get new access token:
curl -X POST https://api.twenty.com/auth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "refresh_token",
    "refresh_token": "REFRESH_TOKEN",
    "client_id": "YOUR_CLIENT_ID",
    "client_secret": "YOUR_CLIENT_SECRET"
  }'

JWT Tokens

For web application sessions.

Login with Credentials

curl -X POST https://api.twenty.com/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "password": "password"
  }'
Response:
{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "user": {
    "id": "user-id",
    "email": "[email protected]",
    "firstName": "John",
    "lastName": "Doe"
  }
}

JWT Structure

Twenty JWTs contain:
{
  "sub": "user-id",
  "workspaceId": "workspace-id",
  "email": "[email protected]",
  "iat": 1709546400,
  "exp": 1709548200
}

Token Expiration

Default token lifetimes:
  • Access Token - 30 minutes
  • Refresh Token - 90 days
  • Login Token - 15 minutes
  • File Token - 1 day
  • Password Reset Token - 5 minutes
Configure via environment variables:
ACCESS_TOKEN_EXPIRES_IN=30m
REFRESH_TOKEN_EXPIRES_IN=90d
LOGIN_TOKEN_EXPIRES_IN=15m
FILE_TOKEN_EXPIRES_IN=1d
PASSWORD_RESET_TOKEN_EXPIRES_IN=5m

Permissions

Workspace Roles

API access is controlled by workspace roles:
  • Admin - Full access to all data and settings
  • Member - Access to assigned records
  • Custom Roles - Granular permission control

Permission Flags

API keys inherit permissions from the workspace:
  • READ - Read access to objects
  • WRITE - Create and update records
  • DELETE - Delete records
  • API_KEYS_AND_WEBHOOKS - Manage API keys and webhooks

Check Permissions

query GetCurrentUser {
  currentUser {
    id
    email
    role
    permissions {
      object
      actions
    }
  }
}

Rate Limiting

Default Limits

  • 100 requests per minute per API key
  • Shared across GraphQL and REST APIs
  • Per workspace rate limits

Configure Rate Limits

Adjust via environment variables:
API_RATE_LIMITING_TTL=60000        # Window in milliseconds
API_RATE_LIMITING_LIMIT=100         # Requests per window

Rate Limit Headers

Every response includes rate limit information:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1709546460

Handle 429 Errors

async function makeRequestWithRateLimit(fn) {
  try {
    return await fn();
  } catch (error) {
    if (error.response?.status === 429) {
      const resetTime = error.response.headers['x-ratelimit-reset'];
      const waitTime = (resetTime * 1000) - Date.now() + 1000; // +1s buffer
      
      console.log(`Rate limited. Waiting ${waitTime}ms`);
      await new Promise(resolve => setTimeout(resolve, waitTime));
      
      return await fn(); // Retry
    }
    throw error;
  }
}

Security Best Practices

1

Use HTTPS Only

Always use HTTPS in production to encrypt API keys in transit
2

Store Secrets Securely

Use environment variables or secret managers, never hardcode
3

Rotate Keys Regularly

Change API keys periodically and when team members leave
4

Monitor Usage

Track API usage to detect unauthorized access
5

Principle of Least Privilege

Grant minimum permissions necessary

Secure Storage

// Use environment variables
const apiKey = process.env.TWENTY_API_KEY;

// Or use dotenv
require('dotenv').config();
const apiKey = process.env.TWENTY_API_KEY;

Secret Managers

const AWS = require('aws-sdk');
const secretsManager = new AWS.SecretsManager();

async function getApiKey() {
  const secret = await secretsManager.getSecretValue({
    SecretId: 'twenty-api-key',
  }).promise();
  
  return JSON.parse(secret.SecretString).apiKey;
}

OAuth Implementation

Build OAuth-based integrations:

Complete OAuth Example

const express = require('express');
const session = require('express-session');
const axios = require('axios');

const app = express();

app.use(session({
  secret: 'your-session-secret',
  resave: false,
  saveUninitialized: false,
}));

const TWENTY_AUTH_URL = 'https://api.twenty.com/auth/authorize';
const TWENTY_TOKEN_URL = 'https://api.twenty.com/auth/token';
const CLIENT_ID = process.env.TWENTY_CLIENT_ID;
const CLIENT_SECRET = process.env.TWENTY_CLIENT_SECRET;
const REDIRECT_URI = 'https://your-app.com/callback';

// Step 1: Redirect to Twenty
app.get('/connect', (req, res) => {
  const state = generateRandomString();
  req.session.oauthState = state;
  
  const authUrl = `${TWENTY_AUTH_URL}?${
    new URLSearchParams({
      client_id: CLIENT_ID,
      redirect_uri: REDIRECT_URI,
      response_type: 'code',
      scope: 'read:people write:people',
      state,
    })
  }`;
  
  res.redirect(authUrl);
});

// Step 2: Handle callback
app.get('/callback', async (req, res) => {
  const { code, state } = req.query;
  
  // Verify state
  if (state !== req.session.oauthState) {
    return res.status(403).send('Invalid state');
  }
  
  try {
    // Exchange code for tokens
    const response = await axios.post(TWENTY_TOKEN_URL, {
      grant_type: 'authorization_code',
      code,
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET,
      redirect_uri: REDIRECT_URI,
    });
    
    const { access_token, refresh_token } = response.data;
    
    // Store tokens securely
    req.session.accessToken = access_token;
    req.session.refreshToken = refresh_token;
    
    res.send('Connected successfully!');
  } catch (error) {
    console.error('OAuth error:', error);
    res.status(500).send('Authentication failed');
  }
});

// Use access token
app.get('/people', async (req, res) => {
  const accessToken = req.session.accessToken;
  
  if (!accessToken) {
    return res.redirect('/connect');
  }
  
  try {
    const response = await axios.post(
      'https://api.twenty.com/graphql',
      {
        query: '{ people { edges { node { id firstName } } } }',
      },
      {
        headers: {
          'Authorization': `Bearer ${accessToken}`,
        },
      }
    );
    
    res.json(response.data);
  } catch (error) {
    if (error.response?.status === 401) {
      // Token expired, try refresh
      return res.redirect('/refresh');
    }
    throw error;
  }
});

function generateRandomString() {
  return Math.random().toString(36).substring(7);
}

JWT Validation

For custom authentication flows:

Verify JWT Token

const jwt = require('jsonwebtoken');

function verifyToken(token, appSecret) {
  try {
    const decoded = jwt.verify(token, appSecret);
    return {
      valid: true,
      userId: decoded.sub,
      workspaceId: decoded.workspaceId,
    };
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      return { valid: false, reason: 'expired' };
    }
    return { valid: false, reason: 'invalid' };
  }
}

Middleware Example

function authenticateJWT(req, res, next) {
  const authHeader = req.headers.authorization;
  
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing authorization header' });
  }
  
  const token = authHeader.substring(7);
  const result = verifyToken(token, process.env.APP_SECRET);
  
  if (!result.valid) {
    return res.status(401).json({ error: `Invalid token: ${result.reason}` });
  }
  
  req.userId = result.userId;
  req.workspaceId = result.workspaceId;
  next();
}

app.use('/api', authenticateJWT);

Multi-Workspace Authentication

For applications supporting multiple workspaces:
class TwentyClient {
  constructor() {
    this.workspaces = new Map();
  }
  
  // Add workspace credentials
  addWorkspace(workspaceId, apiKey) {
    this.workspaces.set(workspaceId, {
      apiKey,
      client: axios.create({
        baseURL: 'https://api.twenty.com',
        headers: {
          'Authorization': `Bearer ${apiKey}`,
        },
      }),
    });
  }
  
  // Get client for workspace
  getClient(workspaceId) {
    const workspace = this.workspaces.get(workspaceId);
    if (!workspace) {
      throw new Error(`Workspace ${workspaceId} not configured`);
    }
    return workspace.client;
  }
  
  // Make request to specific workspace
  async query(workspaceId, query, variables) {
    const client = this.getClient(workspaceId);
    const response = await client.post('/graphql', {
      query,
      variables,
    });
    return response.data;
  }
}

// Usage
const twentyClient = new TwentyClient();
twentyClient.addWorkspace('workspace-1', 'api-key-1');
twentyClient.addWorkspace('workspace-2', 'api-key-2');

const people1 = await twentyClient.query('workspace-1', PEOPLE_QUERY);
const people2 = await twentyClient.query('workspace-2', PEOPLE_QUERY);

Troubleshooting

Causes:
  • API key is invalid or revoked
  • API key not in Authorization header
  • Token expired
Solutions:
  • Verify API key is correct
  • Check header format: Authorization: Bearer KEY
  • Refresh token if expired
  • Generate new API key if needed
Causes:
  • Insufficient permissions
  • Workspace role doesn’t allow operation
  • Custom permissions not granted
Solutions:
  • Check user role in workspace settings
  • Request admin to grant permissions
  • Verify API key has required permissions
Solutions:
  • Implement token refresh logic
  • Use refresh tokens to get new access tokens
  • Increase token lifetime in server config (if self-hosting)
Cause: CORS policy prevents browser requestsSolution: Make API requests from your backend, not browser:
Browser -> Your Backend -> Twenty API
This also keeps API keys secure.

Testing Authentication

Test API Key

# Simple test query
curl https://api.twenty.com/graphql \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "{ __typename }"}'

# Should return: {"data": {"__typename": "Query"}}

Test OAuth Flow

  1. Navigate to /connect in your application
  2. Authorize the application
  3. Verify redirect to callback with code
  4. Check tokens are received and stored
  5. Make test API request

Next Steps

GraphQL API

Learn the GraphQL API

REST API

Use the REST API

JavaScript SDK

SDK with built-in auth

Webhooks

Secure webhook endpoints

Build docs developers (and LLMs) love