Every screen in the dealer app is a public API call away — the app and the API call the same service layer, so a number here is the number on the screen. REST with a uniform { ok, data } envelope, cursor pagination, integer-cent money, and an OpenAPI 3.1 spec generated from the same validators the server enforces.
A read-only sandbox returns the same Unit records as the production /api/v1/units (demo responses skip the { ok, data } envelope). Permissive rate limit (100/min). No bearer token needed.
/api/v1/dashboardstable/api/v1/unitsstable/api/v1/unitsstable/api/v1/units/{unitId}stable/api/v1/units/{unitId}stable/api/v1/units/{unitId}stable/api/v1/leadsstable/api/v1/leadsstable/api/v1/leads/{leadId}stable/api/v1/leads/{leadId}stable/api/v1/leads/{leadId}stable/api/v1/contactsstable/api/v1/contacts/{contactId}stable/api/v1/dealsstable/api/v1/dealsstable/api/v1/deals/{dealId}stable/api/v1/deals/{dealId}stable/api/v1/servicestable/api/v1/servicestable/api/v1/service/{roId}stable/api/v1/callsstable/api/v1/rooftopsstable/api/v1/reportsstableThat is the whole thing — every route above exists today. Request and response schemas are in the full API reference, generated from the same OpenAPI document the server serves.
/api/v1/contactsPlanned — Q3 2026/api/v1/service/{roId}Planned — Q3 2026/api/v1/calls/{callId}Planned — Q3 2026/api/v1/calls/{callId}/transcriptPlanned — Q3 2026/api/v1/herald/dispatchPlanned — Q3 2026/api/v1/webhooksPlanned — Q3 2026These return 404 today. They are listed because you would otherwise go looking for them: every one is a read or a feature the data layer does not expose yet, and we would rather name the gap than let you discover it in production. Nothing above is behind a flag or a sales call.
const BASE = 'https://autodealerpro.io/api/v1';
const headers = {
authorization: `Bearer ${process.env.ADP_API_KEY}`,
'content-type': 'application/json',
};
// Your key is bound to one tenant — there is no tenant parameter to get wrong.
async function adp<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(BASE + path, { ...init, headers });
const body = await res.json();
if (!body.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
return body.data as T;
}
// Page through inventory until the cursor runs out
let cursor: string | null = null;
do {
const q = new URLSearchParams({ status: 'available', limit: '50' });
if (cursor) q.set('cursor', cursor);
const page = await adp<{ items: unknown[]; nextCursor: string | null }>(
`/units?${q}`,
);
console.log(page.items.length);
cursor = page.nextCursor;
} while (cursor);
// Create a lead — contact is found-or-created by phone, then email
const lead = await adp('/leads', {
method: 'POST',
body: JSON.stringify({
rooftopId, // from GET /v1/rooftops
source: 'website',
intent: 'specific_unit',
interestedUnitId: unitId,
contact: { fullName: 'Marcus Webb', phone: '+14155550118' },
}),
});# Every create needs a rooftop id — start here
curl https://autodealerpro.io/api/v1/rooftops \
-H "Authorization: Bearer $ADP_API_KEY"
# List inventory (the key decides the tenant; RLS enforces it)
curl "https://autodealerpro.io/api/v1/units?status=available&limit=50" \
-H "Authorization: Bearer $ADP_API_KEY"
# Create a lead
curl -X POST https://autodealerpro.io/api/v1/leads \
-H "Authorization: Bearer $ADP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"rooftopId": "'"$ADP_ROOFTOP_ID"'",
"source": "website",
"contact": { "fullName": "Marcus Webb", "phone": "+14155550118" }
}'
# Drop a price. Money is integer cents, always.
curl -X PATCH https://autodealerpro.io/api/v1/units/$UNIT_ID \
-H "Authorization: Bearer $ADP_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "askingPriceCents": 3495000 }'
# Retire a unit — archives it, never deletes the row
curl -X DELETE https://autodealerpro.io/api/v1/units/$UNIT_ID \
-H "Authorization: Bearer $ADP_API_KEY"lead.createdlead.qualifiedlead.converteddeal.createddeal.signeddeal.fundedunit.createdunit.soldunit.agedcall.completedcall.escalatedservice.openedservice.completedSend Authorization: Bearer adp_live_…. Keys are stored as SHA-256 hashes — the plaintext is shown once, at creation. Revoked and expired keys are rejected with 401.
A key is bound to one tenant, and every query runs inside a Postgres row-level-security transaction scoped to it. There is no tenant parameter to tamper with: changing an id in a URL returns 404, not someone else's data.
Every key also carries a fixed set of scopes, chosen at creation. Each endpoint requires exactly one; a key without it gets 403 insufficient_scope, with error.details.requiredScope naming what was missing. Scopes cannot be edited after creation — rotate the key instead.
Keys issued before scope enforcement shipped have an empty scopes array. They are grandfathered to full read/write access on their tenant so existing integrations keep working, and they will be denied in a future release. Treat them as admin credentials, and replace them with explicitly scoped keys in Settings → API keys.
| Surface | Limit | Keyed by |
|---|---|---|
| /api/v1/* | 600 / min | API key or user |
| /api/demo/* | 100 / min | IP |
| Failed bearer auth | 30 / min | IP |
One flat limit, the same on every plan — we have not built per-plan tiering, so we don't print a table pretending we have. Headers: RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset (IETF httpapi draft), plus Retry-After on 429 per RFC 6585.
Scopes are resource:action. write does not imply read — an integration that both imports and exports inventory needs units:read and units:write. Soft deletes are writes.
| Scope | Grants | Endpoints |
|---|---|---|
| units:read | Read inventory | GET /v1/units, /v1/units/{id} |
| units:write | Create, update and archive inventory | POST /v1/units · PATCH, DELETE /v1/units/{id} |
| leads:read | Read CRM leads | GET /v1/leads, /v1/leads/{id} |
| leads:write | Create, update and close leads | POST /v1/leads · PATCH, DELETE /v1/leads/{id} |
| deals:read | Read F&I deals | GET /v1/deals, /v1/deals/{id} |
| deals:write | Create and update deals | POST /v1/deals · PATCH /v1/deals/{id} |
| service:read | Read repair orders | GET /v1/service |
| service:write | Create and update repair orders | POST /v1/service · PATCH /v1/service/{id} |
| contacts:read | Read customer contacts | GET /v1/contacts |
| contacts:write | Create and update contacts | POST /v1/contacts · PATCH /v1/contacts/{id} |
| calls:read | Read Herald call logs | GET /v1/calls |
| rooftops:read | Read rooftop configuration | GET /v1/rooftops |
| reports:read | Read reports and the dashboard bundle | GET /v1/reports, /v1/dashboard |
| * | Everything above, including resources added later | All of /v1 |
Session (browser) requests do not use scopes — they are governed by role-based access control instead. The scope each endpoint requires is also declared under security in the OpenAPI spec.
The no-auth demo API is open right now — no signup. Production API keys are provisioned by us while self-serve key management is built; email us and you'll have one the same day — no developer-relations cosplay, no "tell us about your use case" survey.