課題: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日間の期間もキャッシュも同一です。初回レンダリングメールの画像は、製品自身によるデモンストレーションなのです。
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}'{
"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…"
}ttl_seconds: 604800(最大値の7日間)にすると、コストの上限は画像1枚につき週1クレジットになります。1時間のTTLに renew: true を組み合わせることもできますが、頻繁に開かれる受信トレイでは、開かれている限り1時間ごとに再課金されます。メール本体
テンプレートはインラインスタイルを使った React Email です。メールクライアントはスタイルシートを削除してしまうためです。レンダリング画像は淡い背景の上の枠付きテーブルセルに配置し、Retinaディスプレイでも鮮明に表示されるよう 480 ピクセルでリクエストして 240 ピクセルで表示します。上にはピル型のラベル、下には2行のキャプションを添えています。透過PNGこそがこの製品の核心なので、これは維持します。アルファチャンネルを黒く描画する古いOutlookでは、単色のセル背景に format: "jpg" のレンダリングを置くのがフォールバックです。
<!-- 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回の呼び出しで最大50台まで発行できます。
urlとrenews_untilをメッセージと一緒に保存してください。 - URLを、幅・高さ・代替テキストを指定した
<img>に入れます。代替テキストには「車」ではなく車両名を記載します(例:「2024 Porsche 911、赤、フロント3/4ビュー」)。 - 残高を維持する。残高ゼロの状態で更新すると402が返され、クレジットが入金されるまで画像は表示されないままです。ダッシュボードで自動チャージを有効にすれば、この障害は起こりません。
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時間配信を続けます。