# 認証とリクエスト署名

> プロジェクト UUID と API キーを使用して、HMAC-SHA256 で API リクエストに署名します。

すべての API リクエスト（受信 Webhook を除く）には、プロジェクト UUID とリクエスト署名を含める必要があります。署名は、リクエストがあなたから送信されたものであり、途中で誰にも改ざんされていないことを証明します。

## API キー

2328.io は、同一の署名アルゴリズムを共有しつつ異なるエンドポイントに対応する **2 つのキー** を使用します：

| Key | Used for |
|-----|----------|
| **API key** | 支払い、固定ウォレット、残高、為替レート、および支払い／固定ウォレット Webhook の検証 |
| **Payout API key** | すべての `/v1/payout/*` エンドポイントおよび出金 Webhook の検証 |

両方のキーは [2328.io](https://2328.io) のプロジェクト設定にあります。以下の例では便宜上「API key」と総称していますが、呼び出すエンドポイントに応じて適切なキーに置き換えてください。

> **INFO:** **絶対に** 2 つのキーを混在させないでください。通常の API キーで出金リクエストに署名する（または出金キーで支払いリクエストに署名する）と署名エラーが返されます。

## 必須ヘッダー

| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Content-Type` | string | yes | 常に `application/json` |
| `project` | string | yes | プロジェクト UUID |
| `sign` | string | yes | API キーで計算したリクエストの HMAC-SHA256 署名 |
| `User-Agent` | string | はい | あなたのアプリケーションを識別します（例: `MyShop/1.4 (+https://myshop.example)`）。`User-Agent` がないリクエストはブロックされる場合があります。 |

## 署名の仕組み

署名はリクエストボディのフィンガープリントだと考えてください。次の手順で生成します：

1. ボディを JSON にシリアライズします（コンパクト — 余分な空白なし）。
2. その JSON を Base64 でエンコードします。この手順により、入力が言語間で正規化されます — 純粋な ASCII になれば、どの言語でも HMAC に対して同じバイト列が生成されます。
3. API キーで Base64 文字列の **HMAC-SHA256** を計算し、結果を小文字の 16 進数に変換します。

**GET** やボディを持たないその他のリクエストタイプでは、JSON の代わりに空文字列に署名します。

> **INFO:** 空文字列の署名は、ある API キーに対して定数になります。GET 呼び出しが多い場合はキャッシュできます。

## 実装例

<CodeSnippet name="apiSign" langs="php,js,ts,python,go" />

### ボディなしリクエスト（GET）

ボディが空のリクエスト（例：`GET /v1/payout/status/{uuid}`）では、空文字列に署名します：

<CodeSnippet name="apiSignBodyless" langs="curl,php,js,ts,python,go" />

## 完全なリクエスト例

<CodeSnippet name="fullRequestExample" langs="curl,php,js,ts,python,go" />

> **DANGER:** **API キーをクライアントサイドのコードに絶対に公開しないでください。** リクエストはバックエンドで署名してください。API キーが漏洩すると、誰でもマーチャントアカウントにフルアクセスできてしまいます。

## Webhook 署名の検証

2328.io から Webhook が送信される際、同じアルゴリズムが逆向きに実行されます：

1. ペイロードから `sign` フィールドを取り出します。
2. 残りのフィールドを JSON エンコードします（コンパクト、空白なし）。
3. その文字列を Base64 エンコードします。
4. 適切なキーで `HMAC-SHA256` を計算します。
5. 受信した `sign` と **定数時間** の比較（`hash_equals`、`crypto.timingSafeEqual`、`hmac.compare_digest`、`subtle.ConstantTimeCompare`、`OpenSSL.fixed_length_secure_compare`）で比較します。

署名キーは Webhook の発生元によって異なります：

| Webhook | Key to verify with |
|---------|---------------------|
| 支払い／固定ウォレットの Webhook（`/v1/payment`、`/v1/static-wallet`） | **API key** |
| 出金 Webhook（`/v1/payout`） | **Payout API key** |

> **WARNING:** **検証でよくある落とし穴。** あなたの JSON エンコーダーは送信側と**完全に同じバイト列**を生成する必要があります — そうでないと Base64 が変わり、署名が一致しません。

- **Go**: `json.NewEncoder` を使い、`SetEscapeHTML(false)` を設定してください。デフォルトの `json.Marshal` は `<`、`>`、`&` を `<` にエスケープし、署名を壊します。
- **Python**: `json.dumps` に `ensure_ascii=False` を渡してください。これがないと、非 ASCII（キリル文字、中国語など）が `\uXXXX` にエスケープされます。
- **コンパクト JSON**: フィールド間に空白を入れないこと（Python では `separators=(",", ":")`）。
- **フィールド順序**（Go）: 素の `map[string]any` は再エンコード時にキーをランダム化します。`json.RawMessage`、順序付き struct を使うか、生バイトから `sign` を取り除いてください。

検証が失敗し続ける場合は、ペイロードに対して自分で `apiSign` を実行してみてください — 受信した `sign` と同じ 16 進文字列を生成する必要があります。

> **INFO:** **有効な署名はリプレイを防ぎません。** 署名は webhook が 2328.io から来たことを証明するだけで — 攻撃者が*キャプチャした* webhook を後から再送信するのを防ぐものではありません。資金を入金する前に、必ず `uuid`（静的ウォレットの場合は `txid`）で冪等性を確認してください。署名が欠落しているか不正な場合は HTTP `401` で拒否してください。

完全なコード例は **[Webhook 通知](/docs/webhooks#verifying-the-signature)** にあります。リトライ処理と冪等性のルールは [ベストプラクティス](/docs/webhooks#best-practices) を参照してください。