---
title: コールバック | RunAPI
description: タスクコールバックの配信を安全に受信して検証します。
url: https://runapi.ai/ja/docs/guides/task-api/callbacks.md
canonical: https://runapi.ai/ja/docs/guides/task-api/callbacks
locale: ja
---

> HTML 版: https://runapi.ai/ja/docs/guides/task-api/callbacks
> エージェント向けサイトインデックス: https://runapi.ai/llms.txt

# コールバック

公開HTTPSエンドポイントでTaskのライフサイクルイベントを受け取るには、コールバックを使用してください。JSONボディを処理する前に必ずすべてのコールバックを検証し、アカウント向けに署名された配信のみがアプリケーションの状態を変更できるようにしてください。

## コールバックURLを設定する

タスクを作成するときに公開 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"
}
```

## コールバックシークレットを作成する

[認証ガイド](https://runapi.ai/ja/docs/guides/authentication.md)に従ってサインインし、APIキーを開いてタスクを作成するアカウントのCallback Secretを作成します。値をシークレットマネージャーに保存し、コールバックレシーバーのみが利用できるようにしてください。これは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
```

コールバックシークレットを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/ja/docs/guides/task-api/quickstart.md)に戻ってください。

---

## RunAPI のその他の情報

- [ホーム](https://runapi.ai/ja/.md)
- [モデルカタログ](https://runapi.ai/ja/models.md)
- [料金](https://runapi.ai/ja/pricing.md)
- [プロバイダー](https://runapi.ai/ja/models)
- [ドキュメント](https://runapi.ai/ja/docs/guides)
- [SDK](https://runapi.ai/ja/sdk.md)
- [CLI](https://runapi.ai/ja/cli.md)
- [MCP Server](https://runapi.ai/ja/mcp.md)
- [Claude Code と Cursor](https://runapi.ai/ja/claude-code-vs-cursor.md)
- [Cursor API セットアップ](https://runapi.ai/ja/cursor-api-setup.md)
- [RunAPI と OpenRouter の比較](https://runapi.ai/ja/openrouter-alternative.md)
- [エンタープライズ](https://runapi.ai/ja/contact.md)
- [お問い合わせ](https://runapi.ai/ja/contact.md)
- [利用規約](https://runapi.ai/ja/terms.md)
- [プライバシー](https://runapi.ai/ja/privacy.md)
- [エージェント向けサイトインデックス](https://runapi.ai/llms.txt)

お問い合わせ: contact@runapi.ai

## 構造化データ

```json
[
  {
    "@context": "https://schema.org",
    "inLanguage": "ja",
    "@type": "WebSite",
    "name": "RunAPI",
    "url": "https://runapi.ai/ja",
    "potentialAction": {
      "@type": "SearchAction",
      "target": {
        "@type": "EntryPoint",
        "urlTemplate": "https://runapi.ai/ja/models?q={search_term_string}"
      },
      "query-input": "required name=search_term_string"
    }
  },
  {
    "@context": "https://schema.org",
    "inLanguage": "ja",
    "@type": "Organization",
    "name": "RunAPI",
    "url": "https://runapi.ai/ja",
    "logo": {
      "@type": "ImageObject",
      "url": "https://runapi.ai/jaicon.svg"
    },
    "sameAs": [
      "https://github.com/runapi-ai"
    ]
  },
  {
    "@context": "https://schema.org",
    "inLanguage": "ja",
    "@type": "TechArticle",
    "headline": "コールバック",
    "description": "タスクコールバックの配信を安全に受信して検証します。",
    "url": "https://runapi.ai/ja/docs/guides/task-api/callbacks",
    "mainEntityOfPage": "https://runapi.ai/ja/docs/guides/task-api/callbacks"
  }
]
```
