EmailVerify LogoEmailVerify

Typeform

Email checker for Typeform. Verify emails in Typeform responses and integrations.

EmailVerify와 Typeform을 통합하여 응답이 제출될 때 이메일 주소를 검증하고 고품질 리드와 유효한 연락처 정보를 확보하세요.

통합 방법

방법적합 대상복잡도
웹훅실시간 검증낮음
Zapier노코드 자동화낮음
API 통합커스텀 워크플로보통

방법 1: 웹훅 통합

폼 응답이 제출될 때 이메일을 검증하기 위한 웹훅을 설정하세요.

웹훅 엔드포인트 생성

// pages/api/typeform/webhook.js
import crypto from 'crypto';
import EmailVerify from '@emailverify/sdk';

const bv = new EmailVerify(process.env.EMAILVERIFY_API_KEY);

export default async function handler(req, res) {
    if (req.method !== 'POST') {
        return res.status(405).json({ error: 'Method not allowed' });
    }

    // Verify Typeform signature
    const signature = req.headers['typeform-signature'];
    if (!verifySignature(req.body, signature)) {
        return res.status(401).json({ error: 'Invalid signature' });
    }

    const { form_response } = req.body;

    // Extract email from answers
    const email = extractEmail(form_response.answers);

    if (!email) {
        return res.status(200).json({ message: 'No email found' });
    }

    // Verify email
    const result = await bv.verify({ email });

    // Process based on result
    await processVerification(form_response, email, result);

    res.status(200).json({ success: true });
}

function verifySignature(payload, signature) {
    const secret = process.env.TYPEFORM_WEBHOOK_SECRET;
    const hash = crypto
        .createHmac('sha256', secret)
        .update(JSON.stringify(payload))
        .digest('base64');

    return `sha256=${hash}` === signature;
}

function extractEmail(answers) {
    const emailAnswer = answers.find(
        answer => answer.type === 'email'
    );

    return emailAnswer?.email || null;
}

async function processVerification(response, email, result) {
    const responseId = response.token;

    // Store verification result
    await storeResult(responseId, {
        email,
        status: result.status,
        score: result.score,
        disposable: result.is_disposable,
        verified_at: new Date().toISOString(),
    });

    // Take action based on result
    if (result.status === 'invalid' || result.is_disposable) {
        // Flag or notify
        await sendNotification({
            type: 'invalid_email',
            response_id: responseId,
            email,
            reason: result.status === 'invalid'
                ? 'Invalid email address'
                : 'Disposable email detected',
        });
    } else if (result.status === 'valid') {
        // Add to CRM, email list, etc.
        await addToCRM(email, response);
    }
}

Typeform에서 웹훅 구성

  1. Typeform에서 폼을 열기
  2. ConnectWebhooks 클릭
  3. Add a webhook 클릭
  4. 웹훅 URL 입력: https://yoursite.com/api/typeform/webhook
  5. 시크릿을 복사하여 환경 변수에 추가

방법 2: Zapier 통합

노코드 자동화를 위해 Zapier를 사용하여 Typeform을 EmailVerify에 연결하세요.

Zap 구성

  1. 트리거: Typeform → New Entry
  2. 액션: Webhooks by Zapier → POST

웹훅 구성

URL: https://api.emailverify.ai/v1/verify
Method: POST
Headers:
  Authorization: Bearer YOUR_API_KEY
  Content-Type: application/json
Data:
{
  "email": "{{email_field}}",
  "webhook_url": "https://hooks.zapier.com/hooks/catch/xxx/yyy"
}

결과 처리

검증 결과를 처리하기 위해 두 번째 Zap을 추가하세요:

  1. 트리거: Webhooks by Zapier → Catch Hook
  2. 필터: status = "invalid" 또는 disposable = true인 경우에만 계속
  3. 액션: Gmail → Send Email (팀에 알림) 또는 Google Sheets → Create Row (로그)

방법 3: API 통합

두 API를 사용하여 커스텀 통합을 구축하세요.

Typeform 응답 핸들러

// services/typeform-verification.ts
import EmailVerify from '@emailverify/sdk';

interface TypeformAnswer {
  type: string;
  email?: string;
  text?: string;
  field: {
    id: string;
    ref: string;
    type: string;
  };
}

interface TypeformResponse {
  token: string;
  submitted_at: string;
  answers: TypeformAnswer[];
}

interface VerificationResult {
  responseId: string;
  email: string;
  status: string;
  score: number;
  isValid: boolean;
  isDisposable: boolean;
}

class TypeformVerificationService {
  private bv: EmailVerify;

  constructor() {
    this.bv = new EmailVerify(process.env.EMAILVERIFY_API_KEY!);
  }

  async verifyResponse(response: TypeformResponse): Promise<VerificationResult | null> {
    const email = this.extractEmail(response.answers);

    if (!email) {
      return null;
    }

    const result = await this.bv.verify({ email });

    return {
      responseId: response.token,
      email,
      status: result.status,
      score: result.score,
      isValid: result.status === 'valid' && !result.is_disposable,
      isDisposable: result.is_disposable,
    };
  }

  private extractEmail(answers: TypeformAnswer[]): string | null {
    const emailAnswer = answers.find(a => a.type === 'email');
    return emailAnswer?.email || null;
  }

  async verifyBulkResponses(responses: TypeformResponse[]): Promise<VerificationResult[]> {
    const emails = responses
      .map(r => this.extractEmail(r.answers))
      .filter((email): email is string => email !== null);

    if (emails.length === 0) {
      return [];
    }

    // Use bulk verification
    const job = await this.bv.verifyBulk(emails);

    // Wait for completion
    let status;
    do {
      await this.sleep(5000);
      status = await this.bv.getBulkJobStatus(job.job_id);
    } while (status.status !== 'completed');

    const results = await this.bv.getBulkJobResults(job.job_id);

    // Map results back to responses
    return responses
      .map(response => {
        const email = this.extractEmail(response.answers);
        if (!email) return null;

        const result = results.results.find(r => r.email === email);
        if (!result) return null;

        return {
          responseId: response.token,
          email,
          status: result.status,
          score: result.score,
          isValid: result.status === 'valid' && !result.is_disposable,
          isDisposable: result.is_disposable,
        };
      })
      .filter((r): r is VerificationResult => r !== null);
  }

  private sleep(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

export const typeformVerification = new TypeformVerificationService();

Typeform 응답 가져오기

// services/typeform-api.ts
interface TypeformAPIResponse {
  items: TypeformResponse[];
  page_count: number;
  total_items: number;
}

class TypeformAPI {
  private apiKey: string;
  private baseUrl = 'https://api.typeform.com';

  constructor(apiKey: string) {
    this.apiKey = apiKey;
  }

  async getResponses(formId: string, options: {
    since?: string;
    until?: string;
    page_size?: number;
  } = {}): Promise<TypeformAPIResponse> {
    const params = new URLSearchParams();

    if (options.since) params.append('since', options.since);
    if (options.until) params.append('until', options.until);
    if (options.page_size) params.append('page_size', options.page_size.toString());

    const response = await fetch(
      `${this.baseUrl}/forms/${formId}/responses?${params}`,
      {
        headers: {
          'Authorization': `Bearer ${this.apiKey}`,
        },
      }
    );

    if (!response.ok) {
      throw new Error(`Typeform API error: ${response.statusText}`);
    }

    return response.json();
  }

  async getAllResponses(formId: string): Promise<TypeformResponse[]> {
    const allResponses: TypeformResponse[] = [];
    let hasMore = true;
    let before: string | undefined;

    while (hasMore) {
      const params: Record<string, string> = { page_size: '1000' };
      if (before) params.before = before;

      const queryString = new URLSearchParams(params).toString();
      const response = await fetch(
        `${this.baseUrl}/forms/${formId}/responses?${queryString}`,
        {
          headers: {
            'Authorization': `Bearer ${this.apiKey}`,
          },
        }
      );

      const data = await response.json();

      if (data.items.length === 0) {
        hasMore = false;
      } else {
        allResponses.push(...data.items);
        before = data.items[data.items.length - 1].token;
      }
    }

    return allResponses;
  }
}

export const typeformAPI = new TypeformAPI(process.env.TYPEFORM_API_KEY!);

사용 예시

import { typeformAPI } from './services/typeform-api';
import { typeformVerification } from './services/typeform-verification';

// Verify recent responses
async function verifyRecentResponses(formId: string) {
  const yesterday = new Date();
  yesterday.setDate(yesterday.getDate() - 1);

  const { items } = await typeformAPI.getResponses(formId, {
    since: yesterday.toISOString(),
    page_size: 100,
  });

  console.log(`Verifying ${items.length} responses...`);

  const results = await typeformVerification.verifyBulkResponses(items);

  // Generate report
  const valid = results.filter(r => r.isValid).length;
  const invalid = results.filter(r => !r.isValid).length;

  console.log(`Results: ${valid} valid, ${invalid} invalid`);

  // Flag invalid responses
  for (const result of results) {
    if (!result.isValid) {
      await flagInvalidResponse(result);
    }
  }

  return results;
}

async function flagInvalidResponse(result: VerificationResult) {
  // Add to flagged list, send notification, etc.
  console.log(`Flagging response ${result.responseId}: ${result.email}`);
}

실시간 폼 검증

Typeform은 기본적으로 커스텀 검증을 지원하지 않지만, 임베드된 Typeform이 있는 랜딩 페이지를 사용할 수 있습니다.

사전 검증 랜딩 페이지

<!DOCTYPE html>
<html>
<head>
    <title>Contact Form</title>
    <style>
        .form-container {
            max-width: 500px;
            margin: 50px auto;
            padding: 20px;
        }

        .pre-check {
            margin-bottom: 20px;
        }

        .pre-check input {
            width: 100%;
            padding: 12px;
            font-size: 16px;
            border: 1px solid #ddd;
            border-radius: 4px;
        }

        .pre-check .status {
            margin-top: 8px;
            font-size: 14px;
        }

        .pre-check .status.valid { color: #28a745; }
        .pre-check .status.invalid { color: #dc3545; }
        .pre-check .status.checking { color: #666; }

        .typeform-container {
            display: none;
        }

        .typeform-container.visible {
            display: block;
        }
    </style>
</head>
<body>
    <div class="form-container">
        <div class="pre-check">
            <label>First, let's verify your email:</label>
            <input type="email" id="email" placeholder="your@email.com" />
            <div class="status" id="status"></div>
        </div>

        <div class="typeform-container" id="typeform">
            <div data-tf-widget="FORM_ID" data-tf-hidden="email="></div>
        </div>
    </div>

    <script src="//embed.typeform.com/next/embed.js"></script>
    <script>
        const emailInput = document.getElementById('email');
        const statusEl = document.getElementById('status');
        const typeformContainer = document.getElementById('typeform');

        let verifiedEmail = null;

        emailInput.addEventListener('blur', async function() {
            const email = this.value;

            if (!email || !email.includes('@')) {
                return;
            }

            statusEl.textContent = 'Checking email...';
            statusEl.className = 'status checking';

            try {
                const response = await fetch('/api/verify-email', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ email }),
                });

                const result = await response.json();

                if (result.valid) {
                    statusEl.textContent = '✓ Email verified! Please continue below.';
                    statusEl.className = 'status valid';
                    verifiedEmail = email;

                    // Show Typeform with pre-filled email
                    showTypeform(email);
                } else {
                    statusEl.textContent = '✗ ' + (result.message || 'Please enter a valid email');
                    statusEl.className = 'status invalid';
                }
            } catch (error) {
                // Allow on error
                statusEl.textContent = '';
                showTypeform(email);
            }
        });

        function showTypeform(email) {
            typeformContainer.classList.add('visible');

            // Update hidden field
            const widget = typeformContainer.querySelector('[data-tf-widget]');
            widget.setAttribute('data-tf-hidden', `email=${encodeURIComponent(email)}`);

            // Reinitialize Typeform embed
            window.tf.createWidget();
        }
    </script>
</body>
</html>

응답 처리 워크플로

완전한 워크플로 핸들러

// services/workflow.js
import { typeformAPI } from './typeform-api';
import { typeformVerification } from './typeform-verification';

class ResponseWorkflow {
    async processNewResponse(response) {
        // Step 1: Verify email
        const verification = await typeformVerification.verifyResponse(response);

        if (!verification) {
            return { status: 'no_email' };
        }

        // Step 2: Route based on verification result
        if (!verification.isValid) {
            return await this.handleInvalidEmail(response, verification);
        }

        // Step 3: Process valid response
        return await this.handleValidEmail(response, verification);
    }

    async handleInvalidEmail(response, verification) {
        // Log invalid email
        await this.logToDatabase({
            response_id: response.token,
            email: verification.email,
            status: 'rejected',
            reason: verification.isDisposable ? 'disposable' : 'invalid',
            submitted_at: response.submitted_at,
        });

        // Notify team
        await this.sendSlackNotification({
            text: `⚠️ Invalid email submission detected`,
            blocks: [
                {
                    type: 'section',
                    text: {
                        type: 'mrkdwn',
                        text: `*Email:* ${verification.email}\n*Reason:* ${verification.isDisposable ? 'Disposable email' : 'Invalid email'}\n*Score:* ${verification.score}`,
                    },
                },
            ],
        });

        return { status: 'rejected', verification };
    }

    async handleValidEmail(response, verification) {
        // Extract all form data
        const formData = this.extractFormData(response);

        // Add to CRM
        await this.addToCRM({
            email: verification.email,
            ...formData,
            source: 'typeform',
            email_score: verification.score,
        });

        // Add to email list
        await this.addToMailchimp(verification.email, formData);

        // Log success
        await this.logToDatabase({
            response_id: response.token,
            email: verification.email,
            status: 'processed',
            submitted_at: response.submitted_at,
        });

        return { status: 'processed', verification };
    }

    extractFormData(response) {
        const data = {};

        for (const answer of response.answers) {
            const fieldRef = answer.field.ref;

            switch (answer.type) {
                case 'text':
                case 'short_text':
                case 'long_text':
                    data[fieldRef] = answer.text;
                    break;
                case 'email':
                    data[fieldRef] = answer.email;
                    break;
                case 'number':
                    data[fieldRef] = answer.number;
                    break;
                case 'choice':
                    data[fieldRef] = answer.choice?.label;
                    break;
                case 'choices':
                    data[fieldRef] = answer.choices?.labels;
                    break;
                default:
                    data[fieldRef] = answer[answer.type];
            }
        }

        return data;
    }

    async logToDatabase(data) {
        // Implement your database logging
    }

    async sendSlackNotification(message) {
        await fetch(process.env.SLACK_WEBHOOK_URL, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(message),
        });
    }

    async addToCRM(data) {
        // Implement your CRM integration
    }

    async addToMailchimp(email, data) {
        // Implement your Mailchimp integration
    }
}

export const responseWorkflow = new ResponseWorkflow();

모범 사례

1. 항상 웹훅 서명 검증

function verifySignature(payload, signature) {
    const secret = process.env.TYPEFORM_WEBHOOK_SECRET;
    const hash = crypto
        .createHmac('sha256', secret)
        .update(JSON.stringify(payload))
        .digest('base64');

    return `sha256=${hash}` === signature;
}

2. API 오류 우아하게 처리

try {
    const result = await bv.verify({ email });
    // Process result
} catch (error) {
    console.error('Verification failed:', error);
    // Don't reject the submission on API errors
    await processWithoutVerification(response);
}

3. 사전 검증된 이메일에 숨겨진 필드 사용

사전 검증 페이지를 사용할 때 검증된 이메일을 숨겨진 필드로 전달하세요:

data-tf-hidden="email={{verified_email}}"

관련 리소스

On this page