Zoom API를 연동해 화상 회의실을 자동 생성하는 시스템을 구축해보았는가?
다만 2023년 이후 기존 JWT 앱 인증 방식이 완전히 지원 중단(Deprecated)되어, 예전에 작성한 코드가 401 Unauthorized 에러를 뱉거나 Server-to-Server OAuth 2.0 인증 절차에서 헤매는 개발자들이 많다.
이번에는 Zoom의 변경된 인증 표준인 Server-to-Server OAuth 2.0의 동작 원리부터 인증 토큰 발급, 그리고 PHP cURL을 이용한 실제 미팅 생성 및 토큰 캐싱 기법까지 완벽하게 정리해서 소개하겠다.

 

1. Zoom JWT 인증 중단과 Server-to-Server OAuth 2.0의 등장

기존 Zoom API는 단순히 API Key와 Secret을 조합하여 서명한 JWT(JSON Web Token)를 헤더에 실어 보내면 동작했다. 구현은 쉬웠지만 JWT 토큰이 유출될 경우 권한 관리가 어렵고 보안상 취약점이 컸다.
이에 따라 Zoom은 JWT 인증을 공식 폐기하고, 사용자 개입 없이 백엔드 서버 간 직접 통신할 수 있는 Server-to-Server OAuth 2.0 방식을 도입했다.

구분기존 JWT 인증 (Deprecated)Server-to-Server OAuth 2.0
인증 방식API Key/Secret 기반 static 서명OAuth 2.0 client_credentials (Basic Auth)
토큰 수명임의 설정 (장기 유효 토큰 가능)1시간 (3600초) 단기 액세스 토큰
보안성취약 (키 유출 시 전체 권한 노출)우수 (Scope 기반 세부 권한 제어)
발급 서버클라이언트 단에서 직접 생성 가능Zoom OAuth 서버 인증 거침 (POST 요청)

 

2. Server-to-Server OAuth 2.0 연동 4단계 준비 과정

Server-to-Server OAuth 방식을 이용하려면 Zoom App Marketplace에서 앱 등록 후 필요한 키 정보를 확보해야 한다.

1) Zoom App Marketplace 접속: App Marketplace 로그인 후 'Develop' -> 'Build App' 클릭
2) Server-to-Server OAuth 선택: 앱 유형 중 'Server-to-Server OAuth'를 선택하여 앱 생성
3) 자격 증명 확인: App Credentials 메뉴에서 Account ID, Client ID, Client Secret 세 가지 핵심 키를 복사
4) Scope 권한 설정: Scopes 탭에서 미팅 생성을 위한 meeting:write:admin 또는 meeting:write 권한 추가 후 앱 활성화(Activate)

 

3. 실전 PHP cURL 코드: 인증 토큰 발급 및 화상 회의 생성

이제 PHP에서 Server-to-Server OAuth 액세스 토큰을 발급받고, 이 토큰을 사용해 새로운 Zoom 화상 회의를 자동 생성하는 예제를 살펴보자.

 

잘못된 구현 예시 (JWT 방식 사용 또는 인증 헤더 포맷 오류)

✗ 이미 폐기된 JWT 헤더를 사용하거나, OAuth 인증 시 Basic Auth 헤더 대신 일반 POST 파라미터로만 Client ID/Secret을 전달해 401/400 에러가 발생하는 패턴이다.

<?php
// ✗ 잘못된 방식: 폐기된 JWT 방식을 그대로 사용하거나 인증 헤더 부재
$jwtToken = "eyJhbGciOiJIUzI1NiJ9..."; // 401 Unauthorized 발생!

$ch = curl_init('https://api.zoom.us/v2/users/me/meetings');
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Authorization: Bearer ' . $jwtToken, // 더 이상 작동하지 않음
    'Content-Type: application/json'
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
?>

 

올바른 구현 예시 (Server-to-Server OAuth 2.0 & Meeting API)

✓ Client ID와 Client Secret을 Base64로 인코딩하여 HTTP Basic Authorization 헤더로 전달한 뒤, 발급받은 액세스 토큰으로 API를 호출하는 정석 구현이다.

<?php
/**
 * Zoom Server-to-Server OAuth 2.0 액세스 토큰 발급 함수
 */
function getZoomAccessToken(string $accountId, string $clientId, string $clientSecret): ?string {
    $url = 'https://zoom.us/oauth/token?grant_type=account_credentials&account_id=' . urlencode($accountId);
    
    // Client ID와 Secret을 Basic Auth 포맷으로 인코딩
    $authHeader = base64_encode($clientId . ':' . $clientSecret);

    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL => $url,
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => [
            'Authorization: Basic ' . $authHeader,
            'Content-Type: application/x-www-form-urlencoded'
        ]
    ]);

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($httpCode === 200 && $response) {
        $data = json_decode($response, true);
        return $data['access_token'] ?? null;
    }

    return null;
}

/**
 * Zoom 미팅 생성 함수
 */
function createZoomMeeting(string $accessToken, string $topic, string $startTime): ?array {
    $url = 'https://api.zoom.us/v2/users/me/meetings';

    $postData = [
        'topic' => $topic,
        'type' => 2, // 예정된 회의(Scheduled Meeting)
        'start_time' => $startTime, // ISO 8601 형식 (예: 2026-04-01T10:00:00Z)
        'duration' => 60,
        'timezone' => 'Asia/Seoul',
        'settings' => [
            'host_video' => true,
            'participant_video' => true,
            'join_before_host' => false
        ]
    ];

    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL => $url,
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POSTFIELDS => json_encode($postData),
        CURLOPT_HTTPHEADER => [
            'Authorization: Bearer ' . $accessToken,
            'Content-Type: application/json'
        ]
    ]);

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($httpCode === 201 && $response) {
        return json_decode($response, true);
    }

    return null;
}

// --- 실전 사용 예시 ---
$accountId = 'YOUR_ACCOUNT_ID';
$clientId = 'YOUR_CLIENT_ID';
$clientSecret = 'YOUR_CLIENT_SECRET';

// 1. 액세스 토큰 발급
$accessToken = getZoomAccessToken($accountId, $clientId, $clientSecret);

if ($accessToken) {
    // 2. 미팅 자동 생성 요청
    $meetingInfo = createZoomMeeting($accessToken, '백엔드 개발팀 주간 회의', '2026-04-01T14:00:00Z');
    print_r($meetingInfo);
} else {
    echo "Zoom 토큰 발급 실패!";
}
?>

 

실행 결과 예시
{
  "id": 89412345678,
  "topic": "백엔드 개발팀 주간 회의",
  "type": 2,
  "start_time": "2026-04-01T14:00:00Z",
  "duration": 60,
  "timezone": "Asia/Seoul",
  "start_url": "https://zoom.us/s/89412345678?tk=...",
  "join_url": "https://zoom.us/j/89412345678?pwd=...",
  "password": "123456"
}

 

4. 주의사항 및 흔히 하는 실수

Zoom Server-to-Server OAuth를 실무에 도입할 때 자주 발생하는 실수 목록이다.

매 요청마다 OAuth 토큰을 재발급받는 경우
액세스 토큰의 유효 기간은 1시간(3600초)이다. API 요청이 들어올 때마다 OAuth 토큰 발급 API를 호출하면 Zoom의 Rate Limit(호출 제한) 조치를 받아 서버 전체 통신이 블록될 수 있다.
Redis나 파일/DB 캐시를 활용해 토큰 재사용하기
발급받은 토큰은 Redis나 파일 캐시 시스템에 3,500초(약 58분) 동안 저장해두고, 만료 직전에만 새 토큰을 갱신하도록 캐싱 로직을 추가하자.

Basic Auth 헤더 미적용 및 Base64 인코딩 누락
POST 파라미터로 Client ID/Secret을 넘기면 Zoom OAuth API가 400 Bad Request를 반환한다. 반드시 Authorization: Basic base64(client_id:client_secret) 포맷을 준수해야 한다.

Scope 권한 부족으로 인한 4071 / 3000 에러
앱 생성 후 meeting:write:admin 권한을 부여했더라도 Zoom App Marketplace에서 앱을 **Activate(활성화)** 하지 않거나 조직 관리자 승인을 받지 않으면 권한 에러가 발생하므로 체크해야 한다.

 

5. 마무리

Zoom API의 Server-to-Server OAuth 2.0 전환은 보안성과 토큰 관리의 효율성을 높이기 위한 필수적 조치다. 작은 최적화와 올바른 인증 습관이 모여 서비스의 안정성을 만든다는 점을 잊지 말자. 이 글의 인증 헤더 구현 및 cURL 통신 예제를 참고해 연동 작업을 진행하면, 401 Unauthorized 인증 에러 없이 안정적인 Zoom 화상회의 자동화 시스템을 완성할 수 있을 것이다.