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

# 비동기 작업 Webhook

> ToAPIs 서명 완료/실패 이벤트

> [!NOTE]
>
> **중국 본토 사용자 안내:** 중국 본토 사용자는 `https://toapis.cn` 를 API 엔드포인트(Base URL)로 사용해 주세요. 본 문서의 예시에서 `https://toapis.com` 을 `https://toapis.cn` 로 바꿔 주세요.

비동기 이미지와 비디오 작업에 `generation.completed`, `generation.failed`를 전송하고 테스트는 `endpoint.test`입니다. 최소 1회 전달이므로 동일 event ID를 멱등 처리하세요.

Token 편집에서 HTTPS 기본 URL, 한 번만 표시되는 32-byte secret, 활성화와 테스트를 설정합니다. `callback_url`이 없으면 기본 URL을 사용하며, 지정 시 같은 scheme/host/port의 path/query만 변경할 수 있습니다. 미설정 시 `400 webhook_not_configured`입니다.

```json theme={null}
{"id":"evt_xxx","type":"generation.completed","api_version":"v1","created_at":1783953302,"data":{"task_id":"tsk_xxx","client_business_id":"order_123","task_type":"image","model":"model-name","status":"SUCCESS","progress":"100%","result":{"type":"image","data":[{"url":"https://cdn.example/result.png"}]},"usage":{"charge_type":"per_request","requests":1},"billing":{"status":"settled","credits":"100","cost_usd":"0.5"},"error":null,"created_at":1783953000,"finished_at":1783953300,"completed_at":1783953300,"expires_at":1784039700}}
```

최종 실패는 내부 재시도와 환불 또는 과금 확정이 끝난 뒤에만 전송됩니다.

식별자 의미와 처리 방법은 [오류 코드 빠른 참조](https://docs.toapis.com/docs/ko/api-reference/errors.md) 를 확인하세요. Webhook 의 `data.error` 는 일부 작업 조회의 `reason` 필드를 보장하지 않습니다.

```json theme={null}
{"id":"evt_failure","type":"generation.failed","api_version":"v1","created_at":1783953302,"data":{"task_id":"tsk_failure","client_business_id":"order_456","task_type":"video","model":"model-name","status":"FAILURE","progress":"100%","result":null,"billing":{"status":"refunded","credits":"0","cost_usd":"0"},"error":{"code":"generation_failed","message":"Generation failed after retries"},"created_at":1783953000,"finished_at":1783953300}}
```

```json theme={null}
{"id":"evt_test","type":"endpoint.test","api_version":"v1","created_at":1783953302,"data":{"task_id":"endpoint_test","task_type":"image","status":"SUCCESS","progress":"100%","result":{"message":"ToAPIs webhook endpoint test"},"error":null,"created_at":0}}
```

`task_type`: `image|video`. 결과는 공개 작업 조회 형식과 같으며 channel ID, upstream task ID/key, credential, PrivateData는 포함하지 않습니다.

## 사용량, 과금 및 작업 조회의 차이

이미지와 동영상 성공 이벤트는 `data.usage`를 포함할 수 있습니다. 과금 수량, 확인 가능한 모델 토큰 내역, 도구 사용 횟수의 정의는 작업 조회와 같습니다. [작업 사용량 필드](https://docs.toapis.com/docs/ko/api-reference/account/pricing.md)를 참고하세요. 동영상 이벤트는 `data.generation`도 포함할 수 있습니다. Seedance 메타데이터와 호환 사용량은 [동영상 작업 조회](https://docs.toapis.com/docs/ko/api-reference/tasks/video-status.md)를 참고하세요.

`data.billing`은 `status`, `credits`, `cost_usd`를 포함할 수 있습니다. 금액은 십진수 문자열이며 `pending`이면 생략합니다. `refunded`는 순 청구 금액이 0으로 확인되었음을 뜻합니다. 확인할 수 없는 객체는 생략하지만 무료라는 의미는 아닙니다. Webhook과 작업 조회는 같은 확인 조건을 사용합니다. 제출 시 한 번에 과금되는 고정 과금 작업을 포함해 과금이 확인되면 둘 다 `settled`와 실제 금액을 반환합니다. `data.usage`에 `charge_type` 등 과금 수량이 있으면 `data.billing`도 항상 `settled`입니다.

### `data.billing` 필드 값

| 필드 | 타입 | 값 |
| - | - | - |
| `status` | string | `pending`: 정산 또는 환불이 아직 확인되지 않았으며 금액을 생략합니다. `settled`: 최종 청구 금액이 확정되어 실제 금액을 반환하며, 무료 작업은 `"0"`입니다. `refunded`: 환불되었거나 실패 후 순 청구 금액이 없는 것으로 확인되었으며 두 금액 모두 `"0"`입니다 |
| `credits` | string | 실제로 청구된 ToAPIs 크레딧을 `"100"`과 같은 십진수 문자열로 반환합니다. `settled`와 `refunded`일 때만 반환합니다 |
| `cost_usd` | string | 같은 청구의 USD 금액을 `"0.5"`와 같은 십진수 문자열로 반환합니다. `credits`와 같은 조건에서 반환하며 할인과 배율을 포함하고 공급자 원가가 아닙니다 |

성공 이벤트는 `settled`이며, 정산에서 선차감 금액을 전부 돌려준 경우에는 `refunded`입니다. 실패 이벤트는 환불이 완료된 뒤 전송되며 `refunded`입니다. 금액을 확인할 수 없으면 `data.billing` 객체 전체를 생략합니다.

### `data.usage` 필드 값

| 필드 | 타입 | 값 |
| - | - | - |
| `charge_type` | string | 과금 방식: `per_request`, `per_second`, `per_token` |
| `requests` | integer | 과금 요청 횟수. `per_request`일 때 반환합니다. 이미지 요청의 `n`을 직접 곱하지 않습니다 |
| `billed_tokens` | integer | 가격 계산에 사용한 토큰 수. `per_token`일 때 반환합니다 |
| `output_seconds` | integer | 과금 대상 출력 동영상 길이, 정수 초 단위. `per_second`일 때 반환합니다 |
| `input_seconds` | integer | 과금 대상 입력 동영상 길이, 정수 초 단위. 0이면 생략합니다 |
| `input_images` | integer | 무료 한도를 제외하고 과금 대상이 된 입력 이미지 수. 0이면 생략합니다 |
| `input_tokens`, `output_tokens`, `total_tokens` | integer | 모델이 보고한 입력, 출력, 전체 토큰 수 |
| `input_tokens_details` | object | 이미지 입력 세부 내역: `text_tokens`, `image_tokens`, `cached_tokens`, `cached_tokens_details.text_tokens`, `cached_tokens_details.image_tokens`. 캐시된 토큰은 전체 입력 토큰 수에 이미 포함됩니다 |
| `output_tokens_details` | object | 이미지 출력 세부 내역: `text_tokens`, `image_tokens`. 텍스트와 사고 과정의 가격이 같아 구분하지 않은 경우 `text_or_reasoning_tokens`가 추가되며 `text_tokens`에 포함하지 않습니다 |
| `prompt_tokens`, `completion_tokens` | integer | 동영상 모델이 보고한 토큰 수 |
| `tool_usage` | object | 도구 사용 횟수: `web_search`, `image_search`, `grounding_queries`. 사용한 경우에만 반환합니다 |

`charge_type`부터 `input_images`까지는 과금 수량이며 `data.billing.status=settled`와 함께만 반환합니다. 토큰 내역과 도구 사용 횟수는 업스트림 모델이 제공한 대로 독립적으로 반환합니다. 반환하지 않는 필드는 0으로 채우지 않고 생략합니다.

Webhook은 이벤트 생성 시점의 스냅샷이며 작업 조회는 가장 최근에 확인된 상태를 읽습니다. 누락된 사용량이나 금액을 기다리지 않고 전달하며 재시도 규칙도 바꾸지 않습니다. 생성된 이벤트를 나중에 보완하지 않고 재시도 시 원래 이벤트 본문을 사용합니다.

이벤트 구조와 `api_version: "v1"`은 유지됩니다. Webhook은 `data.task_id`, `SUCCESS` / `FAILURE`, 문자열 진행률 `"100%"`를 사용합니다. 작업 조회는 `id`, 소문자 상태, 정수 진행률을 사용합니다. 시간 필드는 공개 `generation.task` 조회와 이름과 의미가 같으며 모두 Unix 초입니다. `data.created_at`은 작업 제출 시각, `data.finished_at`은 종료 시각이며, 성공 시 `data.completed_at`과 `data.expires_at`(결과 만료 시각, 종료 시각 + 24시간)도 반환합니다. 값이 없는 필드는 생략합니다. 기존 `data.submitted_at`은 `data.created_at`으로 대체되어 더 이상 반환되지 않습니다. 이벤트 최상위 `created_at`은 이벤트 생성 시각이며 작업 제출 시각이 아닙니다. 접근 정책에 따라 두 응답의 미디어 URL은 다를 수 있습니다.

### Seedance 동영상 성공 이벤트 예시

예시는 필드 구조를 보여 주며 모델의 현재 가격이나 고정 과금 방식을 나타내지 않습니다. 실제 작업에서 반환된 필드와 금액을 확인하세요.

Seedance 성공 이벤트의 `data.result.data[]`에도 `fallback_url`과 `fallback_last_frame_url`이 포함될 수 있습니다. `url`과 `last_frame_url`을 우선 사용하고 원본 URL에 접근할 수 없을 때만 해당 대체 URL을 사용하세요. 대체 필드 생략은 작업 실패를 의미하지 않습니다. 이벤트는 생성 시점의 URL을 유지하며 이후 사본이 변경되어도 기존 이벤트나 재시도 페이로드를 수정하지 않습니다. 필드 정의와 사용 방법은[동영상 작업 조회](https://docs.toapis.com/docs/ko/api-reference/tasks/video-status.md)를 참고하세요.

```json theme={null}
{
  "id": "evt_seedance_example",
  "type": "generation.completed",
  "api_version": "v1",
  "created_at": 1792400061,
  "data": {
    "task_id": "task_seedance_example",
    "client_business_id": "order_123",
    "task_type": "video",
    "model": "seedance-2-global",
    "status": "SUCCESS",
    "progress": "100%",
    "result": {
      "type": "video",
      "data": [
        {
          "url": "https://cdn.example/result.mp4",
          "fallback_url": "https://files.toapis.com/videos/task_seedance_example/result.mp4",
          "format": "mp4",
          "last_frame_url": "https://cdn.example/last-frame.jpg",
          "fallback_last_frame_url": "https://files.toapis.com/videos/task_seedance_example/last-frame.jpg"
        }
      ]
    },
    "usage": {
      "charge_type": "per_token",
      "billed_tokens": 87300,
      "completion_tokens": 87300,
      "total_tokens": 87300
    },
    "billing": {
      "status": "settled",
      "credits": "87.3",
      "cost_usd": "0.4365"
    },
    "generation": {
      "duration": 5,
      "resolution": "720p",
      "ratio": "16:9",
      "generate_audio": true,
      "usage": {
        "completion_tokens": 87300,
        "total_tokens": 87300
      }
    },
    "error": null,
    "created_at": 1792400000,
    "finished_at": 1792400060,
    "completed_at": 1792400060,
    "expires_at": 1792486460
  }
}
```

```text theme={null}
User-Agent: ToAPIs-Webhook/1.0
X-ToAPIs-Webhook-Id
X-ToAPIs-Webhook-Timestamp
X-ToAPIs-Webhook-Signature: v1=<current>[,v1=<previous>]
```

서명 원문은 `event_id + "." + timestamp + "." + raw_request_body`, 알고리즘은 HMAC-SHA256입니다. 최초 요청은 5분 초과 timestamp를 거부하고 secret 회전 후 24시간은 신/구 서명을 모두 허용합니다.

```python Python theme={null}
def verify(headers, raw_body, secrets):
    event_id=headers["X-ToAPIs-Webhook-Id"]; timestamp=int(headers["X-ToAPIs-Webhook-Timestamp"])
    if abs(int(time.time())-timestamp)>300: return False
    signed=f"{event_id}.{timestamp}.".encode()+raw_body
    received=[v.strip()[3:] for v in headers["X-ToAPIs-Webhook-Signature"].split(",") if v.strip().startswith("v1=")]
    return any(hmac.compare_digest(hmac.new(s.encode(),signed,hashlib.sha256).hexdigest(),r) for s in secrets for r in received)
```

```javascript Node.js theme={null}
function verify(headers,rawBody,secrets){const id=headers['x-toapis-webhook-id'];const ts=Number(headers['x-toapis-webhook-timestamp']);if(!Number.isFinite(ts)||Math.abs(Date.now()/1000-ts)>300)return false;const signed=Buffer.concat([Buffer.from(`${id}.${ts}.`),rawBody]);const received=headers['x-toapis-webhook-signature'].split(',').filter(v=>v.trim().startsWith('v1=')).map(v=>v.trim().slice(3));return secrets.some(secret=>received.some(sig=>{const expected=crypto.createHmac('sha256',secret).update(signed).digest('hex');return expected.length===sig.length&&crypto.timingSafeEqual(Buffer.from(expected),Buffer.from(sig));}));}
```

```go Go theme={null}
func Verify(id,ts,sig string,body []byte,secrets []string) bool { unix,err:=strconv.ParseInt(ts,10,64);now:=time.Now().Unix();if err!=nil||now-unix>300||unix-now>300{return false};signed:=append([]byte(id+"."+ts+"."),body...);for _,secret:=range secrets{mac:=hmac.New(sha256.New,[]byte(secret));mac.Write(signed);expected:=hex.EncodeToString(mac.Sum(nil));for _,v:=range strings.Split(sig,","){if hmac.Equal([]byte(expected),[]byte(strings.TrimPrefix(strings.TrimSpace(v),"v1="))){return true}}};return false }
```

첫 전달은 즉시 수행하며 실패 후 10초, 30초, 2분, 10분, 1시간, 6시간, 24시간에 재시도합니다. `2xx`만 성공이고 `3xx`는 따라가지 않으며 `4xx/5xx`는 재시도합니다. timeout은 10초입니다. 매 전달마다 HTTPS, userinfo, fragment, 잘못된 port, SSRF, DNS/IP와 DNS rebinding을 검증합니다. 실패 시 상태 조회를 대체 경로로 사용하고 만료 가능한 결과 URL을 즉시 저장하세요. 실명 인증, 결제, OAuth callback은 제외됩니다. [속도 제한](https://docs.toapis.com/docs/ko/api-reference/rate-limits/async-tasks.md).


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