役割と範囲
自社 ERP(NetSuite)を持つ EC 企業が SaaS の配送ツールで出荷しており、それに伴うコストと、キャリア契約に対する限られたコントロールを抱えていました。CarrierAPI はそのセルフホスト型の置き換えです。ラベル作成、料金比較、荷物追跡、複数キャリアにまたがるバッチ処理を1つの API で担い、旧ツールと同じ接続点を持たせたことで、NetSuite 側から見た移行は作り直しではなくドロップイン置換になりました。
- 配送業者ごとのアダプタ構成(コアは PostNL、DHL Express、UPS。追加アダプタとして DHL Parcel、FedEx、GLS、そして SendCloud 自体)。新しい業者の追加でシステムの他の部分に手を入れる必要はありません。
- NetSuite 内での旧 SaaS ツールのドロップイン置換:カスタムフィールド名は同一、既存の SuiteScript 連携への変更は最小限。
- NetSuite の Item Fulfillment レコードへの直接連携に加え、配送業者と並んで Shopify 同期と Amazon 連携も提供。
- キャリア認証情報の管理方法は2通り:UI 経由(データベース保存、推奨)または環境変数経由(既存デプロイ向けのレガシー経路)。
成果
- 配送コストとキャリア契約を自社の手に取り戻し、SaaS の中間レイヤーの料金や条件に縛られない状態に。
- これまで誤って解析されていた一群の住所について、住所認識を修正(番地の抽出が、通り名の後の最初の数字ではなく最後の数字を誤って拾っていた——番地とシャレー番号の両方を持つホリデーパークの住所などで誤りが出ていました)。
- UPS が請求書のエクスポート形式を2度変更した後も、請求突合は動作し続けました。列の判定を固定位置ではなく内容ベースにし、請求書自身が示す合計金額と突き合わせているためです。
- 削除済みユーザーが最大24時間アクセスできてしまうセッションの穴を塞ぎました。セッショントークンの署名と有効期限しか検証しておらず、アカウントがまだ存在するかを見ていなかったためです。
アーキテクチャ
各配送業者は共通インターフェース(出荷の作成・キャンセル、料金照会、追跡、利用可能サービス)の背後にあるアダプタとして実装されており、新しい業者を追加してもシステムの他の部分には影響しません。バックエンドは FastAPI(Python、非同期)で、主データベースは PostgreSQL、ラベルのバッチ処理には Redis ベースのバックグラウンドワーカー(arq)を使用。数百枚のラベルを一度に作成しても API はブロックされません。ラベル自体は MinIO(S3 互換)に保存し、ストレージを特定のクラウドに縛られないようにしています。フロントエンドは出荷、キャリア設定、認証情報を管理する Next.js/React のダッシュボード。全体は Traefik の背後で Docker Compose によりセルフホストされ、Coolify でデプロイしています。
モデル選定
AI/LLM は使っていません。これは意図的に決定論的なシステムです——配送ラベル、料金、追跡は生成されるものではなく、予測可能で検証可能でなければなりません。ここでの要となる判断はセルフホストか SaaS かでした。SaaS の配送ツールは立ち上げが速い一方、セルフホストのプラットフォームはキャリア契約のコントロールをもたらし、中間レイヤーへのラベル単位・出荷単位の手数料をなくし、機能(たとえば NetSuite との直接連携)を SaaS ベンダーの対応可否を待たずに必要な形で作れます。
コストとレイテンシ
- 出荷ごとに継続的に発生する SaaS 手数料ではなく、セルフホストのインフラ(Docker Compose、PostgreSQL、Redis、MinIO)。
- バッチのラベル生成は Redis キュー(arq)で非同期に実行されるため、数百枚を一度に作成しても依頼者を待たせず、API の応答性を保てます。
- 請求突合は実際の UPS 請求書で検証済み:1,275 行・4,855.72 ユーロの請求書や 2,275 行のエクスポートを含み、列判定の修正後はいずれも正しく処理されました。
学んだこと
オランダの住所解析は特定の住所群で誤作動していました(「Hessenweg 83 41」のように、番地とは別にシャレー番号を持つホリデーパークの住所)。パーサーが通り名の後の最初の数字ではなく、行内の最後の数字を番地として読んでいたためです。共通パーサーを導入し、実際の事例で検証して解決しました。UPS の請求書取り込みは、UPS がエクスポート形式を変更した(列数と列順がずれた)ことで2度壊れました。固定の列位置に依存する方式をやめ、内容ベースの列判定に置き換え、その結果を請求書自身が示す合計金額と突き合わせています。住所検証の警告は当初、出荷の作成そのものを完全にブロックしていました。これは自動でのラベル購入のみをブロックする挙動に戻し、フラグの立った出荷が止まりきるのではなく、可視のまま再試行できるようにしました。さらにセッションの脆弱性:削除済みユーザーが最大24時間アクセスを保持していた問題は、トークンを署名と有効期限だけで検証し、アカウントの存在を確認していなかったことが原因で、リクエストごとにアカウントの存在を検証するよう修正しました。