> ## Documentation Index
> Fetch the complete documentation index at: https://studio-docs.thezet.io/llms.txt
> Use this file to discover all available pages before exploring further.

# 웹훅

> 작업이 끝나면 고객사 서버로 알려드려요.

계속 조회하지 않아도, 작업이 끝나면 고객사 서버로 알려드려요. API로 만든 작업만 보내고, 사이트에서 만든 작업은 보내지 않아요.

## 등록하기

1. [설정의 웹훅 화면](https://studio.thezet.io/settings/webhooks)을 열어요.
2. **Add Endpoint**에 받을 주소를 넣고, 받을 이벤트를 골라요. 이벤트는 하나 이상 골라야 하고, 주소는 `https://`여야 해요.
3. 만든 엔드포인트의 **Signing Secret**(`whsec_`로 시작해요)을 고객사 서버에 보관해요.

같은 화면에서 전달 기록을 보고, 실패한 이벤트를 다시 보내고, 테스트 이벤트를 보내고, 시크릿을 바꿀 수 있어요. 이 화면은 영어로만 제공돼요.

## 이벤트

| 이벤트 | 뜻 |
| - | - |
| `generation.succeeded` | 작업이 끝나 결과를 받을 수 있어요. 금액이 청구됐어요. |
| `generation.failed` | 작업이 실패했어요. 금액은 돌려드렸어요. |
| `generation.canceled` | 작업을 취소했어요. 금액은 돌려드렸어요. |

```json theme={null}
{
  "type": "generation.succeeded",
  "timestamp": "2026-11-05T09:30:12.000Z",
  "data": {
    "id": "0b5f3c1e-7a2d-4c11-9f0e-2d6a8e1b4c77",
    "status": "succeeded",
    "terminal": true,
    "model": "z-image-turbo",
    "modality": "image",
    "price_usd": "0.0100",
    "error_code": null,
    "created_at": "2026-11-05T09:30:01.000Z",
    "finished_at": "2026-11-05T09:30:12.000Z"
  }
}
```

**이벤트에는 프롬프트, 입력, 결과 주소가 없어요.** 결과는 `data.id`로 `GET /generations/{id}`를 호출해서 받아요. `error_code`는 [오류](https://studio-docs.thezet.io/errors.md#작업-실패)의 코드와 같아요.

## 서명 확인

주소를 아는 누구나 요청을 보낼 수 있으니, **처리하기 전에 꼭 서명을 확인해 주세요.** 요청에는 `svix-id`, `svix-timestamp`, `svix-signature` 헤더가 와요. 아래 라이브러리에 헤더를 그대로 넘기면 돼요.

* **원본 본문을 넘겨요.** JSON으로 파싱한 뒤 다시 만든 문자열은 서명이 맞지 않아요.
* **서버 시계를 맞춰 주세요.** 서명 시각이 5분 넘게 차이 나면 오래된 요청으로 보고 거절해요.

[Standard Webhooks](https://www.standardwebhooks.com) 규격이에요. 그 규격의 라이브러리를 쓰려면 헤더 이름을 `webhook-id`, `webhook-timestamp`, `webhook-signature`로 바꿔서 넘겨 주세요.

```typescript TypeScript theme={null}
import { Webhook } from "svix"; // npm install svix

const webhook = new Webhook(process.env.ZET_WEBHOOK_SECRET!);

export async function POST(request: Request) {
  const body = await request.text();
  try {
    webhook.verify(body, Object.fromEntries(request.headers)); // 서명이 다르면 예외를 던져요.
  } catch {
    return new Response("invalid signature", { status: 400 });
  }
  const event = JSON.parse(body);
  await queue.add(event.data.id); // 오래 걸리는 일은 응답한 뒤에 처리해요.
  return new Response(null, { status: 204 });
}
```

```python Python theme={null}
import json, os
from flask import Flask, request
from svix.webhooks import Webhook, WebhookVerificationError  # pip install svix

app = Flask(__name__)
webhook = Webhook(os.environ["ZET_WEBHOOK_SECRET"])

@app.post("/webhooks/zet")
def receive():
    body = request.get_data()
    try:
        webhook.verify(body, request.headers)  # 서명이 다르면 예외를 던져요.
    except WebhookVerificationError:
        return "invalid signature", 400
    event = json.loads(body)
    queue.add(event["data"]["id"])  # 오래 걸리는 일은 응답한 뒤에 처리해요.
    return "", 204
```

## 응답과 재시도

30초 안에 `2xx`로 답하면 전달한 거예요. 그 밖의 응답은 실패로 보고, 리다이렉트(`3xx`)도 따라가지 않아요.

실패하면 이 간격으로 다시 보내요. 앞 시도에서부터 잰 시간이에요.

| 시도 | 기다리는 시간 |
| - | - |
| 1 | 바로 |
| 2 | 5초 |
| 3 | 5분 |
| 4 | 30분 |
| 5 | 2시간 |
| 6 | 5시간 |
| 7 | 10시간 |
| 8 | 10시간 |

## 꼭 지켜 주세요

* **같은 이벤트가 두 번 올 수 있어요.** 작업 하나에 이벤트는 하나뿐이니 `data.id`로 이미 처리했는지 확인해 주세요.
* **순서를 기대하지 마세요.** 나중에 끝난 작업의 이벤트가 먼저 올 수 있어요.
* **웹훅만 믿지 마세요.** 드물게 늦거나 전달되지 않을 수 있어요. 오래 끝나지 않는 작업은 `GET /generations/{id}`로 확인하면 정확한 상태를 알 수 있어요.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.