API docs
Bolt-on ReconOS to whatever recon tool you already run. Bearer-token API, signed webhooks, embeddable widgets.
Base URL: https://3-130-85-220.sslip.io
Authentication
All requests use a Bearer API key in the Authorization header. Mint a key in Settings → API keys. The plaintext is shown once.
curl -H "Authorization: Bearer rcn_..." \ https://3-130-85-220.sslip.io/api/v1/vehicles
read allows GET. write allows everything. A read key on a write endpoint returns 403 forbidden.Vehicles
/api/v1/vehiclesread scopePaginated list, dealership-scoped.
| Field | Type | Notes |
|---|---|---|
| stage | string? | Filter: detail | mechanical | photos | frontline |
| limit | int? | Default 50, max 200 |
| offset | int? | Default 0 |
curl -H "Authorization: Bearer rcn_..." \ "https://3-130-85-220.sslip.io/api/v1/vehicles?stage=detail&limit=20"
{
"vehicles": [
{
"id": "uuid",
"vin": "1HGBH41JXMN109186",
"year": 2021,
"make": "Honda",
"model": "Accord LX",
"miles": 42100,
"color_hex": "#dc2626",
"stage": "detail",
"stage_entered_at": "2026-05-12T14:30:00Z",
"est_retail": 22500,
"purchased_at_amount": 18200,
"recon_spend": 1247.50,
"parts_status": "none",
"assigned_tech": "Mike R.",
"created_at": "2026-05-12T14:30:00Z"
}
],
"pagination": { "total": 18, "limit": 20, "offset": 0, "has_more": false }
}recon_spend is the running total of parts + labor + sublet for this car (USD). Operator-maintained from the dashboard. Combine with est_retail and purchased_at_amount to compute gross margin per car: retail minus acquired minus recon./api/v1/vehicles/:id/forecastread scopePredicted completion date for one in-flight vehicle. Returns optimistic / expected / pessimistic timestamps from per-stage quantiles (p25/median/p75) over the dealership's own last-90-day historical pace, with a cold-start fallback for new accounts. Returns 400 if the vehicle is already on frontline.
curl -H "Authorization: Bearer rcn_..." \ https://3-130-85-220.sslip.io/api/v1/vehicles/<vehicle_id>/forecast
{
"vehicle": {
"id": "uuid",
"vin": "1HGBH41JXMN109186",
"year": 2021,
"make": "Honda",
"model": "Accord LX",
"stage": "mechanical"
},
"forecast": {
"vehicle_id": "uuid",
"current_stage": "mechanical",
"days_in_current_stage": 2.5,
"expected_remaining_days": 3.5,
"optimistic_remaining_days": 2.5,
"pessimistic_remaining_days": 4.25,
"expected_ready_at": "2026-05-17T00:14:55Z",
"optimistic_ready_at": "2026-05-16T00:14:55Z",
"pessimistic_ready_at": "2026-05-17T18:14:55Z",
"cold_start": false
}
}cold_start: true means we fell back to bundled industry defaults because the dealership doesn't have enough completed cars yet. Treat the timestamps as wider-band estimates in that case./api/v1/vehicleswrite scopeCreate a vehicle. Returns 201 with the inserted row.
curl -X POST \
-H "Authorization: Bearer rcn_..." \
-H "Content-Type: application/json" \
-d '{
"vin": "1HGBH41JXMN109186",
"year": 2021,
"make": "Honda",
"model": "Accord LX",
"miles": 42100,
"stage": "detail",
"est_retail": 22500,
"purchased_at_amount": 18200,
"stock_number": "T5832",
"intake_source": "trade_in"
}' \
https://3-130-85-220.sslip.io/api/v1/vehiclesvin (17 chars), year, make, model. Year must be 1900 to 2100. Everything else is optional with sensible defaults.stage and ReconOS picks the landing stage from your dealership's intake rules. intake_source can be any of: trade_in, auction, dealer_trade, repo, wholesale, customer, other. If omitted but stock_number is present, we apply your configured stock-prefix regex rules to detect source from the prefix (e.g. ^T → trade-in). Configure both in Settings → Intake rules.Reference benchmarks
/api/v1/benchmarksread scopeYour last-30-day median time-to-line against a fixed reference sample of shops in the same volume bucket. The sample is seeded industry data — it is never built from other ReconOS dealerships, so nothing here exposes another customer. Same numbers our dashboard and weekly digest show.
curl -H "Authorization: Bearer rcn_..." \ https://3-130-85-220.sslip.io/api/v1/benchmarks
{
"bucket": "medium",
"bucket_label": "15–45 cars/month",
"you": { "completed": 22, "median_ttl": 7 },
"peers": {
"rooftop_count": 14,
"completed": 312,
"median_ttl": 8,
"p25_ttl": 6,
"p75_ttl": 11
},
"percentile": 73,
"comparison_source": "reference_sample"
}percentile is 0 to 100. 100 = fastest in your bucket.null if you have no completions yet in the window.Webhooks
ReconOS pushes events to your endpoint instead of you polling. Add a subscription in Settings → Webhooks. Each subscription gets a signing secret (shown once).
Event types
| Field | Type | Notes |
|---|---|---|
| vehicle.created | fires | A new vehicle was added to recon |
| vehicle.stage_changed | fires | Any stage transition (detail → mechanical, etc.) |
| vehicle.completed | fires | Stage moved to frontline. Always paired with stage_changed |
| vehicle.flagged_stuck | fires | Vehicle exceeded your stage-median threshold. Fires once on the transition INTO stuck, not while still stuck |
| photo.uploaded | fires | A tech uploaded a photo |
| sms.sent | fires | Tech or parts-vendor SMS dispatched |
Delivery shape
POST to your URL with JSON body. Headers carry the event type and an HMAC-SHA-256 signature you verify against your secret.
POST https://your-server/webhook
Content-Type: application/json
X-ReconOS-Event: vehicle.stage_changed
X-ReconOS-Delivery: <delivery uuid>
X-ReconOS-Signature: sha256=<hex>
User-Agent: ReconOS-Webhook/1
{
"event_type": "vehicle.stage_changed",
"delivery_id": "<delivery uuid>",
"timestamp": "2026-05-13T22:00:00.000Z",
"payload": {
"vehicle_id": "<uuid>",
"vin": "1HGBH41JXMN109186",
"from_stage": "photos",
"to_stage": "frontline",
"to": "frontline",
"changed_at": "2026-05-13T22:00:00.000Z",
"actor_user_id": "<uuid>"
}
}Verifying the signature
Compute hmac_sha256(secret, raw_body) and compare (constant-time) to the hex after sha256=. If they match, the payload came from us and wasn't tampered with.
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody, signatureHeader, secret) {
const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
const got = (signatureHeader ?? "").replace(/^sha256=/, "");
if (got.length !== expected.length) return false;
return timingSafeEqual(Buffer.from(got), Buffer.from(expected));
}Retries
failed and visible in Settings → Webhooks for debugging. Subscribers should be idempotent. The same delivery_id may arrive more than once.DMS write-back presets
Push ReconOS stage changes back to your DMS so inventory records stay in sync without anyone re-keying. Layered on the webhook outbox: same HMAC signing, same retry policy, same audit log. The payload is rewritten into the vendor's expected shape and the right vendor-specific header is set.
Configure in Settings → DMS write-back. Pick CDK Drive, Reynolds ERA, or Dealertrack; paste the partner endpoint URL; customize the stage → status-code mapping. Owner-only.
Supported presets
| Field | Type | Notes |
|---|---|---|
| cdk | preset | CDK Drive, Fortellis status-push endpoint shape |
| reynolds | preset | Reynolds ERA, RCI partner endpoint shape |
| dealertrack | preset | Dealertrack partner integration shape |
Event coverage
vehicle.stage_changed only. That's the operationally critical signal for keeping the DMS's inventory status accurate. Other webhook event types pass through the preset transformer; if it returns no mapping they're marked failed-non-retryable with a descriptive error.CDK Drive payload shape
POST https://your-cdk-partner-endpoint/status
Content-Type: application/json
X-ReconOS-Event: vehicle.stage_changed
X-ReconOS-Delivery: <delivery uuid>
X-ReconOS-Signature: sha256=<hex>
X-CDK-Status-Push: v1
User-Agent: ReconOS-Webhook/1
{
"vin": "1HGBH41JXMN109186",
"reconStatus": "RECON_PHOTO",
"previousStatus": "RECON_MECH",
"timestamp": "2026-05-13T22:00:00Z",
"source": "ReconOS"
}Reynolds ERA payload shape
POST https://your-reynolds-partner-endpoint/status
Content-Type: application/json
X-ReconOS-Event: vehicle.stage_changed
X-Reynolds-Event: vehicle.status
{
"vin": "1HGBH41JXMN109186",
"status": "R_PHOTOS",
"ts": "2026-05-13T22:00:00Z"
}Dealertrack payload shape
POST https://your-dealertrack-partner-endpoint/status
Content-Type: application/json
X-ReconOS-Event: vehicle.stage_changed
X-Dealertrack-Source: reconos
{
"vin": "1HGBH41JXMN109186",
"inventoryStatus": "RECON_PHOTOS",
"changedAt": "2026-05-13T22:00:00Z"
}Stage → status-code mapping
Each preset ships with default mappings between ReconOS stages and DMS status codes. Most CDK installs use slightly different codes per dealer; override per-stage in the settings UI. The value sent in reconStatus / status /inventoryStatus is whatever you configure (the defaults below are starting points, not contracts).
| Field | Type | Notes |
|---|---|---|
| detail | default → CDK | RECON_DETAIL |
| mechanical | default → CDK | RECON_MECH |
| photos | default → CDK | RECON_PHOTO |
| frontline | default → CDK | FRONTLINE_READY |
hmac_sha256(secret, raw_body) and compare against X-ReconOS-Signature. The vendor-specific headers (X-CDK-Status-Push etc.) are advisory metadata; only the ReconOS headers participate in auth.Inbound integrations
ReconOS can receive stage-change events from whatever recon tool your dealership runs today (Rapid Recon, CDK, Reynolds). Use that tool as your daily kanban; let ReconOS layer analytics + audit + the bolt-on API surface on top. We do not write back. This is one-way ingest.
1. Set up the signing secret
In ReconOS, go to Settings → Rapid Recon integration and click Generate signing secret. You'll see the plaintext exactly once. Copy it into Rapid Recon's outbound-webhook config along with the URL below.
2. Endpoint shape
POST https://3-130-85-220.sslip.io/api/integrations/rapid-recon/webhook/<your-dealership-id>
X-Signature: sha256=<hex-hmac-of-raw-body>
Content-Type: application/json
{
"event_id": "evt_abc123",
"event_type": "vehicle.synced",
"occurred_at": "2026-05-14T15:23:00Z",
"vehicle": {
"external_id": "T5832",
"vin": "1HGBH41JXMN109186",
"year": 2021,
"make": "Honda",
"model": "Accord LX",
"miles": 42100,
"current_step": "Photography",
"purchased_at_amount": 18200,
"internet_price": 22500,
"assigned_tech": "Mike Rodriguez"
}
}event_id, event_type, occurred_at, vehicle.external_id, vehicle.vin (17 chars). Everything else is optional. Step names auto-translate via aliases. "Body Shop" → mechanical, "Lot Ready" → frontline, etc.3. Idempotency
Upserted by (dealership_id, external_source, external_id). Calling twice with the same external_id updates the same row, so your retry logic doesn't duplicate cars. The first event creates the vehicle and logs a stage_change recon_event; subsequent events update fields and log a stage_change only when the stage actually transitions.
Response codes
| Field | Type | Notes |
|---|---|---|
| 200 | ok | Received. Body: { received: true, vehicle_id, was_created, stage_changed } |
| 400 | shape | Body malformed or required fields missing |
| 401 | auth | X-Signature header missing or doesn't match |
| 404 | config | No active integration credential for this dealership. Generate one in Settings. |
| 500 | server | Database error. Safe to retry. |
Embed widgets
/embed/* currently resolves to the sign-in redirect, so a <script> tag pointing at one loads HTML where JavaScript is expected and fails silently on your page. This section documents the intended shape so integrators can plan against it; it is not a working integration today. Read the same numbers from /api/v1/benchmarks in the meantime.Drop a <script> tag into any HTML page (your existing recon tool's custom panel, intranet, Slack canvas, Confluence page) and a ReconOS panel renders inline. Auth is a read-scope API key in the URL (same trust model as a Stripe Publishable Key).
Reference benchmark
<script src="https://3-130-85-220.sslip.io/embed/benchmark.js"
data-key="rcn_..."
data-width="420"
data-height="320"></script>Bottleneck this week
<script src="https://3-130-85-220.sslip.io/embed/bottleneck.js"
data-key="rcn_..."
data-width="420"
data-height="320"></script>allow-scripts allow-same-origin only, no top-navigation). The widget is read-only and rotatable. Revoke the key in Settings to instantly disable every embed using it.Rate limits
Each API key gets 120 requests per minute. The limit is per-key (not per-IP), so a noisy script on one machine doesn't affect the same dealership's other integrations.
When you hit the limit, you get a 429 with a Retry-After header (seconds until the window resets). Well-behaved clients back off until that time.
Retry-After: 27 X-RateLimit-Limit: 120 X-RateLimit-Remaining: 0
Error shape
All errors share a JSON shape. The HTTP status code tells you what kind:
| Field | Type | Notes |
|---|---|---|
| 400 | shape | Body missing required fields, malformed JSON, etc. |
| 401 | auth | No Authorization header, malformed key, or revoked key |
| 403 | scope | Key is read-scope but the route needs write |
| 404 | shape | Resource doesn't exist OR doesn't belong to your dealership |
| 429 | rate | Rate limited. See Rate limits section |
| 500 | server | Our bug. Surface this to support with the X-Request-ID if present. |
{ "error": "human-readable message",
"details": { "optional": "context" } }Questions or missing endpoint? hello@reconos.com. Design-partner dealers get endpoint requests prioritized.