---
title: 콜백 | RunAPI
description: Task 콜백 전달을 안전하게 수신하고 확인합니다.
url: https://runapi.ai/ko/docs/guides/task-api/callbacks.md
canonical: https://runapi.ai/ko/docs/guides/task-api/callbacks
locale: ko
---

> HTML 버전: https://runapi.ai/ko/docs/guides/task-api/callbacks
> 에이전트용 사이트 색인: https://runapi.ai/llms.txt

# 콜백

공개 HTTPS 엔드포인트에서 Task 수명 주기 이벤트를 수신하려면 콜백을 사용하세요.
JSON 본문을 처리하기 전에 모든 콜백을 검증하여 계정에 서명된 전달만이
애플리케이션 상태를 변경할 수 있도록 하세요.

## 콜백 URL 구성

Task를 생성할 때 공개 HTTPS `callback_url`을 추가합니다. 이벤트 본문과 생명주기 상태는 엔드포인트에 따라 다르며, 콜백 페이로드에 대한 자세한 내용은 해당 엔드포인트의 API 참조를 확인하십시오.

```json
{
  "model": "flux-2-pro-text-to-image",
  "prompt": "A product photograph on a clean studio background",
  "callback_url": "https://your-domain.com/webhooks/runapi"
}
```

## Callback Secret 만들기

[인증 가이드](https://runapi.ai/ko/docs/guides/authentication.md)에 따라 로그인한 후 API 키를 열고 Task를 생성하는 계정에 대한 콜백 시크릿을 생성하세요. 값을 시크릿 매니저에 저장하고 콜백 수신기에서만 사용할 수 있도록 하세요. 이것은 API 키가 아니며 Task 요청에 절대 전송해서는 안 됩니다.

비밀은 해당 계정의 콜백에 서명합니다. 비밀을 교체하면 이후 전달의 서명이 변경되므로, 모든 콜백 수신자를 즉시 업데이트하고, 진행 중인 전달을 처리하는 데 필요한 시간 동안만 두 값을 모두 사용할 수 있는 상태로 유지하세요.

## 콜백 서명 확인

각 콜백은 `Content-Type: application/json`을 가진 HTTP `POST`입니다.
RunAPI는 이 요청에 `Authorization` 헤더를 추가하지 않습니다. 본문을 역직렬화하기 전에 다음 헤더를 확인하세요:

| 헤더 | 의미 |
|----------
| `X-Callback-Id` | 이번 전송 시도에 대한 고유 식별자. |
| `X-Callback-Timestamp` | 전송이 서명된 시점의 Unix 타임스탬프(초 단위). |
| `X-Callback-Signature` | Base64로 인코딩된 HMAC-SHA-256 서명. |

수정되지 않은 요청 본문 바이트를 사용하여 다음과 같이 정확하게 서명 값을 작성하십시오:

```text
X-Callback-Id + "." + X-Callback-Timestamp + "." + raw request body
```

Callback Secret을 Base64로 디코딩하고, 해당 값에 대해 HMAC-SHA-256을 계산한 후 결과를
Base64로 인코딩하여 타이밍 안전 비교 방식으로
`X-Callback-Signature`와 비교하십시오. 검증 전에 JSON을 파싱하거나
재직렬화하지 마십시오.

### JavaScript

```javascript
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyCallback({headers, rawBody, callbackSecret}) {
  const callbackId = headers["x-callback-id"];
  const timestamp = Number(headers["x-callback-timestamp"]);
  const signature = headers["x-callback-signature"];

  if (!callbackId || !signature || !Number.isSafeInteger(timestamp)) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;

  const signedContent = `${callbackId}.${timestamp}.${rawBody}`;
  const expected = createHmac("sha256", Buffer.from(callbackSecret, "base64"))
    .update(signedContent, "utf8")
    .digest();
  const received = Buffer.from(signature, "base64");

  return expected.length === received.length && timingSafeEqual(expected, received);
}
```

프레임워크의 원시 요청 본문 문자열을 `rawBody`에 전달하세요. 파싱된 데이터에
`JSON.stringify`를 호출하지 마세요.

### Python

```python
import base64
import hashlib
import hmac
import time


def verify_callback(headers, raw_body, callback_secret):
    callback_id = headers.get("X-Callback-Id")
    timestamp = headers.get("X-Callback-Timestamp")
    signature = headers.get("X-Callback-Signature")

    if not callback_id or not timestamp or not signature:
        return False

    try:
        timestamp = int(timestamp)
        secret = base64.b64decode(callback_secret, validate=True)
        received = base64.b64decode(signature, validate=True)
    except (ValueError, TypeError):
        return False

    if abs(time.time() - timestamp) > 300:
        return False

    signed_content = f"{callback_id}.{timestamp}.{raw_body}".encode("utf-8")
    expected = hmac.new(secret, signed_content, hashlib.sha256).digest()
    return hmac.compare_digest(expected, received)
```

HTTP 프레임워크가 수신한 정확한 원시 요청 본문 문자열을
`raw_body`로 전달하세요.

## 안전한 전달 처리

* 콜백이 처리 대기 중으로 수락된 후에만 `2xx` 응답을 반환하십시오. `2xx`가 아닌 응답이나 전송 실패 후에는 최대 10회까지 재전송을 시도합니다.
* 15초 이내에 응답하십시오. 처리 속도가 느린 작업은 HTTP 응답을 차단하는 대신 검증 후 큐에 넣으십시오.
* 서명 헤더가 누락되었거나, 서명이 유효하지 않거나, 타임스탬프가 수신기에서 정의한 허용 범위를 벗어난 콜백은 거부하십시오.

## 검증 문제 해결

* **서명 불일치:** Callback Secret이 작업과 동일한 계정에 속하는지 확인하고, Base64로 디코딩한 후 파싱된 JSON이 아닌 원본 바디에 서명하세요.
* **서명 헤더 누락:** 상태 변경에 콜백을 사용하기 전에 Callback Secret을 생성하세요.
* **타임스탬프 거부됨:** 수신자의 클럭을 동기화하고 배포 환경에 적합한 허용 오차를 사용하세요.

설정 및 수명 주기 안내에 대해서는 [Task API
빠른 시작](https://runapi.ai/ko/docs/guides/task-api/quickstart.md)으로 돌아가세요.

---

## RunAPI의 더 많은 정보

- [홈](https://runapi.ai/ko/.md)
- [모델 카탈로그](https://runapi.ai/ko/models.md)
- [요금](https://runapi.ai/ko/pricing.md)
- [제공사](https://runapi.ai/ko/models)
- [문서](https://runapi.ai/ko/docs/guides)
- [SDK](https://runapi.ai/ko/sdk.md)
- [CLI](https://runapi.ai/ko/cli.md)
- [MCP Server](https://runapi.ai/ko/mcp.md)
- [Claude Code와 Cursor](https://runapi.ai/ko/claude-code-vs-cursor.md)
- [Cursor API 설정](https://runapi.ai/ko/cursor-api-setup.md)
- [RunAPI와 OpenRouter 비교](https://runapi.ai/ko/openrouter-alternative.md)
- [엔터프라이즈](https://runapi.ai/ko/contact.md)
- [문의](https://runapi.ai/ko/contact.md)
- [약관](https://runapi.ai/ko/terms.md)
- [개인정보](https://runapi.ai/ko/privacy.md)
- [에이전트용 사이트 색인](https://runapi.ai/llms.txt)

문의: contact@runapi.ai

## 구조화된 데이터

```json
[
  {
    "@context": "https://schema.org",
    "inLanguage": "ko",
    "@type": "WebSite",
    "name": "RunAPI",
    "url": "https://runapi.ai/ko",
    "potentialAction": {
      "@type": "SearchAction",
      "target": {
        "@type": "EntryPoint",
        "urlTemplate": "https://runapi.ai/ko/models?q={search_term_string}"
      },
      "query-input": "required name=search_term_string"
    }
  },
  {
    "@context": "https://schema.org",
    "inLanguage": "ko",
    "@type": "Organization",
    "name": "RunAPI",
    "url": "https://runapi.ai/ko",
    "logo": {
      "@type": "ImageObject",
      "url": "https://runapi.ai/koicon.svg"
    },
    "sameAs": [
      "https://github.com/runapi-ai"
    ]
  },
  {
    "@context": "https://schema.org",
    "inLanguage": "ko",
    "@type": "TechArticle",
    "headline": "콜백",
    "description": "Task 콜백 전달을 안전하게 수신하고 확인합니다.",
    "url": "https://runapi.ai/ko/docs/guides/task-api/callbacks",
    "mainEntityOfPage": "https://runapi.ai/ko/docs/guides/task-api/callbacks"
  }
]
```
