OpenAI API Webhook 보안, 서명 검증·재시도·중복 처리를 나누는 법

AI API Webhook 보안을 상징하는 화면 없는 서버 장치와 잠긴 메모 카드, 연결 케이블이 놓인 현실적인 작업대

AI를 활용하여 생성한 이미지입니다.

OpenAI API의 Webhook은 백그라운드 응답이나 배치 작업이 끝났다는 사실을 서버로 알려주는 HTTP POST 방식이에요. 폴링만으로 상태를 확인하는 것보다 반응이 빠르지만, 공개 URL을 하나 열어 두는 순간 보안·재시도·중복 처리 문제가 함께 생깁니다. OpenAI는 프로젝트 Webhook에 서명 비밀을 발급하고, 서버에서 요청의 출처를 검증하는 흐름을 안내해요.[1]

중급 단계에서 자주 놓치는 부분은 “서명만 확인하면 끝”이라고 생각하는 거예요. 실제 운영에서는 빠른 2xx 응답, 서명 검증, 이벤트 ID 중복 제거, 처리 실패 재시도, 비즈니스 작업의 멱등성까지 분리해야 합니다.

Webhook의 흐름을 먼저 나눠 봐요

백그라운드 응답을 만들면 OpenAI가 지정한 endpoint로 이벤트를 보냅니다. Webhook에는 이벤트 종류와 응답 ID 같은 데이터가 들어오고, 서버는 이를 받아 후속 작업을 시작해요. 공식 문서는 백그라운드 응답이 생성된 뒤 response.completed 같은 이벤트를 구독하는 예를 보여 줍니다.[1][2]

구간 서버가 해야 할 일 실패했을 때
수신 원문 body와 헤더를 읽기 서명 검증 전에는 작업을 실행하지 않기
검증 Webhook secret으로 서명 확인 실패는 4xx로 종료하고 비밀값을 로그에 남기지 않기
응답 수초 안에 2xx를 반환 무거운 처리는 큐나 백그라운드 워커로 넘기기
처리 이벤트 ID를 기준으로 한 번만 반영 중복 수신은 성공적으로 무시하기

Endpoint를 만들 때 먼저 제한할 것

Webhook URL은 인터넷에서 접근할 수 있어야 하므로, 관리자 페이지나 내부 API와 같은 경로에 섞지 않는 게 좋아요. URL 자체에 API 키나 Webhook secret을 넣지 말고, 서버의 비밀 저장소에서 환경 변수로 읽도록 구성하세요. API 키를 코드와 설정 파일에서 분리해 보관하는 방법은 OpenAI API 키 안전 보관 가이드에서 확인할 수 있어요. OpenAI 문서도 endpoint를 만들 때 이름, 공개 URL, 구독할 이벤트 종류를 설정하고 발급된 signing secret을 서버 검증에 사용하도록 안내합니다.[1]

처음에는 필요한 이벤트 하나만 구독하세요. 배치 완료와 백그라운드 응답 완료를 모두 처리해야 하는지, 실제로 어떤 이벤트가 비즈니스 작업을 시작하는지 먼저 정하면 테스트 범위가 줄어들어요. 개발·스테이징·운영 endpoint를 분리하고, 운영 secret을 로컬 테스트 서버에 복사하지 않는 것도 기본 경계입니다.

서명을 검증하기 전에 body를 바꾸지 않아요

서명은 수신한 요청의 내용과 헤더를 바탕으로 계산되므로, JSON을 파싱한 뒤 다시 직렬화하거나 공백을 정리한 문자열을 검증 대상으로 사용하면 안 됩니다. 프레임워크의 JSON middleware가 원문 body를 버리지 않는지 먼저 확인하세요. 검증에 필요한 값은 Webhook secret, webhook-id, webhook-timestamp, webhook-signature 헤더와 원문 body입니다.[1]

raw_body = request.get_data()
webhook_id = request.headers.get("webhook-id")
timestamp = request.headers.get("webhook-timestamp")
signature = request.headers.get("webhook-signature")

# SDK 또는 Standard Webhooks 방식의 검증 helper 사용
verify_signature(raw_body, webhook_id, timestamp, signature, secret)

위 코드는 특정 프레임워크의 완성 코드가 아니라 처리 순서를 보여 주는 예시예요. 직접 문자열을 조합해 검증 함수를 새로 만들기보다, 사용하는 SDK와 Standard Webhooks 규격의 검증 helper가 요구하는 인자를 그대로 연결하세요. 시간 검증과 재전송 공격 방어를 제공하는지도 함께 확인해야 합니다.

검증 실패와 처리 실패를 나눠요

서명 검증 실패는 요청이 신뢰할 수 없다는 뜻이므로 비즈니스 작업을 실행하지 않아야 해요. 반면 서명은 올바르지만 데이터베이스가 잠깐 멈춘 경우는 처리 실패입니다. 두 상황을 같은 500 오류로 묶으면 공격과 일시 장애를 구분하기 어려워집니다.

검증 실패에는 이벤트 본문이나 secret을 로그로 남기지 말고, 요청 시각·실패 유형·내부 추적 ID 정도만 기록하세요. 처리 실패는 재시도 가능한 오류와 영구 오류를 나눠 큐에 넣을지, 운영 알림을 보낼지 결정해야 합니다.

2xx 응답은 빠르게 반환해요

OpenAI는 Webhook endpoint가 성공적인 2xx를 빠르게 반환하지 않으면 요청을 다시 보내며, 실패한 전달을 최대 72시간 동안 지수 백오프로 재시도할 수 있다고 설명합니다. 3xx 리디렉션은 따라가지 않고 실패로 처리하므로, endpoint가 최종 주소에서 직접 2xx를 반환하도록 해야 해요.[1]

따라서 요청을 받은 HTTP 프로세스에서 파일 변환, 대규모 데이터베이스 갱신, 외부 API 호출을 모두 끝낸 뒤 응답하려고 하면 안 됩니다. 서명을 확인하고 이벤트를 내부 큐에 안전하게 기록한 다음 2xx를 반환하고, 실제 작업은 워커가 처리하도록 나누는 편이 좋습니다. 큐 기록 자체가 실패했다면 2xx를 반환하지 않아 재전송 기회를 남겨야 해요.

중복 이벤트를 전제로 저장해요

OpenAI 문서도 드문 시스템 문제로 같은 Webhook 이벤트가 중복 전달될 수 있으며, webhook-id를 멱등성 키로 사용할 수 있다고 안내합니다.[1] 데이터베이스에 이벤트 ID를 유일값으로 저장하고, 이미 처리한 ID가 다시 들어오면 작업을 한 번 더 실행하지 않고 성공 응답을 반환하세요.

if event_store.exists(webhook_id):
    return "already accepted", 200

event_store.insert(webhook_id, status="received")
queue.enqueue(webhook_id, event)
return "accepted", 200

큐에 넣은 뒤 워커가 실패하면 이벤트 상태를 processing에서 retryable로 바꾸는 식으로 추적합니다. 외부 시스템에 이메일을 보내거나 결제를 반영하는 작업은 Webhook ID만으로 충분하지 않을 수 있으니, 실제 대상 리소스 ID에도 별도의 멱등성 키를 두는 편이 안전해요.

백그라운드 응답과 Webhook을 함께 쓸 때

백그라운드 모드는 응답을 비동기로 실행하고 나중에 조회할 수 있게 합니다. OpenAI는 백그라운드 응답을 지원하기 위해 응답 데이터를 임시로 저장할 수 있고, 스트리밍을 켠 응답만 새 스트림을 시작할 수 있다고 설명해요.[2] 요청 상태 확인·폴링·취소까지 구현하는 흐름은 Responses API 백그라운드 실행 가이드에 정리되어 있어요. Webhook 이벤트의 응답 ID를 그대로 신뢰해 화면에 결과를 표시하기보다, 워커가 해당 ID와 이벤트 상태를 대조한 뒤 최종 결과를 읽도록 설계하세요.

Webhook이 늦게 도착하거나 중복될 수 있다는 전제를 두면, 사용자가 새로고침했을 때 같은 결과를 두 번 만들지 않는 구조를 만들기 쉬워집니다. 이벤트 순서가 바뀌는 경우에도 상태가 뒤로 돌아가지 않도록 허용 상태 전이를 정의해 두세요.

배포 전 점검표

  • 운영 endpoint와 개발 endpoint의 secret을 분리하기
  • 원문 body를 보존한 상태에서 서명을 검증하기
  • 검증 실패와 내부 처리 실패를 다른 로그·응답 정책으로 나누기
  • 수신 후 빠르게 2xx를 반환하고 무거운 작업은 워커로 넘기기
  • webhook-id를 유일값으로 저장해 중복 처리를 막기
  • 3xx 리디렉션 없이 최종 HTTPS endpoint가 직접 응답하는지 확인하기
  • 로그·오류 보고서에 secret과 원문 입력이 남지 않는지 확인하기

Webhook은 “알림을 받는 URL”이 아니라 외부 시스템이 내부 작업을 시작시키는 입구예요. 서명 검증만 붙인 뒤 끝내지 말고, 빠른 응답과 중복 제거를 먼저 안정화한 다음 실제 자동화를 연결하는 순서가 좋습니다.

Sources

  1. [1] OpenAI API documentation, Webhooks
  2. [2] OpenAI API documentation, Background mode

함께 읽을 글