Le problème : une image qui doit encore fonctionner en mars
Chaque compte Car Image API reçoit un petit mot de Jonathan environ une heure après son premier rendu. La chose la plus évidente à mettre dans cet e-mail, c'est le rendu lui-même : la Porsche 911 2024 ou la Dodge Charger 2006 exacte que la personne vient de demander, comme preuve que l'intégration fonctionne et comme petite démonstration de savoir-faire.
L’e-mail est le pire support possible pour une URL signée. Une page est générée à nouveau à chaque visite ; un e-mail est généré une seule fois et ouvert quand le lecteur s’y met, un jour, un mois ou un an plus tard, souvent via le proxy d’images de Gmail. Les URL de livraison signées que proposait déjà l’API avaient une durée de validité (TTL) maximale de sept jours : le bon plafond pour un navigateur, mais l’assurance d’une image cassée dans la boîte de réception dès la deuxième semaine. Joindre ou intégrer le PNG alourdit chaque message.
Nous voulions une URL qui se comporte comme une image hébergée, continue de fonctionner tant que quelqu’un est susceptible d’ouvrir l’e-mail, et facture malgré tout au compte un montant juste pour ce qu’elle sert réellement.
La solution : des URL signées à renouvellement automatique
Nous avons ajouté un seul paramètre à POST /api/v1/image-urls : renew: true. Une URL renouvelable est générée et facturée exactement comme les autres : un crédit, valable ttl_seconds. La différence tient à ce qui se passe après le TTL. Au lieu d'une erreur 403, le premier chargement dans chaque nouvelle fenêtre de ttl_seconds refacture un crédit au compte émetteur et sert l'image ; tous les chargements suivants dans cette fenêtre sont gratuits et peuvent être mis en cache publiquement, comme pour la première fenêtre. Cela continue jusqu'à renews_until, au maximum un an plus tard.
Cette conception a donné trois propriétés, chacune importante pour l’e-mail :
- Rien à renvoyer ni à régénérer. L'URL de l'e-mail reste la même pour toujours. Pas de tâche cron qui réécrit les liens stockés, pas de second e-mail avec une nouvelle image.
- Payez à l'ouverture, pas à la durée. Une période de validité pendant laquelle personne n'ouvre l'e-mail ne coûte rien. Un e-mail lu par une personne un seul jour coûte le crédit pour lequel l'URL a été émise ; un e-mail ouvert chaque semaine pendant un an coûte environ 52 crédits, soit cinq centimes.
- Facturation idempotente. Chaque fenêtre correspond à une ligne du registre identifiée par la clé
renew:<url id>:<window>, unique par URL et par fenêtre. Cinquante ouvertures en une semaine, ou deux proxys de messagerie en concurrence lors de la première ouverture, ne sont facturées qu'une seule fois.
Les e-mails de cycle de vie génèrent ces URL via le même chemin de code que l'endpoint public, sous forme de jetons de plateforme : liés à aucune clé API et jamais facturés, puisqu'ils nous appartiennent, mais servis par la même route GET /api/v1/delivery/{token}, avec les mêmes fenêtres de sept jours et la même mise en cache. L'image de l'e-mail du premier rendu, c'est le produit qui fait sa propre démonstration.
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 (sept jours, le maximum) plafonne donc le coût à un crédit par image et par semaine. Un TTL d'une heure avec renew: true fonctionne aussi, mais une boîte de réception très active serait refacturée à chaque heure où elle est ouverte.L’e-mail lui-même
Le modèle est en React Email avec des styles inline, car les clients de messagerie suppriment les feuilles de style. Le rendu est placé dans une cellule de tableau bordée sur un fond doux, demandé à 480 pixels et affiché à 240 pour rester net sur les écrans Retina, avec une pastille au-dessus et deux lignes de légende en dessous. Le PNG transparent est la raison d'être du produit, nous le conservons donc ; sur un ancien Outlook qui affiche la transparence en noir, un rendu format: "jpg" sur une cellule à fond uni sert de solution de repli.
<!-- 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>Tout le reste du message est du texte : ce qu'est l'URL, pourquoi elle continue de fonctionner, la requête POST exacte pour en générer une pour son propre véhicule, et un lien vers cette page. L'e-mail comporte un lien de désabonnement et les en-têtes List-Unsubscribe, et chaque lien produit est balisé pour que nous puissions voir, dans le rapport d'acquisition, si l'article que vous lisez a fait revenir quelqu'un.
Faites-le vous-même en trois étapes
- Créez les URL côté serveur, au moment de construire l'e-mail. Un appel par véhicule, ou jusqu'à cinquante par appel pour un récapitulatif. Stockez
urletrenews_untilavec le message. - Placez l’URL dans une balise
<img>avec une largeur, une hauteur et un texte alternatif. Le texte alternatif nomme le véhicule (« Porsche 911 2024, rouge, vue avant trois-quarts »), pas simplement « voiture ». - Gardez votre compte approvisionné. Un renouvellement sur un solde vide renvoie 402 et l'image reste cassée jusqu'à l'arrivée des crédits ; la recharge automatique dans le tableau de bord élimine ce risque de panne.
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 référence complète des paramètres, les en-têtes de cache et les codes d’erreur se trouvent dans la documentation des URL signées. La compétence d’agent fournie avec le plugin connaît les mêmes règles : un assistant à qui l’on demande de « mettre cette voiture dans la newsletter » génère une URL renouvelable au lieu de coller une clé dans un modèle.
Questions fréquentes
- Pourquoi ne pas joindre l’image ou l’intégrer en base64 ?
- Les pièces jointes et le base64 alourdissent chaque message. Une URL signée ne pèse que quelques centaines d’octets et se charge via le proxy d’images du client de messagerie, comme n’importe quelle image hébergée.
- Combien coûte une URL à renouvellement automatique ?
- Un crédit à sa création, puis un crédit pour chaque nouvelle fenêtre de ttl_seconds au cours de laquelle elle est réellement ouverte, jusqu'à renews_until. Avec un TTL de sept jours, cela représente au plus un crédit par semaine et par image, et zéro pour les semaines où personne n'ouvre l'e-mail. 1 $US permet d'acheter 100 crédits.
- Que se passe-t-il si le compte n'a plus de crédits ?
- Le chargement renvoie 402 Insufficient credits et l’image apparaît cassée jusqu’à ce que le solde soit rechargé ; l’URL elle-même reste valide et fonctionne de nouveau au chargement suivant. Le rechargement automatique dans le tableau de bord évite à un compte de production de se heurter à ce mur.
- Cela fonctionne-t-il dans Gmail, Outlook et Apple Mail ?
- Oui. Le point de terminaison de diffusion est un simple GET public avec les en-têtes Cache-Control, ETag et CORS, exactement ce qu'attendent les proxys d'images. La transparence PNG s'affiche dans tous les clients actuels ; pour les très anciennes versions d'Outlook, demandez le format jpg sur fond uni.
- Une URL peut-elle être révoquée ?
- Révoquez la clé API qui l’a générée et chaque URL issue de cette clé renverra une erreur 403 à son prochain chargement, renouvellements compris. Les caches partagés continuent ensuite de la servir pendant une heure au maximum.