"""OpenAI webhook receiver — Python/FastAPI + official SDK `webhooks.unwrap`, with a manual-HMAC route. STATUS: UNVERIFIED as a running server (fastapi/uvicorn not installed in this repo; py_compile OK); the verification logic is LIVE_VERIFIED offline by offline_test.py (2026-09-18). pip install openai fastapi uvicorn OPENAI_WEBHOOK_SECRET=whsec_... uvicorn server_fastapi:app --port 8000 Register https:///webhook in the dashboard (Settings -> Project -> Webhooks) or via POST /v1/webhook_endpoints, then send a test event (POST /v1/webhook_endpoints/{id}/test). """ from __future__ import annotations import os from fastapi import BackgroundTasks, FastAPI, Request, Response from openai import InvalidWebhookSignatureError, OpenAI from verify_manual import InvalidSignature, verify as manual_verify SECRET = os.environ["OPENAI_WEBHOOK_SECRET"] # shown once at creation / rotation client = OpenAI(webhook_secret=SECRET) # OPENAI_API_KEY needed only to call the API back app = FastAPI() _seen: set[str] = set() # dedupe on webhook-id (redeliveries possible for up to 72 h) def handle(event) -> None: if event.type == "response.completed": resp = client.responses.retrieve(event.data.id) print("response done:", resp.id, (resp.output_text or "")[:80]) elif event.type in {"batch.completed", "fine_tuning.job.succeeded", "eval.run.succeeded", "video.completed"}: print(event.type, event.data.id) else: print("event", event.type) @app.post("/webhook") async def webhook(request: Request, tasks: BackgroundTasks) -> Response: raw = await request.body() # signature covers the RAW body — never re-serialise try: event = client.webhooks.unwrap(raw, request.headers) # raises on bad signature / stale timestamp (300 s) except InvalidWebhookSignatureError: return Response("Invalid signature", status_code=400) delivery_id = request.headers.get("webhook-id", "") if delivery_id in _seen: return Response(status_code=200) _seen.add(delivery_id) tasks.add_task(handle, event) # ack immediately, work in background return Response(status_code=200) @app.post("/webhook-manual") async def webhook_manual(request: Request) -> Response: raw = await request.body() try: event = manual_verify(raw, request.headers, SECRET) except InvalidSignature: return Response("Invalid signature", status_code=400) print("verified (manual)", event["type"], event["data"]["id"]) return Response(status_code=200)