Claude Media
法人番号Web-APIで取引先名簿を名寄せする — Claude Codeで商号・所在地の変更を追う

法人番号Web-APIで取引先名簿を名寄せする — Claude Codeで商号・所在地の変更を追う

国税庁の法人番号Web-APIで取引先マスタと突合し、商号変更・所在地変更・閉鎖をClaude Codeで洗い出す手順。変更履歴と差分取得の使い分けつき。

取引先マスタの商号が、登記上の名称とずれていませんか。社名変更や本店移転は、取引先から連絡が来ないまま進むことが少なくありません。請求書や契約書の宛名を間違える原因になります。

国税庁の法人番号システムには、法人番号をキーに最新の商号と所在地を取れるWeb-APIがあります。マスタの法人番号で引けば、名称と住所のずれが一覧で出ます。名寄せ(表記の違う同一法人をひとつにまとめる作業)の土台に使えます。突合スクリプトはClaude Codeに書かせ、判定の分岐をテストで固めるのが早道です。ここでは、使える機能の整理から、スクリプト、差分取得による変更の追跡までを順に扱います。適格請求書の登録番号を照合したい場合は、適格請求書発行事業者のWeb-APIをClaude Codeで呼ぶ手順が別の仕組みを扱っています。

名寄せに使う3機能と、変更履歴の取り方

法人番号システムのWeb-APIは、REST方式で次の3機能を提供します。

機能エンドポイント向く場面
法人番号を指定エンドポイント/4/num向く場面マスタの法人番号から最新の商号・所在地を引く
法人名を指定エンドポイント/4/name向く場面法人番号が空の取引先を名称から探す
取得期間を指定エンドポイント/4/diff向く場面期間内に変更があった法人だけを拾う

先頭の 4 はAPIのバージョンです。Ver.2.0で英語表記、Ver.3.0でフリガナ、Ver.4.0で「検索対象除外」とされた法人の情報が加わりました。法人名を指定する機能はVer.2.0以降で使えます。新規に作るなら、すべてを含むVer.4.0で足ります。

名寄せの軸にするのは法人番号(13桁)です。番号は商号や所在地が変わっても変わりません。名称で突合する方式は表記ゆれに弱いので、番号があるものは番号で引き、無いものだけ名称検索に回します。

法人番号を指定する機能は、history=1 を付けると変更履歴も返します。履歴は古い順に並び、最新の1件だけ latest が 1 になります。公式の例では、商号が変わった法人に対し「変更前」「変更後」の2件が返ります。

curl -s "https://api.houjin-bangou.nta.go.jp/4/num\
?id=$HOUJIN_APP_ID&number=8040001999013&type=12&history=1"

type は応答形式で、01 がCSV(Shift-JIS)、02 がCSV(Unicode)、12 がXML(Unicode)です。文字コードの扱いを避けたいので、以降はXMLを使います。法人番号は、カンマ区切りで最大10件まで1回に渡せます。

Web-APIでは全件データを取得できません。全件が要る場合は、同じサイトの基本3情報ダウンロードを使う設計になります。

使い始める前に済ませる準備

アプリケーションIDを申請する

利用にはアプリケーションID(13桁)が必要です。発行に費用はかかりません。ただし発行まで2週間から1か月ほどかかるので、実装より先に申請します。申請や変更、利用停止の手続きは、国税庁適格請求書発行事業者公表サイトから行います。インボイス側のIDとの関係は、登録番号照合の記事のIDの節に手順があります。

規約で押さえる点

利用規約には、スクリプトの設計に直結する条項があります。

  • IDを第三者に譲渡・貸与・開示してはならない
  • 短時間の大量アクセスは禁止。集中時には利用を制限されることがある
  • 3年以上アクセス実績のないIDは、利用停止の対象になりうる
  • Web-APIで得た情報を使ったサービスを提供する場合は、取得元を明示する

明示する文言は「このサービスは、国税庁法人番号システムのWeb-API機能を利用して取得した情報をもとに作成しているが、サービスの内容は国税庁によって保証されたものではない」です。社内の名簿チェックだけなら公開サービスには当たりません。突合結果を画面や資料として社外に出すなら、この一文を入れておきます。

検証環境で先に試す

国税庁は、架空の法人名を使った検証環境を用意しています。使うには利用希望の連絡が必要で、概ね1週間以内に案内の資料がメールで届きます。IDは本番と検証のどちらでも使えます。

法人番号がない取引先は名称検索で埋める

マスタに法人番号が入っていない行は、法人名を指定する機能で探します。条件は次のとおりです。

条件項目ID値
法人名(必須)項目IDname値UTF-8でURLエンコードした値。1回に1つだけ
検索方式項目IDmode値1 前方一致(既定)、2 部分一致
検索対象項目IDtarget値1 JIS第一・第二水準(あいまい検索)、2 第一〜第四水準(完全一致)、3 英語表記
所在地項目IDaddress値都道府県コード2桁、または市区町村コードまで5桁
法人種別項目IDkind値01 国の機関、02 地方公共団体、03 設立登記法人、04 外国会社等・その他
変更履歴項目IDchange値1 で旧名称も検索対象にする
閉鎖等項目IDclose値0 で閉鎖済みを除く(既定は含める)

前方一致では、「株式会社」などの法人格を除いた名称の先頭から検索します。名簿の「株式会社○○」から法人格を外して name に渡す、という扱いです。部分一致なら法人格を含めた検索もできます。

名称検索には注意が2つあります。

  1. 候補が複数返る: 同名の法人は珍しくありません。address で都道府県や市区町村を絞り、候補が1件に定まったときだけ自動採用します。複数なら人の確認に回します。
  2. change=1 では最新情報が返らない: 旧名称でヒットした場合、返るのは変更前の法人情報で、その法人の最新情報は応答に含まれません。応答の法人番号で、法人番号を指定する機能を引き直します。

結果が多すぎる条件はエラー(エラーコード180)になります。条件を足して絞り直します。名称検索で番号が確定したら、マスタに法人番号を書き戻します。次回以降は番号で引けます。

制約をClaude Codeに渡す

仕様の要点は、毎セッション読み込まれるCLAUDE.mdに書いておきます。ここにあるのは、公式仕様から引いた制約です。

## 法人番号 Web-API
- ホストは api.houjin-bangou.nta.go.jp、バージョンは /4/
- 法人番号は1リクエスト最大10件、カンマ区切り。履歴が要るときは history=1
- 応答はXML(type=12)。latest=1 の行が最新、履歴は古い順
- アプリケーションIDは .env の HOUJIN_APP_ID から読む。コード・ログ・会話に出さない
- リクエストの間は1秒以上空ける(規約: 短時間の大量アクセス禁止)
- 元のCSVは上書きしない。結果は別ファイルへ
- 確認の順序はスタブのテスト、検証環境、本番

CLAUDE.mdの構成はCLAUDE.mdの書き方パターンが参考になります。

IDの漏えいは、権限ルールで読み取りを拒否して防ぎます。公式は Read(./.env) のようにパスを指定する形を例に挙げています。

{
  "permissions": {
    "deny": ["Read(./.env)"]
  }
}

この拒否が効くのは、組み込みのファイルツールと、cat などBashの一部のファイルコマンドです。Pythonスクリプトが自分で .env を読む動作は止められません。厳密に防ぐなら、サンドボックスを併用します。権限ルールの管理は/permissionsコマンドの記事にまとまっています。

依頼は、成果物の形で伝えます。

customers.csv(取引先名,法人番号,所在地)を法人番号Web-API(/4/num、XML、history=1)で
突合する match_houjin.py を書いて。CLAUDE.md の制約を守ること。
出力は別CSV。一致・要更新・閉鎖合併・該当なしを区別し、
要更新は商号と所在地のどちらがずれたかを差分欄に書いて。
先に公式サンプルのXMLでテストを書き、通ってから本番を叩く。

「先にテストを書く」と指示に入れておくと、Claudeはテストを実行して失敗を読みながら直せます。判定ロジックは、通信を伴わずに検証できるからです。

突合スクリプトの中身

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

import csv, os, re, sys, time, unicodedata
import urllib.parse, urllib.request
import xml.etree.ElementTree as ET
 
BASE = "https://api.houjin-bangou.nta.go.jp/4/num"
CHUNK = 10  # 法人番号は1リクエストあたり最大10件
 
def norm(name):
    s = unicodedata.normalize("NFKC", name)
    s = re.sub(r"[\s ]", "", s)
    return re.sub(r"株式会社|\(株\)|有限会社|\(有\)|合同会社|\(同\)", "", s)
 
def parse(xml_text):
    root = ET.fromstring(xml_text)
    recs = []
    for c in root.findall("corporation"):
        recs.append({e.tag: (e.text or "") for e in c})
    return recs
 
def fetch(app_id, numbers):
    q = urllib.parse.urlencode(
        {"id": app_id, "number": ",".join(numbers), "type": "12", "history": "1"},
        safe=",")
    with urllib.request.urlopen(f"{BASE}?{q}", timeout=30) as res:
        return parse(res.read().decode("utf-8"))
 
def address(r):
    return r["prefectureName"] + r["cityName"] + r["streetNumber"]
 
def judge(row, recs):
    if not recs:
        return "該当なし", ""
    latest = [r for r in recs if r["latest"] == "1"][-1]
    if latest["process"] in ("21", "71"):
        return "閉鎖・合併", latest.get("successorCorporateNumber", "")
    notes = []
    if norm(latest["name"]) != norm(row["取引先名"]):
        notes.append("商号: " + latest["name"])
    if norm(address(latest)) != norm(row["所在地"]):
        notes.append("所在地: " + address(latest))
    return ("要更新" if notes else "一致"), " / ".join(notes)
 
def main(src, dst, app_id):
    with open(src, encoding="utf-8-sig", newline="") as f:
        rows = list(csv.DictReader(f))
    by_num = {}
    valid = [r for r in rows if re.fullmatch(r"\d{13}", r["法人番号"])]
    for i in range(0, len(valid), CHUNK):
        part = [r["法人番号"] for r in valid[i:i + CHUNK]]
        for rec in fetch(app_id, part):
            by_num.setdefault(rec["corporateNumber"], []).append(rec)
        time.sleep(1)  # 短時間の大量アクセスは規約で禁止
    with open(dst, "w", encoding="utf-8-sig", newline="") as f:
        w = csv.writer(f)
        w.writerow(["取引先名", "法人番号", "判定", "差分"])
        for r in rows:
            kind, diff = judge(r, by_num.get(r["法人番号"], []))
            w.writerow([r["取引先名"], r["法人番号"], kind, diff])
 
if __name__ == "__main__":
    main(sys.argv[1], sys.argv[2], os.environ["HOUJIN_APP_ID"])

要点は3つです。

  • norm で全角半角を揃え、空白と法人格(株式会社・(株)など)を外してから比べます。「(株)」と「株式会社」の違いを差分にしないためです
  • 比較は latest が 1 の行で行います。履歴の古い行と比べると、変更前の名称に一致して「一致」になりかねません
  • 閉鎖は process が 21(登記記録の閉鎖等)か 71(吸収合併)の行で判定します。successorCorporateNumber に承継先の法人番号が入る場合があるため、差分欄に書き出します

process(処理区分)の主な値は、01 新規、11 商号又は名称の変更、12 国内所在地の変更、13 国外所在地の変更、21 登記記録の閉鎖等、22 閉鎖後の復活等、71 吸収合併、72 吸収合併無効、81 商号の登記の抹消、99 削除です。99 の行は、一連番号・法人番号・更新年月日以外が空になります。

判定結果をテストで固める

判定の分岐は、公式サンプルのXMLからテストを作れます。概要編にある商号変更の例(法人番号8040001999013)を、固定のXMLとして使った例です。

import match_houjin as m
X = """...(公式サンプルのXML。2件の<corporation>、latestが0と1)..."""
 
def test_rename():
    recs = m.parse(X)
    assert len(recs) == 2
    row = {"取引先名": "株式会社商号変更前", "所在地": "千葉県千葉市中央区蘇我5丁目9番1号"}
    kind, diff = m.judge(row, recs)
    assert kind == "要更新" and "商号変更後" in diff
 
def test_abbreviation_is_not_a_diff():
    row = {"取引先名": "(株)商号変更後", "所在地": "千葉県千葉市中央区蘇我5丁目9番1号"}
    assert m.judge(row, m.parse(X))[0] == "一致"

この2つのテストは、サンプルをもとにしたXMLで通ることを確かめました。ただしXMLは公式サンプルをもとにした固定データで、国税庁側の挙動を証明するものではありません。確かめられるのは、スクリプトが仕様の項目名どおりに読み、分岐を取りこぼさないかどうかです。実データの動作は、検証環境、本番の順に10件程度から確かめます。

差分取得で商号変更と所在地変更を追う

突合を1回きりで終わらせず、変更が出たら気づける形にするのが、取得期間を指定する機能です。指定した期間に、法人番号の指定、商号・所在地の変更、登記記録の閉鎖等があった法人の情報を返します。

curl -s "https://api.houjin-bangou.nta.go.jp/4/diff\
?id=$HOUJIN_APP_ID&from=2026-10-01&to=2026-10-12&type=12&address=13"

仕様の制約は次のとおりです。

  • from は2015-12-01以降。これより前だとエラー(エラーコード013)になります
  • 期間の最大は50日。to は from から50日以内に置きます
  • 結果が2,000件を超えると分割され、ヘッダーの「分割数」が1以外になります。divide を1から分割数まで増やして取得します
  • address で都道府県(2桁)や市区町村(5桁)、kind で法人種別(最大4種類)を絞れます
  • 結果は更新年月日の昇順、次に法人番号の昇順で並びます

ここで気づく点があります。差分には全国の変更が入るため、そのままでは件数が多くなります。使い方としては、返ってきた法人番号をマスタの法人番号集合と突き合わせ、重なったものだけを「要確認」に回す形が向いています。

運用の型は2つ考えられます。

  1. 月次で全件を引き直す: マスタの法人番号をすべて /4/num で引き、差分欄を出す。取引先が数百社なら、1秒間隔でも1分前後で終わります
  2. 日次・週次で差分を見る: 前回の to の翌日から今日までを /4/diff で取り、マスタと重なる番号だけ通知する。取得漏れを避けるため、前回の終了日を保存しておきます

定期実行の設定をClaude Codeに任せる前に、1回目は手動で走らせます。出力CSVを目で読んでから、スケジューラーに載せます。

判定結果の読み方

出力CSVの判定は、取るべき行動が分かれるように区分します。

判定意味次の行動
一致意味商号・所在地とも正規化後に一致次の行動何もしない
要更新(商号)意味process が 11 の履歴があり、名称がずれている次の行動契約書・請求書の宛名を確認し、マスタを更新
要更新(所在地)意味国内所在地の変更(12)で住所がずれている次の行動送付先と契約書の記載を確認
閉鎖・合併意味21 や 71次の行動取引継続の有無を担当者に確認。承継先法人番号を控える
該当なし意味応答に行がない次の行動番号の入力ミスの可能性。名称検索で再確認

商号の変更と所在地の変更は、changeDate(変更年月日)で発生時期が分かります。「いつからずれていたか」を見たいときに使います。

判定を狂わせやすい点

  • 住所の表記差: 公式サンプルでは所在地が「蘇我5丁目9番1号」と返ります。マスタが「蘇我5-9-1」なら、正規化しても一致しません。比較前に丁目・番・号をハイフンへ寄せる処理を足すか、都道府県と市区町村だけで比べて、丁目以降は人が見る運用にします。この種の誤検出を最初に洗い出すには、実データ10行で出力を読ませるのが確実です
  • 名称のイメージID: 商号がJIS第一・第二水準以外の文字を含む場合や、150文字を超える場合は、nameImageId に値が入ります。文字を取りこぼしている可能性があるため、このときは「要確認」に回します
  • 検索対象除外の法人: Ver.4.0では、検索対象除外の項目が 1 の法人もAPIで取得できます。公表サイトの検索画面では、既定の条件で表示されません。サイトで目視確認したとき、APIでは出る法人が見つからないことがあります
  • アクセス制限: 同じIDで短時間に多数アクセスすると、HTTP 403が返ります。IDが未登録や無効なときは404、公表機能の障害時は500です。403を見たら、リトライを重ねず間隔を空けます
  • エラーの返り方: 誤りのあるリクエストでは、HTTPステータスコードに加え、エラーコードとメッセージがCSV形式で応答すると仕様にあります。XMLのパーサーに渡す前に、先頭が < かどうかで分岐させます

変更の通知先や運用フローは、取引先管理の担当者と決めます。スクリプトが出すのは「ずれの候補」までで、宛名や契約の変更は人が判断します。

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