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

> Boxのフォルダに新しい請求書がないか監視し、Box AIで構造化されたフィールドを抽出し、各ファイルにメタデータを書き戻す、買掛金処理の自動化システムを構築します。

# Box AI Extractを使用した請求書取り込みの自動化

export const RelatedLinks = ({title, items = []}) => {
  const getBadgeClass = badge => {
    if (!badge) return "badge-default";
    const badgeType = badge.toLowerCase().replace(/\s+/g, "-");
    return `badge-${badge === "ガイド" ? "guide" : badgeType}`;
  };
  if (!items || items.length === 0) {
    return null;
  }
  return <div className="my-8">
      {}
      <h3 className="text-sm font-bold uppercase tracking-wider mb-4">{title}</h3>

      {}
      <div className="flex flex-col gap-3">
        {items.map((item, index) => <a key={index} href={item.href} className="py-2 px-3 rounded related_link hover:bg-[#f2f2f2] dark:hover:bg-[#111827] flex items-center gap-3 group no-underline hover:no-underline border-b-0">
            {}
            <span className={`px-2 py-1 rounded-full text-xs font-semibold uppercase tracking-wide flex-shrink-0 ${getBadgeClass(item.badge)}`}>
              {item.badge}
            </span>

            {}
            <span className="text-base">{item.label}</span>
          </a>)}
      </div>
    </div>;
};

export const SignupCTA = ({children}) => {
  return <div className="flex flex-wrap items-center gap-4 p-5 rounded-lg border border-gray-200 dark:border-gray-700 my-6" style={{
    background: "linear-gradient(135deg, rgba(0, 97, 213, 0.06), rgba(0, 97, 213, 0.02))"
  }}>
      <div className="flex-1 text-sm leading-relaxed text-gray-700 dark:text-gray-300" style={{
    minWidth: "280px"
  }}>
        {children}
      </div>
      <div className="flex flex-col items-center gap-2">
        <a href="https://account.box.com/signup/developer#ty9l3" className="signup-cta-button inline-flex items-center whitespace-nowrap px-5 py-2 text-sm font-semibold text-white no-underline">
          {translate("Get started for free")}
        </a>
        <a href="https://account.box.com/developers/console" className="signup-cta-login text-xs text-gray-500 dark:text-gray-400 no-underline whitespace-nowrap">
          {translate("Already have an account? Log in")}
        </a>
      </div>
    </div>;
};

export const Link = ({href, children, className, ...props}) => {
  const localizedHref = localizeLink(href);
  return <a href={localizedHref} className={className} {...props}>
      {children}
    </a>;
};

手作業による請求書処理は時間がかかり、ミスが発生しやすく、スケーリングにコストもかかります。受信トレイに届くすべてのPDFについて、誰かがそれを開き、明細を確認し、そのデータをスプレッドシートやERPシステムに入力する必要があります。

このチュートリアルでは、自動取り込みサービスを構築して手作業を不要にします。指定されたBoxフォルダに新しいPDFファイルが届くと、このサービスはBox AIを呼び出して、ベンダー名、請求書番号、合計金額、日付といった予測されるフィールドを抽出し、それらの値をBoxのメタデータとしてファイルに書き戻します。

## 構築する内容

このチュートリアルの最後には、次のような機能を備えた、動作するPythonサービスが完成します。

* BoxのWebhookを使用して、指定された「Invoices Inbox」フォルダ内の新規ファイルを確認します。
* メタデータテンプレートを使用して、Box AIの抽出 (構造化) エンドポイントを呼び出します。
* 抽出されたキー/値ペアを、Boxのメタデータとしてファイルに書き戻します。
* 必要に応じて、抽出された集計データを記録し、ダウンストリームのERP統合で活用します。

## 前提条件

始める前に、以下が揃っていることを確認してください。

* Box AIが有効化された<Link href="https://www.box.com/pricing">Box Enterpriseアカウント</Link>。
* **クライアント資格情報許可**認証で構成されたBoxアプリケーション。
* Python 3.11以上。
* アプリで以下のスコープが有効になっていること。
  * Boxに格納されているすべてのファイルとフォルダの読み取りと書き込み
  * AIを管理する
  * Webhookを管理する

## 手順

このソリューションでは、以下に示す3つのBox Platform機能を使用します。

| コンポーネント            | 目的                        | API                                             |
| ------------------ | ------------------------- | ----------------------------------------------- |
| **Webhook**        | 受信トレイフォルダに届いた新しいファイルを検出する | `POST /2.0/webhooks`                            |
| **Box AI Extract** | 各請求書PDFから構造化されたフィールドを抽出する | `POST /2.0/ai/extract_structured`               |
| **メタデータ**          | 抽出された値をファイルに書き戻す          | `POST /2.0/files/:id/metadata/:scope/:template` |

<Steps>
  <Step title="メタデータテンプレートの作成">
    メタデータテンプレートは、Box AIが抽出するフィールドを定義します。一度作成すれば、本サービスで処理されるすべての請求書から、この形式で値が返されます。

    <Tip>
      この手順を実行するには、管理者のアクセス権限が必要です。権限がない場合は、Boxの管理者に問い合わせてください。
    </Tip>

    1. [Box管理コンソール](https://app.box.com/master)を開き、\[**メタデータ**] を選択します。
    2. \[**請求書**] タブで、\[**新規**] をクリックして`Invoice`という名前を付けます。
    3. 以下のフィールドを追加します。

    | フィールド名 | 型    | 説明                         |
    | ------ | ---- | -------------------------- |
    | ベンダー名  | テキスト | 請求書を発行するベンダーまたはサプライヤの名前    |
    | 請求書番号  | テキスト | 一意の請求書識別子                  |
    | 請求日    | 日付   | 請求書の発行日                    |
    | 期日     | 日付   | 支払期日                       |
    | 合計額    | 数字   | 税金および手数料を含む合計支払い額          |
    | 通貨     | テキスト | 3文字の通貨コード (例: USD、EUR、GBP) |

    4. \[**テンプレート名**] の下にあるテンプレートキーをコピーします。後の手順で必要になります。
    5. \[**保存**] をクリックします。

    詳しい手順については、<Link href="https://support.box.com/hc/en-us/articles/360044194033-Customizing-Metadata-Templates">メタデータテンプレートのカスタマイズ</Link>を参照してください。
  </Step>

  <Step title="請求書受信トレイフォルダの作成">
    請求書の保存場所として使用する専用のフォルダを、Box内に作成します。

    1. Boxで、`Invoices Inbox`という名前の新しいフォルダを作成します。
    2. **フォルダID**がURLで決まることに注意してください。たとえばURLが`https://app.box.com/folder/123456789`の場合、フォルダIDは`123456789`となります。
    3. **このフォルダをアプリケーションのサービスアカウントと共有します。**CCGアプリケーションは、コンテンツへのアクセス権限が自動的には付与されない別個のサービスアカウントユーザーとして動作するため、この設定が必要です。

    <Warning>
      **この手順は極めて重要です。**これを行わないと、すべてのAPIコールで404「Not Found」エラーが返されます。

      サービスアカウントのメールアドレスを確認するには、[開発者コンソール](https://app.box.com/developers/console)に移動し、アプリを開いて、\[**一般設定**] の下の**サービスアカウントID**を表示します (`AutomationUser_xxxxx_xxxxxx@boxdevedition.com`のような形式になっています)。

      このメールアドレスを、フォルダに対して**編集者**の役割を持つ**コラボレータ**として招待します。アプリがメタデータをファイルに書き戻す必要があるため、編集者のアクセス権限が必要となります。
    </Warning>
  </Step>

  <Step title="開発環境のセットアップ">
    1. ターミナルを開き、新しいプロジェクトディレクトリを作成します。

    ```bash theme={null}
    mkdir invoice-intake && cd invoice-intake
    ```

    2. Pythonの仮想環境を作成してアクティブ化します。

    ```bash theme={null}
    python3 -m venv .venv
    source .venv/bin/activate
    ```

    アクティブ化すると、ターミナルのプロンプトの先頭に`(.venv)`と表示されます。これにより、仮想環境内で作業していることがわかります。

    <Note>
      新しいターミナルウィンドウやタブを開くたびに、プロジェクトディレクトリから`source .venv/bin/activate`を実行して、仮想環境を再アクティブ化する必要があります。コマンドの実行中に`ModuleNotFoundError`が表示される場合、通常、venvがアクティブ化されていないことを意味します。
    </Note>

    3. 必要なパッケージをインストールします。

    ```bash theme={null}
    pip install box-sdk-gen flask python-dotenv
    ```

    4. 資格情報を保存するための`.env`ファイルを作成し、以下の内容を追加します。プレースホルダの値を、Box開発者コンソールで確認した実際の資格情報で置き換えます。

    ```bash theme={null}
    BOX_CLIENT_ID=your_client_id
    BOX_CLIENT_SECRET=your_client_secret
    BOX_ENTERPRISE_ID=your_enterprise_id
    BOX_METADATA_TEMPLATE_KEY=your_metadata_template_key
    INVOICES_FOLDER_ID=your_folder_id
    ```

    <Warning>
      `.env`ファイルはバージョン管理システムにコミットさせないでください。`.env`を`.gitignore`に追加します。
    </Warning>

    <Note>
      **環境変数の理解:** `.env`ファイルには機密情報 (実際の資格情報) が保存されています。Pythonコードは、`os.getenv("VARIABLE_NAME")`を使用してこれらの**名前**を参照することで、その値を読み取ります。たとえば`os.getenv("BOX_CLIENT_ID")`を実行すると、`.env`ファイル内の`BOX_CLIENT_ID=`の隣に保存されている値を検索します。以下の手順でコードをコピーする際は、引用符で囲まれた変数名をそのまま正確に保持してください。実際の資格情報に置き換えないでください。
    </Note>
  </Step>

  <Step title="Boxクライアントの認証">
    プロジェクトディレクトリに`box_client.py`という名前の新しいファイルを作成します。そのファイルを開き、以下のコードを貼り付けます。

    ```python theme={null}
    import os
    from dotenv import load_dotenv
    from box_sdk_gen import (
        BoxClient,
        BoxCCGAuth,
        CCGConfig,
    )

    load_dotenv()

    def get_box_client() -> BoxClient:
        config = CCGConfig(
            client_id=os.getenv("BOX_CLIENT_ID"),
            client_secret=os.getenv("BOX_CLIENT_SECRET"),
            enterprise_id=os.getenv("BOX_ENTERPRISE_ID"),
        )
        auth = BoxCCGAuth(config=config)
        return BoxClient(auth=auth)
    ```

    <Tip>
      エンドユーザーが関与しないサーバー間の自動化には、クライアント資格情報許可をお勧めします。その他の認証オプションについては、<Link href="/guides/authentication/select">認証方法の選択</Link> を参照してください。
    </Tip>
  </Step>

  <Step title="抽出関数の構築">
    `extract.py`という名前の新しいファイルを作成し、以下のコードを貼り付けます。これがサービスの核となる部分です。このコードはファイルIDを受け取り、Box AIを呼び出してメタデータテンプレートを使用してフィールドを抽出し、構造化された結果を返します。

    ```python theme={null}
    import os
    from dotenv import load_dotenv
    from box_sdk_gen import (
        AiItemBase,
        BoxClient,
        CreateAiExtractStructuredMetadataTemplate,
        CreateAiExtractStructuredMetadataTemplateTypeField,
    )

    load_dotenv()

    def extract_invoice_fields(client: BoxClient, file_id: str) -> dict:
        template_key = os.getenv("BOX_METADATA_TEMPLATE_KEY")

        result = client.ai.create_ai_extract_structured(
            items=[AiItemBase(id=file_id)],
            metadata_template=CreateAiExtractStructuredMetadataTemplate(
                template_key=template_key,
                type=CreateAiExtractStructuredMetadataTemplateTypeField.METADATA_TEMPLATE,
                scope="enterprise",
            ),
        )

        return result.to_dict()["answer"]
    ```

    メタデータテンプレートにより、Box AIは検索対象のフィールド、また返すべきデータの種類を正確に把握します。つまり、各仕入先が請求書をどのように書式設定していても、レスポンスの構造は予測可能で一貫したものになります。
  </Step>

  <Step title="ファイルへのメタデータの書き戻し">
    `metadata.py`という名前の新しいファイルを作成し、以下のコードを貼り付けます。この関数は、抽出された値をメタデータインスタンスとしてファイルに追加します。

    ```python theme={null}
    import os
    from dotenv import load_dotenv
    from box_sdk_gen import BoxClient, CreateFileMetadataByIdScope

    load_dotenv()

    def apply_metadata(client: BoxClient, file_id: str, metadata: dict) -> dict:
        template_key = os.getenv("BOX_METADATA_TEMPLATE_KEY")

        attached = client.file_metadata.create_file_metadata_by_id(
            file_id=file_id,
            scope=CreateFileMetadataByIdScope.ENTERPRISE,
            template_key=template_key,
            request_body=metadata,
        )

        return attached.to_dict()
    ```

    メタデータが追加されると、抽出されたフィールドは検索やフィルタが可能になり、Boxウェブアプリ上で表示されるようになります。<Link href="/guides/metadata/queries">メタデータクエリ</Link>を使用することで、一定金額以上の請求書をすべて検索したり、仕入先でフィルタをかけたり、Box Appsでダッシュボードを作成したりすることができます。
  </Step>

  <Step title="Webhookリスナーの作成">
    `app.py`という名前の新しいファイルを作成し、以下のコードを貼り付けます。このFlaskアプリケーションは、受信トレイフォルダに新しいファイルが届いたときに、Webhook通知を受け取ります。

    ```python theme={null}
    import os
    import json

    from dotenv import load_dotenv
    from flask import Flask, request, jsonify

    from box_client import get_box_client
    from extract import extract_invoice_fields
    from metadata import apply_metadata

    load_dotenv()
    app = Flask(__name__)

    @app.route("/webhook", methods=["POST"])
    def handle_webhook():
        payload = request.get_json()

        if payload.get("trigger") != "FILE.UPLOADED":
            return jsonify({"status": "ignored"}), 200

        file_id = payload["source"]["id"]
        file_name = payload["source"]["name"]

        if not file_name.lower().endswith(".pdf"):
            return jsonify({"status": "skipped, not a PDF"}), 200

        client = get_box_client()

        extracted = extract_invoice_fields(client, file_id)
        print(f"Extracted from {file_name}: {json.dumps(extracted, indent=2)}")

        apply_metadata(client, file_id, extracted)
        print(f"Metadata applied to file {file_id}")

        return jsonify({"status": "processed", "file_id": file_id}), 200

    if __name__ == "__main__":
        app.run(port=5000)
    ```

    <Note>
      本番環境では、リクエストがBoxから送信されたものであることを確認するために、Webhook署名を検証する必要があります。実装の詳細については、<Link href="/guides/webhooks/v2/signatures-v2">Webhook署名の検証</Link>を参照してください。
    </Note>

    この時点でプロジェクトディレクトリには、以下のファイルが含まれていることになります。

    ```
    invoice-intake/
    ├── .env
    ├── .venv/
    ├── app.py
    ├── box_client.py
    ├── extract.py
    └── metadata.py
    ```
  </Step>

  <Step title="統合のテスト">
    公開URLやWebhookを設定しなくても、ローカル環境で抽出パイプラインをテストできます。この手順では、新しいファイルが届いた際にBoxから送信される内容をシミュレートします。

    <Note>
      この手順では、**2つのターミナルウィンドウ**を同時に開く必要があります。ターミナル1で、Flaskサーバーを実行します (これは常時稼働させておく必要があります)。ターミナル2では、このサーバーにテストリクエストを送信します。
    </Note>

    **ターミナル1 - サーバーを起動する**

    `invoice-intake`ディレクトリを開いていることと、仮想環境がアクティブ化されていることを確認します。

    ```bash theme={null}
    cd ~/invoice-intake
    source .venv/bin/activate
    python3 app.py
    ```

    次のように表示されます。

    ```
    * Running on http://127.0.0.1:5000
    ```

    このターミナルは起動したままにしておきます。

    **ターミナル2 - テストリクエストを送信する**

    新しいターミナルタブまたはウィンドウを開きます。curlを使用して、シミュレートされたWebhookペイロードを送信します。`<FILE_ID>`を、Boxにアップロードした請求書PDFの**ファイルID**に置き換えます。

    <Warning>
      フォルダIDではなく、ファイルIDを使用します。ファイルIDはファイルのURLによって決まります。URLが`https://app.box.com/file/123456789`の場合、ファイルIDは`123456789`となります。フォルダIDは別のURLパターン (`https://app.box.com/folder/987654321`) によって決まります。
    </Warning>

    ```bash theme={null}
    curl -X POST http://127.0.0.1:5000/webhook \
      -H "Content-Type: application/json" \
      -d '{
        "trigger": "FILE.UPLOADED",
        "source": {
          "id": "<FILE_ID>",
          "name": "sample-invoice.pdf"
        }
      }'
    ```

    **結果の確認**

    ターミナル1に戻ります。抽出されたフィールドが出力され、その後にメタデータが適用されたことを示す確認メッセージが表示されます。

    ```
    Extracted from sample-invoice.pdf: {
      "vendorName": "ACME Corp",
      "invoiceNumber": "INV-001",
      "invoiceDate": "2025-03-15T00:00:00Z",
      "dueDate": "2025-04-15T00:00:00Z",
      "totalAmount": 1250.00,
      "currency": "USD"
    }
    Metadata applied to file 123456789
    ```

    Boxでファイルを開き、\[**メタデータ**] タブをクリックして、値が正しく書き込まれていることを確認します。
  </Step>

  <Step title="Webhookの登録 (本番環境)">
    ローカルのcurlテストではBoxからの送信内容をシミュレートしますが、本番環境への展開では、Boxが実際のWebhook通知を自動的に送信する必要があります。これには、一般公開されたHTTPSエンドポイントが必要です。

    ```bash theme={null}
    curl -X POST https://api.box.com/2.0/webhooks \
      -H "Authorization: Bearer <ACCESS_TOKEN>" \
      -H "Content-Type: application/json" \
      -d '{
        "target": {
          "id": "<FOLDER_ID>",
          "type": "folder"
        },
        "address": "<webhook_url>",
        "triggers": ["FILE.UPLOADED"]
      }'
    ```

    `<FOLDER_ID>`を請求書フォルダのIDに置き換え、`address`をトンネルURLに`/webhook`を付加した形式に更新します。

    登録が完了すると、そのフォルダにPDFファイルをアップロードするたびに、自動的に抽出処理とメタデータの適用が行われます。
  </Step>
</Steps>

## トラブルシューティング

<AccordionGroup>
  <Accordion title="ModuleNotFoundError: 「...」という名前のモジュールが見つからない">
    仮想環境がアクティブ化されていません。`python3`コマンドを実行する前に、プロジェクトディレクトリから`source .venv/bin/activate`を実行してください。新しく開いたターミナルタブは、すべて個別にアクティブ化する必要があります。
  </Accordion>

  <Accordion title="invalid_client: クライアント資格情報が無効である">
    以下のように、`.env`ファイルを確認します。

    * `BOX_CLIENT_ID`および`BOX_CLIENT_SECRET`が、開発者コンソール > \[構成] の値と一致していることを確認します。
    * `BOX_ENTERPRISE_ID`が自分のEnterprise IDであることを確認します (管理コンソール > \[アカウントと請求]、または開発者コンソール > 右上のアイコン > \[Enterprise IDをコピー] で確認できます)。
    * 開発者コンソールでアプリが承認されていることを確認します。
    * アプリの種類がクライアント資格情報許可になっていることを確認します。
  </Accordion>

  <Accordion title="404 Not Found">
    サービスアカウントに、そのファイルまたはフォルダへのアクセス権限がありません。開発者コンソール > \[一般設定] に記載されているサービスアカウントのメールアドレスを、請求書ファイルが保管されているフォルダのコラボレータとして招待し、**編集者**の役割を付与します。
  </Accordion>

  <Accordion title="metadata_templateには、scopeとtemplate_keyの両方が含まれている必要がある">
    `.env`ファイル内の`BOX_METADATA_TEMPLATE_KEY`の値が欠落しているか、空になっています。手順1でメタデータテンプレートを作成した際にメモしておいたテンプレートキーを追加します。
  </Accordion>
</AccordionGroup>

## 省略可: ERPへの合計額の転送

メタデータの構造化が完了したら、ダウンストリームへのデータ転送は簡単です。メタデータの適用後に、ERP統合の手順を追加します。

```python theme={null}
def push_to_erp(extracted: dict, file_id: str):
    """Send extracted totals to your ERP system."""
    erp_payload = {
        "vendor": extracted.get("vendorName"),
        "invoice_number": extracted.get("invoiceNumber"),
        "total": extracted.get("totalAmount"),
        "currency": extracted.get("currency"),
        "due_date": extracted.get("dueDate"),
        "source_file_id": file_id,
    }
    # Replace with your ERP's API endpoint
    # requests.post("https://erp.example.com/api/invoices", json=erp_payload)
    print(f"ERP payload ready: {erp_payload}")
```

## 本番環境へのスケーリング

<AccordionGroup>
  <Accordion title="重複した配信の処理">
    BoxのWebhook配信では、データが重複して送信される場合があります。処理を行う前にメタデータがファイルに既に存在するかどうかを確認することで、ハンドラに冪等性を与えます。`GET /2.0/files/:id/metadata/enterprise/:template`を使用し、インスタンスが既に存在する場合は抽出をスキップします。
  </Accordion>

  <Accordion title="イベントストリームを使用した大規模な処理">
    処理量が多い環境では、Webhookの代わりに<Link href="/guides/events/enterprise-events/for-enterprise">Enterprise Event</Link>の使用を検討してください。Enterprise Eventは、ポーリング方式による永続的なストリームを提供するため、数千件もの請求書のバッチ処理に適しています。
  </Accordion>

  <Accordion title="複雑な請求書での抽出エージェント (強化) の使用">
    請求書に複雑なレイアウト、複数ページにわたる明細、または標準ではない書式が含まれている場合は、精度を高めるために<Link href="/guides/box-ai/quick-start/box-ai-extract-enhanced">抽出エージェント (強化)</Link> を使用します。抽出の呼び出しでこのエージェントを指定します。

    ```python theme={null}
    from box_sdk_gen import AiAgentReference, AiAgentReferenceTypeField

    enhanced_agent = AiAgentReference(
        id="enhanced_extract_agent",
        type=AiAgentReferenceTypeField.AI_AGENT_ID,
    )
    ```
  </Accordion>
</AccordionGroup>

## 次の手順

<CardGroup cols={2}>
  <Card title="営業用RFP回答集" href={localizeLink("/guides/tutorials/sales-rfp-answer-bank")} icon="magnifying-glass" arrow="true">
    Box HubsとBox AIを使用して、営業チーム向けのAI搭載ナレッジベースを構築します。
  </Card>

  <Card title="APIリファレンスの抽出" href={localizeLink("/reference/post-ai-extract-structured")} icon="code" arrow="true">
    抽出 (構造化) のための詳細なAPI仕様を確認します。
  </Card>
</CardGroup>

<RelatedLinks
  title="関連するガイド"
  items={[
{ label: translate("Extract metadata from file (structured)"), href: "/guides/box-ai/ai-tutorials/extract-metadata-structured", badge: "GUIDE" },
{ label: translate("Extract structured data quick start"), href: "/guides/box-ai/quick-start/box-ai-extract", badge: "QUICKSTART" },
{ label: translate("Extract APIs overview and use cases"), href: "/guides/box-ai/ai-tutorials/extract-use-cases", badge: "GUIDE" },
{ label: translate("Verify webhook signatures"), href: "/guides/webhooks/v2/signatures-v2", badge: "GUIDE" },
{ label: translate("Working with metadata"), href: "/guides/metadata/index", badge: "GUIDE" }
]}
/>
