メインコンテンツへスキップ
手作業による請求書処理は時間がかかり、ミスが発生しやすく、スケーリングにコストもかかります。受信トレイに届くすべてのPDFについて、誰かがそれを開き、明細を確認し、そのデータをスプレッドシートやERPシステムに入力する必要があります。 このチュートリアルでは、自動取り込みサービスを構築して手作業を不要にします。指定されたBoxフォルダに新しいPDFファイルが届くと、このサービスはBox AIを呼び出して、ベンダー名、請求書番号、合計金額、日付といった予測されるフィールドを抽出し、それらの値をBoxのメタデータとしてファイルに書き戻します。

構築する内容

このチュートリアルの最後には、次のような機能を備えた、動作するPythonサービスが完成します。
  • BoxのWebhookを使用して、指定された「Invoices Inbox」フォルダ内の新規ファイルを確認します。
  • メタデータテンプレートを使用して、Box AIの抽出 (構造化) エンドポイントを呼び出します。
  • 抽出されたキー/値ペアを、Boxのメタデータとしてファイルに書き戻します。
  • 必要に応じて、抽出された集計データを記録し、ダウンストリームのERP統合で活用します。

前提条件

始める前に、以下が揃っていることを確認してください。
  • Box AIが有効化された
  • クライアント資格情報許可認証で構成されたBoxアプリケーション。
  • Python 3.11以上。
  • アプリで以下のスコープが有効になっていること。
    • Boxに格納されているすべてのファイルとフォルダの読み取り
    • 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
1

メタデータテンプレートの作成

メタデータテンプレートは、Box AIが抽出するフィールドを定義します。一度作成すれば、本サービスで処理されるすべての請求書から、この形式で値が返されます。
この手順を実行するには、管理者のアクセス権限が必要です。権限がない場合は、Boxの管理者に問い合わせてください。
  1. Box管理コンソールを開き、[メタデータ] を選択します。
  2. [請求書] タブで、[新規] をクリックしてInvoiceという名前を付けます。
  3. 以下のフィールドを追加します。
フィールド名説明
ベンダー名テキスト請求書を発行するベンダーまたはサプライヤの名前
請求書番号テキスト一意の請求書識別子
請求日日付請求書の発行日
期日日付支払期日
合計額数字税金および手数料を含む合計支払い額
通貨テキスト3文字の通貨コード (例: USD、EUR、GBP)
  1. [テンプレート名] の下にあるテンプレートキーをコピーします。後の手順で必要になります。
  2. [保存] をクリックします。
詳しい手順については、を参照してください。
2

請求書受信トレイフォルダの作成

請求書の保存場所として使用する専用のフォルダを、Box内に作成します。
  1. Boxで、Invoices Inboxという名前の新しいフォルダを作成します。
  2. フォルダIDがURLで決まることに注意してください。たとえばURLがhttps://app.box.com/folder/123456789の場合、フォルダIDは123456789となります。
  3. このフォルダをアプリケーションのサービスアカウントと共有します。CCGアプリケーションは、コンテンツへのアクセス権限が自動的には付与されない別個のサービスアカウントユーザーとして動作するため、この設定が必要です。
この手順は極めて重要です。これを行わないと、すべてのAPIコールで404「Not Found」エラーが返されます。サービスアカウントのメールアドレスを確認するには、開発者コンソールに移動し、アプリを開いて、[一般設定] の下のサービスアカウントIDを表示します (AutomationUser_xxxxx_xxxxxx@boxdevedition.comのような形式になっています)。このメールアドレスを、フォルダに対して編集者の役割を持つコラボレータとして招待します。アプリがメタデータをファイルに書き戻す必要があるため、編集者のアクセス権限が必要となります。
3

開発環境のセットアップ

  1. ターミナルを開き、新しいプロジェクトディレクトリを作成します。
mkdir invoice-intake && cd invoice-intake
  1. Pythonの仮想環境を作成してアクティブ化します。
python3 -m venv .venv
source .venv/bin/activate
アクティブ化すると、ターミナルのプロンプトの先頭に(.venv)と表示されます。これにより、仮想環境内で作業していることがわかります。
新しいターミナルウィンドウやタブを開くたびに、プロジェクトディレクトリからsource .venv/bin/activateを実行して、仮想環境を再アクティブ化する必要があります。コマンドの実行中にModuleNotFoundErrorが表示される場合、通常、venvがアクティブ化されていないことを意味します。
  1. 必要なパッケージをインストールします。
pip install box-sdk-gen flask python-dotenv
  1. 資格情報を保存するための.envファイルを作成し、以下の内容を追加します。プレースホルダの値を、Box開発者コンソールで確認した実際の資格情報で置き換えます。
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
.envファイルはバージョン管理システムにコミットさせないでください。.env.gitignoreに追加します。
環境変数の理解: .envファイルには機密情報 (実際の資格情報) が保存されています。Pythonコードは、os.getenv("VARIABLE_NAME")を使用してこれらの名前を参照することで、その値を読み取ります。たとえばos.getenv("BOX_CLIENT_ID")を実行すると、.envファイル内のBOX_CLIENT_ID=の隣に保存されている値を検索します。以下の手順でコードをコピーする際は、引用符で囲まれた変数名をそのまま正確に保持してください。実際の資格情報に置き換えないでください。
4

Boxクライアントの認証

プロジェクトディレクトリにbox_client.pyという名前の新しいファイルを作成します。そのファイルを開き、以下のコードを貼り付けます。
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)
エンドユーザーが関与しないサーバー間の自動化には、クライアント資格情報許可をお勧めします。その他の認証オプションについては、 を参照してください。
5

抽出関数の構築

extract.pyという名前の新しいファイルを作成し、以下のコードを貼り付けます。これがサービスの核となる部分です。このコードはファイルIDを受け取り、Box AIを呼び出してメタデータテンプレートを使用してフィールドを抽出し、構造化された結果を返します。
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は検索対象のフィールド、また返すべきデータの種類を正確に把握します。つまり、各仕入先が請求書をどのように書式設定していても、レスポンスの構造は予測可能で一貫したものになります。
6

ファイルへのメタデータの書き戻し

metadata.pyという名前の新しいファイルを作成し、以下のコードを貼り付けます。この関数は、抽出された値をメタデータインスタンスとしてファイルに追加します。
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ウェブアプリ上で表示されるようになります。を使用することで、一定金額以上の請求書をすべて検索したり、仕入先でフィルタをかけたり、Box Appsでダッシュボードを作成したりすることができます。
7

Webhookリスナーの作成

app.pyという名前の新しいファイルを作成し、以下のコードを貼り付けます。このFlaskアプリケーションは、受信トレイフォルダに新しいファイルが届いたときに、Webhook通知を受け取ります。
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)
本番環境では、リクエストがBoxから送信されたものであることを確認するために、Webhook署名を検証する必要があります。実装の詳細については、を参照してください。
この時点でプロジェクトディレクトリには、以下のファイルが含まれていることになります。
invoice-intake/
├── .env
├── .venv/
├── app.py
├── box_client.py
├── extract.py
└── metadata.py
8

統合のテスト

公開URLやWebhookを設定しなくても、ローカル環境で抽出パイプラインをテストできます。この手順では、新しいファイルが届いた際にBoxから送信される内容をシミュレートします。
この手順では、2つのターミナルウィンドウを同時に開く必要があります。ターミナル1で、Flaskサーバーを実行します (これは常時稼働させておく必要があります)。ターミナル2では、このサーバーにテストリクエストを送信します。
ターミナル1 - サーバーを起動するinvoice-intakeディレクトリを開いていることと、仮想環境がアクティブ化されていることを確認します。
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に置き換えます。
フォルダIDではなく、ファイルIDを使用します。ファイルIDはファイルのURLによって決まります。URLがhttps://app.box.com/file/123456789の場合、ファイルIDは123456789となります。フォルダIDは別のURLパターン (https://app.box.com/folder/987654321) によって決まります。
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でファイルを開き、[メタデータ] タブをクリックして、値が正しく書き込まれていることを確認します。
9

Webhookの登録 (本番環境)

ローカルのcurlテストではBoxからの送信内容をシミュレートしますが、本番環境への展開では、Boxが実際のWebhook通知を自動的に送信する必要があります。これには、一般公開されたHTTPSエンドポイントが必要です。
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ファイルをアップロードするたびに、自動的に抽出処理とメタデータの適用が行われます。

トラブルシューティング

仮想環境がアクティブ化されていません。python3コマンドを実行する前に、プロジェクトディレクトリからsource .venv/bin/activateを実行してください。新しく開いたターミナルタブは、すべて個別にアクティブ化する必要があります。
以下のように、.envファイルを確認します。
  • BOX_CLIENT_IDおよびBOX_CLIENT_SECRETが、開発者コンソール > [構成] の値と一致していることを確認します。
  • BOX_ENTERPRISE_IDが自分のEnterprise IDであることを確認します (管理コンソール > [アカウントと請求]、または開発者コンソール > 右上のアイコン > [Enterprise IDをコピー] で確認できます)。
  • 開発者コンソールでアプリが承認されていることを確認します。
  • アプリの種類がクライアント資格情報許可になっていることを確認します。
サービスアカウントに、そのファイルまたはフォルダへのアクセス権限がありません。開発者コンソール > [一般設定] に記載されているサービスアカウントのメールアドレスを、請求書ファイルが保管されているフォルダのコラボレータとして招待し、編集者の役割を付与します。
.envファイル内のBOX_METADATA_TEMPLATE_KEYの値が欠落しているか、空になっています。手順1でメタデータテンプレートを作成した際にメモしておいたテンプレートキーを追加します。

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

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

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

BoxのWebhook配信では、データが重複して送信される場合があります。処理を行う前にメタデータがファイルに既に存在するかどうかを確認することで、ハンドラに冪等性を与えます。GET /2.0/files/:id/metadata/enterprise/:templateを使用し、インスタンスが既に存在する場合は抽出をスキップします。
処理量が多い環境では、Webhookの代わりにの使用を検討してください。Enterprise Eventは、ポーリング方式による永続的なストリームを提供するため、数千件もの請求書のバッチ処理に適しています。
請求書に複雑なレイアウト、複数ページにわたる明細、または標準ではない書式が含まれている場合は、精度を高めるために を使用します。抽出の呼び出しでこのエージェントを指定します。
from box_sdk_gen import AiAgentReference, AiAgentReferenceTypeField

enhanced_agent = AiAgentReference(
    id="enhanced_extract_agent",
    type=AiAgentReferenceTypeField.AI_AGENT_ID,
)

次の手順

営業用RFP回答集

Box HubsとBox AIを使用して、営業チーム向けのAI搭載ナレッジベースを構築します。

APIリファレンスの抽出

抽出 (構造化) のための詳細なAPI仕様を確認します。