El problema: una imagen que tiene que funcionar en marzo
Cada cuenta de Car Image API recibe una breve nota de Jonathan aproximadamente una hora después de su primer render. Lo obvio es incluir en ese correo el propio render: el Porsche 911 de 2024 o el Dodge Charger de 2006 exacto que la persona acaba de pedir, como prueba de que la integración funcionó y como un pequeño alarde.
El correo electrónico es el peor medio posible para una URL firmada. Una página se vuelve a renderizar en cada visita; un correo se renderiza una sola vez y se abre cuando el lector llega a él, un día, un mes o un año después, a menudo a través del proxy de imágenes de Gmail. Las URL de entrega firmadas que la API ya ofrecía tenían un TTL máximo de siete días, que es el límite adecuado para un navegador, pero garantiza una imagen rota en la bandeja de entrada a partir de la segunda semana. Adjuntar o incrustar el PNG aumenta el tamaño de cada mensaje.
Lo que queríamos era una URL que se comportara como una imagen alojada, siguiera funcionando mientras alguien pudiera abrir el correo y, aun así, cobrara a la cuenta de forma justa por lo que realmente sirve.
La solución: URL firmadas que se renuevan automáticamente
Añadimos un indicador a POST /api/v1/image-urls: renew: true. Una URL renovable se genera y se cobra exactamente igual que cualquier otra: un crédito, válida durante ttl_seconds. La diferencia está en lo que ocurre después del TTL. En lugar de un 403, la primera carga en cada nueva ventana de ttl_seconds vuelve a cobrar un crédito a la cuenta emisora y sirve la imagen; cualquier otra carga en esa ventana es gratuita y puede almacenarse en caché públicamente, igual que en la primera ventana. Esto continúa hasta renews_until, como máximo un año después.
De ese diseño surgieron tres propiedades, y cada una resultó importante para el correo electrónico:
- Nada que reenviar ni volver a generar. La URL del correo es la URL para siempre. Sin tareas cron que reescriban enlaces guardados ni un segundo correo con una imagen nueva.
- Pague por aperturas, no por tiempo. Una ventana de validez que nadie abre no cuesta nada. Un correo leído por una persona un solo día cuesta el crédito para el que se generó; uno abierto cada semana durante un año cuesta unos 52 créditos, cinco centavos.
- Facturación idempotente. Cada ventana es una fila del registro contable con la clave
renew:<url id>:<window>, única por URL y ventana. Cincuenta aperturas en una semana, o dos proxies de correo compitiendo en la primera apertura, se pagan una sola vez.
Los correos del ciclo de vida generan estas URL por la misma ruta de código que usa el endpoint público, como tokens de plataforma: no están vinculados a ninguna clave de API y nunca se facturan, porque son nuestros, pero se sirven por la misma ruta GET /api/v1/delivery/{token}, con las mismas ventanas de siete días y el mismo almacenamiento en caché. La imagen del correo del primer render es el producto demostrándose a sí mismo.
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 (siete días, el máximo) fija el costo máximo en un crédito por imagen por semana. Un TTL de una hora con renew: true también funciona, pero una bandeja de entrada muy activa volvería a facturar cada hora en que se abra.El correo en sí
La plantilla usa React Email con estilos en línea, porque los clientes de correo eliminan las hojas de estilo. El render va en una celda de tabla con borde sobre un fondo suave, solicitado a 480 píxeles y mostrado a 240 para que se vea nítido en pantallas retina, con una etiqueta encima y dos líneas de pie debajo. El PNG transparente es la razón de ser del producto, así que lo mantenemos; en un Outlook antiguo que pinta el canal alfa de negro, la alternativa es un render con format: "jpg" sobre una celda con fondo sólido.
<!-- 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>Todo lo demás en el mensaje es texto: qué es la URL, por qué sigue funcionando, el POST exacto para generar una para su propio vehículo y un enlace a esta página. El correo incluye un enlace para darse de baja y las cabeceras List-Unsubscribe, y cada enlace al producto está etiquetado para que podamos ver, en el informe de adquisición, si la historia que está leyendo hizo volver a alguien.
Hágalo usted mismo en tres pasos
- Genérelas en el servidor, al crear el correo. Una llamada por vehículo, o hasta cincuenta por llamada para un resumen. Guarde
urlyrenews_untiljunto con el mensaje. - Ponga la URL en un
<img>con ancho, alto y texto alternativo. El texto alternativo nombra el vehículo ("Porsche 911 2024, rojo, vista frontal de tres cuartos"), no "coche". - Mantenga la cuenta con saldo. Una renovación con saldo cero responde 402 y la imagen sigue rota hasta que lleguen créditos; la recarga automática en el panel elimina ese modo de fallo.
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 itLa referencia completa de parámetros, las cabeceras de caché y los códigos de error están en la documentación de URL firmadas. La skill de agente que se incluye con el plugin conoce las mismas reglas, así que un asistente al que se le pide "pon este coche en el boletín" genera una URL renovable en lugar de pegar una clave en una plantilla.
Preguntas frecuentes
- ¿Por qué no adjuntar la imagen o incrustarla en base64?
- Los adjuntos y el base64 inflan cada mensaje. Una URL firmada ocupa unos cientos de bytes y se carga a través del proxy de imágenes del cliente de correo como cualquier imagen alojada.
- ¿Cuánto cuesta una URL con renovación automática?
- Un crédito al crearla y, después, un crédito por cada ventana adicional de ttl_seconds en la que realmente se abra, hasta renews_until. Con un TTL de siete días, eso supone como máximo un crédito por semana por imagen, y cero las semanas en que nadie abre el correo. Con 1 US$ se compran 100 créditos.
- ¿Qué ocurre si la cuenta se queda sin créditos?
- La carga responde 402 Insufficient credits y la imagen aparece rota hasta que se recarga el saldo; la URL en sí sigue siendo válida y vuelve a funcionar en la siguiente carga. La recarga automática del panel evita que una cuenta de producción se tope alguna vez con ese límite.
- ¿Funciona en Gmail, Outlook y Apple Mail?
- Sí. El endpoint de entrega es un GET público simple con Cache-Control, un ETag y cabeceras CORS, que es exactamente lo que esperan los proxies de imágenes. La transparencia PNG se muestra en todos los clientes actuales; para versiones muy antiguas de Outlook, solicite format jpg sobre un fondo sólido.
- ¿Se puede revocar una URL?
- Revoque la clave de API que la generó y todas las URL de esa clave responderán 403 en su siguiente carga, renovaciones incluidas. Las cachés compartidas siguen sirviéndolas como máximo una hora después.