APIの失敗、クレジットの返還、照合:リクエストの正味課金額を監査する方法
タスク、呼び出し試行、配信状態、ウォレットイベントを分けて扱い、課金前の拒否、空の結果、クレジットの部分返金を二重計上せずに処理する方法を、整数を用いたオフライン例で解説します。
「失敗時は課金しない」を実効性のある製品上の確約とするには、2つの点を明確にする必要があります。何を失敗とみなすか、純課金が発生していないことをどう証明するかです。HTTPステータスコードだけに頼ると、空の結果や部分的な配信を見逃します。一方、残高の変化だけを確認すると、同時実行中のリクエスト、入金、調整が混在します。確実な照合には、個々のリクエストとその関連イベントをあわせて追跡する必要があります。
このノートでは、APIクライアントが照合記録をどのように整理するかを説明し、現在のEveryInfraのコードベースをもとに紛らわしいフィールドを解説します。ここで示すオフライン台帳はアプリケーション設計の例であり、顧客向けの請求明細や会計上の収益認識ルールではありません。個々のアクションの課金条件、通貨単位、最終的な明細は、該当する製品の課金ルールで確認してください。
最初に4つのオブジェクトを分ける
- 業務タスク:承認済みの商品レビュー一式を確認するなど、ユーザーが実際に達成したい目的です。1つのタスクに複数のAPI呼び出しが含まれることがあります。
- 呼び出し試行:実際の1回のリクエストです。タイムスタンプ、パラメータのバージョン、モデルまたはアクション、取得できる場合はリクエストIDを個別に保存します。
- 配信結果:成功、空の結果、部分的な配信、失敗、未確定のいずれかです。これはウォレットの状態ではなく、業務上の状態です。
- ウォレットイベント:試行に関連する課金、返金、その他の明示的な調整で、純変動額の説明に使います。
4つのオブジェクトを1つの成功フィールドに押し込まず、相互に関連付けてください。リクエストが目的の一部を達成した場合、業務側では配信済みのコンテンツを保持しつつ、未完了分の課金を引き続き監査できます。タイムアウトでは配信も精算も未確定のまま残る場合があるため、安易にコストゼロとして記録してはいけません。
業務タスクID、リクエストID、非同期のjob_id、課金参照もそれぞれ別の概念です。サービスが実際に提供する関連付けを保存し、似た文字列から存在しない課金IDを合成しないでください。タスクの照会時に生成されるHTTPリクエストIDで、最初のタスク送信時の関連付け識別子を上書きしてはいけません。
課金前の拒否と課金後の返金:処理経路の違い
2026-09-04時点でデプロイされているソースを基準とすると、EveryInfraのテキストリクエストにおける認証情報、入力、モデル、機能の権限のチェックはすべて課金前に行われます。messagesがない場合は認証より前に入力エラーが返ることがあるため、ここではすべてのエラーに共通する優先順位は定めません。messagesが有効で認証チェックを通過する経路では、未知のモデルは課金発生前に422 unknown_modelを返します。こうした失敗は実行の拒否であり、いったん課金してから返金する処理は発生しません。未知のモデルで503が返るという従来の説明は今後使わないでください。
実行フェーズに入った後で失敗したリクエストでは、課金額の一部を返金する必要が生じる場合があります。実装に返金処理の分岐が存在しても、クライアントがその完了を確認済みとは限りません。レスポンスが失われた場合は、まずリクエスト識別子を使って参照可能な記録を照会してください。証拠がない場合は、エラーテキストだけを根拠に財務記録を変更せず、「精算は監査待ち」の状態を保持してください。
2026-09-02のアーカイブには、失敗後の返金について機密情報を除去したサンプルのほか、空の結果やバッチの部分配信のサンプルが含まれています。これらは、こうしたシナリオをそれぞれ個別に受け入れ確認すべきことを示すものであり、現時点であらゆる機能や失敗の種類を一律に保証するものではありません。今回の対象はソースコードと公開ドキュメントのみで、実課金、請求に対する返金、アカウント操作は行っていません。
加算方法を決める前のフィールド定義の確認
同期データレスポンスを例にとると、billing.chargedはレスポンスにおける課金ステータス、billing.creditsはレスポンスが報告する課金単位数を表し、billing.amountは金額としての意味が必要な表示に使います。クォータの残量はアカウントのスナップショットであり、1回のリクエストのコストとしては扱えません。カタログの定価で実際のリクエスト記録を置き換えることもできません。
計算ミスの原因で最も多いのが部分返金です。現在のデータ実装では、元の料金から計算済みの返金額を差し引いた残りの料金をbilling.creditsに書き込み、それとは別にrefunded_creditsを返します。そのため、billing.creditsからrefunded_creditsをさらに差し引いてはいけません。差し引くと同じ返金が二重に控除されます。この結論はここで検証した同期データの分岐に限られ、すべての製品に自動的に当てはまるわけではありません。
完全に架空の例として、元の課金が30単位で返金が10単位の場合、純課金額は20になります。最終レスポンスでcreditsが20、refunded_creditsが10と報告されていれば、20はすでにそのレスポンスが示す純額です。元のイベント記録を監査する場合は、代わりに30から10を引いてください。20から10を引いて10とするのは誤りです。
同じリクエストに対する各ポーリング試行では、同じステータスが重複して返ることがあります。すべてのレスポンスのcreditsを合計しないでください。レスポンスのスナップショット、課金イベント、精算調整はデータベースに分けて保存し、まずそれぞれに固有で変わらない識別子を使って重複を排除してから集計してください。HTTPログに新しいエントリが記録されても、新たな利用が発生したとは限りません。
アクションごとの空の結果と部分配信の解釈
検索結果が空の場合は、そのツールの有効な配信が他のフィールドに含まれていないかを確認してください。現在の一般検索の実装はresultsとanswerの両方をチェックします。空のresultsだけを見ていると、answerを含むレスポンスを完全に空だと誤判定するおそれがあります。クライアントはツールのドキュメントに記載されたレスポンスに従い、財務レイヤーでコンテンツの価値を推測しないようにしてください。
バッチのデータリクエストでは、対象数と実際の配信数を突き合わせて監査する必要があります。現在の部分返金の分岐ではtotal_targets、billed_targets、refunded_creditsが返され、業務上の結果にはpartialやmissedが含まれることもあります。これらのフィールドは差分の説明に役立ちますが、具体的な値は実際のレスポンスから取得する必要があります。コメント数を成功した対象数として扱うことはできません。
未完了の対象を再処理する前に、配信済みのオブジェクトと元のリクエストを保持し、どの対象が未確定のままかを明確にしてください。バッチ全体を再送すると、既存のコンテンツが重複したり、新たな呼び出し試行が発生したりする場合があります。クライアント側での結果の重複排除と、サーバー側での二重課金の防止は別の処理です。
非同期受付と最終精算の区別
HTTP 202とjob_idは、タスクが処理パイプラインに入ったことを示すもので、ユーザーが結果を取得したことを意味しません。元のタスクを保持し、その最終ステータスを照会してください。タスクの照会が成功しても、照会そのものが完了したことを示すだけなので、タスクがまだ実行中か確定待ちかを確認する必要があります。接続が切れても、キャンセルが成功したと自動的に解釈することはできません。
現在のデータタスクの照会では、主にタスクのステータス、結果、または失敗情報が返され、毎回完全な課金スナップショットが返されるわけではありません。課金フィールドがない場合は、アクセス権限のある課金ポータルで確認してください。ゼロで埋めたり、作成時と完了時のスナップショットを別々の新しい課金として扱ったりしないでください。このノートは、すべての非同期機能が同じ精算タイミングを使うとは主張していません。
単一リクエストのコスト監査には、浮動小数点由来の小数ではなく整数を使用
照合では、まずウォレット、計測単位、リクエストの範囲を確定させる必要があります。複数のウォレット、異なる通貨、表示用の為替レートにまたがる金額をそのまま合計しないでください。インターフェースが整数のクレジットを使う場合は、その元の単位での監査を優先します。境界では安全な整数であることを検証し、JavaScriptで正確に扱える整数の範囲を超える値には明示的な整数表現を使ってください。
以下はカスタムのイベント構造の例です。各エントリは確定済みの同一のリクエスト範囲に属し、creditsには正の整数を表す文字列を使い、kindで課金か返金かを指定します。実際の課金記録の読み取り、データベースへの接続、残高の変更は行いません。イベントが漏れなく揃っていることと課金に関する権限は、呼び出し側が事前に確認すべき前提条件です。
function reconcileAttempt(entries) {
const seen = new Map();
let debited = 0n, refunded = 0n;
for (const e of entries) {
if (typeof e.id !== "string" || !e.id.trim()
|| !["debit", "refund"].includes(e.kind)
|| typeof e.credits !== "string" || !/^[1-9][0-9]*$/.test(e.credits)) {
throw new Error("invalid synthetic ledger entry");
}
const fingerprint = JSON.stringify([e.kind, e.credits]);
if (seen.has(e.id)) {
if (seen.get(e.id) !== fingerprint) throw new Error("conflicting entry");
continue; // duplicate snapshots of the same event are not counted again.
}
seen.set(e.id, fingerprint);
if (e.kind === "debit") debited += BigInt(e.credits);
else refunded += BigInt(e.credits);
}
if (refunded > debited) throw new Error("check scope or missing debit evidence");
return {
debited: debited.toString(), refunded: refunded.toString(),
netCharged: (debited - refunded).toString()
};
}
console.log(reconcileAttempt([
{id: "synthetic-debit", kind: "debit", credits: "30"},
{id: "synthetic-refund", kind: "refund", credits: "10"},
{id: "synthetic-refund", kind: "refund", credits: "10"}
]));結果は課金30、返金10、純課金額20となり、重複して現れた返金イベントは再度カウントされません。同じイベントIDに異なる内容が紐付いている場合、この例では黙って上書きせずに処理を停止します。取得した課金額を返金額が上回る場合も監査を停止します。これは範囲や時間枠が不完全なことに起因する可能性があり、サービス側が過剰に返金したと自動的に判断すべきではありません。
これは完全なウォレットのアルゴリズムではありません。入金、付与、出金、凍結、取り消し、その他の調整はこの例ではモデル化していません。実際のシステムでは、それぞれのイベント定義に従って処理する必要があります。空のリストから計算された0は入力イベントがなかったことを示すだけで、不明なリクエストに課金が発生していないことの証明にはなりません。
JavaScriptのNumberは任意精度の整数ではありません。RFC 8259にあるJSON数値の相互運用範囲に関する記述を踏まえ、ここでは整数を文字列のまま保持してから、明示的にBigIntへ変換しています。これは計算上の表現誤差を減らすだけで、欠落した課金イベントを補うことはできません。
時間枠別監査における期首イベントと期間をまたぐイベントの保持
リクエストはある日に開始して翌日に完了することがあり、返金や取り消しのイベントが元の課金より後に計上されることもあります。リクエストの作成時刻で絞り込むか、ウォレットイベントの発生時刻で絞り込むかによって、得られる集合は異なります。レポートではどちらの時刻を基準にしているかを明記し、期首・期末および期間をまたぐ関連付けを保持してください。そうすることで、前日の課金が見えないまま返金だけを見て、誤った異常アラートを出すことを避けられます。
残高の等式を成り立たせるには、ここで扱った2種類のイベントだけでなく、そのウォレットの期間内のすべての借方と貸方を考慮する必要があります。現在の残高と特定の呼び出し前の残高との差には、同時に実行されたタスクやその他の調整が含まれることもあります。この差はクロスチェックには適していますが、単一リクエストのコストを単独で証明するものではありません。
ウォレットの消費、顧客の現金入金、プロモーションクレジットの使用、会計上の収益は、それぞれ定義が異なります。このノートで扱うのはリクエストとウォレット上の根拠との照合のみで、収益認識や税務に関する結論は示しません。外部向けの財務指標は、プロダクト部門と財務部門の担当者が定義を明確にしてから公開してください。
Stripeのべき等リクエストに関するドキュメントは、比較の参考になります。安全にリトライするには、サービス側が対応するべき等性の仕組みを明示的にサポートしている必要があります。ただし、これはEveryInfraが同じべき等キーを受け付けることを証明するものではありません。本記事の例は、取得したレコードをイベントIDで重複排除するもので、サーバー側で重複課金を防ぐものではありません。この2種類の重複排除を混同しないでください。
差異はレビューキューに振り分け、台帳は自動で変更しない
- 課金記録があるのに紐付けられる配信結果がない場合:非同期処理が未完了か、結果が保存されていないか、関連付けが失われていないかを確認します。
- 課金ステータスが不明な失敗結果:元のリクエストと後続のイベントを監査し、自己判断で返金済みまたは取り消し済みとマークしないでください。
- 同じイベントに異なる内容が紐付いている場合:元の各バージョンとその取得時刻を保存し、黙って上書きする処理を停止します。
- 返金イベントが繰り返し表示される場合:同じイベントが複数回読み込まれていないかを確認し、重複してカウントしないでください。
- 通貨単位や時間枠が一致しない場合:比較範囲を揃えてから差異を評価します。
サポートに問い合わせる際は、必要なリクエストID、タイムスタンプ、機能、ステータス、マスキングした課金参照情報のみを提供してください。APIキー全体、顧客の本文、アカウント全体のエクスポートは送らないでください。差異の切り分けと台帳の変更には別々の権限が必要です。自動生成された異常フラグは、取り消しやその他の会計調整を承認するものではありません。
監査可能なリクエストでは、何を提供したか、どのコスト基準が適用されたか、返金が純額に反映されているか、どのイベントが未確認のままかを説明できる必要があります。これらの関係を明確にすることで、「失敗時は最終的に課金されない」がスローガンにとどまらなくなり、クライアント側で返金を二重に差し引いて新たな課金差異を生むことを防げます。
参考資料 LF-14