Car Image API
Documentation

Webhooks

Account endpoints that hear when a render, 3D model or upgrade is ready; signed deliveries, retries and 30 days of events to poll.

Register an HTTPS endpoint once and Car Image tells it when something about the images your account uses is ready: a new render stored, a 3D model built, a paid quality upgrade delivered. Each delivery is signed, retried when your server is down, and readable again for 30 days. Endpoints are free; every event but the test needs a paid plan in force.

Events

Account webhook event types
TypeWhen
webhook.testSent by POST /api/v1/webhooks/{id}/test to that endpoint only.
image.readyA new render the account requested was made and stored, and can be served.
3d_model.readyA 3D model the account ordered is ready to download.
3d_model.failedA 3D model the account ordered could not be made; its credits were refunded.
image_upgrade.succeededA paid quality upgrade was delivered in place.
image_upgrade.failedA paid quality upgrade could not be delivered; its credits were refunded.
image.upgradedA view the account was served was raised to higher quality in place.
image.issue_reportedSomeone reported a problem with a view the account was served.
image.issue_resolvedA reported problem with a view the account was served was fixed or closed.
image.replacedA view the account was served has new bytes for another reason (re-derived, new paint key).
vehicle.availableA vehicle the account asked or voted for was added to the catalog.
pack.batch.completedEvery item of an Image Pack batch is ready or failed; failed items gave their image back to the pack.
pack.export.readyA ZIP export of an Image Pack is built; its links are valid for 7 days.
pack.window.endingAn Image Pack's 30 days of updates end in 3 days.
pack.window.endedAn Image Pack's 30 days of updates ended; its images stay yours to self-host, and its archive stays downloadable.

3d_model.* carries the same JSON as GET /api/v1/3d/{id} and image_upgrade.* the same as GET /api/v1/images/upgrades/{id}. They arrive on account endpoints in addition to a request's own webhook_url, which keeps working as before.

Updates to images you were served

Every image your account is billed for while a paid plan is in force is watched: when that view is later raised to higher quality (image.upgraded), reported by anyone in the network (image.issue_reported), fixed or closed (image.issue_resolved), or given new bytes for another reason such as a re-render or a new paint key (image.replaced), your endpoints hear about it once. Every color of a view shares one stored image, so the watch is per vehicle and view. If you keep your own copies, fetch the image again from data.url (billed as any image request).

A vehicle's first image is served fast at standard quality and its view redrawn at enhanced quality in the background, usually within a day (first views). When that redraw replaces it, image.upgraded arrives with reason: "first_view". Other reasons are popularity, customer (someone paid for the upgrade) and operator.

Issue events say what kind of problem it is (category: identity, camera, paint or quality), never who reported it or what they wrote.

image.upgraded
{
  "id": "evt_3f1a9c0d2e4b6a8c0e2f4a6b8c0d2e4f",
  "type": "image.upgraded",
  "created_at": "2026-10-23T09:41:05.117Z",
  "data": {
    "object": "image_update",
    "vehicle": {
      "id": "veh_errc87t1jgata",
      "make": "toyota",
      "model": "camry",
      "year": 2025
    },
    "view": "rear-3-4",
    "color": "#1a2b3c",
    "quality": "enhanced",
    "revision": "4c7d…",
    "url": "https://carimage.dev/api/v1/images/car?vehicle=veh_errc87t1jgata&view=rear-3-4&color=%231a2b3c",
    "status": "upgraded",
    "reason": "popularity"
  }
}

vehicle.available tells the accounts of everyone who asked or voted for a vehicle on the request board that it is in the catalog, with the URL that renders it.

Add an endpoint

POST/api/v1/webhooks
API keyfree
POST /api/v1/webhooks body
ParameterTypeDescription
url
string
HTTPS on a public host; no credentials or fragment. One endpoint per URL.
events
string[]
Event types, or ["*"] (the default) for every type, including types added later.
description
string
Optional, up to 200 characters.
bash
curl --fail-with-body \
  -X POST https://carimage.dev/api/v1/webhooks \
  -H "Authorization: Bearer $CAR_IMAGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/hooks/carimage","events":["image.ready","3d_model.ready"],"description":"catalog sync"}'
201 Created
{
  "data": {
    "id": "whe_3f9a1c2b7d5e4f60a1b2c3d4e5f60718",
    "object": "webhook_endpoint",
    "url": "https://example.com/hooks/carimage",
    "events": [
      "image.ready",
      "3d_model.ready"
    ],
    "description": "catalog sync",
    "status": "active",
    "disabled_reason": null,
    "secret_prefix": "whsec_q7Lm",
    "consecutive_failures": 0,
    "failing_since": null,
    "last_success_at": null,
    "created_at": "2026-10-22T18:00:00.000Z",
    "updated_at": "2026-10-22T18:00:00.000Z",
    "secret": "whsec_q7Lm…"
  },
  "entitlement": {
    "plan": "pro",
    "receives": "all"
  },
  "request_id": "req_01j9x…"
}

data.secret is shown once: store it on your server as CAR_IMAGE_WEBHOOK_SECRET. An account holds at most 10 endpoints. Changing endpoints takes an API key; a connected app's token can list them but not change them. You can do all of this on the dashboard's Webhooks page too.

GET/api/v1/webhooks
account:readfree
GET/api/v1/webhooks/{id}
account:readfree
PATCH/api/v1/webhooks/{id}
API keyfree
DELETE/api/v1/webhooks/{id}
API keyfree

PATCH takes url, events, description and status: "disabled" stops deliveries, "active" turns the endpoint back on with a fresh failure count. DELETE removes it with its delivery history.

What a delivery looks like

One POST per event and endpoint, with a JSON body and three headers:

  • X-CarImage-Event: the event type.
  • X-CarImage-Delivery: the delivery id (whd_…), the same on every retry.
  • X-CarImage-Signature: t=<unix seconds>,sha256=<hex>, an HMAC-SHA256 of <t>.<raw body> keyed with the endpoint's whole secret.
POST body
{
  "id": "evt_8c0e2f4a6b1d3c5e7f9a0b2c4d6e8f01",
  "type": "image.ready",
  "created_at": "2026-10-22T18:03:12.481Z",
  "data": {
    "object": "image",
    "vehicle": {
      "id": "veh_errc87t1jgata",
      "make": "toyota",
      "model": "camry",
      "year": 2025
    },
    "view": "rear-3-4",
    "color": "#1a2b3c",
    "quality": "standard",
    "sha256": "9b2e…",
    "url": "https://carimage.dev/api/v1/images/car?vehicle=veh_errc87t1jgata&view=rear-3-4&color=%231a2b3c"
  }
}

Answer with any 2xx within 10 seconds. A timeout, a redirect or any other status is a failed attempt, tried again 1 min, 5 min, 30 min, 2 h, 12 h, 24 h later, then given up (resend it from the dashboard). An endpoint that fails for three days straight is turned off and the account owner gets an email; the events stay readable below. Deliveries can repeat and can arrive out of order, so dedupe on id and read created_at.

Verify the signature

Check the signature over the raw bytes before you parse them, and refuse a timestamp more than five minutes old. The SDK's verifyWebhook() does both and returns the event:

ts
import { verifyWebhook } from "@meterapp/car-image-sdk";

// Read the raw body: the signature covers the exact bytes we sent.
export async function POST(request: Request) {
  const body = await request.text();
  let event;
  try {
    event = await verifyWebhook(body, request.headers.get("x-carimage-signature"), process.env.CAR_IMAGE_WEBHOOK_SECRET!);
  } catch {
    return new Response("bad signature", { status: 401 });
  }
  if (await alreadyHandled(event.id)) return new Response(null, { status: 204 }); // deliveries can repeat
  if (event.type === "image.ready") {
    // event.data: vehicle, view, color, quality, sha256 and url (fetch it again to store a copy)
  }
  return new Response(null, { status: 204 }); // any 2xx within 10 s stops the retries
}

In another language: split the header on commas, recompute HMAC-SHA256(secret, t + "." + body) as hex and compare it in constant time.

Send a test

POST/api/v1/webhooks/{id}/test
API keyfree, every plan
GET/api/v1/webhooks/{id}/deliveries
account:readfree

The test queues a signed webhook.test event for that endpoint alone; it arrives within a minute. The deliveries list shows each attempt with your server's status code.

Poll instead

GET/api/v1/events
account:readfree
bash
curl -s -H "Authorization: Bearer $CAR_IMAGE_API_KEY" \
  "https://carimage.dev/api/v1/events?type=image.ready&since=2026-10-01T00:00:00Z&limit=100"
#  -> data: [events, oldest first], has_more; page on with starting_after=<last id>

The same events, oldest first, for the last 30 days, whether or not an endpoint received them: type, since, starting_after and limit (1–100). A server that cannot take inbound requests can poll this every few minutes.

SDK, CLI and MCP

  • SDK: listWebhooks, createWebhook, getWebhook, updateWebhook, deleteWebhook, testWebhook, listWebhookDeliveries, listEvents and verifyWebhook.
  • CLI: car-image webhooks list|add|show|test|enable|disable|remove|deliveries|events (/docs/cli).
  • MCP: list_webhooks (read only) with ?toolset=all (/docs/mcp).