ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [OpenAI][Responses API] response.failed 뒤 webhook body에는 error가 없고 retrieve 결과만 남을 때 어떤 재조회 단계를 로그에 남기나
    기타개발지식/풀스택개발 2026. 7. 10. 09:15

    IT 리서치 노트

    [OpenAI][Responses API] response.failed 뒤 webhook body에는 error가 없고 retrieve 결과만 남을 때 어떤 재조회 단계를 로그에 남기나

    Responses API를 background mode와 webhook으로 운영하면 `response.failed`를 받는 순간 이미 필요한 정보가 다 왔다고 생각하기 쉽다. 하지만 2026년 7월 10일 기준 OpenAI 공식 문서를 다시 보면 failed webhook payload는 event id와 response id 중심이고, 실제 response 객체의 `error`와 `incomplete_details`는 retrieve 레이어에 있다. 여기에 background polling 창은 대략 10분이다. 이 글은 failed event를 받은 뒤 webhook body에는 error가 없고 retrieve 결과만 남는 구간에서 어떤 재조회 단계를 운영 로그에 남기고 언제 닫는 편이 맞는지 정리한 것이다.

    1. 개요

    결론부터 말하면 response.failed를 받은 뒤에는 receipt를 닫지 말고, 최소 한 번 이상의 retrieve snapshot 단계를 별도로 로그에 남기는 편이 맞다. webhook body는 event와 response id를 주지만, 실제 error code와 human-readable message는 response retrieve 레이어가 더 잘 보여 주기 때문이다. 또한 background mode의 polling 창은 대략 10분이므로, 재조회 단계는 무한 retry보다 시도 횟수 + 마지막 retrieve 시각 + 닫힌 이유를 같이 남기는 구조가 실무적으로 낫다.

    이미 failed와 incomplete 상태 필드 분리 글이 terminal state 종류를 갈랐다면, 이번 글은 failed receipt 뒤 response snapshot을 어떻게 닫을지에 더 가깝다. 또 retrieve 재조회와 감사 로그 분리 글을 이미 읽었다면, 이번 글은 그 재조회 단계를 failed 전용으로 더 좁힌 후속편이다.

    2. 어디서 실제로 막히는가

    현장에서 먼저 꼬이는 지점은 세 가지다. 첫째, failed webhook receipt를 곧바로 장애 closure로 간주해 retrieve 단계 자체를 열지 않는다. 둘째, retrieve는 했지만 attempt count와 마지막 retrieve 시각을 안 남겨, 나중에 왜 여기서 재조회를 멈췄는지 설명하지 못한다. 셋째, background mode의 대략 10분 polling 창과 팀 내부 장기 장애 분석 창을 같은 TTL로 취급해, receipt는 남았는데 response snapshot 근거가 비거나 반대로 raw receipt가 필요 이상으로 오래 남는다.

    OpenAI webhook 타입 레퍼런스는 failed event가 event id와 payload data.id를 가진다고 설명하고, webhooks 가이드는 드물게 중복 webhook 사본이 올 수 있으니 webhook-id로 dedupe하라고 적고 있다. retrieve 레퍼런스는 response 객체에 error와 incomplete_details를 별도 속성으로 둔다. background guide는 polling을 위해 response data를 대략 10분 보관한다고 적는다. 이 문장들을 묶어 읽으면 failed receipt와 retrieve snapshot, retry closure를 같은 단계로 합치면 안 된다는 점이 분명해진다.

    특히 알림 파이프라인, 사람 승인, 비용 정산, 사후 RCA가 모두 같은 response_id를 공유하는 팀에서는 receipt만 남겨서는 질문이 끝나지 않는다. webhook receipt는 '무엇이 들어왔나'를 말하고, retrieve snapshot은 '그때 response가 어떤 상태였나'를 말한다. 두 질문이 다르기 때문에 같은 시각, 같은 TTL, 같은 closure reason으로 묶는 순간 나중에 운영 문서가 모호해진다.

    • 증상: failed webhook이 왔는데 error code 복기가 안 된다.
    • 실패: receipt 수신만으로 closure를 끝내고 retrieve snapshot을 남기지 않는다.
    • 막힘: retrieve attempt 수와 마지막 시각이 없어 polling 창 안에서 무슨 일이 있었는지 설명하지 못한다.
    • 누락: dedupe key, response_id, retrieve closure reason을 서로 다른 질문으로 분리하지 않는다.
    질문 먼저 볼 필드 이유
    중복 delivery였나 webhook-id receipt 중복 차단 질문이다
    response가 실제로 어떤 error를 가졌나 response.error retrieve snapshot 질문이다
    언제 polling closure를 닫았나 attempt_count, last_retrieve_at polling 창 관리 질문이다

    3. 실무에서 적용하는 순서

    가장 덜 꼬이는 구조는 세 단계다. 먼저 receipt 레이어에서 webhook-id, event.id, response_id, received_at을 남긴다. 두 번째로 response.failed를 받으면 즉시 첫 retrieve snapshot을 남긴다. 세 번째로 closure 레이어에서 attempt count, 마지막 retrieve 시각, closure reason을 기록한다. 여기서 핵심은 '몇 번 재조회했는가'보다 '어느 창 안에서 어떤 이유로 닫았는가'를 함께 남기는 것이다.

    1. failed receipt를 받은 즉시 dedupe 후 response_id를 열어 둔다.
    2. 첫 retrieve snapshot에서 status와 error 필드를 저장한다.
    3. retry가 필요하면 attempt 수와 마지막 시각을 누적한다.
    4. about 10분 창을 넘기면 short_retrieve_fallback을 닫는다.
    5. 장기 incident 보존은 closure summary만 따로 남긴다.

    이렇게 하면 raw receipt를 오래 들고 가지 않아도, 왜 해당 failed response를 그 시점에 닫았는지 설명할 수 있다. 예를 들어 첫 retrieve에서 이미 error code가 분명하면 closure reason을 error_visible로 닫고, 첫 retrieve가 일시적으로 비었거나 아직 state가 갱신되지 않았으면 retry_until_window_end로 짧은 재조회를 열 수 있다. 이 부분은 OpenAI 문서가 직접 retry 횟수를 규정하지는 않지만, background data를 대략 10분 보관한다는 문구에서 창 기반 closure가 더 합리적이라는 점을 추론할 수 있다.

    운영 메모 예시
    receipt_stage=open
    retrieve_attempt=2
    last_retrieve_at=2026-07-10T09:26:00+09:00
    closure_reason=error_visible|retry_window_expired
    closure_window=about_10m
    incident_summary_kept=true

    운영 표에 이 다섯 칸이 있으면 receipt, response snapshot, closure summary가 서로 다른 층이라는 점이 명확해진다. 그러면 failed event를 받았는데도 팀마다 retry 해석이 달라지는 문제를 훨씬 줄일 수 있다.

    4. 공식 문서와 예시 화면으로 확인하기

    첫 자료는 webhook 타입 레퍼런스의 failed 이벤트 구간이다. 여기서는 failed webhook이 event 자체의 id와 payload 안 response id만 준다는 점을 먼저 확인해야 한다.

    OpenAI webhook 타입 레퍼런스는 ResponseFailedWebhookEvent가 event id와 payload data.id를 별도로 가진다고 보여 준다.
    OpenAI webhook 타입 레퍼런스는 ResponseFailedWebhookEvent가 event id와 payload data.id를 별도로 가진다고 보여 준다.

    즉 receipt body 자체만으로는 error code나 실제 실패 이유를 충분히 알 수 없다. event를 받자마자 어떤 retrieve 단계를 열어 둘지 운영 로그가 따로 가져가야 한다.

    두 번째 자료는 retrieve 응답 모델이다. 이 레퍼런스에는 response 객체에 error와 incomplete_details가 따로 존재한다는 점이 적혀 있다.

    Responses retrieve 레퍼런스는 response 객체에 error와 incomplete_details를 별도 필드로 둔다.
    Responses retrieve 레퍼런스는 response 객체에 error와 incomplete_details를 별도 필드로 둔다.

    이 구조를 보면 webhook receipt와 retrieve snapshot을 같은 로그 행 하나로 끝내기보다, receipt 이후 실제 response snapshot을 한 번 더 남기는 편이 맞다는 결론이 나온다. failed 뒤에 무엇이 남았는지는 retrieve가 더 잘 말해 준다.

    세 번째 자료는 background mode의 polling 창이다. OpenAI는 response data를 대략 10분 보관해 polling을 가능하게 한다고 설명한다.

    OpenAI background guide는 response data를 대략 10분 보관해 polling을 가능하게 한다고 안내한다.
    OpenAI background guide는 response data를 대략 10분 보관해 polling을 가능하게 한다고 안내한다.

    이 문장을 기준으로 하면 retry count 자체보다 retry가 어느 시각 창 안에 있었는지가 더 중요하다. 운영 문서에 attempt 수와 마지막 retrieve 시각을 같이 남겨야 하는 이유도 여기서 나온다.

    실무에서는 receipt와 retrieve를 단계로 끊어 둔 표가 가장 빠르다. 어느 단계에서 어떤 필드를 남기는지 표로 먼저 쪼개 두면 failed 이후 조사 시간이 크게 줄어든다.

    response.failed 뒤 receipt, retrieve, closure 단계를 나눠 기록하는 운영 표다.
    response.failed 뒤 receipt, retrieve, closure 단계를 나눠 기록하는 운영 표다.

    이 표처럼 단계 이름과 종료 조건을 나눠 두면, 바로 앞 글인 failed와 incomplete 상태 필드 분리 글이 terminal state를 나눴다면 이번 글은 그 뒤의 retrieve closure 기준을 덧붙이는 역할을 하게 된다.

    마지막 자료는 운영 로그 예시다. attempt 수와 마지막 retrieve 시각, 닫힌 이유를 한 레코드에 남겨 두면 며칠 뒤에도 왜 여기서 재조회를 멈췄는지 설명할 수 있다.

    response.failed 뒤 retrieve 재조회 단계를 남기는 로그 스키마 예시다.
    response.failed 뒤 retrieve 재조회 단계를 남기는 로그 스키마 예시다.

    이 구조를 쓰면 event.id와 response_id 보존 기준 글의 키 설계, webhook-id 중복 차단 글의 dedupe 규칙과도 바로 연결된다.

    5. 주의사항과 리스크

    첫 번째 리스크는 failed event를 받자마자 장애 원인을 모두 알 수 있다고 가정하는 것이다. 두 번째 리스크는 retrieve snapshot을 남기더라도 attempt 수와 마지막 시각을 저장하지 않아, 왜 그 지점에서 polling을 멈췄는지 설명하지 못하는 것이다. 세 번째 리스크는 OpenAI의 짧은 polling 창을 팀 내부 장기 장애 보존 창과 같은 값으로 취급하는 것이다.

    운영 전에 확인할 때는 최소한 webhook-id, response_id, retrieve_attempt, last_retrieve_at, closure_reason 다섯 칸을 같은 문서에 정의해 두는 편이 좋다. 이 다섯 칸만 분리해도 failed 이후 로그가 훨씬 빨리 읽힌다.

    • receipt와 response snapshot은 같은 질문이 아니다.
    • retry 수보다 closure 시각과 이유가 더 중요하다.
    • short polling 창과 incident 보존 창은 서로 다른 레이어다.

    6. 결론

    OpenAI Responses API에서 response.failed 뒤 webhook body에는 error가 없고 retrieve 결과가 실질적인 원인 스냅샷이 된다. 그래서 failed receipt를 받았을 때는 receipt를 끝내지 말고 retrieve 재조회 단계를 별도 로그로 열어 두는 편이 맞다. 그리고 그 단계는 무한 retry가 아니라 attempt 수 + 마지막 retrieve 시각 + closure reason으로 닫아야 운영 문서가 짧아진다.

    • failed receipt 뒤에는 retrieve snapshot을 한 번 더 남긴다.
    • closure는 attempt 수와 시각, 이유를 같이 기록한다.
    • about 10분 polling 창은 별도 레이어로 취급한다.

    7. 참고 링크

    1. https://developers.openai.com/api/reference/typescript/resources/webhooks
    2. https://developers.openai.com/api/reference/typescript/resources/responses/methods/retrieve
    3. https://developers.openai.com/api/docs/guides/background
    4. https://developers.openai.com/api/docs/guides/webhooks
Designed by Tistory.