Calendly 커넥터
Tajo를 통해 Calendly를 Brevo에 연결하여 미팅 초대자를 자동으로 연락처로 동기화하고, 예약 이벤트 기반의 이메일 시퀀스를 트리거하며, 영업 및 온보딩 워크플로우를 효율화하세요.
개요
| 속성 | 값 |
|---|---|
| 플랫폼 | Calendly |
| 카테고리 | Scheduling (Custom) |
| 설정 난이도 | 쉬움 |
| 공식 통합 | 아니오 |
| 동기화되는 데이터 | 이벤트, 연락처, 예약, 취소 |
| 인증 방식 | OAuth 2.0 / Personal Access Token |
주요 기능
- 초대자 동기화 - 미팅 초대자로부터 Brevo 연락처를 자동으로 생성합니다
- 예약 트리거 - 미팅이 예약되면 Brevo 자동화를 실행합니다
- 취소 처리 - 취소 시 재참여 플로우를 트리거합니다
- 노쇼 감지 - 초대자가 미팅을 놓쳤을 때 연락처 상태를 업데이트합니다
- 이벤트 타입 매핑 - 다양한 Calendly 이벤트 타입을 Brevo 리스트에 매핑합니다
- Scheduling API - 리디렉션 없이 앱에 스케줄링을 직접 구축합니다
사전 준비 사항
시작하기 전에 다음 사항이 준비되어 있어야 합니다.
- Calendly 계정 (API 액세스를 위한 Professional 플랜 이상)
- Calendly Integrations에서 발급한 Personal Access Token
- API 액세스가 활성화된 Brevo 계정
- 커넥터 권한이 있는 Tajo 계정
인증
Personal Access Token
# Generate at https://calendly.com/integrations/api_webhooksexport CALENDLY_ACCESS_TOKEN=your_personal_access_tokenexport TAJO_API_KEY=your_tajo_api_keyexport BREVO_API_KEY=your_brevo_api_keyOAuth 2.0
// OAuth 2.0 Authorization Code Flowconst authUrl = 'https://auth.calendly.com/oauth/authorize?' + new URLSearchParams({ client_id: process.env.CALENDLY_CLIENT_ID, redirect_uri: 'https://your-app.com/callback', response_type: 'code' });
// Exchange code for tokenconst tokenResponse = await fetch('https://auth.calendly.com/oauth/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'authorization_code', code: authorizationCode, client_id: process.env.CALENDLY_CLIENT_ID, client_secret: process.env.CALENDLY_CLIENT_SECRET, redirect_uri: 'https://your-app.com/callback' })});설정
기본 설정
connectors: calendly: enabled: true access_token: "${CALENDLY_ACCESS_TOKEN}"
sync: contacts: true events: true cancellations: true
event_mapping: discovery_call: list_id: 10 event_type_uri: "https://api.calendly.com/event_types/abc123" demo: list_id: 11 event_type_uri: "https://api.calendly.com/event_types/xyz789"
webhook: signing_key: "${CALENDLY_WEBHOOK_SIGNING_KEY}"필드 매핑
field_mapping: email: email name: FIRSTNAME questions_and_answers: company: COMPANY role: JOB_TITLE phone: SMS event_type_name: CALENDLY_EVENT_TYPE scheduled_at: MEETING_DATE status: BOOKING_STATUSAPI 엔드포인트
| 엔드포인트 | Method | 설명 |
|---|---|---|
https://api.calendly.com/users/me | GET | 현재 사용자 조회 |
https://api.calendly.com/event_types | GET | 이벤트 타입 목록 |
https://api.calendly.com/scheduled_events | GET | 예약된 이벤트 목록 |
https://api.calendly.com/scheduled_events/{uuid} | GET | 예약된 이벤트 조회 |
https://api.calendly.com/scheduled_events/{uuid}/invitees | GET | 초대자 목록 |
https://api.calendly.com/scheduling_links | POST | 스케줄링 링크 생성 |
https://api.calendly.com/webhook_subscriptions | POST | 웹훅 생성 |
https://api.calendly.com/webhook_subscriptions | GET | 웹훅 목록 |
https://api.calendly.com/invitee_no_shows/{uuid} | GET | 노쇼 상태 조회 |
코드 예제
커넥터 초기화
import { TajoClient } from '@tajo/sdk';
const tajo = new TajoClient({ apiKey: process.env.TAJO_API_KEY, brevoApiKey: process.env.BREVO_API_KEY});
await tajo.connectors.connect('calendly', { accessToken: process.env.CALENDLY_ACCESS_TOKEN});예약된 이벤트 목록 조회
// Retrieve scheduled eventsconst response = await fetch( 'https://api.calendly.com/scheduled_events?' + new URLSearchParams({ user: 'https://api.calendly.com/users/YOUR_USER_ID', min_start_time: '2024-01-01T00:00:00Z', max_start_time: '2024-12-31T23:59:59Z', status: 'active', count: 100 }), { headers: { 'Authorization': `Bearer ${process.env.CALENDLY_ACCESS_TOKEN}`, 'Content-Type': 'application/json' } });
const events = await response.json();초대자를 Brevo로 동기화
// Get invitees for a scheduled event and sync to Brevoconst inviteesResponse = await fetch( `https://api.calendly.com/scheduled_events/${eventUuid}/invitees`, { headers: { 'Authorization': `Bearer ${process.env.CALENDLY_ACCESS_TOKEN}` } });
const { collection } = await inviteesResponse.json();
for (const invitee of collection) { await tajo.contacts.sync({ email: invitee.email, attributes: { FIRSTNAME: invitee.name, CALENDLY_EVENT_TYPE: invitee.event, MEETING_DATE: invitee.created_at, BOOKING_STATUS: invitee.status }, listIds: [10] });}웹훅 구독 설정
// Subscribe to Calendly eventsconst webhook = await fetch( 'https://api.calendly.com/webhook_subscriptions', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.CALENDLY_ACCESS_TOKEN}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ url: 'https://api.tajo.io/webhooks/calendly', events: [ 'invitee.created', 'invitee.canceled', 'invitee_no_show.created' ], organization: 'https://api.calendly.com/organizations/YOUR_ORG_ID', scope: 'organization', signing_key: process.env.CALENDLY_WEBHOOK_SIGNING_KEY }) });웹훅 이벤트 처리
app.post('/webhooks/calendly', async (req, res) => { // Verify webhook signature const signature = req.headers['calendly-webhook-signature']; const isValid = verifyCalendlySignature( req.rawBody, signature, process.env.CALENDLY_WEBHOOK_SIGNING_KEY );
if (!isValid) return res.status(401).send('Unauthorized');
const { event, payload } = req.body;
switch (event) { case 'invitee.created': await tajo.contacts.sync({ email: payload.email, attributes: { BOOKING_STATUS: 'booked' }, listIds: [10] }); break; case 'invitee.canceled': await tajo.contacts.update(payload.email, { attributes: { BOOKING_STATUS: 'cancelled' } }); break; case 'invitee_no_show.created': await tajo.contacts.update(payload.email, { attributes: { BOOKING_STATUS: 'no_show' } }); break; }
res.status(200).send('OK');});요청 제한
| 리소스 | 제한 | 비고 |
|---|---|---|
| API 요청 | 분당 6,000회 | 조직 전체 제한 |
| 웹훅 구독 | 조직당 30개 | 모든 이벤트 타입 합산 |
| 스케줄링 링크 | 무제한 | 분당 제한 없음 |
페이지네이션
Calendly API 응답은 커서 기반 페이지네이션을 사용합니다. pagination 객체의 next_page_token을 사용해 추가 결과를 가져오세요. 기본 페이지 크기는 20개 항목이며, 최대 100개입니다.
문제 해결
| 문제 | 원인 | 해결 방법 |
|---|---|---|
| 웹훅 수신 안 됨 | 잘못된 스코프 | 웹훅에 organization 스코프를 사용하세요 |
| 401 Unauthorized | 토큰 만료 | 새 토큰을 생성하거나 OAuth 토큰을 새로 고치세요 |
| 초대자 데이터 누락 | 질문이 구성되지 않음 | 이벤트 타입에 커스텀 질문을 추가하세요 |
| 중복 연락처 | 중복 제거 로직 없음 | upsert의 고유 식별자로 이메일을 사용하세요 |
| Rate limit 429 | 요청이 너무 많음 | 백오프와 요청 배치를 구현하세요 |
디버그 모드
connectors: calendly: debug: true log_level: verbose log_webhooks: true모범 사례
- 웹훅 사용 - 실시간 동기화를 위해
invitee.created와invitee.canceled를 구독하세요 - 커스텀 질문 추가 - 더 풍부한 연락처 프로필을 위해 회사, 역할, 전화번호 데이터를 수집하세요
- 이벤트 타입 매핑 - Calendly 이벤트 타입별로 다른 Brevo 리스트를 할당하세요
- 노쇼 처리 - 리드 스코어링과 후속 시퀀스를 조정하기 위해 노쇼를 추적하세요
- 스케줄링 링크 사용 - 맞춤형 예약 경험을 위해 고유 스케줄링 링크를 생성하세요
- 조직 스코프 설정 - 모든 팀원의 이벤트를 캡처하려면 조직 수준 웹훅을 사용하세요
보안
- OAuth 2.0 - 스코프 기반 토큰 인증
- 웹훅 서명 - 수신 웹훅에 대한 HMAC 서명 검증
- HTTPS 전용 - 모든 API 엔드포인트는 TLS 암호화가 필요합니다
- 토큰 만료 - OAuth 토큰은 만료되며 새로 고침 플로우가 필요합니다
- 최소 스코프 - 필요한 OAuth 스코프만 요청하세요
- 안전한 저장 - 환경 변수 또는 시크릿 매니저에 토큰을 저장하세요