

領収書収集を自動化するとき、一番楽なのはメールに PDF が直接添付されているケースです。しかし現実には、多くの SaaS は本文にリンクを置くだけです。
そしてそのリンクは、しばらくすると失効します。ページを開くと「この URL は期限切れです」という短いメッセージが返るだけになります。
面倒なのは、失効しても HTTP 200 が返ることです。ダウンロードは成功し、ファイルもできます。中身がエラー文字列だけというだけです。これを検証なしで回すと、中身がテキストの「領収書」が会計ソフトに何十件も上がることになります。
本記事では、この問題の回避策として確立した「ポータルから URL を組み立てる」手法を書きます。
領収書リンクが失効する条件と、失効時の見分け方
カスタマーポータルの請求リンクから PDF の URL を組み立てる変換規則
メール本文から URL を抽出するときの実務的な注意点
取得後に必ずやるべき 2 段階の検証
この制約から逆算される「月初に回す」という運用設計
SaaS の領収書を機械的に集めたい人
メール本文からリンクを抽出してファイルを取得する自動化を書いている人
「取得は成功しているのに中身がおかしい」に心当たりのある人
対象の SaaS が決済代行のカスタマーポータルを使っている(領収書メールの送信元で判別できる)
file コマンドと Python の pypdf が使える
自分が支払っているサービスの領収書を自分で取得する用途
実測した結果、領収書メールに入っている PDF リンクは 発行から約 30 日で失効しました。
これが意味するのは、「年末にまとめて 1 年分集める」という運用は成立しないということです。1 月分のメールは 12 月にはとっくに使えなくなっています。
さらに困るのが、失効のシグナルが弱いことです。取得コマンドの終了コードは 0、ファイルも生成されます。成功しているように見えます。しかし型を見るとこうなっています。
file receipt.pdf
# => receipt.pdf: ASCII text ← PDF ではない
だからfile コマンドによる検証を必須にする。これが 1 つ目の教訓です。
では過去分はどうするか。ここで使えるのが、カスタマーポータルに並ぶ請求リンクから、PDF の URL を機械的に導けるという性質です。
多くの SaaS の請求履歴ページには、各行に「表示」リンクが並んでいます。そのリンクは次の形をしています。
表示用(ブラウザで開く):
invoice.代行ドメイン/i/{アカウントID}/{トークン}
PDF 直リンク(こちらを組み立てる):
pay.代行ドメイン/invoice/{アカウントID}/{トークン}/pdf
変換規則は 2 つだけです。
| 変換内容 | before | after |
|---|---|---|
| ホストとパスの先頭 | invoice サブドメイン + /i/ | pay サブドメイン + /invoice/ |
| 末尾 | (なし) | /pdf を追加 |
重要なのは、ポータルが発行するリンクは新鮮だということです。メールのリンクは送信時点で固定されているので古びますが、ポータルを今開けば今のトークンが得られます。
つまり、運用はこうなります。
直近 1 ヶ月分 → メールから URL を抽出して取得(早い)
過去分 → ポータルを開いてリンクを取り、変換して取得
どちらも最終的には HTTP クライアントで直接取得するので、ブラウザのダウンロード機能を使わずに済みます。これは大きな利点です(理由は次回の記事で書きます)。
直近分はメールから拾うのが早いです。ただし、メール本文の取得には実務的な問題が 1 つあります。
HTML メールの本文はとにかく大きい。領収書メール 1 通で 5 万文字を超えることもあります。これをそのまま LLM のコンテキストに入れるのは無駄です。
対策は「ファイルに落として、欲しいものだけ抜く」ことです。本文全体を読まず、決済代行のドメインにマッチするリンクだけを拾います。
このとき一緒にやっておくと良いのが、金額と日付の抽出です。あとでファイル名を付けるときに必要になりますし、間違った月のメールを拾っていないかの確認にも使えます。HTML 本文は標準ライブラリの HTML パーサでタグを落としてから、金額表記や支払日の文字列を探します。
取得したら、必ず 2 段階で検証します。どちらか一方だけでは不十分です。
第 1 段階: それは本当に PDF か。 失効リンク対策です。
file receipts/2026/07/20260726_918_サブスク.pdf
# => PDF document, version 1.4, 1 pages ← OK
第 2 段階: 中身は目的のものか。 別の月・別の金額の領収書を拾っていないかの確認です。
from pypdf import PdfReader
text = PdfReader("20260726_918_サブスク.pdf").pages[0].extract_text()
assert "918" in text
assert "July 26, 2026" in text
第 2 段階を省くと、ポータルの一覧で行を 1 つ間違えたときに気づけません。同じサービスの隔月の領収書は見た目がほとんど同じなので、目視では必ず見逃します。機械的に突合させるのが唯一の解です。
pypdf ならテキスト抽出が瞬時なので、目視確認を完全に置き換えられます。LLM に PDF を見せる必要もありません。
ここまでの制約を並べると、運用の形が自動的に決まります。
メールのリンクは約 30 日で失効する
ポータル経由なら取り直せるが、サービスごとにログインが必要で手間がかかる
だから、月初に前月分をまとめて回すのが最も効率がよい。このときメールはすべて 30 日以内なので、ほぼ全件をメール経由の最短ルートで取得できます。
この逆算は月次締めの skill の先頭に明記してあります。
毎月月初の実行を想定
(領収書メールのリンクが 30 日で失効するため、月初実行が最も効率的)
制約を手順のコメントに残すのは地味ですが効果があります。数ヶ月後の自分が「なぜ月初にやるんだっけ?」と思ったときに答えを返せます。
すべてのサービスが同じ決済代行を使っているわけではありません。実際には 4 パターンに分かれました。
| パターン | 取得方法 | 難易度 |
|---|---|---|
| メールに PDF 直添付 | メールクライアントから保存 | 最も楽 |
| 決済代行リンク型 | URL を組み立てて直接取得 | 楽(本記事) |
| Web ダッシュボード型 | ブラウザ自動操作で取得 | 中(次回) |
| HTML しかない型 | HTML を保存してローカルで PDF 化 | やや面倒(次回) |
重要なのは、ベンダーごとの取得方法を台帳に書いておくことです。毎月同じ調査をやり直すのは無駄なので、一度確立した手順は手順書に残します。台帳にはベンダー種別・目安金額・決済日・取得方法・勘定科目の傾向を並べておきます。
金額の目安と決済日を入れておくのがコツです。カード明細を見たときに「この引き落としはこのサービス」と即座に対応付けできます。
取得の成否を終了コードで判断しない。失効リンクは HTTP 200 でエラー文字列を返すので、file で型を見る
ポータル発行のリンクは新鮮。過去分を取り直すときはメールではなくポータルから
メール本文はファイルに落として、欲しいものだけ抽出する。HTML メールは 5 万文字を超えることがある
リンクと一緒に金額・日付も抽出しておく。ファイル名付けと検証の両方に使える
検証は「型」と「中身」の 2 段階。隔月の領収書は見た目が同じなので目視では必ず見逃す
制約を手順のコメントに残す。「なぜ月初にやるのか」の理由が残っていると、運用が崩れにくい
領収書メールの PDF リンクは約 30 日で失効し、しかも HTTP 200 でエラーを返す
決済代行のカスタマーポータルを使うサービスなら、請求リンクから PDF の URL を機械的に組み立てられる
変換規則は「ホストとパス先頭の置換」と「末尾に /pdf を追加」の 2 つだけ
取得後の検証は file による型確認と、PDF テキスト抽出による中身確認の 2 段階
30 日失効という制約から、「月初に前月分をまとめて回す」という運用が逆算される
ベンダーごとの取得方法は台帳化する。金額の目安と決済日を入れておくと明細との対応付けが早い