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

# キャンペーン予約Webhook

> プロジェクトで予約されたキャンペーンの変更をWebhookで受け取り、予約済みキャンペーンをAPIで取得する方法をご案内します。

広告主のキャンペーンが承認され、パブリッシャーの配信スケジュールと広告ユニットが確保されることを予約といいます。キャンペーン予約Webhookを登録すると、予約済みキャンペーンが承認・変更・一時停止・再開・キャンセル・開始・終了されるたびに、Adropが登録したURLへイベントを送信します。予約済みキャンペーンはAPIで直接取得することもできます。

| 区分 | 方向 | 説明 |
| - | - | - |
| [Webhookの登録](#webhookの登録) | パブリッシャー → Adrop | APIキーで、イベントを受け取るURLを登録・取得・削除します。 |
| [イベントの受信](#イベントの受信) | Adrop → パブリッシャー | 予約済みキャンペーンが変わるたびにイベントを受け取ります。 |
| [予約済みキャンペーンの取得](#予約済みキャンペーンの取得) | パブリッシャー → Adrop | 予約済みキャンペーンを取得します。初回連携時や、イベントを取りこぼしたときに状態を合わせるために使います。 |

イベントと取得結果には、キャンペーンのIDとタイトル、ステータス、広告商品のIDと名前、販売方式、予算、広告主の名前とURL、配信期間、広告ユニット・アプリ・ターゲティング・ターゲティングオプションのIDと名前、ターゲティングポリシー、コンテキストターゲティングで選択されたオプションのコンテキストIDが含まれます。広告主のメールアドレスなどの連絡先、クリエイティブ、単価・割増、決済、審査の内容は含まれません。

***

## 事前準備

* 次の2つの権限（スコープ）を持つAPIキーが必要です。キーはプロジェクトのオーナーが **\[設定]** > **\[連携]** メニューで発行します。キーの発行方法は[連携 & APIキー](/ja/integrations/overview)を参照してください。

  | 権限 | 用途 |
  | - | - |
  | `campaign:subscribe` | Webhookの登録・取得・削除 |
  | `campaign:read` | 予約済みキャンペーンの取得 |

* `campaign:subscribe`はAPIキーの作成時に初期状態で選択されていないため、自分で選択してください。発行済みのキーの権限は変更できません。

* イベントを受け取るサーバーには、インターネットからアクセスできる`https`のURLが必要です。

<Warning>
  APIキーはサーバーにのみ保管し、アプリやウェブページに含めないでください。イベントの署名にもこのキーが使われるため、キーが漏えいした場合はすぐに廃棄し、新しいキーでWebhookを登録し直してください。
</Warning>

***

## 基本情報

| 項目 | 値 |
| - | - |
| Base URL | `https://lake.adrop.io` |
| 認証 | `x-api-key`ヘッダーにAPIキーを指定します。キーによってプロジェクトが決まります。 |
| Content-Type | `application/json` |
| IDの形式 | 26文字のULIDです（例：`01K6Y8X9Z0A1B2C3D4E5F6G7H8`）。 |

| メソッド | パス | 権限 | 説明 |
| - | - | - | - |
| `PUT` | `/webhook` | `campaign:subscribe` | [Webhookの登録・更新](#webhookの登録) |
| `GET` | `/webhook` | `campaign:subscribe` | [Webhook設定の取得](#webhook設定の取得) |
| `DELETE` | `/webhook` | `campaign:subscribe` | [Webhookの削除](#webhookの削除) |
| `GET` | `/campaigns` | `campaign:read` | [予約済みキャンペーン一覧の取得](#予約済みキャンペーン一覧の取得) |
| `GET` | `/campaigns/{campaignId}` | `campaign:read` | [予約済みキャンペーンの個別取得](#予約済みキャンペーンの個別取得) |

***

## Webhookの登録

イベントを受け取るURLを登録します。登録できるのはプロジェクトごとに1つで、すでに登録済みの設定がある場合はこのリクエストで更新します。

### リクエスト

```http theme={null}
PUT /webhook
```

### リクエストボディ

| フィールド | 型 | 必須 | 説明 |
| - | - | - | - |
| `url` | string | はい | イベントを受け取るURL。`https`で始まる必要があり、最大2,048文字です。 |
| `events` | string\[] | いいえ | 受け取る[イベントの種類](#イベントの種類)。省略するか空の配列を送ると、すべてのイベントを受け取ります。 |

* 登録済みの設定は**丸ごと置き換えられます**。一部だけを変える場合も、希望する設定をすべて送信してください。たとえば`events`を省略すると、すべてのイベントを受け取る設定に戻ります。
* 上記2つ以外のフィールドを送ると`400`で失敗します。
* `localhost`、プライベート・内部アドレス、プライベートIPに解決されるドメイン、解決できないドメイン、ユーザー情報を含むURL（`https://user:pw@host`）は登録できません。この場合は`400 WEBHOOK_CONFIG_URL_NOT_ALLOWED`で失敗します。
* 別のAPIキーで登録し直すと、以降のイベントはそのキーで署名されます。[キーの交換と廃棄](#キーの交換と廃棄)を参照してください。
* 登録前に予約されたキャンペーンのイベントは送信されません。登録後に[予約済みキャンペーンの取得](#予約済みキャンペーンの取得)で現在の状態を一度合わせてください。

### リクエスト例

```bash theme={null}
curl -X PUT https://lake.adrop.io/webhook \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://partner.example.com/adrop/booking",
    "events": ["booking.confirmed", "booking.updated", "booking.cancelled"]
  }'
```

### レスポンス例

`200`とともに、保存された設定を返します。

```json theme={null}
{
  "_id": "01J8K2M3N4P5Q6R7S8T9V0W1X2",
  "keyId": "01JRX7AAAAAAAAAAAAAAAAAAAA",
  "url": "https://partner.example.com/adrop/booking",
  "events": ["booking.confirmed", "booking.updated", "booking.cancelled"],
  "active": true,
  "createdAt": "2026-10-02T00:00:00.000Z",
  "updatedAt": "2026-10-02T00:00:00.000Z"
}
```

| フィールド | 説明 |
| - | - |
| `_id` | Webhook設定のID |
| `keyId` | イベントの署名に使われるAPIキーのID。イベントリクエストの`x-adrop-key-id`ヘッダーと同じ値です。 |
| `url` | 登録したURL |
| `events` | 受け取るイベントの種類。空の配列はすべてのイベントを意味します。 |
| `active` | イベントを送信するかどうか。登録に使ったAPIキーを廃棄すると`false`になります。 |
| `createdAt`、`updatedAt` | 作成・更新日時（ISO 8601） |

## Webhook設定の取得

```bash theme={null}
curl https://lake.adrop.io/webhook \
  -H "x-api-key: YOUR_API_KEY"
```

* レスポンスは[Webhookの登録](#webhookの登録)のレスポンスと同じ形式です。
* 登録済みの設定がない場合は`404 WEBHOOK_NOT_FOUND`を返します。
* `active`が`false`の場合は、登録に使ったAPIキーが廃棄されたため、イベントの送信が止まっています。新しいキーで登録し直すと送信が再開されます。

## Webhookの削除

```bash theme={null}
curl -X DELETE https://lake.adrop.io/webhook \
  -H "x-api-key: YOUR_API_KEY"
```

| レスポンス | 意味 |
| - | - |
| `{ "result": true }` | 設定を削除しました。以降、イベントは送信されません。 |
| `{ "result": false }` | 削除する設定がありません。 |

***

## イベントの受信

予約済みキャンペーンが変わると、Adropが登録したURLへイベントを`POST`で送信します。

### イベントの種類

| `eventType` | 送信されるタイミング |
| - | - |
| `booking.confirmed` | キャンペーンが承認され、予約が確定したとき。承認が取り消された（`booking.cancelled`）あとに再び承認されると、もう一度送信されます。 |
| `booking.updated` | 予約済みキャンペーンのタイトル、広告商品、販売方式、予算、広告主の名前・URL、配信期間、ターゲティング、広告ユニットが変わったとき |
| `booking.paused` | 予約済みキャンペーンが一時停止されたとき |
| `booking.resumed` | 一時停止されたキャンペーンが再開されたとき |
| `booking.cancelled` | 予約が解除されたとき。キャンペーンのキャンセル・削除と、承認済みのキャンペーンを広告主が再審査に出して承認が取り消された場合を含みます。一度も承認されていないキャンペーンがキャンセルされた場合は送信されません。 |
| `booking.started` | キャンペーンの開始日時を過ぎたあと。一時停止中のキャンペーンにも送信されます。 |
| `booking.finished` | キャンペーンの終了日時を過ぎたあと |

* 1回の変更で送信されるイベントは最大1件です。同時に変わったほかの値は、そのイベントの`payload`に含まれます。
* `booking.updated`は、パブリッシャーに表示される値が実際に変わった場合にのみ送信されます。ターゲティングオプションが削除されてキャンペーンのターゲティング条件が変わった場合は送信されます。
* 次の場合はイベントが送信されません。変わった値は、次のイベントや[予約済みキャンペーンの取得](#予約済みキャンペーンの取得)に反映されます。
  * 広告商品、広告ユニット、アプリ、ターゲティング、ターゲティングオプションの**名前**が変わった場合
  * クリエイティブが承認・却下・変更された場合
  * パブリッシャーが新しいターゲティングオプションを作成するか、下位オプションをオプショングループの外に移動して、予約済みキャンペーンにそのオプションの設定が自動で付いた場合
* `booking.started`と`booking.finished`は、日時を過ぎてから最大1時間遅れて届くことがあります。正確な日時は`payload.startTime`と`payload.endTime`で確認してください。2つのイベントは、日時を過ぎてから24時間以内に1回だけ送信されます。

### リクエストヘッダー

| ヘッダー | 説明 |
| - | - |
| `Content-Type` | `application/json` |
| `x-adrop-key-id` | 署名に使ったAPIキーのID。Webhook設定の`keyId`と同じです。 |
| `x-adrop-signature` | `t={timestamp},v1={署名}`形式の署名。[署名の検証](#署名の検証)を参照してください。 |
| `x-adrop-event-id` | ボディの`eventId`と同じ値 |

### イベントボディ

| フィールド | 型 | 説明 |
| - | - | - |
| `eventId` | string | イベントID（ULID）。再送されても同じ値のため、重複処理を防ぐキーとして使ってください。 |
| `eventType` | string | [イベントの種類](#イベントの種類) |
| `timestamp` | number | イベントが発生した日時（エポックミリ秒）。キャンペーンが変わった日時で、`booking.started`と`booking.finished`は送信が決まった日時です。送信日時ではないため、先に起きたイベントほど小さい値になり、再送されても変わりません。 |
| `payload` | object | イベント発生時点のキャンペーンの全情報。[キャンペーン情報](#キャンペーン情報)を参照してください。 |

```http リクエスト例 theme={null}
POST /adrop/booking HTTP/1.1
Host: partner.example.com
Content-Type: application/json
x-adrop-key-id: 01JRX7AAAAAAAAAAAAAAAAAAAA
x-adrop-event-id: 01K6Z3A4B5C6D7E8F9G0H1J2K3
x-adrop-signature: t=1790899200000,v1=a3b8f2c4d5e6f7081a2b3c4d5e6f7081a2b3c4d5e6f7081a2b3c4d5e6f7081a2b

{
  "eventId": "01K6Z3A4B5C6D7E8F9G0H1J2K3",
  "eventType": "booking.confirmed",
  "timestamp": 1790899200000,
  "payload": {
    "campaignId": "01K6Y8X9Z0A1B2C3D4E5F6G7H8",
    "title": "秋の新作プロモーション",
    "status": "예약됨",
    "product": "01HD38N9K3EA5HB322NJMTU1NN",
    "productTitle": "メインバナー CPM",
    "pricingType": "CPM",
    "budget": 5000000,
    "advertiser": { "name": "Adropデモブランド", "url": "https://brand.example.com" },
    "startTime": 1791126000000,
    "endTime": 1792421999000,
    "units": [
      {
        "app": "01HD38P0M4FB6JC433PKFNV2PP",
        "unit": "01HD38Q1R5GC7KD544QLGW3QQ",
        "title": "メインバナー",
        "appTitle": "Adropデモアプリ"
      }
    ],
    "targetings": [
      {
        "targeting": "01HX5K8ZJ0TARGETING000001",
        "title": "興味関心",
        "kind": "오디언스",
        "policies": {
          "01HX5K8ZJ0AAAAAAAAAAAAAAA1": { "policy": "any", "select": "included", "title": "スポーツ" }
        }
      },
      {
        "targeting": "01HX5K8ZJ0TARGETING000002",
        "title": "記事セクション",
        "kind": "문맥",
        "policies": {
          "01HX5K8ZJ0DDDDDDDDDDDDDDD4": {
            "policy": "any",
            "select": "included",
            "title": "旅行",
            "contexts": ["travel", "hotel", "flight"],
            "contextCount": 3
          }
        }
      }
    ]
  }
}
```

すべてのイベントは同じ構造で、`eventType`とその時点の値だけが異なります。オブジェクト内のキーの順序に意味はありません。

### キャンペーン情報

`payload`と[予約済みキャンペーンの取得](#予約済みキャンペーンの取得)の結果は同じ構造です。フィールド名の末尾の`?`は、値がない場合にキー自体が省略される任意フィールドであることを示します。

| フィールド | 型 | 説明 |
| - | - | - |
| `campaignId` | string | キャンペーンID |
| `title?` | string | 広告主が入力したキャンペーンのタイトル |
| `status` | string | イベント発生時点（取得時点）のキャンペーンのステータス。[ステータスの値](#ステータスの値)を参照してください。 |
| `product` | string | 予約された広告商品のID |
| `productTitle?` | string | 広告商品の名前 |
| `pricingType` | string | 予約時点の販売方式。`CPM`、`CPC`、`CPP`、`인하우스`（インハウス）のいずれかです。値が保存されていない例外的なキャンペーンでは空文字列です。 |
| `budget` | number | キャンペーンの予算（広告商品の通貨単位）。精算金額ではありません。 |
| `advertiser?` | object | 広告主が入力した広告主情報。`name?`（広告主の名前）と`url?`（広告主のURL）が含まれます。 |
| `startTime` | number | 配信開始日時（エポックミリ秒） |
| `endTime` | number | 配信終了日時（エポックミリ秒） |
| `units` | object\[] | 予約された広告ユニット。同じ広告ユニットは1回だけ含まれます。 |
| `targetings` | object\[] | キャンペーンに適用されたターゲティング。ない場合は空の配列です。 |

`units`の各項目には次のフィールドがあります。

| フィールド | 説明 |
| - | - |
| `app` | アプリID |
| `unit` | 広告ユニットID |
| `title?` | 広告ユニットの名前 |
| `appTitle?` | アプリの名前 |

`targetings`の各項目には次のフィールドがあります。ターゲティングとターゲティングオプションはIDで照合し、名前は人が見分けるためだけに使ってください。

| フィールド | 説明 |
| - | - |
| `targeting` | ターゲティングID |
| `title?` | ターゲティングの名前 |
| `kind?` | ターゲティングの種類。`오디언스`（オーディエンス）または`문맥`（コンテキスト）です。 |
| `policies` | ターゲティングオプションIDをキーとするオブジェクト。各値に次のフィールドが含まれます。 |
| `policies.*.policy` | 広告商品でこのオプションに設定されたルール。`any`（自由選択）、`included`（必須含む）、`excluded`（必須除外）、`none`（使用しない） |
| `policies.*.select?` | 広告主がキャンペーンで選んだ方式。`included`（含む）または`excluded`（除外）で、選んでいない場合はキーがないか`none`です。 |
| `policies.*.title?` | ターゲティングオプションの名前 |
| `policies.*.contexts?` | コンテキストターゲティングでキャンペーンが含むまたは除外として選択したオプションのコンテキストID。最大200個まで含まれます。オプショングループの場合は、自身のコンテキストIDと下位オプションのコンテキストIDを合わせた一覧（重複を除く）です。 |
| `policies.*.contextCount?` | `contexts`の総数。`contexts`に含まれる数より大きい場合は、200個を超えて一部だけが含まれています。 |

* `contexts`と`contextCount`は、コンテキストターゲティングでキャンペーンが含むまたは除外として選択したオプションにのみ含まれます。オーディエンスターゲティングや、選択されていないオプションには含まれません。
* 選択されたオプションにコンテキストIDがない場合、`contexts`は`[]`、`contextCount`は`0`です。
* 200個を超えるコンテキストIDは[コンテキストターゲティングAPI](/ja/integrations/context-targeting/overview)で取得してください。

<Note>
  任意フィールド（`title`、`productTitle`、`advertiser`、`units`の`title`・`appTitle`、`targetings`の`title`・`kind`、`policies`の`title`・`contexts`・`contextCount`）は、イベントの送信時に取得した値です。対象の項目が削除されて取得できない場合はキーが省略されてIDだけが残るため、任意フィールドがなくてもエラーとして扱わないでください。
</Note>

### ステータスの値

`status`は次の文字列のいずれかです。文字列をそのまま比較してください。

| 値 | 意味 |
| - | - |
| `예약됨` | 承認済みで、開始日時の前です。 |
| `진행중` | 開始日時を過ぎ、終了前です。 |
| `정지됨` | 承認済みのキャンペーンが終了前に一時停止しています。 |
| `종료됨` | 承認済みのキャンペーンの終了日時を過ぎました。一時停止中でも、終了日時を過ぎると`종료됨`です。 |
| `취소됨` | キャンセル・削除されたか、承認が取り消されたまま終了日時を過ぎました。キャンセル・削除されたキャンペーンは、終了日時を過ぎても`취소됨`です。 |
| `심사중` | `booking.cancelled`でのみ使われます。承認済みのキャンペーンを広告主が終了前に再審査に出し、承認が取り消された状態です。 |

`status`はクリエイティブの承認状況とは関係ありません。

***

## 署名の検証

すべてのイベントリクエストには`x-adrop-signature`ヘッダーが含まれます。リクエストを処理する前に署名を検証してください。

```text theme={null}
x-adrop-signature: t={timestamp},v1={hex(HMAC-SHA256(signingSecret, "{timestamp}.{rawBody}"))}
```

* `signingSecret`は、Webhookの登録に使ったAPIキーをSHA-256でハッシュ化した16進数の文字列です。別途発行されるものではなく、APIキーから直接計算します。
* `t`は署名した日時（エポックミリ秒）です。現在時刻との差が5分を超える場合は拒否してください。
* 再送のたびに`t`と署名は新しく生成されますが、ボディ（`eventId`と`timestamp`を含む）は同じです。
* どのAPIキーで署名されたかは`x-adrop-key-id`で確認します。キーの交換中は、両方のキーで署名されたイベントが届く場合があります。

<Warning>
  署名は、受け取ったリクエストボディの元のバイト列で検証してください。パースしたJSONオブジェクトを文字列に戻すと、キーの順序や空白が変わって検証に失敗します。
</Warning>

```javascript Node.js（Express）の例 theme={null}
const crypto = require('crypto')
const express = require('express')

const app = express()

// 署名の検証に使う元のボディを保持します
app.use(express.json({
  verify: (req, _res, buf) => { req.rawBody = buf }
}))

const sha256Hex = (value) => crypto.createHash('sha256').update(value).digest('hex')

// keyId → signingSecret。キーの交換中は以前のキーも登録します
const SIGNING_SECRETS = new Map([
  [process.env.ADROP_KEY_ID, sha256Hex(process.env.ADROP_API_KEY)],
])

function parseSignature(header) {
  if (!header) return null
  const parts = Object.fromEntries(
    header.split(',').map((part) => {
      const i = part.indexOf('=')
      return i > 0 ? [part.slice(0, i), part.slice(i + 1)] : [part, '']
    })
  )
  const ts = Number(parts.t)
  if (!Number.isFinite(ts) || !parts.v1) return null
  return { ts, sig: parts.v1 }
}

function verifySignature(rawBody, signatureHeader, signingSecret) {
  const parsed = parseSignature(signatureHeader)
  if (!parsed) return false
  if (Math.abs(Date.now() - parsed.ts) > 5 * 60 * 1000) return false

  const expected = crypto
    .createHmac('sha256', signingSecret)
    .update(`${parsed.ts}.${rawBody.toString('utf8')}`)
    .digest()
  const provided = Buffer.from(parsed.sig, 'hex')
  return expected.length === provided.length && crypto.timingSafeEqual(expected, provided)
}

app.post('/adrop/booking', async (req, res) => {
  const signingSecret = SIGNING_SECRETS.get(req.get('x-adrop-key-id'))
  if (!signingSecret || !verifySignature(req.rawBody, req.get('x-adrop-signature'), signingSecret)) {
    return res.status(401).send('Invalid signature')
  }

  const { eventId, eventType, timestamp, payload } = req.body
  // イベントの処理は下記の「イベント処理のルール」に従ってください
  await handleBookingEvent({ eventId, eventType, timestamp, payload })
  res.sendStatus(200)
})
```

***

## レスポンスと再送

* イベントを処理したら、**10秒以内に**`2xx`で応答してください。レスポンスボディは使われません。時間のかかる処理は非同期に回し、先に応答してください。
* 一時的な障害の場合は`5xx`で応答してください。`3xx`と`4xx`は再送されません。

| 項目 | 値 |
| - | - |
| 応答の待機時間 | 10秒 |
| 成功の判定 | `2xx`の応答 |
| 送信回数 | 最大5回（初回1回と再送4回） |
| 再送の間隔 | 約10秒、20秒、40秒、80秒と長くなります。 |
| 再送する場合 | `5xx`の応答、ネットワークエラー、応答待機時間の超過、一時的なDNSエラー |
| 再送しない場合 | `3xx`の応答（リダイレクトはたどりません）、`4xx`の応答、URLがプライベートIPに解決される場合、ドメインが存在しない場合 |
| すべて失敗した場合 | そのイベントは再送されません。取りこぼしたイベントは[予約済みキャンペーンの取得](#予約済みキャンペーンの取得)で状態を合わせてください。 |

<Warning>
  署名エラーで`401`を返したり、デプロイ中に一時的に`404`や`429`などの`4xx`を返したりしても、そのイベントは再送されません。一時的な障害には`5xx`で応答してください。
</Warning>

### イベント処理のルール

1. **`eventId`で重複を除外してください。** 同じイベントが最大5回届くことがあります。処理済みの`eventId`が届いた場合は再処理せず、`2xx`で応答してください。`eventId`は処理が成功したあとに記録してください。そうすれば、保存に失敗して`5xx`で応答したイベントが再送されたときに、もう一度処理できます。
2. **到着順に頼らないでください。** 再送や同時に起きた変更のため、順序が入れ替わって届くことがあり、`eventId`の大小でも順序はわかりません。キャンペーンごとに最後に適用した`timestamp`を保持し、それより**小さい**`timestamp`のイベントだけを無視してください。同じ値の場合は適用します。
3. **`payload`で上書きしてください。** `payload`は毎回キャンペーンの全情報のため、`campaignId`ごとに上書きすれば、イベントを取りこぼしたり順序が入れ替わったりしても最新の状態にそろいます。任意フィールドのない`payload`で上書きすると、以前に保存した名前も消えます。
4. **`eventType`は変化、`status`は状態です。** 2つの値は常に対応するとは限りません。たとえば、終了前に承認が取り消されたキャンペーンの`booking.cancelled`は`status`が`심사중`で、一時停止中に終了したキャンペーンの`booking.finished`は`종료됨`です。
5. **`budget`は精算金額ではありません。** 精算や売上の集計には[レポートAPI](/ja/integrations/reports)を使ってください。

### 受信するイベントの選択

`events`で受信するイベントを選ぶときに`booking.confirmed`と`booking.cancelled`だけを選ぶと、承認後の配信期間、ターゲティング、広告ユニット、予算、広告主情報の変更（`booking.updated`）を取りこぼします。変更を反映する必要がある場合は、`booking.updated`も受信するか、`events`を空にしてすべてのイベントを受信してください。

## キーの交換と廃棄

イベントは、**最後にWebhookを登録したAPIキー**で署名されます。キーを交換するときは、次の手順に従ってください。

<Steps>
  <Step title="新しいAPIキーを発行する">
    `campaign:read`と`campaign:subscribe`の権限で新しいAPIキーを発行します。
  </Step>

  <Step title="両方のキーで検証するようにデプロイする">
    受信サーバーが新しいキーと以前のキーの両方で署名を検証するようにデプロイします。どちらのキーで署名されたかは`x-adrop-key-id`で区別します。
  </Step>

  <Step title="新しいキーで登録し直す">
    新しいキーで`PUT /webhook`を呼び出します。以降に送信されるイベントは新しいキーで署名されます。
  </Step>

  <Step title="移行期間を設ける">
    登録時点ですでに送信中だったイベントや再送を待っていたイベントは、以前のキーで署名されて届くことがあります。10分以上待ってください。
  </Step>

  <Step title="以前のキーを廃棄する">
    コンソールで以前のキーを廃棄し、受信サーバーからも以前のキーの検証を削除します。
  </Step>
</Steps>

<Warning>
  Webhookの登録に使ったAPIキーを廃棄すると、Webhookが無効（`active: false`）になり、イベントの送信が止まります。キーを交換するときは、必ず新しいキーで登録し直した**あとに**以前のキーを廃棄してください。
</Warning>

### セキュリティの推奨事項

* APIキーはシークレット管理ツールなど安全な場所にのみ保管し、設定ファイルやリポジトリ、メッセンジャーで共有しないでください。
* キーが漏えいした可能性がある場合は、すぐにコンソールで廃棄してください。廃棄はすぐに反映され、そのキーで登録したWebhookも無効になります。
* 署名の検証に失敗したリクエストは処理しないでください。アクセスログでは`x-adrop-signature`の値をマスクしてください。

***

## 予約済みキャンペーンの取得

予約済みキャンペーンを取得します。Webhookを初めて登録したあとに現在の状態を合わせたり、取りこぼしたイベントを復旧したりするときに使います。`campaign:read`権限が必要です。

取得対象は、承認済みのキャンペーンのうち、終了日時を過ぎていないもの（配信中または開始前）です。各項目は、イベントの`payload`と同じ[キャンペーン情報](#キャンペーン情報)の構造です。

### 予約済みキャンペーン一覧の取得

```http theme={null}
GET /campaigns
```

| クエリパラメータ | 型 | 必須 | 説明 |
| - | - | - | - |
| `unit` | string | いいえ | この広告ユニットを含むキャンペーンだけを取得します。最大64文字 |
| `next` | string | いいえ | 前のレスポンスの`next`の値。次のページを取得します。 |
| `limit` | number | いいえ | 1ページあたりの項目数。1〜120、デフォルトは20 |

* 終了日時（`endTime`）が遅い順、同じ場合はキャンペーンIDの降順に並びます。
* レスポンスに`next`がある場合は、次のページがあります。`next`の値をそのまま次のリクエストの`next`に指定してください。最後のページには`next`がありません。

```bash リクエスト例 theme={null}
curl "https://lake.adrop.io/campaigns?unit=01HD38Q1R5GC7KD544QLGW3QQ&limit=20" \
  -H "x-api-key: YOUR_API_KEY"
```

```json レスポンス例 theme={null}
{
  "items": [
    {
      "campaignId": "01K6Y8X9Z0A1B2C3D4E5F6G7H8",
      "title": "秋の新作プロモーション",
      "status": "예약됨",
      "product": "01HD38N9K3EA5HB322NJMTU1NN",
      "productTitle": "メインバナー CPM",
      "pricingType": "CPM",
      "budget": 5000000,
      "advertiser": { "name": "Adropデモブランド", "url": "https://brand.example.com" },
      "startTime": 1791126000000,
      "endTime": 1792421999000,
      "units": [
        {
          "app": "01HD38P0M4FB6JC433PKFNV2PP",
          "unit": "01HD38Q1R5GC7KD544QLGW3QQ",
          "title": "メインバナー",
          "appTitle": "Adropデモアプリ"
        }
      ],
      "targetings": []
    }
  ],
  "next": "01K6Y8X9Z0A1B2C3D4E5F6G7H8"
}
```

予約済みキャンペーンをすべて最初から最後まで読み込んで保存する例です。

```javascript 全件同期の例 theme={null}
async function syncAllBookings() {
  let next
  do {
    const query = new URLSearchParams({ limit: '100', ...(next && { next }) })
    const res = await fetch(`https://lake.adrop.io/campaigns?${query}`, {
      headers: { 'x-api-key': process.env.ADROP_API_KEY },
    })
    if (!res.ok) throw new Error(`Request failed: ${res.status}`)
    const { items, next: nextCursor } = await res.json()
    for (const item of items) await saveBooking(item.campaignId, item)
    next = nextCursor
  } while (next)
}
```

取得結果にないキャンペーンは、キャンセルされたか終了したキャンペーンです。キャンセルの有無と日時は、取得APIではなく`booking.cancelled`イベントで確認してください。

### 予約済みキャンペーンの個別取得

```http theme={null}
GET /campaigns/{campaignId}
```

```bash リクエスト例 theme={null}
curl https://lake.adrop.io/campaigns/01K6Y8X9Z0A1B2C3D4E5F6G7H8 \
  -H "x-api-key: YOUR_API_KEY"
```

* レスポンスは、一覧の`items`の1項目と同じ構造です。
* `campaignId`がULIDの形式でない場合は`400`を返します。
* キャンセル・終了したキャンペーン、承認されていないキャンペーン、ほかのプロジェクトのキャンペーン、存在しないキャンペーンは、すべて`404 CAMPAIGN_NOT_FOUND`を返します。キャンセルの有無は`booking.cancelled`イベントで確認してください。

***

## エラー

| HTTP | コード | 原因 | 対処 |
| - | - | - | - |
| `400` | （検証エラー） | `url`が`https`でないか2,048文字を超えている、`events`に存在しないイベントの種類がある、定義されていないフィールドがある、`limit`が範囲外、`campaignId`がULIDの形式でない | リクエストを修正してください。 |
| `400` | `WEBHOOK_CONFIG_URL_NOT_ALLOWED` | 登録できないURL（プライベート・内部アドレス、ユーザー情報を含むURL、プライベートIPに解決されるか解決できないドメイン） | インターネットからアクセスできる公開の`https`のURLを使ってください。 |
| `400` | `WEBHOOK_CONFIG_EVENT_TYPE_MISMATCH` | `events`に、キャンペーン予約Webhookが送信しないイベントの種類が含まれている | `events`を[イベントの種類](#イベントの種類)の7つの値に限定してください。 |
| `401` | - | APIキーがない、正しくない、または廃棄されている | APIキーを確認してください。 |
| `403` | - | 必要な権限がないキー（`/webhook`は`campaign:subscribe`、`/campaigns`は`campaign:read`） | 権限のあるキーを使ってください。 |
| `404` | `WEBHOOK_NOT_FOUND` | `GET /webhook`：登録済みの設定がない | `PUT /webhook`で登録してください。 |
| `404` | `AUTH_KEY_NOT_FOUND` | リクエストのAPIキーが廃棄されたか、このプロジェクトのキーではない | 有効なキーでリクエストし直してください。 |
| `404` | `CAMPAIGN_NOT_FOUND` | `GET /campaigns/{campaignId}`：取得できないキャンペーン | イベントや一覧の取得で状態を確認してください。 |
| `502` | - | 一時的なサーバーエラー | しばらくしてから同じリクエストを再送してください。このAPIのリクエストは繰り返し送っても安全です。 |

エラーコードは、レスポンスボディの`errorCode`または`message`フィールドに含まれます。

***

## 連携チェックリスト

* [ ] `campaign:read`と`campaign:subscribe`の権限を持つAPIキーを発行し、安全に保管しています。
* [ ] `PUT /webhook`で`https`のURLを登録し、`GET /webhook`で`active: true`を確認しました。
* [ ] 正常・偽造・期限切れのリクエストで署名の検証を確認し、元のボディで検証しています。
* [ ] `eventId`で重複を除外し、保持している値より小さい`timestamp`のイベントだけを無視しています。
* [ ] 10秒以内に`2xx`で応答し、一時的な障害には`5xx`で応答しています。
* [ ] 任意フィールドがなくてもエラーとして扱わず、`contextCount`が`contexts`の数より大きい場合は一部だけが含まれているものとして扱っています。
* [ ] `GET /campaigns`で、現在予約されているキャンペーンを最初に一度同期しました。
* [ ] キーの交換時に両方のキーで検証する手順を用意しています。

***

## 関連ドキュメント

<CardGroup cols={2}>
  <Card title="連携 & APIキー" icon="key" href="/ja/integrations/overview">
    APIキーの発行と権限の設定方法
  </Card>

  <Card title="コンテキストターゲティングAPI" icon="code" href="/ja/integrations/context-targeting/overview">
    ターゲティングオプションとコンテキストIDをサーバーから管理する方法
  </Card>
</CardGroup>


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