Car Image API

Customers · Car Image API

How to embed car images in email with signed URLs that never break

Car Image API puts every customer's first render in their inbox with an auto-renewing signed URL. The exact mechanism, the credit math and the code to do it yourself.

The problem: an image that has to work in March

Every account on Car Image API gets a short note from Jonathan about an hour after its first render. The obvious thing to put in that email is the render itself: the exact 2024 Porsche 911 or 2006 Dodge Charger the person just asked for, as proof the integration worked and as a small brag.

Email is the worst possible surface for a signed URL. A page is re-rendered on every visit; an email is rendered once and opened whenever the reader gets to it, a day, a month or a year later, often through Gmail's image proxy. The signed delivery URLs the API already offered had a maximum TTL of seven days, which is the right ceiling for a browser but guarantees a broken image in an inbox by week two. Attaching or inlining the PNG bloats every message and freezes the render, so a quality upgrade to the underlying image never reaches the reader.

What we wanted was a URL that behaves like a hosted image, keeps working for as long as anyone might open the email, and still charges the account fairly for what it actually serves.

The solution: auto-renewing signed URLs

We added one flag to POST /api/v1/image-urls: renew: true. A renewable URL is minted and charged exactly like any other, one credit, valid for ttl_seconds. The difference is what happens after the TTL. Instead of a 403, the first load in each new window of ttl_seconds re-bills the issuing account one credit and serves the image; every further load in that window is free and publicly cacheable, the same as the first window. That continues until renews_until, up to a year out.

Three properties fell out of that design, and each one mattered for email:

  • Nothing to re-send or re-mint. The URL in the email is the URL forever. No cron job rewriting stored links, no second email with a fresh image.
  • Pay for opens, not for time. A window that nobody opens costs nothing. An email read by one person on one day costs the one credit it was minted for; one opened every week for a year costs about 52 credits, five cents.
  • Idempotent billing. Each window is a ledger row keyed renew:<url id>:<window>, unique per URL and window. Fifty opens in a week, or two mail proxies racing on the first open, pay once.

The lifecycle emails mint these URLs through the same code path the public endpoint uses, as platform tokens: bound to no API key and never billed, because they are ours, but served by the identical GET /api/v1/delivery/{token} route, with the identical seven-day windows and the identical caching. The image in the first-render email is the product demonstrating itself.

Mint one renewable URL for the customer's first render
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…"
}

The email itself

The template is React Email with inline styles, because mail clients strip stylesheets. The render sits in a bordered table cell on a soft background, requested at 480 pixels and shown at 240 so it stays crisp on retina screens, with a pill above it and two caption lines under it. Transparent PNG is the whole point of the product, so we keep it; on an old Outlook that paints alpha black, a format: "jpg" render on a solid cell background is the fallback.

The card, as plain 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>

Everything else in the message is text: what the URL is, why it keeps working, the exact POST to mint one for their own vehicle, and a link to this page. The email carries an unsubscribe link and the List-Unsubscribe headers, and every product link is tagged so we can see, in the acquisition report, whether the story you are reading brought anyone back.

Do it yourself in three steps

  1. Mint server-side, when you build the email. One call per vehicle, or up to fifty per call for a digest. Store url and renews_until with the message.
  2. Put the URL in an <img> with width, height and alt text. Alt text names the vehicle ("2024 Porsche 911, red, front three-quarter view"), not "car".
  3. Keep the account funded. A renewal on an empty balance answers 402 and the image stays broken until credits land; auto-reload on the dashboard removes that failure mode.
With the 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

The full parameter reference, caching headers and error codes are on the signed URL docs. The agent skill that ships with the plugin knows the same rules, so an assistant asked to "put this car in the newsletter" mints a renewable URL instead of pasting a key into a template.

Frequently asked questions

Why not attach the image or inline it as base64?
Attachments and base64 bloat every message and freeze the render at send time. A signed URL is a few hundred bytes, loads through the mail client's image proxy like any hosted image, and picks up quality upgrades to the underlying render on the next open.
What does an auto-renewing URL cost?
One credit when it is created, then one credit for each further window of ttl_seconds in which it is actually opened, until renews_until. With a seven-day TTL that is at most one credit per week per image, and zero for weeks nobody opens the email. $1 buys 1,000 credits.
What happens if the account runs out of credits?
The load answers 402 Insufficient credits and the image shows as broken until the balance is topped up; the URL itself stays valid and resumes on its next load. Auto-reload on the dashboard keeps a production account from ever hitting that wall.
Does this work in Gmail, Outlook and Apple Mail?
Yes. The delivery endpoint is a plain public GET with Cache-Control, an ETag and CORS headers, which is exactly what image proxies expect. PNG transparency renders in every current client; for very old Outlook versions request format jpg on a solid background.
Can a URL be revoked?
Revoke the API key that minted it and every URL from that key answers 403 on its next load, renewals included. Shared caches keep serving for at most an hour after that.