O problema: uma imagem que precisa funcionar em março
Toda conta na Car Image API recebe um breve recado do Jonathan cerca de uma hora após a primeira renderização. O mais óbvio a incluir nesse e-mail é a própria renderização: exatamente o Porsche 911 2024 ou o Dodge Charger 2006 que a pessoa acabou de pedir, como prova de que a integração funcionou e como um pequeno motivo de orgulho.
O e-mail é a pior superfície possível para uma URL assinada. Uma página é renderizada de novo a cada visita; um e-mail é renderizado uma vez e aberto quando o leitor chega a ele, um dia, um mês ou um ano depois, muitas vezes pelo proxy de imagens do Gmail. As URLs de entrega assinadas que a API já oferecia tinham TTL máximo de sete dias, o limite certo para um navegador, mas que garante uma imagem quebrada na caixa de entrada já na segunda semana. Anexar o PNG ou incorporá-lo inline deixa cada mensagem mais pesada.
O que queríamos era uma URL que se comportasse como uma imagem hospedada, continuasse funcionando enquanto alguém pudesse abrir o e-mail e, ainda assim, cobrasse da conta de forma justa pelo que realmente entrega.
A solução: URLs assinadas com renovação automática
Adicionamos uma flag ao POST /api/v1/image-urls: renew: true. Uma URL renovável é gerada e cobrada exatamente como qualquer outra: um crédito, válida por ttl_seconds. A diferença está no que acontece depois do TTL. Em vez de um 403, o primeiro carregamento em cada nova janela de ttl_seconds cobra novamente um crédito da conta emissora e entrega a imagem; todos os demais carregamentos nessa janela são gratuitos e podem ser armazenados em cache publicamente, assim como na primeira janela. Isso continua até renews_until, com prazo de até um ano.
Três propriedades surgiram desse design, e cada uma fez diferença para o e-mail:
- Nada para reenviar ou gerar de novo. A URL no e-mail é a URL para sempre. Nenhum cron job reescrevendo links armazenados, nenhum segundo e-mail com uma imagem nova.
- Pague por aberturas, não por tempo. Uma janela que ninguém abre não custa nada. Um e-mail lido por uma pessoa em um dia custa o único crédito para o qual foi gerado; um aberto toda semana durante um ano custa cerca de 52 créditos, cinco centavos.
- Cobrança idempotente. Cada janela é uma linha do registro contábil com a chave
renew:<url id>:<window>, única por URL e janela. Cinquenta aberturas em uma semana, ou dois proxies de e-mail disputando a primeira abertura, pagam uma vez só.
Os e-mails de ciclo de vida geram essas URLs pelo mesmo caminho de código que o endpoint público usa, como tokens de plataforma: não vinculados a nenhuma chave de API e nunca cobrados, porque são nossos, mas servidos pela mesma rota GET /api/v1/delivery/{token}, com as mesmas janelas de sete dias e o mesmo cache. A imagem no e-mail da primeira renderização é o produto demonstrando a si mesmo.
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 (sete dias, o máximo) limita o custo a um crédito por imagem por semana. Um TTL de uma hora com renew: true também funciona, mas uma caixa de entrada movimentada geraria nova cobrança a cada hora em que fosse aberta.O e-mail em si
O template é feito em React Email com estilos inline, porque os clientes de e-mail removem as folhas de estilo. A renderização fica em uma célula de tabela com borda sobre um fundo suave, solicitada com 480 pixels e exibida com 240 para continuar nítida em telas retina, com um selo acima e duas linhas de legenda abaixo. O PNG transparente é a essência do produto, então o mantemos; em um Outlook antigo que pinta o canal alfa de preto, uma renderização com format: "jpg" sobre um fundo de célula sólido é a alternativa.
<!-- 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 o resto da mensagem é texto: o que é a URL, por que ela continua funcionando, o POST exato para gerar uma para o próprio veículo e um link para esta página. O e-mail traz um link de cancelamento de inscrição e os cabeçalhos List-Unsubscribe, e cada link de produto é marcado para que possamos ver, no relatório de aquisição, se a história que você está lendo trouxe alguém de volta.
Faça você mesmo em três etapas
- Gere no servidor, ao montar o e-mail. Uma chamada por veículo, ou até cinquenta por chamada para um resumo. Armazene
urlerenews_untiljunto com a mensagem. - Coloque a URL em uma
<img>com largura, altura e texto alternativo. O texto alternativo nomeia o veículo ("Porsche 911 2024, vermelho, vista frontal três-quartos"), não "carro". - Mantenha a conta com saldo. Uma renovação com saldo zerado responde 402 e a imagem continua quebrada até os créditos entrarem; a recarga automática no painel elimina esse modo de falha.
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 itA referência completa de parâmetros, os cabeçalhos de cache e os códigos de erro estão na documentação de URLs assinadas. A skill de agente que acompanha o plugin conhece as mesmas regras; assim, um assistente a quem se pede para "colocar este carro na newsletter" gera uma URL renovável em vez de colar uma chave em um template.
Perguntas frequentes
- Por que não anexar a imagem ou incorporá-la em base64?
- Anexos e base64 incham cada mensagem. Uma URL assinada tem algumas centenas de bytes e carrega pelo proxy de imagens do cliente de e-mail como qualquer imagem hospedada.
- Quanto custa uma URL com renovação automática?
- Um crédito quando é criada e, depois, um crédito para cada janela adicional de ttl_seconds em que ela de fato é aberta, até renews_until. Com um TTL de sete dias, isso dá no máximo um crédito por semana por imagem, e zero nas semanas em que ninguém abre o e-mail. US$ 1 compra 100 créditos.
- O que acontece se a conta ficar sem créditos?
- O carregamento responde 402 Insufficient credits e a imagem aparece quebrada até que o saldo seja recarregado; a URL em si continua válida e volta a funcionar no próximo carregamento. A recarga automática no painel evita que uma conta de produção chegue a esse limite.
- Isso funciona no Gmail, no Outlook e no Apple Mail?
- Sim. O endpoint de entrega é um GET público simples com Cache-Control, um ETag e cabeçalhos CORS, exatamente o que os proxies de imagem esperam. A transparência do PNG é exibida em todos os clientes atuais; para versões muito antigas do Outlook, solicite o formato jpg com fundo sólido.
- É possível revogar uma URL?
- Revogue a chave de API que a gerou e todas as URLs dessa chave respondem 403 no próximo carregamento, renovações incluídas. Caches compartilhados continuam servindo por no máximo uma hora depois disso.