Car Image API

Клиенты · Car Image API

Как встраивать изображения автомобилей в письма с помощью подписанных URL, которые никогда не ломаются

Car Image API отправляет первый рендер каждому клиенту на почту с автоматически продлеваемым подписанным URL: механизм, расчёты и код, чтобы сделать это самостоятельно.

Проблема: изображение, которое должно работать и в марте

Каждый аккаунт в Car Image API примерно через час после первого рендера получает короткое письмо от Джонатана. Самое очевидное, что можно вложить в это письмо, — сам рендер: тот самый Porsche 911 2024 года или Dodge Charger 2006 года, который человек только что запросил, как доказательство того, что интеграция работает, и как маленький повод похвастаться.

Электронное письмо — худшее из возможных мест для подписанного URL. Страница заново отрисовывается при каждом посещении; письмо отрисовывается один раз и открывается, когда до него дойдут руки у читателя, — через день, месяц или год, часто через прокси изображений Gmail. Подписанные URL доставки, которые API уже предлагал, имели максимальный TTL в семь дней — разумный предел для браузера, но гарантия битого изображения во входящих уже на второй неделе. Вложение PNG или встраивание его в письмо раздувает каждое сообщение.

Нам был нужен URL, который ведёт себя как размещённое изображение, работает столько, сколько кто-либо может открыть письмо, и при этом честно списывает со счёта только за то, что действительно отдаёт.

Решение: автоматически обновляемые подписанные URL

Мы добавили в POST /api/v1/image-urls один флаг: renew: true. Обновляемый URL создаётся точно так же, как любой другой, учитывает своё изображение как изображение автомобиля за месяц и действует в течение ttl_seconds. Разница в том, что происходит после истечения TTL. Вместо ответа 403 первая загрузка в каждом новом окне длиной ttl_seconds обновляет URL и отдаёт изображение; при обновлении изображение учитывается один раз в том месяце, на который оно приходится, и больше ничего не списывается, если в этом месяце оно уже учтено. Все последующие загрузки в этом окне допускают публичное кэширование, так же как в первом окне. Так продолжается до renews_until — не более чем на год вперёд.

Из такой архитектуры вытекли три свойства, и каждое оказалось важным для писем:

  • Ничего не нужно пересылать или выпускать заново. URL в письме остаётся неизменным навсегда. Никаких cron-задач, переписывающих сохранённые ссылки, никаких повторных писем со свежим изображением.
  • Считаются открытия, а не время. Окно, которое никто не открыл, не учитывается. Письмо, прочитанное одним человеком в один день, — это одно изображение автомобиля, для которого оно создано; письмо, открываемое каждую неделю в течение года, учитывает это изображение один раз в каждом месяце открытия — максимум двенадцать раз.
  • Идемпотентный учёт. Каждое окно записывается один раз с ключом 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, почему он продолжает работать, точный 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 за 49 $ в месяц включает 2 500 изображений автомобилей в месяц.
Что происходит, когда изображения автомобилей за месяц закончились?
Загрузка возвращает 402, и изображение отображается как битое, пока лимит месяца не обновится или тариф не будет повышен; сам URL остаётся действительным и снова заработает при следующей загрузке. Изображение, уже учтённое в этом месяце, продолжает отдаваться.
Работает ли это в Gmail, Outlook и Apple Mail?
Да. Эндпоинт доставки — это обычный публичный GET с заголовками Cache-Control, ETag и CORS, именно то, чего ожидают прокси изображений. Прозрачность PNG отображается во всех современных почтовых клиентах; для очень старых версий Outlook запрашивайте format jpg на сплошном фоне.
Можно ли отозвать URL?
Отзовите API-ключ, с помощью которого он создан, и каждый URL этого ключа при следующей загрузке ответит 403, включая продлённые. Общие кеши после этого продолжают отдавать изображение не дольше часа.