Проблема: изображение, которое должно работать и в марте
Каждый аккаунт в 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}, с теми же семидневными окнами и тем же кешированием. Изображение в письме о первом рендере — это продукт, демонстрирующий сам себя.
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 (семь дней, максимум) означает не более одного обновления в неделю на изображение, а изображение учитывается не более одного раза в месяц, как бы часто оно ни обновлялось. TTL в один час с renew: true тоже работает, но в активно используемом почтовом ящике URL будет обновляться каждый час, когда письмо открывают.Само письмо
Шаблон написан на React Email со встроенными стилями, потому что почтовые клиенты удаляют таблицы стилей. Рендер размещён в ячейке таблицы с рамкой на мягком фоне: он запрашивается с размером 480 пикселей и показывается в 240, чтобы оставаться чётким на Retina-экранах; над ним — плашка, под ним — две строки подписи. Прозрачный 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, почему он продолжает работать, точный POST-запрос, чтобы получить такой же для своего автомобиля, и ссылка на эту страницу. В письме есть ссылка для отписки и заголовки List-Unsubscribe, а каждая ссылка на продукт размечена, чтобы в отчёте о привлечении мы видели, вернула ли кого-нибудь история, которую вы читаете.
Сделайте это сами за три шага
- Создавайте на сервере, когда формируете письмо. Один вызов на автомобиль или до пятидесяти за вызов для дайджеста. Сохраняйте
urlиrenews_untilвместе с сообщением. - Вставьте URL в
<img>с указанием ширины, высоты и альтернативного текста. В альтернативном тексте назовите автомобиль («2024 Porsche 911, красный, вид спереди в три четверти»), а не просто «автомобиль». - Следите за лимитом аккаунта. Продление засчитывает изображение один раз в том месяце, на который оно приходится; если изображения автомобилей по тарифу исчерпаны, оно возвращает 402 до сброса лимита 1-го числа, поэтому подбирайте тариф под количество отдаваемых изображений.
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, включая продлённые. Общие кеши после этого продолжают отдавать изображение не дольше часа.