Car Image API

客户 · Car Image API

如何使用永不失效的签名 URL 在邮件中嵌入汽车图片

Car Image API 通过可自动续期的签名 URL,将每位客户的首张渲染图发送到其收件箱:本文介绍其机制、计算方式,以及自行实现所需的代码。

问题:一张到三月份仍须可用的图片

Car Image API 上的每个账户在首次渲染约一小时后,都会收到 Jonathan 发来的一封简短邮件。这封邮件里最顺理成章的内容就是渲染图本身:正是对方刚刚请求的那辆 2024 Porsche 911 或 2006 Dodge Charger,既证明集成已成功运行,也算小小地炫耀一下。

电子邮件是签名 URL 最糟糕的使用场景。网页每次访问都会重新渲染;而邮件只渲染一次,读者可能在一天、一个月甚至一年之后才打开,且常常经由 Gmail 的图片代理。API 此前提供的签名分发 URL 最长 TTL 为七天,这对浏览器而言是合理的上限,但放在收件箱里,到第二周图片必然失效。而将 PNG 作为附件或内嵌则会让每封邮件都变得臃肿。

我们想要的是这样一个 URL:表现得像托管图片一样,只要有人可能打开这封邮件就一直有效,同时仍按实际提供的内容向账户公平计费。

解决方案:自动续期的签名 URL

我们为 POST /api/v1/image-urls 新增了一个参数:renew: true。可续期 URL 的生成方式与其他 URL 完全相同,其图片计为当月的一张车辆图片,有效期为 ttl_seconds。区别在于 TTL 到期之后:不再返回 403,而是在每个新的 ttl_seconds 时间窗口内的首次加载时续期该 URL 并提供图片;续期会在其所在月份计一次该图片,如果该图片当月已计入,则不再额外计数。该窗口内的后续加载均可公开缓存,与第一个窗口相同。这一过程持续到 renews_until,最长可达一年。

这一设计自然带来了三个特性,每一个对邮件都至关重要:

  • 无需重新发送或重新生成。邮件中的 URL 永久有效。无需定时任务改写已存储的链接,也无需再发一封带新图片的邮件。
  • 按打开次数计,而非按时间计。无人打开的时间窗口不计入任何用量。一封仅由一人在某一天阅读的邮件,只计为当初为其生成的那一张车辆图片;一封一年内每周都被打开的邮件,在每个有打开的月份中各计一次该图片,最多计十二次。
  • 幂等计数。每个时间窗口只记录一次,以 renew:<url id>:<window> 为键,对每个 URL 和窗口唯一。一周内打开五十次,或两个邮件代理在首次打开时同时请求,都只计一次。

生命周期邮件通过与公共端点相同的代码路径生成这些 URL,作为平台令牌:它们不绑定任何 API 密钥,也从不计费,因为它们属于我们自己,但由完全相同的 GET /api/v1/delivery/{token} 路由提供服务,具有完全相同的七天有效期和完全相同的缓存机制。首次渲染邮件中的图片,正是产品在展示自己。

为客户的首个渲染图生成一个可续期的 URL
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 并使用内联样式,因为邮件客户端会剥离样式表。渲染图放在浅色背景上带边框的表格单元格中,以 480 像素请求、以 240 像素显示,以便在 Retina 屏幕上保持清晰;图片上方有一个胶囊标签,下方有两行说明文字。透明 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 标头,每个产品链接都带有追踪标记,以便我们在获客报告中看到您正在阅读的这篇案例是否带回了用户。

三步自行实现

  1. 在构建邮件时于服务端生成。每辆车调用一次;若为摘要邮件,每次调用最多可生成五十个。将 url 和 renews_until 与邮件一同存储。
  2. 将 URL 放入带有宽度、高度和替代文本的 <img> 中。替代文本应写明具体车辆(“2024 Porsche 911,红色,前侧四分之三视角”),而不是“汽车”。
  3. 确保账户额度充足。续期会在其发生的月份将图片计入一次;如果套餐的车辆图片已用完,则会返回 402,直到每月 1 日配额重置,因此请根据您提供的图片数量选择合适的套餐。
使用 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 费用是多少?
创建时计为当月的一张车辆图片,之后在每个实际打开续期窗口的月份将同一图片计一次,直至 renews_until;无人打开邮件的月份不计数。Pro 每月 US$49,每月包含 2,500 张车辆图片。
本月车辆图片用完后会怎样?
加载会返回 402,图片将显示为损坏,直到当月额度重置或套餐升级;URL 本身仍然有效,并会在下次加载时恢复。本月已计数的图片会继续正常提供。
这在 Gmail、Outlook 和 Apple Mail 中都能正常显示吗?
可以。交付端点是普通的公开 GET 请求,带有 Cache-Control、ETag 和 CORS 标头,这正是图片代理所需要的。所有当前的邮件客户端都能正确显示 PNG 透明背景;对于非常旧的 Outlook 版本,请以纯色背景请求 jpg 格式。
URL 可以撤销吗?
吊销生成该 URL 的 API 密钥后,该密钥生成的所有 URL 在下次加载时都会返回 403,续期的 URL 也不例外。此后共享缓存最多还会继续提供一小时。