パスポートの写真は、ほとんどのアプリが受け取るファイルの中で最も機微なものです。顔、氏名、生年月日、旅券番号が写っていて、それだけで別の場所に口座を開けてしまいます。それなのに多くのアップロードの流れでは、その画像がそもそも存在する必要があるのかを誰かが問う前に、5 回も 6 回もコピーされています。
この記事では、この問題をエンジニアリングの側から考えます。GDPR がデータの保存について何と言っているかを引用し、パスポート画像が知らないうちに溜まっていく場所を一つずつ確認し、私たちが「読んで、返して、忘れる」と呼ぶパターンを説明します。画像は読み取られ、結果が返され、画像そのものは何も残りません。最後に、あなたの仕事として残る部分を扱います。一部の事業者にはコピーの保存が義務づけられており、2027 年 7 月からは EU が新しい法律でそれを明文化するからです。
これは、身分証明書を読み取って結果を JSON で返す パスポート・身分証 OCR API の doc.cheap のブログです。製品に関する部分は、その点を踏まえてお読みください。
GDPR が実際に求めていること
GDPR は「パスポート画像を決して保存してはならない」とは言っていません。もっと役に立つことを言っています。必要なものを、必要な期間だけ保持し、それ以上は保持しない、ということです。Reg. (EU) 2016/679 の第 5 条第 1 項が原則を定めています。そのうち 2 つが、設計の大部分を左右します。
| 原則 | 第 5 条第 1 項の文言(英語版) | 画像にとっての意味 |
|---|---|---|
| データの最小化、(c) 号 | "adequate, relevant and limited to what is necessary in relation to the purposes for which they are processed" | 処理に必要なのが氏名、生年月日、旅券番号であれば、それらを読み取った後の画像そのものは必要ないかもしれません。 |
| 記録保存の制限、(e) 号 | "kept in a form which permits identification of data subjects for no longer than is necessary for the purposes for which the personal data are processed" | どのコピーにも終了日が必要です。「削除する暇がなかった」は終了日ではありません。 |
第 5 条第 2 項は説明責任を加えます。これらの原則を守っていることを示せなければなりません。さらに第 25 条第 1 項、データ保護バイデザインは、"appropriate technical and organisational measures, such as pseudonymisation, which are designed to implement data-protection principles, such as data minimisation, in an effective manner" を求めています。つまり、データの最小化のような原則を効果的に実現するための、仮名化などの適切な技術的・組織的な措置です。
これらを合わせて読むと、法律の問題がエンジニアリングの問題に変わります。画像のコピーが少ないほど、説明し、保護し、バックアップし、最終的に空にしなければならない場所も少なくなります。一度も書き込まれなかったコピーだけが、そのどの作業も必要としません。
パスポート画像が行き着く場所
ほとんどのチームは、意図して 1 か所に画像を保存しています。問題は、誰も選ばなかった場所です。自分の流れと照らし合わせて確認するためのリストを挙げます。
| 場所 | 画像がそこに届く経路 |
|---|---|
| アップロード用バケット | クライアントがまずオブジェクトストレージにアップロードし、バックエンドがそこから読み込みます。オブジェクトはリクエストより長く残ります。 |
| リクエストログ | ロギング用のミドルウェアがリクエストボディを書き出します。base64 の画像はリクエストボディそのものです。 |
| エラーレポート | 例外トラッカーが、失敗したリクエストのペイロードを添付します。 |
| キューとリトライ | ジョブのメッセージが画像を運び、デッドレターキューが失敗したものを何週間も保持します。 |
| バックアップとスナップショット | その日に取ったデータベースやディスクのスナップショットには、それ以前に書き込まれたすべての画像が、行を削除した後もずっと残ります。 |
| サポートチケット | ユーザーが「アップロードがうまくいかなかったので」と写真をメールで送り直します。 |
| アナリティクスとセッションリプレイ | ツールが、選択したファイルのプレビューも含めてページを記録します。 |
| OCR プロバイダー | 文書を読み取るサービスが、独自の保存ルールのもとで自前のコピーを保持します。 |
最後の行は、あなたが最も制御しにくいものです。自分のログなら直せます。ベンダーが持つコピーはそのベンダーの設定に従うので、それがどうなっているかを尋ねる必要があります。
パターン:読んで、返して、忘れる
このパターンは簡単に言い表せます。画像は 1 回のリクエストの間だけ、メモリ上にしか存在しません。リクエストから出ていくのは読み取り結果、つまり抽出された値です。画像そのものはまったく出ていきません。
- 画像を直接認識に送ります。 間にアップロード用バケットを挟みません。大きなファイルのためにバケットが避けられない場合は、オブジェクトの寿命を数分にして、呼び出しが返ったら削除します。
- レスポンスから必要なものをすぐに使います。 所持者の顔写真などの切り抜き画像は、そのレスポンスの中にしか存在しません。自撮り写真と顔写真を照合する流れなら、このタイミングで行います。
- 画像ではなく読み取り結果を保持します。 処理に必要なフィールドを、自分の保存ルールのもとで保存します。生年月日や旅券番号も個人データなので、これらにも終了日が必要です。
- 画像をログとエラーレポートに入れないようにします。 すべての呼び出し側が覚えていることに頼るのではなく、境界の 1 か所でボディを取り除きます。
- 結果を書き残します。 上の表の各場所について、画像がそこに届きうるか、届かないならなぜかを記録します。その記録こそが、第 5 条第 2 項が求める説明責任です。
私たちの API が画像をどう扱うか
doc.cheap が同じ問題をどう扱っているかを、データ保持とプライバシー のページの説明に沿って紹介します。
- 画像は一切保存されません。 リクエストの間だけメモリ上にあり、認識エンジンに渡され、レスポンスが書き出された時点で消えます。ディスク、オブジェクトストア、ログのいずれにも渡りません。
- 切り抜き画像も保存されません。 文書の切り抜き、所持者の顔写真、署名は、それを生成した呼び出しのレスポンスで返されます。後で
GET /v1/scans/{id}で読み直したスキャンでは、画像の欄はすべてnullになっています。 - 保持できるのは読み取り結果だけで、 それも指定した期間に限られます。
retain_hoursオプションでリクエストごとに 0 から 8760 時間(1 年)まで設定できます。明示した値は常にアカウントの設定より優先されます。 retain_hours: 0は何も書き込みません。 すぐに期限切れになる行ではなく、行そのものがありません。消すものも、バックアップに残るものも、エクスポートするものもありません。それでもスキャンは 1 回のスキャンとして数えられます。- 残りはアカウントのデフォルトがカバーします。 リクエストで期間を指定しない場合は、アカウントの履歴設定が適用されます。24 時間、7 日、1 か月、1 年のいずれかです。新しいアカウントはダッシュボードに履歴が表示されるよう 1 年から始まります。設定を短くすると、すでに保存されている行にも適用され、各行はそれぞれの作成時刻から計算されます。
- 保持された行には小さな画像が 1 枚残ります。 長辺 96 px 以下、16 KiB 以下のサムネイルで、ダッシュボードの操作ログで行を見分けられるように表示されます。API からは読み取れません。サムネイルは行とともに消えます。
- 1 件のスキャンを早めに削除できます。 ライブキーで
DELETE /v1/scans/{id}を送ると、結果、履歴の行、サムネイルが削除されます。取り消しはできません。
設定の手順は 履歴の保持期間を管理する ガイドで一つずつ説明しており、データの処理方法 のページにはその要約があります。
以下は requests を使った Python での保存なしの呼び出しです。公開サンドボックスキー sk_sandbox_public はドキュメントに掲載されていて、登録は不要です。アドレスごとに合計 10 件まで無料で文書を認識でき、1 時間あたり最大 10 リクエストです。保存なしはご自身のアカウントの設定なので、ライブキーが必要です。公開サンドボックスはアカウントではありません。すべてのスキャンを小さな画像とともにサービス自身のログに記録するので、送るのはテスト画像だけにし、本物の文書は送らないでください。
import base64
import uuid
import requests
API = "https://api.doc.cheap/v1/scans"
KEY = "sk_sandbox_public" # 本番では自分のライブキーを使う
def read_and_forget(path):
with open(path, "rb") as f:
image = base64.b64encode(f.read()).decode("ascii")
response = requests.post(
API,
headers={
"Authorization": f"Bearer {KEY}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"image": image,
# 0: ライブキーなら、このスキャンについて API 側には何も書き込まれない。
# False: この流れでは使わないので、顔写真の切り抜きは不要。
"options": {"retain_hours": 0, "return_portrait": False},
},
timeout=30,
)
response.raise_for_status()
scan = response.json()
del image # 呼び出しが返ったらすぐにローカルのコピーを捨てる
if scan["meta"]["status"] != "recognized":
return None
# 処理に必要な読み取り結果を、自分の保存ルールのもとで保持する。
return {
"scan_id": scan["meta"]["id"],
"document_number": scan["document"]["number"],
"expiry_date": scan["document"]["expiry_date"],
"birth_date": scan["holder"]["birth_date"],
"mrz_status": scan["mrz"]["status"],
}
保存なしには、驚かされる前に知っておくべきコストが 1 つあります。通常、Idempotency-Key があればリトライ時に最初の結果が返されます。retain_hours: 0 では返すべき保存済みの結果がないため、24 時間の間、同じキーでのリトライは二重に応答される代わりに、HTTP 409 とコード idempotency_replay_unavailable で拒否されます。この応答は「最初の呼び出しは通った」という意味だと受け取り、すでに手元にある結果を使ってください。
スキャンを読み直すと、設計のもう一つの面が見えます。サンドボックスキーでは、どの id でも何も読み直せません。私たちは 2026 年 10 月 5 日に、sk_sandbox_public で次のリクエストを送りました。
curl https://api.doc.cheap/v1/scans/<SCAN_ID> \
-H "Authorization: Bearer sk_sandbox_public"
返ってきたのは HTTP 404 でした(メッセージは省略しています)。
{
"error": {
"code": "not_found",
"message": "No scan with id …",
"docs_url": "https://doc.cheap/docs/errors/not_found"
}
}
ライブキーでも、retain_hours: 0 で行ったスキャンや、保持期間が過ぎたスキャンには同じ 404 が返ります。この点でサービスを比較しているなら、パスポート OCR API の比較 が出発点になります。何を返すかだけでなく、画像がどこへ行くのかをそれぞれに尋ねてください。
あなたの仕事として残るもの
「読んで、返して、忘れる」は API 側のコピーをなくします。しかし、あなたの事業が何を保持しなければならないかを決めるものではありません。一部の事業者にとって答えは「コピー」であり、法律がそう定めています。
EU の新しいマネーロンダリング対策法 Reg. (EU) 2024/1624 は 2027 年 7 月 10 日から適用されます。第 90 条は次のように定めています。"It shall apply from 10 July 2027, except in relation to obliged entities referred to in Article 3, points (3)(n) and (o), to which it shall apply from 10 July 2029." つまり 2027 年 7 月 10 日から適用され、第 3 条 (3)(n) と (o) の義務主体についてだけは 2029 年 7 月 10 日から適用されます。記録の保存に関する第 77 条は、銀行やその他の金融事業者などの義務主体に、次のものの保存を求めています。
"a copy of the documents and information obtained in the performance of customer due diligence pursuant to Chapter III, including information obtained through electronic identification means;"
言い換えると、顧客デューデリジェンスの際に取得した文書と情報の写しで、電子的な本人確認手段で取得した情報も含みます。
第 77 条第 3 項は期間を定めています。記録は "retained for a period of 5 years commencing on the date of the termination of the business relationship"、つまり業務関係の終了日から 5 年間保存され、その後 "obliged entities shall delete personal data upon expiry of the five-year period"、つまり 5 年が過ぎたら個人データを削除しなければなりません。第 77 条第 2 項は、一定の条件のもとで、コピーの代わりに "a retention of the references to such information"、つまりその情報への参照だけを保存することを認めています。
つまり義務主体であれば、API 側で保存しないことで記録保存の義務がなくなるわけではありません。変わるのは記録の置き場所です。自分のストアが唯一のコピーになり、上で見た記録保存の制限の原則は引き続きそこに適用されます。業務関係が終わってから 5 年が経てば、それは削除されます。設計の仕事は、上の表のような意図しない山ではなく、1 つの場所、1 人の責任者、暗号化、アクセス制御、削除ジョブを備えた、意図的なストアにすることです。
義務主体でない場合は、まず素朴な問いを立ててください。フィールドを読み取った後に、処理の中で画像を必要とするものはあるでしょうか。正直に答えると「ない」ということがよくあります。
チェックリスト
- 「パスポート画像が行き着く場所」の表の各場所を、自分の流れと照らし合わせて確認した。
- 画像は直接認識に送るか、寿命が数分のバケットを経由する。
- 切り抜き画像はレスポンスのハンドラー内で使い、どこにも書き込まない。
- OCR の呼び出しで保存期間を意図的に設定し、読み直す必要がなければ
retain_hours: 0にする。 - リトライ処理が 409
idempotency_replay_unavailableの応答を扱う。 - ログとエラーレポートは、1 つの境界でリクエストボディを取り除く。
- 保持するフィールドには終了日があり、何かがそれを削除する。
- 法律がコピーを求める場合、それは独自の削除日を持つ 1 つの意図的なストアに置く。
これはエンジニアリングの観点からのまとめであり、法的助言ではありません。この記事が見落としている、画像が漏れうる場所を見つけたら、admin@doc.cheap までお知らせください。
あなたへの質問: 誰も保持するつもりのなかった身分証明書のコピーを、最後に見つけたのはどこでしたか。下のコメント欄で教えてください。