Car Image API

お客様事例 · Car Image API

リンク切れしない署名付きURLでメールに車の画像を埋め込む方法

Car Image APIは、すべてのお客様の最初のレンダーを自動更新される署名付きURLで受信トレイに届けています。その仕組み、クレジットの計算、そしてご自身で実装するためのコードを紹介します。

課題:3月に開いても表示される画像

Car Image APIのすべてのアカウントには、最初のレンダーから約1時間後にJonathanから短いメッセージが届きます。そのメールに載せるものとして真っ先に思い浮かぶのは、レンダーそのものです。ユーザーがリクエストしたばかりの2024年式 Porsche 911や2006年式 Dodge Chargerを、連携がうまくいった証拠として、そしてちょっとした自慢として見せるのです。

署名付き URL にとって、メールは最も相性の悪い掲載先です。Web ページは訪問のたびに再レンダリングされますが、メールは一度だけレンダリングされ、読者が開くのは翌日かもしれず、1か月後や1年後かもしれません。しかも多くの場合 Gmail の画像プロキシを経由します。API が従来提供していた署名付き配信 URL の TTL は最大7日間でした。ブラウザには適切な上限ですが、受信トレイでは2週目には確実に画像が表示されなくなります。PNG を添付やインライン埋め込みにすると、すべてのメッセージが肥大化します。

私たちが求めていたのは、ホスティングされた画像のように振る舞い、誰かがメールを開く可能性がある限り機能し続け、しかも実際に配信した分だけアカウントに公正に課金される URL でした。

解決策:自動更新される署名付きURL

POST /api/v1/image-urlsにフラグを1つ追加しました。renew: trueです。更新可能なURLは他のURLとまったく同じように発行・課金され、1クレジットでttl_secondsの間有効です。違いはTTL経過後の挙動にあります。403を返す代わりに、ttl_secondsごとの新しい期間における最初の読み込みで発行元アカウントに1クレジットが再課金され、画像が配信されます。その期間内のそれ以降の読み込みは無料で、最初の期間と同様に公開キャッシュが可能です。これは最長1年先のrenews_untilまで続きます。

この設計から3つの特性が生まれ、そのどれもがメールにとって重要でした。

  • 再送も再発行も不要。メール内のURLはずっと同じURLのままです。保存済みリンクを書き換えるcronジョブも、新しい画像を載せた2通目のメールも必要ありません。
  • 時間ではなく開封に対して支払い。誰にも開かれない期間には費用はかかりません。1 人が 1 日だけ読んだメールは、発行時の 1 クレジットのみです。1 年間毎週開かれるメールでも約 52 クレジット、5 セントです。
  • 冪等な課金。各期間はrenew:<url id>:<window>をキーとする台帳の行で、URLと期間ごとに一意です。1週間に50回開封されても、最初の開封時に2つのメールプロキシが競合しても、課金は1回だけです。

ライフサイクルメールは、公開エンドポイントと同じコードパスでこれらのURLをプラットフォームトークンとして発行します。当社自身のものなのでAPIキーに紐づかず課金もされませんが、配信は同一のGET /api/v1/delivery/{token}ルートで行われ、7日間の期間もキャッシュも同一です。初回レンダリングメールの画像は、製品自身によるデモンストレーションなのです。

顧客の最初のレンダリング用に更新可能な URL を 1 つ発行する
curl -X POST https://carimage.dev/api/v1/image-urls \
  -H "Authorization: Bearer $CAR_IMAGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"images":[{"make":"porsche","model":"911","year":2024,"view":"front-3-4","color":"red","width":480}],"ttl_seconds":604800,"renew":true}'
201 Created
{
  "data": [
    {
      "id": "3f0c2a7e-1c1e-4c2a-9c39-4f1f0a4b2d10",
      "url": "https://carimage.dev/api/v1/delivery/eyJhbGciOi…",
      "expires_at": "2026-09-21T09:00:00.000Z",
      "max_uses": 0,
      "renews_until": "2027-09-14T09:00:00.000Z",
      "vehicle": {
        "make": "porsche",
        "model": "911",
        "year": 2024,
        "view": "front-3-4",
        "color": "red",
        "width": 480,
        "format": "png"
      }
    }
  ],
  "billing": {
    "charged_on": "creation",
    "credits_charged": 1,
    "credits_remaining": 4869,
    "credits_per_url": 1,
    "renewal": {
      "window_seconds": 604800,
      "credits_per_window": 1,
      "until": "2027-09-14T09:00:00.000Z"
    }
  },
  "request_id": "req_01j9x…"
}

メール本体

テンプレートはインラインスタイルを使った React Email です。メールクライアントはスタイルシートを削除してしまうためです。レンダリング画像は淡い背景の上の枠付きテーブルセルに配置し、Retinaディスプレイでも鮮明に表示されるよう 480 ピクセルでリクエストして 240 ピクセルで表示します。上にはピル型のラベル、下には2行のキャプションを添えています。透過PNGこそがこの製品の核心なので、これは維持します。アルファチャンネルを黒く描画する古いOutlookでは、単色のセル背景に format: "jpg" のレンダリングを置くのがフォールバックです。

カード(プレーンHTML)
<!-- inline styles only: mail clients strip stylesheets -->
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
  <tr>
    <td align="center" style="padding:24px;background:#fafaf9;border:1px solid #e7e5e4;border-radius:16px;">
      <img
        src="https://carimage.dev/api/v1/delivery/eyJhbGciOi…"
        alt="2024 Porsche 911, red, front three-quarter view"
        width="240" height="240"
        style="display:block;margin:0 auto;max-width:100%;height:auto;"
      />
      <p style="margin:14px 0 2px;font:700 18px/26px -apple-system,Segoe UI,Roboto,sans-serif;color:#1c1917;">2024 Porsche 911</p>
      <p style="margin:0;font:13px/20px ui-monospace,Menlo,monospace;color:#78716c;">Front three-quarter · Red</p>
    </td>
  </tr>
</table>

メッセージのそれ以外の部分はすべてテキストです。そのURLが何なのか、なぜ使い続けられるのか、ご自身の車両用にURLを発行するための正確なPOSTリクエスト、そしてこのページへのリンクです。メールには配信停止リンクとList-Unsubscribeヘッダーが含まれ、すべての製品リンクにはタグが付いているため、今お読みのこの事例が誰かを呼び戻したかどうかを獲得レポートで確認できます。

3ステップでご自身で実装する

  1. メール作成時にサーバー側で発行します。車両1台につき1回の呼び出し、ダイジェストなら1回の呼び出しで最大50台まで発行できます。url と renews_until をメッセージと一緒に保存してください。
  2. URLを、幅・高さ・代替テキストを指定した<img>に入れます。代替テキストには「車」ではなく車両名を記載します(例:「2024 Porsche 911、赤、フロント3/4ビュー」)。
  3. 残高を維持する。残高ゼロの状態で更新すると402が返され、クレジットが入金されるまで画像は表示されないままです。ダッシュボードで自動チャージを有効にすれば、この障害は起こりません。
TypeScript SDK を使う場合
import { CarImageClient } from "@meterapp/car-image-sdk";

const client = new CarImageClient({ apiKey: process.env.CAR_IMAGE_API_KEY });

// One call per email: the vehicle the person rendered, at 2x the display width.
const { data } = await client.createImageUrls(
  [{ make: "porsche", model: "911", year: 2024, view: "front-3-4", color: "red", width: 480 }],
  { ttlSeconds: 604_800, renew: true, renewDays: 365 }
);
const src = data[0].url; // drop into <img src>, store data[0].renews_until next to it

パラメーターの完全なリファレンス、キャッシュヘッダー、エラーコードは署名付きURLのドキュメントに掲載しています。プラグインに同梱されたエージェントスキルも同じルールを把握しているため、「この車をニュースレターに入れて」と頼まれたアシスタントは、テンプレートにキーを貼り付けるのではなく、更新可能なURLを発行します。

よくある質問

画像を添付したり、base64でインライン埋め込みしたりしないのはなぜですか?
添付ファイルやbase64はメッセージを肥大化させます。署名付きURLはわずか数百バイトで、他のホスト画像と同様にメールクライアントの画像プロキシ経由で読み込まれます。
自動更新されるURLの料金はいくらですか?
作成時に1クレジット、その後は renews_until まで、実際に開封された ttl_seconds の期間ごとに1クレジットです。TTL が7日間なら、画像1枚につき最大で週1クレジット、誰もメールを開かない週は0クレジットです。$1 で 100 クレジットを購入できます。
アカウントのクレジットがなくなるとどうなりますか?
読み込み時に 402 Insufficient credits が返され、残高がチャージされるまで画像はリンク切れとして表示されます。URL 自体は有効なままで、次回の読み込みから再び表示されます。ダッシュボードで自動チャージを有効にしておけば、本番アカウントがこの問題に直面することはありません。
Gmail、Outlook、Apple Mail で動作しますか?
はい。配信エンドポイントはCache-Control、ETag、CORSヘッダーを備えたシンプルな公開GETで、まさに画像プロキシが想定する形式です。PNGの透過は現行のすべてのメールクライアントで表示されます。非常に古いOutlookに対応する場合は、単色背景でjpg形式をリクエストしてください。
URL を無効化できますか?
発行元のAPIキーを無効化すると、そのキーから発行されたすべてのURLは、更新分も含めて次回の読み込み時に403を返します。共有キャッシュはその後も最大1時間配信を続けます。