서비스 내에서 화상 상담이나 온라인 강의를 자동화하기 위해 Zoom 미팅을 백엔드 시스템과 연동해야 했던 적이 있을 것이다.
다만 기존의 단순했던 JWT(JSON Web Token) 인증 방식이 공식 폐기되고 Server-to-Server OAuth 인증 구조로 변경되면서 토큰 발급 과정에서 오류를 겪는 경우가 많다.
이번에는 Zoom Server-to-Server OAuth의 동작 원리부터 PHP cURL 기반의 Access Token 발급, 그리고 미팅 자동 생성 API 연동법까지 완벽하게 정리해서 소개하겠다.
과거 Zoom API는 계정의 API Key와 Secret으로 간단히 JWT를 만들어 서명하는 방식을 지원했다. 하지만 보안 강화 조치로 인해 JWT 앱 유형은 완전히 중단되었다.
현재 사용자 개입 없이 백엔드 서버 간 통신으로 Zoom API를 제어하려면 반드시 Server-to-Server OAuth 앱을 사용해야 한다.
Server-to-Server OAuth 방식은 클라이언트 측 사용자의 로그인 인증 없이, 서버 대 서버 통신으로 계정 수준의 Access Token을 발급받는다. 이 토큰은 1시간(3,600초) 동안 유효하며, 토큰 발급 시 Account ID, Client ID, Client Secret 3가지 값이 사용된다.
| 구분 | 기존 JWT 방식 (폐기됨) | Server-to-Server OAuth (현재 방식) |
|---|---|---|
| 인증 주체 | API Key / Secret 기반 자체 서명 | Zoom OAuth Server로부터 Access Token 발급 |
| 토큰 유효기간 | 자율 설정 (최대 1년 등) | 1시간 (3,600초 고정) |
| 권한 관리 | 계정 전체 권한 부여 | Scope(범위) 단위 세분화된 권한 제어 |
| 보안성 | 낮음 (키 유출 시 위험) | 높음 (주기적 토큰 갱신 및 Scope 제한) |
코드 구현에 앞서 Zoom App Marketplace 개발자 콘솔에서 필수 정보를 발급받아야 한다.
1. Zoom App Marketplace(marketplace.zoom.us)에 접속하여 로그인한다.
2. 우측 상단 'Develop' 메뉴에서 'Build App'을 클릭한 뒤 Server-to-Server OAuth 앱 유형을 선택하고 생성한다.
3. App Credentials 항목에서 Account ID, Client ID, Client Secret 값을 복사해둔다.
4. Information 항목에서 필수 기본 정보를 입력한다.
5. Scopes 항목으로 이동하여 연동에 필요한 권한을 추가한다. 미팅 생성을 위해 meeting:write:admin 또는 meeting:write 권한을 반드시 추가하고 승인받아야 한다.
6. 마지막으로 'Activate App' 버튼을 눌러 앱을 활성화한다.
대부분의 개발자가 실수하는 부분은 OAuth 토큰 요청 시 Client ID와 Client Secret을 HTTP Basic Authentication 헤더로 전달하지 않거나, grant_type 파라미터 설정을 잘못하여 401 Unauthorized 에러를 만나는 것이다.
다음은 폐기된 JWT 토큰을 직접 생성하거나, OAuth 토큰 요청 시 POST 바디로 인증키를 잘못 전달하는 형태다.
<?php
// ✗ 잘못된 예시: 구버전 JWT 토큰 사용 및 OAuth 요청 구조 오류
$clientId = "YOUR_CLIENT_ID";
$clientSecret = "YOUR_CLIENT_SECRET";
// 잘못된 접근 1: 구버전 Firebase JWT 라이브러리로 토큰을 생성하려고 함 (현재 API에서 거부됨)
// $jwt = JWT::encode($payload, $clientSecret, 'HS256');
// 잘못된 접근 2: Basic Auth 헤더 없이 POST Body로만 credential 전송
$ch = curl_init("https://zoom.us/oauth/token?grant_type=account_credentials");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'client_id' => $clientId,
'client_secret' => $clientSecret
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
// 결과: {"reason":"Invalid authorization header","error":"unauthorized"} 에러 발생
?>
올바른 구현 방식은 Client ID와 Client Secret을 base64_encode("CLIENT_ID:CLIENT_SECRET") 형태로 변환하여 Authorization 헤더에 넣어 토큰을 요청하고, 취득한 Bearer 토큰으로 REST API를 호출하는 것이다.
<?php
class ZoomApiManager {
private string $accountId;
private string $clientId;
private string $clientSecret;
public function __construct(string $accountId, string $clientId, string $clientSecret) {
$this->accountId = $accountId;
$this->clientId = $clientId;
$this->clientSecret = $clientSecret;
}
/**
* Server-to-Server Access Token 발급
*/
public function getAccessToken(): string {
$url = "https://zoom.us/oauth/token?grant_type=account_credentials&account_id=" . urlencode($this->accountId);
$credentials = base64_encode($this->clientId . ":" . $this->clientSecret);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Basic " . $credentials,
"Content-Type: application/x-www-form-urlencoded"
]
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
throw new Exception("Zoom 토큰 발급 실패 [HTTP {$httpCode}]: " . $response);
}
$data = json_decode($response, true);
return $data['access_token'];
}
/**
* Zoom 미팅 생성 API 호출
*/
public function createMeeting(string $accessToken, string $topic, string $startTime, int $duration = 60): array {
$url = "https://api.zoom.us/v2/users/me/meetings";
$payload = [
'topic' => $topic,
'type' => 2, // 예약된 미팅 (Scheduled Meeting)
'start_time' => $startTime, // ISO 8601 포맷 (예: 2026-04-15T14:00:00Z)
'duration' => $duration,
'timezone' => 'Asia/Seoul',
'settings' => [
'host_video' => true,
'participant_video' => true,
'join_before_host' => false,
'mute_upon_entry' => true,
'auto_recording' => 'none'
]
];
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => json_encode($payload),
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) {
throw new Exception("Zoom 미팅 생성 실패 [HTTP {$httpCode}]: " . $response);
}
return json_decode($response, true);
}
}
// 실무 적용 예시
try {
$zoom = new ZoomApiManager(
'YOUR_ACCOUNT_ID',
'YOUR_CLIENT_ID',
'YOUR_CLIENT_SECRET'
);
// 1. Access Token 취득
$accessToken = $zoom->getAccessToken();
// 2. 미팅 생성
$meeting = $zoom->createMeeting(
$accessToken,
'개발팀 백엔드 아키텍처 실무 워크숍',
'2026-04-15T14:00:00Z',
60
);
echo "미팅 ID: " . $meeting['id'] . "\n";
echo "참가자 URL: " . $meeting['join_url'] . "\n";
echo "호스트 시작 URL: " . $meeting['start_url'] . "\n";
} catch (Exception $e) {
echo "오류 발생: " . $e->getMessage();
}
?>
{
"id": 84920491029,
"topic": "개발팀 백엔드 아키텍처 실무 워크숍",
"type": 2,
"status": "waiting",
"start_time": "2026-04-15T14:00:00Z",
"duration": 60,
"timezone": "Asia/Seoul",
"created_at": "2026-03-31T05:20:10Z",
"start_url": "https://zoom.us/s/84920491029?zak=eyJ0eXAi...",
"join_url": "https://zoom.us/j/84920491029?pwd=...",
"password": "123456"
}
실무 연동 시 빈번하게 나타나는 예외 사항과 방어 조치를 정리했다.
✗ **미팅 생성 시 API 매번 호출마다 Access Token 재발급하기**
✓ Access Token은 유효기간이 3,600초(1시간)이다. API 요청이 발생할 때마다 토큰 발급 API를 호출하면 Zoom의 Rate Limit에 걸린다. 발급받은 토큰은 Redis나 Memcached 같은 메모리 캐시 서버에 저장하고, 만료 5분 전까지 재사용하는 로직을 반드시 구현하자.
✗ **Scopes 권한 미부여로 인한 HTTP 403 / 400 에러**
✓ App Marketplace에서 Scopes 설정을 추가하지 않고 코드만 실행하면 권한 없음 에러가 발생한다. 미팅 생성을 하려면 최소한 meeting:write:admin 또는 meeting:write 권한이 앱 설정에 포함되어 있어야 한다.
✗ **날짜 포맷 및 타임존 오류**
✓ start_time 파라미터 전달 시 ISO 8601 포맷(YYYY-MM-DDTHH:mm:ssZ) 규칙을 엄격히 준수해야 한다. 일반 Y-m-d H:i:s 포맷으로 보내면 400 Bad Request가 발생한다.
Zoom Server-to-Server OAuth API 연동은 변경된 인증 방식을 바르게 이해하고 규격에 맞춰 Basic Auth 헤더와 Scopes 권한만 챙기면 매우 안정적으로 동작한다. 토큰 갱신 캐싱 로직이라는 작은 최적화가 모여서 백엔드 API 시스템의 전체 성능과 안정성을 완성한다는 점을 잊지 말자. 이 글의 cURL 연동 코드와 캐싱 방안을 참고해 시스템에 바로 적용해본다면, 자동화된 화상 미팅 인프라를 손쉽게 구축할 수 있을 것이다.