メインコンテンツへスキップ
kt-tech.blog

【実装】会計 API とブラウザ自動操作の役割分担 — 同期明細は必ず UI から登録する(確定申告自動化 第3回)

実装9分で読めます

この記事でわかること

  • 同期明細に対して API で取引を作ると何が起きるのか(消込が発生せず二重計上になる)
  • API と UI の役割を分ける境界線と、その判定基準を 1 つにする方法
  • 添付の API が「全件再送」形式のときに金額を壊さない書き方

会計 SaaS は freee を使用(同様の口座・カード同期機能を持つ SaaS なら考え方は共通)/ OAuth2 のアクセストークンを取得済み / ブラウザ自動操作は Claude Code の Chrome 連携を使用

概要: 会計 SaaS の API を使えば取引をプログラムから作れます。しかしカード・口座の自動同期明細に対して API で取引を作ると、明細と消込まれず二重計上になります。この事故を実際にやってから、API は「取得と添付」専用、「登録」はブラウザ自動操作という役割分担に落ち着きました。その理由と実装を整理します。

はじめに

会計 SaaS には便利な機能があります。クレジットカードや銀行口座を連携しておくと、利用明細が自動で取り込まれ、「この明細はこの勘定科目ですか?」と推測してくれるやつです。

これを自動化しようとしたとき、最初に思いつくのは「API で取引を作ればいい」です。実際、多くの会計 SaaS には取引作成の API があります。

私もそうしました。そして帳簿を壊しました

本記事は、その失敗から導き出した役割分担の話です。結論はシンプルです。

同期明細に紐づく登録は、必ず UI から行う。API は取得と添付に限定する。


1. 何が起きたか — 二重計上の仕組み

会計 SaaS の中では、「同期された明細」と「取引」は別のオブジェクトです。

【同期明細】              【取引】
カード会社から取得      帳簿に乗る仕訳
金額・日付・店名          借方科目・貸方科目・金額
ステータス = 未登録           ↑
     │                        │
     └──── 「消込」で紐づく ──┘

正しい手順では、同期明細を UI 上で登録すると、取引が作られると同時に明細と取引が消込まれます。明細は「登録済み」になり、残高は合います。

ところが API で取引を作るとこうなります。

【同期明細】              【API で作った取引】
ステータス = 未登録のまま    帳簿には乗る
     │                        │
     └─ 紐づかない(消込なし)─┘

明細は未登録のまま残り、取引だけが別に存在する状態になります。このあと自動化の機能が同じ明細を登録すると、同じ支出が 2 件の取引になります

しかも厄介なのは、このズレがすぐには見えないことです。帳簿を見ても取引は普通に並んでいます。登録残高と同期残高を突き合わせて初めて気づくタイプの壊れ方です。

2. 境界線を引く

この仕様を踏まえて、役割をこう分けました。

操作 手段 理由
明細の一覧取得 API 読み取り専用。副作用なし
取引の一覧取得 API 同上。領収書未添付の抽出に使う
領収書のアップロード API ファイルボックスへの追加。明細と無関係
取引への領収書添付 API 既存取引の更新。消込状態は変わらない
同期明細の登録 UI 消込が発生するのは UI 経由のみ
明細の無視 / 無視解除 UI ステータス変更の API が存在しない
私用振替(科目変更) API すでに消込済みの取引を書き換えるだけ
同期外の支出(現金等)の計上 API 元々紐づく明細がないので消込問題が起きない

見通しをよくするために言い換えると、判定基準は 1 つだけです。

その操作は「同期明細のステータス」を変える必要があるか? → あるなら UI、ないなら API

この一文を skill の先頭に書いておくと、迷ったときに自分で判断できます。

これは同期外の支出(私用カードや現金で払った事業経費)を計上するときにも効いてきます。そこにはそもそも同期明細が存在しないので、API で作っても何も壊れません。

3. API 側の実装 — 薄い CLI ラッパーにする

API 側は Python の薄い CLI ラッパーにまとめています。コマンドは 6 つだけ。

Bash
# 明細の一覧(status 1 = 未登録)
python3 scripts/freee_api.py txns --start 2026-07-01 --end 2026-07-31 --status 1

# 領収書未添付の取引を抽出
python3 scripts/freee_api.py deals --start 2026-07-01 --end 2026-07-31 --no-receipt

# 領収書をアップロードして取引に添付し、検証する
python3 scripts/freee_api.py upload <pdf> --date 2026-07-26 --description "..."
python3 scripts/freee_api.py attach --deal <deal_id> --receipt <receipt_id>
python3 scripts/freee_api.py get-deal <deal_id>

設計上のポイントは 3 つあります。

出力をすべて JSON に統一する。 LLM がそのまま読め、python3 -c でその場の集計もできます。整形は呼び出し側の仕事にします。

ページングを中で閉じる。 一覧系は 100 件ごとにカーソルして全件集めてから返します。呼び出し側が offset を意識すると、取りこぼしに気づかないバグを生むので閉じこめます。

401 で自動リフレッシュする。 アクセストークンの失効を呼び出し側に漏らさない。リフレッシュトークンがローテーションする実装なら、新しい値を必ず保存します。

Python
def request(method, path, params=None, body=None, multipart=None, _retried=False):
    ...
    except urllib.error.HTTPError as e:
        if e.code == 401 and not _retried and TOKENS.get("refresh_token"):
            refresh_access_token()
            return request(method, path, params=params, body=body,
                           multipart=multipart, _retried=True)
        ...

_retried フラグで 1 回だけに限定するのがポイントです。リフレッシュ自体が失敗するケースで無限ループになります。

4. 添付 API が「全件再送」のときの安全な書き方

取引への領収書添付は、多くの会計 API で取引全体の更新として実装されています。つまり「領収書 ID だけ追加」という部分更新はできず、明細行を含めて全部再送する必要があります。

ここで雑に書くと、金額や勘定科目が消えます。安全な手順はこうです。

  1. 対象の取引を GET で取得する
  2. 返ってきた明細行を、id を含めたまま再送の body に入れる
  3. そこに領収書 ID の配列を足して PUT する
  4. もう一度 GET して、金額・ステータス・添付が意図通りか確かめる

明細行の id を落とすと「既存行の更新」ではなく「新規行の追加」と解釈され、金額が二重になることがあります。ここは実際に検証して確かめました。

検証はこの形で十分です。

Bash
python3 scripts/freee_api.py get-deal <deal_id> | python3 -c "
import json, sys
d = json.load(sys.stdin); x = d.get('deal', d)
print('金額:', x['amount'])
print('ステータス:', x['status'])
print('領収書:', [r['id'] for r in x.get('receipts', [])])
print('科目:', [dt['account_item_id'] for dt in x['details']])
"

「エラーが出なかった」を成功と見なさない。これがこの手の API を扱うときの鉄則です。

5. UI 側の実装 — 下の行から登録する

登録はブラウザ自動操作で行います。ここにも地味なコツが 1 つあります。

未登録明細の一覧は、1 件登録するとその行が消えて下から詰まります。上から順に登録すると、クリック座標が毎回ずれていきます。

対策は単純で、最下部の行から順に登録する。こうすれば上の行の位置は動きません。

さらに、1 件ごとにスクリーンショットを取って仕訳プレビューを確認します。このプレビューには借方の科目と金額が出るので、意図した仕訳になっているかをその場で検査できます。

登録後は API 側で確認します。未登録明細が 0 件になれば完了です。

Bash
python3 scripts/freee_api.py txns --start 2026-07-01 --end 2026-07-31 --status 1
# => {"count": 0, "wallet_txns": []}

UI で登録して API で検証する。この往復が、両方を使う設計のちょうどいいところです。

6. 分担を skill に定着させる

この手のルールは、書いておかないと忘れます。人も忘れますし、LLM はもっと忘れます(その場では API で作る方が早いので、親切心でそうしてしまう)。

skill の先頭にこう書いています。

**重要な前提**: 同期明細の登録は必ず UI から行う。
API で取引を作っても同期明細と消込されず、登録残高と同期残高が二重にずれる。
**API は「一覧取得」と「登録後の検証・領収書添付」専用**。

ポイントは、禁止事項だけでなく理由と代替手段をセットで書くことです。「API で作るな」だけだと、別の状況で同じ失敗をします。「なぜダメか」「ではどうするか」があって初めて守られます。

同じことをベンダー台帳側にも 1 行入れてあります。私用振替の手順のところに、「新規取引の作成は二重登録になるので不可。自動登録された取引の科目を変更する」と明記する、という形です。


Tips

  • 判定基準は「同期明細のステータスを変える必要があるか」の 1 つ。あるなら UI、ないなら API
  • 同期外の支出(現金・連携外カード)は API で作っても安全。紐づく明細がないので消込問題が起きない
  • 全件再送型の更新 API では、既存行の id を必ず含める。落とすと新規行扱いになって金額が二重になる
  • 更新系の後は必ず GET で検証する。金額・ステータス・添付・科目の 4 点を見れば十分
  • UI の一覧は下の行から処理する。上からだと行が詰まって座標がずれていく
  • skill には禁止事項と一緒に「理由」と「代替手段」を書く。禁止だけだと別の状況で同じ失敗をする

まとめ

  • 会計 SaaS では「同期明細」と「取引」は別オブジェクトで、UI 登録時に「消込」で紐づく
  • API で取引を作ると消込が発生せず、明細が未登録のまま残って二重計上になる
  • 境界線は「同期明細のステータスを変える必要があるか」。あれば UI、なければ API
  • API 側は JSON 出力の薄い CLI ラッパーにし、ページングとトークン更新を中で閉じる
  • 全件再送型の更新 API では既存行の id を保持し、更新後は必ず GET で検証する
  • この分担は忘れるので、理由と代替手段をセットで skill と台帳に書いて定着させる

参考リンク

更新履歴

  1. 誤字を修正(貫方科目→貸方科目、斑な→曖昧な)
  2. 記事内リンクの修正

確定申告自動化

6

個人事業主の経理を、UIを作らず Claude Code の skill だけで回した記録。領収書の自動取得から電子帳簿保存法の要件、freee への添付、月次締めまで。

  1. 1【設計】UIを作らず Claude Code の skill だけで確定申告を自動化する — プロジェクト全体像(確定申告自動化 第1回)
  2. 2【設計】電子帳簿保存法の検索要件を「ファイル名規約」だけで満たす — DB を作らないという選択(確定申告自動化 第2回)
  3. 3表示中【実装】会計 API とブラウザ自動操作の役割分担 — 同期明細は必ず UI から登録する(確定申告自動化 第3回)
  4. 4【実装】メールリンクの 30 日失効と戦う — 決済ポータルから領収書 PDF の URL を組み立てる(確定申告自動化 第4回)
  5. 5【トラブルシューティング】ブラウザ自動操作でファイルを集めるときの落とし穴集(確定申告自動化 第5回)
  6. 6【設計】月次締めを skill にする — 定型作業を「一言」に畳む設計(確定申告自動化 第6回・完)
この連載の一覧を見る