Authentication
API keys, scopes, the device flow and where keys must never go.
Bearer keys
Every API request carries a key that starts with cimg_. Send it in the Authorization header. The x-api-key header is accepted for compatibility, but Authorization: Bearer is preferred.
curl -H "Authorization: Bearer $CAR_IMAGE_API_KEY" \
https://carimage.dev/api/v1/account<img> tags and anything else a browser can see, mint signed URLs from your backend instead.Creating and revoking keys
Keys are created in the dashboard or by the CLI. The plaintext is shown exactly once; the server stores only a SHA-256 hash plus a short prefix for display. You can hold up to 10 active keys per account — one per environment or agent is a good habit, because revocation is instant and per-key.
- Dashboard keys get
images:readandaccount:read. - CLI keys (approved in the browser at
/activate) also getbilling:write, so an agent can hand you a Stripe Checkout link when credits run low.
Scopes
| Parameter | Type | Description |
|---|---|---|
images:read | scope | Fetch images, create signed URLs, search and resolve vehicles, send feedback. |
account:read | scope | Read the credit balance and pricing from GET /api/v1/account. |
billing:write | scope | Create hosted Stripe Checkout URLs to buy credits. Never completes a charge — a human still pays in the browser. Granted to keys from the CLI device flow, and to a connected app only when the consent screen listed it. The Billing Portal (invoices, saved cards) needs a key or the dashboard: its URL is itself a credential, so connected apps cannot obtain one. |
billing:purchase | scope | Pay for a plan or an open plan invoice with a payment token (a Stripe Shared Payment Token), only within the monthly agent spending limit the owner set in the dashboard. Off by default and never granted by the CLI device flow or to a connected app: the owner turns it on per key in the dashboard's Let agents buy card. A key with allowed origins cannot hold it. |
A request outside a key's scopes fails with 403; the response body names the scope that is missing.
Device flow (CLI and agents)
npx @meterapp/car-image login requests a device code, prints a short user code and opens https://carimage.dev/activate. You sign in and approve the code; the new key is delivered only to the waiting CLI, stored at ~/.config/car-image-api/config.json with mode 0600, and never shown in the browser.
/api/auth/device/code/api/auth/device/tokenCodes expire after ten minutes. Polling before approval returns authorization_pending; the key is returned exactly once, after which the code is consumed.
OAuth for connectors
A chat client cannot be handed a key, so the MCP endpoint speaks the MCP authorization spec: OAuth 2.1 with Supabase Auth as the authorization server and carimage.dev as the protected resource and consent screen. Point a connector at https://carimage.dev/api/mcp and the client discovers everything else. Sign-in is the same account you use here.
- Authorization code with PKCE and dynamic client registration (RFC 7591) — no client secrets, nothing configured by hand.
- Discovery starts at
/.well-known/oauth-protected-resource(RFC 9728), named by theWWW-Authenticateheader on any unauthenticated request; it points at the authorization server's own metadata. - You approve at
/oauth/consent. The screen names the app, the account, your balance and exactly what the app will be able to do. - A connected app holds
images:readandaccount:read: it can render images and read your balance, and can never buy credits or open billing.
/.well-known/oauth-protected-resource/oauth/consentChecking your balance
/api/v1/accountThe balance and the prices it buys, abridged. The full response also carries the plan in force as data.plan (see Pricing & billing), the last 30 days as data.usage_30d, the calling key as data.key and every plan as data.pricing.plans.
{
"data": {
"credits": 4871,
"credits_value_usd": 48.71,
"auto_reload": false,
"has_payment_method": true,
"pricing": {
"credits_per_image": 1,
"credits_per_dollar": 100,
"packs": [
{
"credits": 100,
"cents": 100,
"label": "$1 — 100 credits"
},
{
"credits": 500,
"cents": 500,
"label": "$5 — 500 credits"
},
{
"credits": 1000,
"cents": 1000,
"label": "$10 — 1,000 credits"
},
{
"credits": 2500,
"cents": 2500,
"label": "$25 — 2,500 credits"
},
{
"credits": 10000,
"cents": 10000,
"label": "$100 — 10,000 credits"
}
]
}
},
"links": {
"create_checkout": "/api/v1/billing/checkout",
"billing_portal": "/api/v1/billing/portal",
"pricing": "/pricing",
"docs": "/docs",
"openapi": "/openapi.json",
"mcp": "/api/mcp"
},
"request_id": "req_01j9x…"
}Environment variables
The CLI, the SDK and the local MCP server all read the same two variables:
| Parameter | Type | Description |
|---|---|---|
CAR_IMAGE_API_KEYrequired | string | Your cimg_ key. Inject it as a secret in CI or an agent sandbox. |
CAR_IMAGE_API_URL | url | Override the API origin (self-hosting, staging).Default: https://carimage.dev |