Developer docs
Programmatic access to Local Deal Intelligence — search, listing detail, and dealer opportunity data for CRM/DMS integrations and other scripted use. This page covers the endpoints intended for that; the full interactive schema for every endpoint is at /api/docs (Swagger UI) if you need it.
Authentication
Two ways to authenticate, depending on whether a person or a script is calling:
- Session token —
Authorization: Bearer <token>, obtained fromPOST /auth/loginorPOST /auth/signup. Expires after 7 days. This is what the frontend itself uses. - API key —
X-API-Key: <key>, issued from the dealer dashboard (bottom "API keys" card) orPOST /api-keyswith a session token. Meant for unattended scripts — no interactive login, no password stored anywhere. Requires a plan with API access (Dealer Pro in the default seeded plans); issuing a key on a plan without it returns403. Only a hash of the key is stored server -side — if you lose the raw value, revoke it and issue a new one, it's never shown again.
Not every endpoint accepts both. The ones that accept an API key are called out below; everything else needs a session token.
# Issue a key (needs a session token first)
curl -X POST 'https://your-deployment/api-keys' \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"name": "inventory-sync-script"}'
# -> {"id": "...", "name": "inventory-sync-script", "key": "ldi_live_...", "key_prefix": "ldi_live_ab12", "created_at": "..."}
# The "key" value is shown exactly once — store it now.
# Use it
curl 'https://your-deployment/search?lat=52.52&lon=13.405&radius_km=40' \
-H "X-API-Key: ldi_live_..."Rate limits & plan tiers
Every request is rate-limited by IP address, independent of plan: 120 requests/minute by default. A handful of expensive endpoints (LLM-backed explanations/negotiation, the AI research agent, source discovery) have a stricter 5–10 requests/minute limit. Exceeding a limit returns 429 Too Many Requests.
Separately, GET /search is metered per plan when called with a session token or API key — a searches_per_day cap that resets daily (see the pricing page for what each tier includes). Anonymous, unauthenticated search is not metered this way, but is still subject to the per-IP rate limit above. Exceeding your daily search allowance returns 429 with a plan-specific message rather than the generic rate-limit response.
CRM/DMS export (/dealer/opportunities/export.csv, /dealer/opportunities/export-webhook) and API key issuance are gated by plan, not by a request rate — a plan that doesn't include them returns 403 regardless of how many requests you've made.
Search listings
GET /search — the core listing search, radius-based around a lat/lon point. Accepts a session token, an API key, or no auth at all (results are identical either way; only the per-plan daily quota differs).
curl 'https://your-deployment/search?lat=52.52&lon=13.405&radius_km=40&category=car&max_price=20000' \
-H "X-API-Key: ldi_live_..."Query parameters: lat, lon (required), radius_km (default 40, max 500), category, min_price, max_price, limit (default 50, max 200), offset.
[
{
"id": "b9ba0d40-...",
"title": "2018 Toyota Corolla",
"price": 14500.0,
"currency": "EUR",
"category": "car",
"attributes": { "year": 2018, "mileage_km": 62000, "fuel_type": "petrol" },
"city": "Berlin",
"country": "DE",
"dealer_name": "Acme Motors",
"url": "https://source-site.example/listing/123",
"status": "active",
"last_checked_at": "2026-08-17T10:00:00Z",
"distance_km": 3.2,
"value_score": 78,
"flags": ["below_market", "trusted_dealer"],
"duplicate_count": 1
}
]Listing detail
GET /listings/{id} — full detail for one listing, including other sources it's also listed on. No authentication required. Returns everything /search does, plus first_seen_at, last_seen_at, and duplicate_listings.
curl 'https://your-deployment/listings/b9ba0d40-cd33-43f3-8c00-dc0ea1274910'Dealer opportunities
Dealer-only endpoints (role dealer or admin) — a ranked feed of underpriced listings near a point, plus market context. All three accept an API key.
GET /dealer/market-overview— price distribution and competitor activity for an area.GET /dealer/opportunities— the ranked feed itself, same params as/searchplusmin_value_score(default filters to above-average deals).GET /dealer/opportunities/export.csv— the same feed as a CSV download. Requires a plan with CRM export enabled.POST /dealer/opportunities/export-webhook— pushes the same feed as JSON to a webhook URL you supply (body:{"webhook_url": "https://..."}). Also requires CRM export enabled; the URL is validated against SSRF (must be HTTPS, must not resolve to a private/internal address).
curl 'https://your-deployment/dealer/opportunities?lat=52.52&lon=13.405&radius_km=40&min_value_score=70' \
-H "X-API-Key: ldi_live_..."
curl -X POST 'https://your-deployment/dealer/opportunities/export-webhook?lat=52.52&lon=13.405&radius_km=40' \
-H "X-API-Key: ldi_live_..." -H 'Content-Type: application/json' \
-d '{"webhook_url": "https://your-crm.example.com/hooks/deals"}'Error reference
Every error is a standard FastAPI JSON body: {"detail": "..."} (a string, or a structured list for request validation errors). Status codes you'll actually see:
400— malformed request (e.g. an invalid webhook URL).401— missing, invalid, expired, or revoked credentials (bad session token or API key).403— authenticated, but not allowed: wrong role, or your plan doesn't include this feature.404— the resource doesn't exist, or (for owned resources like alerts/API keys) it exists but isn't yours.422— request parameters failed validation (out-of-range radius, bad lat/lon, etc.) — the response body lists which field.429— rate limit or daily search quota exceeded (see above).