Claude Media
適格請求書発行事業者のWeb-APIをClaude Codeで呼ぶ — 登録番号を一括照合

適格請求書発行事業者のWeb-APIをClaude Codeで呼ぶ — 登録番号を一括照合

取引先の登録番号を国税庁のWeb-APIで一括照合するPythonスクリプトを、Claude Codeに書かせて検証する手順。アプリケーションIDの扱いと規約の注意点つき。

取引先から届いた請求書の登録番号(T+13桁)が本物かどうかは、国税庁の適格請求書発行事業者公表システムで確かめられます。1件ずつブラウザで検索するなら数分で済みますが、取引先が数十社を超えると手が止まります。

公表システムにはWeb-API機能があり、登録番号を最大10件ずつ渡して判定に必要な情報を取得できます。呼び出すスクリプトはClaude Codeに書かせるのが早いです。ただし、アプリケーションIDの管理と利用規約には気をつける点があります。この記事は、準備から実行、結果の読み方までを順に扱います。

Web-APIを使うために先に済ませること

Web-APIの利用には「アプリケーションID」が要ります。発行は無料です。

インボイス用アプリケーションIDは、次の流れで発行されます。

  1. 発行届出でメールアドレスを仮登録する
  2. 届いたURLから基本情報を入力し、「公表情報の取得方法及び利用方法」が分かる資料を提出する(本登録)
  3. 国税庁の審査で不備がなければ、IDがメールで届く(13桁)

審査があるため、即日では使えません。「法人番号システムWeb-API」だけを使える法人番号用のIDもありますが、インボイスの登録番号を引くには「インボイス用」が必要です。法人番号用IDに、あとからインボイスWeb-APIの権限を追加する手続きも用意されています。

利用規約への同意も前提になります。禁止事項には「短時間における大量アクセスその他本機能の運用に支障を与えること」があり、利用が著しく集中したときは国税庁が利用を制限できると定められています。確認した範囲では、1分あたりの上限回数のような数値は規約に書かれていません。数値がないぶん、スクリプト側でリクエスト間隔を空ける作りにします。

アクセス実績のないIDにも期限があります。3年以上アクセスがないアプリケーションIDは、利用停止の対象になり得ます。

3つのエンドポイントと、リクエストの形

公表システムのWeb-APIは、REST方式です。機能は3つあり、請求書の番号照合で主に使うのは1と3です。

機能パス必須の条件使いどころ
登録番号を指定パス/1/num必須の条件number, type使いどころ現在の登録状況を引く
取得期間を指定パス/1/diff必須の条件取得期間の開始日・終了日, type使いどころ更新差分の追跡
登録番号と日付を指定パス/1/valid必須の条件number, day, type使いどころ取引日時点の登録状況を引く

先頭の 1 はバージョンで、半角の「1」を指定します。本番のホストは web-api.invoice-kohyo.nta.go.jp です。

登録番号は T に13桁の数字を続けた形で、カンマ区切りで最大10件まで渡せます。type は応答形式で、01 がCSV、11 がXML、21 がJSONです。文字コードはUnicodeです。/num では、履歴情報を含めるかを history で選べます。省略すると 0(履歴なし)として扱われ、リクエスト時点の最新情報だけが返ります。

呼び出しの例は次のとおりです。

curl -s "https://web-api.invoice-kohyo.nta.go.jp/1/num\
?id=$INVOICE_APP_ID&number=T8040001999011&type=21&history=0"

応答のJSONは、ヘッダ情報(lastUpdateDate / count / divideNumber / divideSize)と、announcement 配列に入る公表情報でできています。仕様書のサンプルでは、公表情報に registratedNumber(登録番号)、process(事業者処理区分)、kind(人格区分)、registrationDate(登録年月日)、disposalDate(取消年月日)、expireDate(失効年月日)、name(氏名又は名称)などが並びます。項目名の綴りは仕様書のとおり registratedNumber で、registered ではありません。

Claude Codeに渡す前提を先に決める

スクリプトを頼む前に、Claude Code側で3つを決めておきます。

アプリケーションIDはコードにも会話にも書かない

IDは環境変数 INVOICE_APP_ID に入れ、スクリプトは os.environ から読ませます。.env を使うなら、Claude Codeの読み取りを拒否する権限ルールを設定します。公式ドキュメントに、Read(./.env) のようなパスを指定する形の例があります。

{
  "permissions": {
    "deny": ["Read(./.env)"],
    "allow": ["Bash(python3 check_invoice.py *)"]
  }
}

allow の書き方は、公式の Bash(npm run *) と同じ形です。ただし公式は、Bashの権限パターンで引数を絞る書き方は壊れやすいと注意しています。許可はスクリプト名のレベルにとどめ、権限の全体像は/permissionsコマンドで権限ルールを管理する手順で確認してください。

仕様の要点をCLAUDE.mdに固定する

CLAUDE.mdは、毎セッションの開始時にコンテキストへ読み込まれます。公式は「検証できるほど具体的に書く」ことを勧めています。Web-APIの制約は次のように書けます。

## 適格請求書 Web-API
- ホストは web-api.invoice-kohyo.nta.go.jp(検証時は kensyo.invoice-kohyo.nta.go.jp)
- 1リクエストの登録番号は最大10件。カンマ区切り
- アプリケーションIDは環境変数 INVOICE_APP_ID から読む。コード・ログに出さない
- リクエスト間は1秒以上空ける(規約: 短時間の大量アクセス禁止)
- 検証は検証環境で行い、本番は tests が通ってから

書き方の型は、Excel VBAをCLAUDE.mdで規約化する記事と同じです。長くなるなら、CLAUDE.mdの構成はCLAUDE.mdの書き方パターンを参考にしてください。公式は1ファイル200行未満を目安にしています。

入力と出力の形を決める

入力は、取引先,登録番号 の2列のCSVにします。出力は、判定と公表上の名称を足した別のCSVです。元のCSVは上書きさせません。

一括照合スクリプトをClaude Codeに書かせる

指示は、仕様の中身ではなく成果物の形で伝えます。

invoices.csv(取引先,登録番号)の登録番号を、国税庁のWeb-API(/1/num、JSON)で
照合する check_invoice.py を書いて。CLAUDE.md の制約を守ること。
出力は結果CSV。形式エラー・公表情報なし・取消・失効を区別して。

書き上がったコードの主要部は、次のような形になります(公式仕様の項目名に沿って組んだ例で、Claudeの出力そのものではありません)。

NUMBER_RE = re.compile(r"^T\d{13}$")
CHUNK = 10  # 1リクエストの登録番号は最大10件
 
def fetch(app_id, numbers, day=None):
    params = {"id": app_id, "number": ",".join(numbers), "type": "21"}
    if day:
        endpoint, params["day"] = "valid", day
    else:
        endpoint, params["history"] = "num", "0"
    url = f"{BASE}/1/{endpoint}?{urllib.parse.urlencode(params, safe=',')}"
    with urllib.request.urlopen(url, timeout=30) as res:
        return json.load(res).get("announcement", [])
 
def judge(rec):
    if rec.get("disposalDate"):
        return f"取消({rec['disposalDate']})"
    if rec.get("expireDate"):
        return f"失効({rec['expireDate']})"
    return "登録中"
check_invoice.py の全体
import argparse, csv, json, os, re, sys, time
import unicodedata, urllib.error, urllib.parse, urllib.request
 
BASE = os.environ.get("INVOICE_API_BASE", "https://web-api.invoice-kohyo.nta.go.jp")
NUMBER_RE = re.compile(r"^T\d{13}$")
CHUNK = 10  # 1リクエストの登録番号は最大10件
 
def fetch(app_id, numbers, day=None):
    params = {"id": app_id, "number": ",".join(numbers), "type": "21"}
    if day:
        endpoint, params["day"] = "valid", day
    else:
        endpoint, params["history"] = "num", "0"
    url = f"{BASE}/1/{endpoint}?{urllib.parse.urlencode(params, safe=',')}"
    with urllib.request.urlopen(url, timeout=30) as res:
        return json.load(res).get("announcement", [])
 
def judge(rec):
    if rec.get("disposalDate"):
        return f"取消({rec['disposalDate']})"
    if rec.get("expireDate"):
        return f"失効({rec['expireDate']})"
    return "登録中"
 
def norm(name):
    s = unicodedata.normalize("NFKC", name)  # 全角半角を統一
    s = s.replace("(株)", "株式会社")
    return re.sub(r"\s+", "", s)
 
def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("src")
    ap.add_argument("dst")
    ap.add_argument("--day", help="基準日 YYYY-MM-DD(/valid を使う)")
    args = ap.parse_args()
    app_id = os.environ["INVOICE_APP_ID"]  # コードにも会話にも書かない
 
    with open(args.src, newline="", encoding="utf-8") as f:
        rows = list(csv.DictReader(f))  # 列: 取引先,登録番号
 
    valid = sorted({r["登録番号"] for r in rows if NUMBER_RE.match(r["登録番号"])})
    found = {}
    for i in range(0, len(valid), CHUNK):
        try:
            for rec in fetch(app_id, valid[i:i + CHUNK], args.day):
                found[rec["registratedNumber"]] = rec
        except urllib.error.HTTPError as e:
            print(e.read().decode())  # エラー本文はCSV形式で返る
            sys.exit(1)
        time.sleep(1)  # リクエスト間隔を空ける
 
    with open(args.dst, "w", newline="", encoding="utf-8") as f:
        w = csv.writer(f)
        w.writerow(["取引先", "登録番号", "判定", "公表上の名称"])
        for r in rows:
            num = r["登録番号"]
            if not NUMBER_RE.match(num):
                w.writerow([r["取引先"], num, "形式エラー", ""])
            elif num not in found:
                w.writerow([r["取引先"], num, "公表情報なし", ""])
            else:
                rec = found[num]
                result = judge(rec)
                if norm(rec.get("name", "")) != norm(r["取引先"]):
                    result += " / 要確認(名称不一致)"
                w.writerow([r["取引先"], num, result, rec.get("name", "")])
 
if __name__ == "__main__":
    main()

呼び出し側では、番号を10件ずつに区切り、区切りごとに1秒待ちます。safe=',' を付けるのは、番号の区切りのカンマがURLエンコードされないようにするためです。

--day を付けると、/valid で基準日時点の状態を引きます。請求書の取引日を渡せば、「その日に登録があったか」を確かめられます。

export INVOICE_APP_ID=<届いた13桁のID>
python3 check_invoice.py invoices.csv result.csv --day 2026-08-31

検証環境で先に動かす

本番に流す前に、検証環境で挙動を確かめます。検証環境には、架空の事業者のサンプルデータが入っています。使うにはインボイス用アプリケーションIDが必要で、リクエストの形は本番と異なります。

機能検証環境のURL
登録番号を指定検証環境のURLhttps://kensyo.invoice-kohyo.nta.go.jp/バージョン/num?id=…&…
取得期間を指定検証環境のURLhttps://kensyo.invoice-kohyo.nta.go.jp/バージョン/diff?id=…&…
登録番号と日付を指定検証環境のURLhttps://kensyo.invoice-kohyo.nta.go.jp/バージョン/valid?id=…&…

バージョンと条件は本番と同じです。スクリプトのホストは、環境変数 INVOICE_API_BASE で切り替えられます。全体コードの BASE がその役割を持ちます。

INVOICE_API_BASE=https://kensyo.invoice-kohyo.nta.go.jp \
  python3 check_invoice.py sample.csv result.csv

ここでClaude Codeに実行させ、出力を読ませます。「result.csvを見て、判定が仕様書の登録・取消・失効のどれに当たるか確認して」と頼めば、コードの修正まで一続きで回せます。仕様書では、検証環境のサンプルデータを登録番号を活用するシステムの開発・改修以外の用途に使わないよう求めています。

検証環境には、もう1つ注意があります。Webブラウザ経由でWeb-APIを使うと、取得したデータが表示できないことがあります。Javaやスクリプトなどのプログラムからのリクエストではこの事象は起きないとされています。動作確認はcurlかスクリプトで行います。

結果の読み方

judge の判定は、disposalDate(取消年月日)と expireDate(失効年月日)が空かどうかで決まります。

出力意味
登録中意味最新の公表情報に取消・失効の日付がない
取消意味登録が取り消された年月日が入っている
失効意味登録の効力が失われた年月日が入っている
公表情報なし意味応答に番号が含まれなかった
形式エラー意味T+数字13桁の形でない
要確認意味判定の後ろに付く印。公表上の名称と取引先名が一致しない

「公表情報なし」は、次の理解にもとづく扱いです。count(総件数)は指定条件に合致したデータの件数を表すため、公表されていない番号は応答に現れないはずです。該当なしの応答が仕様書に明記されているかは確認できていません。検証環境で、存在しない番号を1件混ぜて挙動を見ておくと確実です。

形式エラーは、リクエストを送る前に弾きます。10件を超えるとエラーコード0002、14桁でない番号は0004、T+13桁の形でない番号は0005が返るため、送信前の検査で余計なリクエストを減らせます。

もう1つの見どころは名称の照合です。公表システムの name と、請求書の取引先名が違えば、番号の書き間違いや別法人の可能性があります。全体コードの norm は、全角半角や空白を正規化し、(株) を株式会社に直したうえで比較します。一致しなければ「要確認」を付けて、人が見ます。

法人の登録番号は T+法人番号(13桁)で、個人事業者は T+法人番号と重複しない13桁の数字です。個人事業者は屋号や通称で請求書を出すことも多く、公表上の氏名と取引先名が一致しないのは珍しくありません。応答には tradeName(主たる屋号)や popularName_previousName(通称・旧姓)の項目もあるので、個人は名称の一致判定を緩めて人が見る前提にします。

つまずきやすいエラー

HTTPコード主な原因
400コード0001 / 0003主な原因登録番号が指定されていない
400コード0002主な原因登録番号が10件を超えた
400コード0004 / 0005主な原因14桁でない / T+数字13桁でない
400コード0301〜0303主な原因/valid の判定基準日が未指定・形式違い・実在しない日付
400コード0501〜0504主な原因応答形式(type)の指定漏れ・不正
403コードなし主な原因アプリケーションIDがアクセス制御中
404コードなし主な原因IDが無効・未登録、または該当する機能のURLがない
500コードなし主な原因システムエラー

エラー時は、HTTPステータスに加えて、エラーコードとメッセージがCSV形式で返ります。JSONで頼んでも、エラー本文はJSONではない点が落とし穴です。全体コードが HTTPError の本文をそのまま表示するのは、これが理由です。

/diff は、取得期間の開始日と終了日の間が50日を超えると0205のエラーになります。開始日は2021年10月1日以降しか指定できません。データが500件を超えるときは、応答が分割され、divideNumber と divideSize が一致するまで分割番号を指定して追加リクエストを送る決まりです。番号照合では1リクエストが最大10件なので、分割は起きません。

結果を社内システムや公開サービスに載せるとき

社内の経理チェックに使う分には、追加の表示義務は仕様から読み取れません。取得した情報を使ったサービスを公開する場合は、次の文言を利用者が参照できる場所に明示するよう求められています。

このサービスは、適格請求書発行事業者公表システムWeb-API機能を利用して取得した情報をもとに作成しているが、サービスの内容は国税庁によって保証されたものではない

表示場所の指定はありません。Claude Codeで作るツールを他社に配るときは、この一文をREADMEか画面に入れておきます。

もうひとつ、個人事業者の情報の扱いにも配慮が要ります。取得データを名簿として蓄積したり、目的外に流用したりする前に、規約と個人情報の保護方針を読み直してください。

自分の側の登録手続きは、別の記事にまとめています。Claudeで開業届・青色申告・インボイス登録を整理する方法を参照してください。Claude自体のサービス料金の請求書にある登録番号や消費税の扱いは、請求先住所と税金計算の解説にあります。

運用に乗せる前の確認

  • 検証環境で、登録中・取消・失効・存在しない番号・形式エラーの5パターンが出ることを確認した
  • アプリケーションIDが、リポジトリにもCLAUDE.mdにも入っていない
  • リクエスト間隔を空けている(規約の禁止事項に触れないため)
  • 基準日つきで引くか、現在の状態で引くかを、請求書の用途に合わせて決めた
  • 「要確認」の行を、人が見る運用に載せた

照合の結果は、取引先の状態が変われば変わります。月次の締めごとにスクリプトを回し、結果のCSVを日付つきで残しておけば、後から「その時点でどう見えていたか」を追えます。

この記事を共有:XはてブLinkedIn