国税庁インボイス API を TypeScript で叩いて適格請求書発行事業者を検証する
この記事で解説する国税庁インボイス API は、MCP サーバー pay-per-call-mcp から Claude 経由でも利用できます。
この記事でわかること
- 国税庁インボイス検索 Web-API のエンドポイントと認証方法
- TypeScript で適格請求書発行事業者番号(T+13桁)を検証する実装
- 一括検証・エラーハンドリング・レート制限への対応
- 登録番号の有効性・登録取消・廃業のステータス判定
- 実務での活用例(請求書受領時の自動チェック、freee 連携)
はじめに
インボイス制度(適格請求書等保存方式)が 2023 年 10 月に開始されて以来、仕入税額控除の要件として 適格請求書発行事業者の登録番号を検証する 必要が生じました。
経理担当者や開発者が直面する現実的な課題がこれです。
- 取引先から届いた請求書に書かれた登録番号「T1234567890123」は本当に有効か?
- 登録が取り消されていないか?
- 屋号と実際の法人名が一致しているか?
これを手動で 国税庁適格請求書発行事業者公表サイト に毎回アクセスして確認するのは現実的ではありません。件数が多い場合は特に。
幸い、国税庁は 無料・認証不要の公式 Web API を提供しています。本記事では TypeScript でこの API を叩いて請求書番号を自動検証する実装を解説します。
国税庁インボイス API とは
国税庁が運営する「適格請求書発行事業者公表システム Web-API 機能」です。
特徴
| 項目 | 内容 |
|---|---|
| 提供元 | 国税庁(NTA) |
| 料金 | 無料 |
| 認証 | 不要(APIキー不要) |
| レート制限 | 1リクエストあたり最大10件(複数番号同時照会可) |
| データ更新 | 毎日更新 |
| 形式 | JSON / XML |
登録番号は T + 13桁の数字で構成されます(法人番号ベース、または個人事業主向けの番号)。
API の仕様
エンドポイント
GET https://web.invoice-kohyo.nta.go.jp/api/1/invoice/validate
主なクエリパラメータ
| パラメータ | 必須 | 説明 |
|---|---|---|
number |
必須 | 登録番号(T除く13桁)。複数指定する場合はカンマ区切り(最大10件) |
type |
任意 | レスポンス形式。21=JSON(デフォルト)、11=XML |
リクエスト例
GET https://web.invoice-kohyo.nta.go.jp/api/1/invoice/validate?number=1234567890123&type=21
レスポンス形式(JSON)
{
"code": "000",
"intellectualProperty": "国税庁",
"updateDate": "2024-01-15",
"announcement": {
"date": "2024-01-15",
"count": "1",
"trader": [
{
"sequenceNumber": "1",
"registratedNumber": "T1234567890123",
"process": "01",
"correct": "0",
"kind": "1",
"country": "",
"latest": "1",
"name": "株式会社サンプル",
"address": "東京都千代田区1-1-1",
"addressPrefecture": "東京都",
"addressCity": "千代田区",
"addressStreetNumber": "1-1-1",
"kana": "カブシキガイシャサンプル",
"publicDate": "2023-10-01",
"updateDate": "2023-10-01",
"disposalDate": "",
"expireDate": "",
"close": "0",
"closeDate": "",
"successorName": "",
"change": ""
}
]
}
}
レスポンスコード(code フィールド)
| コード | 意味 |
|---|---|
000 |
正常 |
010 |
登録番号が存在しない |
011 |
登録が抹消されている |
100 |
パラメータエラー |
200 |
システムエラー |
TypeScript での実装
型定義
// types/invoice.ts
/** 適格請求書発行事業者の情報 */
export interface InvoiceTrader {
/** 連番 */
sequenceNumber: string;
/** 登録番号(T付き13桁) */
registratedNumber: string;
/** 処理区分: "01"=新規, "02"=更新, "03"=廃業 */
process: string;
/** 訂正区分: "0"=訂正なし, "1"=訂正あり */
correct: string;
/** 人格区分: "1"=法人, "2"=個人 */
kind: string;
/** 国内外区分 */
country: string;
/** 最新情報フラグ: "1"=最新 */
latest: string;
/** 氏名または名称 */
name: string;
/** 所在地(本店・主たる事務所) */
address: string;
/** 都道府県 */
addressPrefecture: string;
/** 市区町村 */
addressCity: string;
/** 丁目番地 */
addressStreetNumber: string;
/** カナ氏名 */
kana: string;
/** 登録年月日 */
publicDate: string;
/** 更新年月日 */
updateDate: string;
/** 登録取消年月日(空文字 = 現在有効) */
disposalDate: string;
/** 失効年月日 */
expireDate: string;
/** 廃業フラグ: "0"=現役, "1"=廃業 */
close: string;
/** 廃業年月日 */
closeDate: string;
/** 承継先名称 */
successorName: string;
/** 変更事項 */
change: string;
}
export interface InvoiceAnnouncement {
date: string;
count: string;
trader: InvoiceTrader[];
}
export interface InvoiceApiResponse {
/** レスポンスコード: "000"=正常 */
code: string;
intellectualProperty: string;
updateDate: string;
announcement: InvoiceAnnouncement;
}
/** 検証結果 */
export interface InvoiceVerificationResult {
registrationNumber: string;
isValid: boolean;
isActive: boolean;
trader: InvoiceTrader | null;
error: string | null;
}
検証関数の実装
// lib/verifyInvoice.ts
import type {
InvoiceApiResponse,
InvoiceVerificationResult,
} from "./types/invoice";
const NTA_API_BASE = "https://web.invoice-kohyo.nta.go.jp/api/1";
/**
* 登録番号の形式バリデーション
* "T" + 13桁の数字
*/
function validateFormat(registrationNumber: string): boolean {
return /^T\d{13}$/.test(registrationNumber);
}
/**
* 国税庁インボイス API で適格請求書発行事業者を検証する
*
* @param registrationNumber - 登録番号(例: "T1234567890123")
* @returns 検証結果
*/
export async function verifyInvoiceNumber(
registrationNumber: string
): Promise<InvoiceVerificationResult> {
// フォーマットチェック
if (!validateFormat(registrationNumber)) {
return {
registrationNumber,
isValid: false,
isActive: false,
trader: null,
error: `登録番号の形式が不正です: ${registrationNumber}(正しい形式: T + 13桁の数字)`,
};
}
// "T" を除いた13桁の番号を使う
const numberOnly = registrationNumber.slice(1);
const url = `${NTA_API_BASE}/invoice/validate?number=${numberOnly}&type=21`;
let response: Response;
try {
response = await fetch(url, {
headers: {
Accept: "application/json",
},
// タイムアウト対応(Node.js 18+ / Bun / Deno)
signal: AbortSignal.timeout(10_000),
});
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return {
registrationNumber,
isValid: false,
isActive: false,
trader: null,
error: `API リクエスト失敗: ${message}`,
};
}
if (!response.ok) {
return {
registrationNumber,
isValid: false,
isActive: false,
trader: null,
error: `HTTP エラー: ${response.status} ${response.statusText}`,
};
}
let data: InvoiceApiResponse;
try {
data = (await response.json()) as InvoiceApiResponse;
} catch {
return {
registrationNumber,
isValid: false,
isActive: false,
trader: null,
error: "レスポンスの JSON パース失敗",
};
}
// APIレベルのエラーチェック
if (data.code !== "000") {
const codeMessages: Record<string, string> = {
"010": "登録番号が存在しません",
"011": "登録が抹消されています",
"100": "パラメータエラー",
"200": "システムエラー",
};
return {
registrationNumber,
isValid: false,
isActive: false,
trader: null,
error: codeMessages[data.code] ?? `APIエラーコード: ${data.code}`,
};
}
const traders = data.announcement?.trader ?? [];
if (traders.length === 0) {
return {
registrationNumber,
isValid: false,
isActive: false,
trader: null,
error: "事業者情報が見つかりませんでした",
};
}
const trader = traders[0];
// 有効性の判断
// - disposalDate が空 = 登録取消なし
// - close が "0" = 廃業していない
const isActive =
trader.disposalDate === "" &&
trader.close === "0" &&
trader.latest === "1";
return {
registrationNumber,
isValid: true,
isActive,
trader,
error: null,
};
}
/**
* 複数の登録番号を一括検証する(最大10件)
*/
export async function verifyMultipleInvoiceNumbers(
registrationNumbers: string[]
): Promise<InvoiceVerificationResult[]> {
if (registrationNumbers.length > 10) {
throw new Error("一度に検証できる登録番号は最大10件です");
}
// フォーマット違反はAPIを呼ばずに即エラー
const invalid = registrationNumbers.filter((n) => !validateFormat(n));
if (invalid.length > 0) {
// 有効なものだけAPIに投げ、無効分はエラー結果を返す
const validNumbers = registrationNumbers.filter((n) => validateFormat(n));
const [apiResults, ...errorResults] = await Promise.all([
validNumbers.length > 0
? fetchMultiple(validNumbers)
: Promise.resolve([]),
...invalid.map((n) =>
Promise.resolve<InvoiceVerificationResult>({
registrationNumber: n,
isValid: false,
isActive: false,
trader: null,
error: `登録番号の形式が不正です: ${n}`,
})
),
]);
return [...apiResults, ...errorResults];
}
return fetchMultiple(registrationNumbers);
}
async function fetchMultiple(
registrationNumbers: string[]
): Promise<InvoiceVerificationResult[]> {
const numbersOnly = registrationNumbers.map((n) => n.slice(1)).join(",");
const url = `${NTA_API_BASE}/invoice/validate?number=${numbersOnly}&type=21`;
const response = await fetch(url, {
headers: { Accept: "application/json" },
signal: AbortSignal.timeout(10_000),
});
if (!response.ok) {
throw new Error(`HTTP エラー: ${response.status}`);
}
const data = (await response.json()) as InvoiceApiResponse;
const traders = data.announcement?.trader ?? [];
return registrationNumbers.map((regNum) => {
const trader = traders.find(
(t) => t.registratedNumber === regNum
);
if (!trader) {
return {
registrationNumber: regNum,
isValid: false,
isActive: false,
trader: null,
error: "登録番号が存在しません",
};
}
const isActive =
trader.disposalDate === "" &&
trader.close === "0" &&
trader.latest === "1";
return {
registrationNumber: regNum,
isValid: true,
isActive,
trader,
error: null,
};
});
}
実行例・レスポンスサンプル
// main.ts
import { verifyInvoiceNumber, verifyMultipleInvoiceNumbers } from "./lib/verifyInvoice";
// 単件検証
const result = await verifyInvoiceNumber("T1234567890123");
console.log(result);
出力(有効な事業者の場合)
{
"registrationNumber": "T1234567890123",
"isValid": true,
"isActive": true,
"trader": {
"sequenceNumber": "1",
"registratedNumber": "T1234567890123",
"process": "01",
"correct": "0",
"kind": "1",
"country": "",
"latest": "1",
"name": "株式会社サンプル",
"address": "東京都千代田区1-1-1",
"addressPrefecture": "東京都",
"addressCity": "千代田区",
"addressStreetNumber": "1-1-1",
"kana": "カブシキガイシャサンプル",
"publicDate": "2023-10-01",
"updateDate": "2023-10-01",
"disposalDate": "",
"expireDate": "",
"close": "0",
"closeDate": "",
"successorName": "",
"change": ""
},
"error": null
}
出力(存在しない番号の場合)
{
"registrationNumber": "T9999999999999",
"isValid": false,
"isActive": false,
"trader": null,
"error": "登録番号が存在しません"
}
複数件を一括検証
const results = await verifyMultipleInvoiceNumbers([
"T1234567890123",
"T9876543210987",
"T0000000000000",
]);
for (const r of results) {
if (r.isActive) {
console.log(`✓ ${r.registrationNumber} — ${r.trader?.name}`);
} else {
console.log(`✗ ${r.registrationNumber} — ${r.error}`);
}
}
実際のユースケース
1. 請求書受取時の自動検証
請求書の PDF や CSV を受け取った際に、登録番号フィールドをパースして verifyInvoiceNumber を呼ぶパイプラインを組むと、経理担当者が手作業で確認する手間を省けます。
async function processReceivedInvoice(invoice: {
vendor: string;
registrationNumber: string;
amount: number;
}) {
const verification = await verifyInvoiceNumber(invoice.registrationNumber);
if (!verification.isActive) {
// Slack 通知や承認フロー停止など
throw new Error(
`請求書の登録番号が無効です: ${verification.error ?? "不明"}`
);
}
// 名称の一致チェック(任意)
const apiName = verification.trader?.name ?? "";
if (!apiName.includes(invoice.vendor) && !invoice.vendor.includes(apiName)) {
console.warn(
`事業者名が一致しない可能性があります。請求書: "${invoice.vendor}" / 国税庁DB: "${apiName}"`
);
}
// 以降、仕訳計上など
}
2. freee 連携での活用
freee の取引先マスタに登録されたインボイス番号を定期的に照合して、登録取消や廃業を検知することができます。freee API で取引先一覧を取得し、verifyMultipleInvoiceNumbers に渡すだけです。
// 取引先一覧を freee から取得し、10件ずつバッチ検証
async function auditFreeePartners(partners: { name: string; invoiceNumber: string }[]) {
const chunkSize = 10;
for (let i = 0; i < partners.length; i += chunkSize) {
const chunk = partners.slice(i, i + chunkSize);
const results = await verifyMultipleInvoiceNumbers(
chunk.map((p) => p.invoiceNumber)
);
for (const r of results) {
if (!r.isActive) {
console.warn(`[要確認] ${r.registrationNumber}: ${r.error}`);
}
}
// レート制限を考慮して少し待つ
await new Promise((resolve) => setTimeout(resolve, 500));
}
}
Claude / AI エージェントに叩かせる場合の注意点
AI エージェントがこの API を使う場合に気をつけるべき点をまとめます。
エラーハンドリングを明示する
AI エージェントは「API が返した値」をそのまま信頼しがちです。code !== "000" のケースや HTTP エラーを確実にハンドリングする実装を渡してください。上記の verifyInvoiceNumber 関数はその点を考慮済みです。
取得データは読み取り専用
国税庁 API はあくまで 参照専用 です。エージェントが「登録番号を更新する」「削除する」といった操作は API 上不可能であることを明示しておくと、幻覚による誤操作を防げます。
キャッシュを活用する
同一番号を何度も叩く場合はキャッシュ(例: Map<string, InvoiceVerificationResult> や Redis)を挟んでください。データは毎日更新されるので、TTL は 24 時間程度が妥当です。
個人事業主の番号は慎重に
個人事業主の登録番号は法人番号とは異なる体系で、住所情報が公開されます。エージェントがログに出力したり外部サービスに送信したりしないよう、ハンドリングに注意してください。
まとめ
- 国税庁インボイス API は 無料・認証不要 で、TypeScript の
fetch一発で呼べる - レスポンスの
disposalDate・close・latestフィールドを組み合わせて「現在有効かどうか」を判定する - 最大10件の一括照会が可能なので、バッチ処理にも対応できる
- freee などの会計ソフトの取引先マスタと組み合わせると、定期的な登録状況の監査を自動化できる
インボイス番号の検証は「一回やれば終わり」ではありません。登録取消や廃業は随時発生するため、定期的な自動チェックの仕組みを作っておくことをお勧めします。
おまけ: Claude に自動検証させる
上記の実装を毎回自分でセットアップするのが手間な場合、LemonCake の japan-tax-check プロンプトを使うと、MCP 経由で Claude がインボイス番号を検証できるようになります。
「T1234567890123 は有効な適格請求書発行事業者ですか?」
と聞くだけで、Claude が国税庁 API を叩いて結果を返してくれます。複数番号の一括確認や、freee の取引先リストとの突き合わせもチャット上で完結します。
詳細は以下のリンクをご覧ください。
- npm: https://www.npmjs.com/package/pay-per-call-mcp
- Glama: https://glama.ai/mcp/servers/@evid-ai/pay-per-call-mcp
よくある質問
Q. インボイス番号のチェックは義務ですか?
A. 仕入税額控除を適用するためには、受け取った請求書が「適格請求書」であることの確認が必要です。番号が無効・取消済みの事業者からの請求書では控除が認められない可能性があります。
Q. API キーの取得はどこでできますか?
A. 国税庁インボイス制度適格請求書発行事業者公表サイト(https://www.invoice-kohyo.nta.go.jp/)からアプリケーション ID を無料取得できます。
Q. 個人事業主も番号を持っていますか?
A. 課税事業者として登録した個人事業主も T+13桁の登録番号を持ちます。ただし免税事業者(年間売上 1,000 万円以下)は登録していない場合があり、その場合は仕入税額控除の対象外となります。