

会計 SaaS には便利な機能があります。クレジットカードや銀行口座を連携しておくと、利用明細が自動で取り込まれ、「この明細はこの勘定科目ですか?」と推測してくれるやつです。
これを自動化しようとしたとき、最初に思いつくのは「API で取引を作ればいい」です。実際、多くの会計 SaaS には取引作成の API があります。
私もそうしました。そして帳簿を壊しました。
本記事は、その失敗から導き出した役割分担の話です。結論はシンプルです。
同期明細に対して API で取引を作ると何が起きるのか(消込問題)
API と UI の役割を分ける具体的な境界線
API 側でやるべきこと(一覧取得・アップロード・添付・検証)の実装
添付の API が「全件再送」形式のときの安全な書き方
この分担を skill にどう書いて守らせるか
会計 SaaS の API を叩いて経理を自動化しようとしている人
「自動同期された明細」と「自分で作った取引」の関係が斑な人
API とブラウザ自動操作を併用する設計の実例を見たい人
会計 SaaS は freee を使用(同様の同期機能を持つ SaaS なら考え方は共通)
OAuth2 のアクセストークンを取得済み
ブラウザ自動操作は Claude Code の Chrome 連携を使用
会計 SaaS の中では、「同期された明細」と「取引」は別のオブジェクトです。
【同期明細】 【取引】
カード会社から取得 帳簿に乗る仕訳
金額・日付・店名 借方科目・貫方科目・金額
ステータス = 未登録 ↑
│ │
└──── 「消込」で紐づく ──┘
正しい手順では、同期明細を UI 上で登録すると、取引が作られると同時に明細と取引が消込まれます。明細は「登録済み」になり、残高は合います。
ところが API で取引を作るとこうなります。
【同期明細】 【API で作った取引】
ステータス = 未登録のまま 帳簿には乗る
│ │
└─ 紐づかない(消込なし)─┘
明細は未登録のまま残り、取引だけが別に存在する状態になります。このあと自動化の機能が同じ明細を登録すると、同じ支出が 2 件の取引になります。
しかも厄介なのは、このズレがすぐには見えないことです。帳簿を見ても取引は普通に並んでいます。登録残高と同期残高を突き合わせて初めて気づくタイプの壊れ方です。
この仕様を踏まえて、役割をこう分けました。
| 操作 | 手段 | 理由 |
|---|---|---|
| 明細の一覧取得 | API | 読み取り専用。副作用なし |
| 取引の一覧取得 | API | 同上。領収書未添付の抽出に使う |
| 領収書のアップロード | API | ファイルボックスへの追加。明細と無関係 |
| 取引への領収書添付 | API | 既存取引の更新。消込状態は変わらない |
| 同期明細の登録 | UI | 消込が発生するのは UI 経由のみ |
| 明細の無視 / 無視解除 | UI | ステータス変更の API が存在しない |
| 私用振替(科目変更) | API | すでに消込済みの取引を書き換えるだけ |
| 同期外の支出(現金等)の計上 | API | 元々紐づく明細がないので消込問題が起きない |
見通しをよくするために言い換えると、判定基準は 1 つだけです。
この一文を skill の先頭に書いておくと、迷ったときに自分で判断できます。
これは同期外の支出(私用カードや現金で払った事業経費)を計上するときにも効いてきます。そこにはそもそも同期明細が存在しないので、API で作っても何も壊れません。
API 側は Python の薄い CLI ラッパーにまとめています。コマンドは 6 つだけ。
# 明細の一覧(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 で自動リフレッシュする。 アクセストークンの失効を呼び出し側に漏らさない。リフレッシュトークンがローテーションする実装なら、新しい値を必ず保存します。
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 回だけに限定するのがポイントです。リフレッシュ自体が失敗するケースで無限ループになります。
取引への領収書添付は、多くの会計 API で取引全体の更新として実装されています。つまり「領収書 ID だけ追加」という部分更新はできず、明細行を含めて全部再送する必要があります。
ここで雑に書くと、金額や勘定科目が消えます。安全な手順はこうです。
対象の取引を GET で取得する
返ってきた明細行を、id を含めたまま再送の body に入れる
そこに領収書 ID の配列を足して PUT する
もう一度 GET して、金額・ステータス・添付が意図通りか確かめる
明細行の id を落とすと「既存行の更新」ではなく「新規行の追加」と解釈され、金額が二重になることがあります。ここは実際に検証して確かめました。
検証はこの形で十分です。
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 を扱うときの鉄則です。
登録はブラウザ自動操作で行います。ここにも地味なコツが 1 つあります。
未登録明細の一覧は、1 件登録するとその行が消えて下から詰まります。上から順に登録すると、クリック座標が毎回ずれていきます。
対策は単純で、最下部の行から順に登録する。こうすれば上の行の位置は動きません。
さらに、1 件ごとにスクリーンショットを取って仕訳プレビューを確認します。このプレビューには借方の科目と金額が出るので、意図した仕訳になっているかをその場で検査できます。
登録後は API 側で確認します。未登録明細が 0 件になれば完了です。
python3 scripts/freee_api.py txns --start 2026-07-01 --end 2026-07-31 --status 1
# => {"count": 0, "wallet_txns": []}
UI で登録して API で検証する。この往復が、両方を使う設計のちょうどいいところです。
この手のルールは、書いておかないと忘れます。人も忘れますし、LLM はもっと忘れます(その場では API で作る方が早いので、親切心でそうしてしまう)。
skill の先頭にこう書いています。
**重要な前提**: 同期明細の登録は必ず UI から行う。
API で取引を作っても同期明細と消込されず、登録残高と同期残高が二重にずれる。
**API は「一覧取得」と「登録後の検証・領収書添付」専用**。
ポイントは、禁止事項だけでなく理由と代替手段をセットで書くことです。「API で作るな」だけだと、別の状況で同じ失敗をします。「なぜダメか」「ではどうするか」があって初めて守られます。
同じことをベンダー台帳側にも 1 行入れてあります。私用振替の手順のところに、「新規取引の作成は二重登録になるので不可。自動登録された取引の科目を変更する」と明記する、という形です。
判定基準は「同期明細のステータスを変える必要があるか」の 1 つ。あるなら UI、ないなら API
同期外の支出(現金・連携外カード)は API で作っても安全。紐づく明細がないので消込問題が起きない
全件再送型の更新 API では、既存行の id を必ず含める。落とすと新規行扱いになって金額が二重になる
更新系の後は必ず GET で検証する。金額・ステータス・添付・科目の 4 点を見れば十分
UI の一覧は下の行から処理する。上からだと行が詰まって座標がずれていく
skill には禁止事項と一緒に「理由」と「代替手段」を書く。禁止だけだと別の状況で同じ失敗をする
会計 SaaS では「同期明細」と「取引」は別オブジェクトで、UI 登録時に「消込」で紐づく
API で取引を作ると消込が発生せず、明細が未登録のまま残って二重計上になる
境界線は「同期明細のステータスを変える必要があるか」。あれば UI、なければ API
API 側は JSON 出力の薄い CLI ラッパーにし、ページングとトークン更新を中で閉じる
全件再送型の更新 API では既存行の id を保持し、更新後は必ず GET で検証する
この分担は忘れるので、理由と代替手段をセットで skill と台帳に書いて定着させる