Overview
Integrations allow you to:- Sync data - Keep data in sync across platforms
- Automate workflows - Trigger actions across systems
- Enrich data - Enhance records with external data sources
- Connect services - Link Twenty with tools your team uses
Integration Architecture
Types of Integrations
1. Webhook-Based Integration
Listen to Twenty events and react in external systems:// Example: Slack notification integration
const express = require('express');
const { WebClient } = require('@slack/web-api');
const app = express();
const slack = new WebClient(process.env.SLACK_TOKEN);
app.post('/webhook/slack-notify', async (req, res) => {
const { operation, record } = req.body;
if (operation === 'opportunity.created') {
await slack.chat.postMessage({
channel: '#sales',
text: `🎯 New opportunity: *${record.name}*`,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*${record.name}*\nAmount: $${record.amount}\nStage: ${record.stage}`,
},
},
{
type: 'actions',
elements: [
{
type: 'button',
text: { type: 'plain_text', text: 'View in Twenty' },
url: `https://app.twenty.com/objects/opportunity/${record.id}`,
},
],
},
],
});
}
res.sendStatus(200);
});
2. Bi-Directional Sync
Sync data both ways between Twenty and external service:const { CoreApiClient } = require('twenty-sdk');
const twentyClient = new CoreApiClient({
apiKey: process.env.TWENTY_API_KEY,
apiUrl: process.env.TWENTY_API_URL,
});
class HubSpotIntegration {
constructor(hubspotApiKey, twentyClient) {
this.hubspot = new HubSpotClient(hubspotApiKey);
this.twenty = twentyClient;
}
// Sync contact from HubSpot to Twenty
async syncContactToTwenty(hubspotContactId) {
const contact = await this.hubspot.contacts.getById(hubspotContactId);
const person = await this.twenty.createOne('person', {
firstName: contact.properties.firstname,
lastName: contact.properties.lastname,
email: contact.properties.email,
phone: contact.properties.phone,
externalId: contact.id,
source: 'hubspot',
});
return person;
}
// Sync contact from Twenty to HubSpot
async syncContactToHubSpot(twentyPersonId) {
const person = await this.twenty.findOne('person', twentyPersonId);
const contact = await this.hubspot.contacts.create({
properties: {
firstname: person.firstName,
lastname: person.lastName,
email: person.email,
phone: person.phone,
twenty_id: person.id,
},
});
// Store external ID
await this.twenty.updateOne('person', twentyPersonId, {
externalId: contact.id,
});
return contact;
}
// Handle webhook from Twenty
async handleTwentyWebhook(payload) {
const { operation, record } = payload;
if (operation === 'person.created' && !record.externalId) {
await this.syncContactToHubSpot(record.id);
} else if (operation === 'person.updated' && record.externalId) {
await this.hubspot.contacts.update(record.externalId, {
properties: {
firstname: record.firstName,
lastname: record.lastName,
email: record.email,
},
});
}
}
}
3. Data Enrichment
Enrich Twenty records with external data:const { CoreApiClient } = require('twenty-sdk');
const clearbit = require('clearbit')(process.env.CLEARBIT_API_KEY);
app.post('/webhook/enrich-company', async (req, res) => {
const { operation, record } = req.body;
if (operation === 'company.created' && record.website) {
try {
// Enrich company data from Clearbit
const enrichedData = await clearbit.Company.find({
domain: record.website,
});
// Update Twenty record
await twentyClient.updateOne('company', record.id, {
industry: enrichedData.category.industry,
employeeCount: enrichedData.metrics.employees,
annualRevenue: enrichedData.metrics.estimatedAnnualRevenue,
description: enrichedData.description,
logo: enrichedData.logo,
linkedInUrl: enrichedData.linkedin.handle,
twitterUrl: enrichedData.twitter.handle,
});
console.log('Enriched company:', record.name);
} catch (error) {
console.error('Enrichment failed:', error);
}
}
res.sendStatus(200);
});
4. OAuth Integration
Build integrations requiring OAuth authentication:const express = require('express');
const { CoreApiClient } = require('twenty-sdk');
const { google } = require('googleapis');
class GoogleCalendarIntegration {
constructor() {
this.oauth2Client = new google.auth.OAuth2(
process.env.GOOGLE_CLIENT_ID,
process.env.GOOGLE_CLIENT_SECRET,
process.env.GOOGLE_REDIRECT_URI
);
}
// Step 1: Generate OAuth URL
getAuthUrl(userId) {
return this.oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: ['https://www.googleapis.com/auth/calendar'],
state: userId, // Pass user ID to link accounts
});
}
// Step 2: Handle OAuth callback
async handleCallback(code, userId) {
const { tokens } = await this.oauth2Client.getToken(code);
// Store tokens securely
await db.integrations.upsert({
userId,
provider: 'google-calendar',
accessToken: encrypt(tokens.access_token),
refreshToken: encrypt(tokens.refresh_token),
});
}
// Step 3: Sync events
async syncEventsToTwenty(userId) {
const integration = await db.integrations.findOne({ userId });
this.oauth2Client.setCredentials({
access_token: decrypt(integration.accessToken),
refresh_token: decrypt(integration.refreshToken),
});
const calendar = google.calendar({ version: 'v3', auth: this.oauth2Client });
const events = await calendar.events.list({
calendarId: 'primary',
timeMin: new Date().toISOString(),
maxResults: 100,
});
// Create activities in Twenty
for (const event of events.data.items) {
await twentyClient.createOne('activity', {
title: event.summary,
type: 'MEETING',
startDate: event.start.dateTime,
endDate: event.end.dateTime,
description: event.description,
externalId: event.id,
});
}
}
}
Integration Patterns
Polling Pattern
Periodically check for changes:class PollingIntegration {
constructor(twentyClient, externalApi) {
this.twenty = twentyClient;
this.external = externalApi;
}
async poll() {
// Get last sync time
const lastSync = await this.getLastSyncTime();
// Fetch changes from external service
const changes = await this.external.getChangesSince(lastSync);
// Apply changes to Twenty
for (const change of changes) {
if (change.type === 'contact') {
await this.syncContact(change.data);
}
}
// Update last sync time
await this.saveLastSyncTime(new Date());
}
// Run every 5 minutes
start() {
setInterval(() => this.poll(), 5 * 60 * 1000);
}
}
Event-Driven Pattern
React immediately to changes using webhooks:class EventDrivenIntegration {
// Handle Twenty webhook
async handleTwentyEvent(payload) {
const { operation, record } = payload;
// Push to external service immediately
if (operation.startsWith('person.')) {
await this.syncToExternalService(record);
}
}
// Handle external webhook
async handleExternalEvent(payload) {
const { event, data } = payload;
// Update Twenty immediately
if (event === 'contact.updated') {
await this.syncToTwenty(data);
}
}
}
Error Handling
Retry Strategy
const retry = async (fn, maxAttempts = 3, delay = 1000) => {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxAttempts) throw error;
await new Promise(resolve => setTimeout(resolve, delay * attempt));
}
}
};
app.post('/webhook', async (req, res) => {
try {
await retry(async () => {
await processWebhook(req.body);
});
res.sendStatus(200);
} catch (error) {
console.error('Failed after retries:', error);
res.status(500).send('Processing failed');
}
});
Dead Letter Queue
app.post('/webhook', async (req, res) => {
try {
await processWebhook(req.body);
res.sendStatus(200);
} catch (error) {
// Store failed webhook for manual review
await db.failedWebhooks.create({
payload: req.body,
error: error.message,
timestamp: new Date(),
});
// Return 200 to prevent retries
res.sendStatus(200);
}
});
Publishing to Marketplace
Share your integration with the Twenty community:- Package your app - Ensure all code is in
packages/twenty-apps/community/your-app - Add documentation - Include README.md with setup instructions
- Test thoroughly - Verify all features work
- Submit PR - Open pull request to Twenty repository
- Review process - Maintainers review and provide feedback
Marketplace Requirements
- Clear documentation
- Error handling
- Security best practices
- MIT or AGPL-3.0 license
- Working example/demo
Integration Examples
Zapier Integration
Twenty has an official Zapier integration inpackages/twenty-zapier/.
Slack Bot
const { App } = require('@slack/bolt');
const { CoreApiClient } = require('twenty-sdk');
const slackApp = new App({
token: process.env.SLACK_BOT_TOKEN,
signingSecret: process.env.SLACK_SIGNING_SECRET,
});
const twenty = new CoreApiClient({
apiKey: process.env.TWENTY_API_KEY,
apiUrl: process.env.TWENTY_API_URL,
});
// Slash command: /create-contact John Doe [email protected]
slackApp.command('/create-contact', async ({ command, ack, respond }) => {
await ack();
const [firstName, lastName, email] = command.text.split(' ');
try {
const person = await twenty.createOne('person', {
firstName,
lastName,
email,
});
await respond(`Created contact: ${person.firstName} ${person.lastName}`);
} catch (error) {
await respond(`Error: ${error.message}`);
}
});
slackApp.start(3000);
Email Service Integration
const sgMail = require('@sendgrid/mail');
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
app.post('/webhook/send-welcome-email', async (req, res) => {
const { operation, record } = req.body;
if (operation === 'person.created' && record.email) {
const msg = {
to: record.email,
from: '[email protected]',
subject: `Welcome ${record.firstName}!`,
html: `
<h1>Welcome to our CRM!</h1>
<p>Hi ${record.firstName},</p>
<p>We're excited to have you in our system.</p>
`,
};
await sgMail.send(msg);
// Log email sent in Twenty
await twentyClient.createOne('activity', {
type: 'EMAIL',
title: 'Welcome email sent',
person: { connect: record.id },
completedAt: new Date(),
});
}
res.sendStatus(200);
});
OAuth Flow Implementation
Complete OAuth Example
const express = require('express');
const session = require('express-session');
const passport = require('passport');
const OAuth2Strategy = require('passport-oauth2');
const app = express();
app.use(session({ secret: 'your-session-secret' }));
app.use(passport.initialize());
app.use(passport.session());
// Configure OAuth strategy
passport.use('external-service', new OAuth2Strategy({
authorizationURL: 'https://external.com/oauth/authorize',
tokenURL: 'https://external.com/oauth/token',
clientID: process.env.EXTERNAL_CLIENT_ID,
clientSecret: process.env.EXTERNAL_CLIENT_SECRET,
callbackURL: 'https://your-app.com/oauth/callback',
},
async (accessToken, refreshToken, profile, done) => {
// Store tokens for this user
await db.integrations.upsert({
userId: profile.id,
accessToken: encrypt(accessToken),
refreshToken: encrypt(refreshToken),
});
done(null, profile);
}
));
// Start OAuth flow
app.get('/oauth/connect',
passport.authenticate('external-service')
);
// Handle callback
app.get('/oauth/callback',
passport.authenticate('external-service'),
(req, res) => {
res.send('Integration connected successfully!');
}
);
Testing Integrations
Unit Tests
const { CoreApiClient } = require('twenty-sdk');
describe('HubSpot Integration', () => {
let integration;
let mockTwentyClient;
let mockHubSpotClient;
beforeEach(() => {
mockTwentyClient = {
createOne: jest.fn(),
updateOne: jest.fn(),
};
mockHubSpotClient = {
contacts: {
getById: jest.fn(),
create: jest.fn(),
},
};
integration = new HubSpotIntegration(
mockHubSpotClient,
mockTwentyClient
);
});
it('should sync contact from HubSpot to Twenty', async () => {
mockHubSpotClient.contacts.getById.mockResolvedValue({
id: 'hs-123',
properties: {
firstname: 'John',
lastname: 'Doe',
email: '[email protected]',
},
});
await integration.syncContactToTwenty('hs-123');
expect(mockTwentyClient.createOne).toHaveBeenCalledWith('person', {
firstName: 'John',
lastName: 'Doe',
email: '[email protected]',
externalId: 'hs-123',
source: 'hubspot',
});
});
});
Integration Tests
describe('Integration E2E', () => {
it('should handle full sync flow', async () => {
// Create person in Twenty
const person = await twentyClient.createOne('person', {
firstName: 'Test',
lastName: 'User',
email: '[email protected]',
});
// Wait for webhook to process
await new Promise(resolve => setTimeout(resolve, 2000));
// Verify contact created in external service
const externalContact = await externalApi.findByEmail('[email protected]');
expect(externalContact).toBeDefined();
expect(externalContact.firstName).toBe('Test');
});
});
Best Practices
Handle Rate Limits
Respect API rate limits with exponential backoff and queuing
Idempotent Operations
Use external IDs to prevent duplicate records on retries
Graceful Degradation
Continue working if external service is down
Monitor Health
Track sync status and alert on failures
Idempotency Keys
async function syncContact(externalContact) {
// Check if already synced
const existing = await twenty.findMany('person', {
filter: { externalId: { eq: externalContact.id } },
limit: 1,
});
if (existing.length > 0) {
// Update existing
return await twenty.updateOne('person', existing[0].id, {
firstName: externalContact.firstName,
// ... other fields
});
} else {
// Create new
return await twenty.createOne('person', {
firstName: externalContact.firstName,
externalId: externalContact.id,
// ... other fields
});
}
}
Deployment
Environment Variables
.env
# Twenty credentials
TWENTY_API_KEY=your-api-key
TWENTY_API_URL=https://api.twenty.com
# External service credentials
EXTERNAL_API_KEY=external-service-key
EXTERNAL_CLIENT_ID=oauth-client-id
EXTERNAL_CLIENT_SECRET=oauth-client-secret
# Integration settings
WEBHOOK_SECRET=your-webhook-secret
SYNC_INTERVAL=300000
Docker Deployment
Dockerfile
FROM node:24-alpine
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
COPY . .
EXPOSE 3000
CMD ["node", "src/index.js"]
docker-compose.yml
services:
integration:
build: .
ports:
- "3000:3000"
environment:
- TWENTY_API_KEY=${TWENTY_API_KEY}
- TWENTY_API_URL=${TWENTY_API_URL}
- EXTERNAL_API_KEY=${EXTERNAL_API_KEY}
restart: unless-stopped
Next Steps
Custom Apps
Build custom applications
Webhooks
Learn more about webhooks
SDK Reference
Complete SDK documentation
GraphQL API
Use the GraphQL API
