Back to top

Developer API Reference

Build smarter routing into your product. One endpoint, conditional data layers, predictable responses.

Overview

The Viscar Developer API is a JSON-over-HTTPS interface for route planning enriched with traffic-control signs, turn-by-turn maneuvers, ETA-aware weather forecasts and per-segment road-quality scoring. Every endpoint lives under a versioned path so existing integrations stay stable as the platform evolves.

Base URL https://api.viscarapp.com/dev/v1

All requests and responses use UTF-8 JSON. Coordinates are decimal degrees (WGS84). Distances are in meters, durations in seconds - SI units throughout the public contract.

Authentication

Every request needs an API key. Generate one in your Dashboard after creating an account. Keys start with the prefix vsk_live_.

Pass the key in either header - both are accepted, pick whichever fits your client better:

Headers - option A X-Api-Key: vsk_live_abc123...
Headers - option B Authorization: Bearer vsk_live_abc123...

Requests without a valid key return 401 Unauthorized with an error envelope. Keep keys server-side - never ship them in a mobile bundle or web frontend you don't fully control.

Rate Limits & Plans

Every API key is tied to a plan. Plans differ in daily request quota and the number of API keys you can issue. Your current plan and remaining quota are shown in your dashboard - see pricing for the current product tiers and what each plan includes.

PlanRequests / dayAPI keys
free2501
developer1 5005
startup8 00020
enterprise50 000unlimited

Exceeding your daily quota returns 429 Too Many Requests. In V1 every public endpoint is reachable from the Free tier - higher plans raise the quota; they do not unlock additional endpoints. Endpoint-level plan gating will appear when future tiers introduce restricted features; until then every endpoint carries the ALL PLANS badge.

An additional, orthogonal axis is the user role (set in your account, separate from your billing plan): an admin user's API keys bypass plan-tier gates and the daily quota - so internal tooling and support stay reachable without forcing the team onto a higher tier.

Beta note: paid plans are visible but not purchasable yet - pricing and access policy is pending review with legal counsel. The Free tier is fully operational; upgrade flows will open after launch.

Versioning

The API version sits in the URL path (/dev/v1/...). When a breaking change is unavoidable a new version is published alongside the existing one - older versions keep working for a deprecation window of at least 12 months.

Within a major version we add fields but never rename or remove them. Clients should ignore unknown response fields to stay forward-compatible.

Build Route

POST /dev/v1/routes ALL PLANS

Computes a route between two coordinates and returns all data layers by default. Drop the cost-bearing optional layers you don't need (weather, quality) by listing them in exclude.

Request body

JSON { "origin": { "lat": 34.0522, "lon": -118.2437 }, "destination": { "lat": 34.0900, "lon": -118.1850 }, "exclude": ["quality"], "avoid": ["tolls"], "profile": "car", "course": 87 }

Omit exclude (or send []) to receive the full response. The example above keeps weather and drops quality.

Fields

FieldTypeRequiredDescription
originobject {lat, lon}yesStart coordinate. lat ∈ [-90, 90], lon ∈ [-180, 180].
destinationobject {lat, lon}yesEnd coordinate. Same range rules as origin.
excludestring[]noOptional layers to drop (opt-out). Allowed values: weather, quality. All layers are returned by default; omit or send [] for the full response. The always-on core (summary, geometry, maneuvers, signs) cannot be excluded - putting any of those names here yields 400.
avoidstring[]noRoad classes to avoid. Allowed: highways, tolls.
profilestringnoVehicle profile. Currently only car is supported.
courseintnoDriver's heading at origin in degrees from true north (0..360 inclusive). Tells the router not to suggest an immediate U-turn when the vehicle is already moving. Omit (or send 0) when the vehicle is stationary or heading is unknown.

Response shell

Every successful response carries the always-on core (request, summary, segments, maneuvers, signs, attribution) plus the optional blocks (weather, quality) - all present by default. Any block you list in exclude is omitted from the JSON entirely.

Segments are the structural backbone - each carries its own coordinate array, length, duration and max-speed. Maneuvers, signs and quality all reference segments by sid.

JSON { "route_id": "x9y8z7...", "request": { /* always present - echoed input parameters */ }, "summary": { /* always present - distance, duration, segment_count */ }, "segments": [ /* always present - sid, length, duration, geometry per item */ ], "maneuvers": { /* always present - turn-by-turn with from_sid / to_sid */ }, "signs": { /* always present - items tagged with segment sid */ }, "weather": { /* present by default - omit via exclude: ["weather"] */ }, "quality": { /* present by default - omit via exclude: ["quality"] - { segments, potholes, anomalies } */ }, "attribution": { /* always present - see Attribution section */ } }

Block - Request echo always present

Echoes back the validated, normalized parameters that produced this response. Lets you confirm how the server interpreted your input and reconstruct the call from a logged response.

JSON "request": { "origin": { "lat": 34.0522, "lon": -118.2437 }, "destination": { "lat": 34.0900, "lon": -118.1850 }, "course": 87, "avoid": ["tolls"], "profile": "car", "exclude": ["quality"] }

Block - Summary always present

Route totals + segment count for quick skim before iterating segments[].

JSON "summary": { "distance_m": 12420.5, "duration_s": 1086.2, "segment_count": 47 }

Block - Segments always present

The structural backbone of the response. Each item carries its own coordinate array, the length and duration of that piece of the drive, and the posted max-speed. Maneuvers live in their own top-level array (next section) and reference segments via from_sid / to_sid.

Segment geometries do not duplicate the shared junction point - each coordinate appears exactly once across the route, in driving order. Concatenating the geometry arrays segment-by-segment yields the full route polyline with no dedup step.

JSON - one segment { "sid": 8842711, "length_m": 245.3, "duration_s": 18.4, "max_speed_kmh": 60, "geometry": [ [-118.2437, 34.0522], [-118.2402, 34.0526] ] }

Coordinate order is [lon, lat] per RFC 7946 - wrap the array in a { "type": "LineString", "coordinates": … } object on the client if your mapping library wants a full GeoJSON geometry. max_speed_kmh stays in kilometers per hour because that's the unit the posted signs use; 0 means the underlying road has no speed limit known to the graph.

Block - Maneuvers always present

Top-level turn-by-turn instructions in driving order. Each item identifies the segments it connects with explicit from_sid + to_sid - joins onto the response's segments[] array without coordinate lookup.

JSON "maneuvers": { "items": [ { "type": "right", "from_sid": 8842710, "to_sid": 8842711, "location": { "lat": 34.0526, "lon": -118.2402 }, "to_street": "South Hill Street" }, { "type": "roundabout_exit", "from_sid": 8842714, "to_sid": 8842715, "location": { "lat": 34.0721, "lon": -118.2055 }, "to_street": "2nd exit — Sunset Blvd" } ] }

Maneuver type values

TypeDescription
straightContinue straight through the junction.
leftTurn left.
rightTurn right.
exit_leftTake the exit on the left (motorway / link).
exit_rightTake the exit on the right (motorway / link).
keep_leftKeep left where the road splits.
keep_rightKeep right where the road splits.
u_turnMake a U-turn.
roundabout_leftEnter a roundabout, overall direction left.
roundabout_rightEnter a roundabout, overall direction right.
roundabout_straightEnter a roundabout, continuing straight.
roundabout_u_turnEnter a roundabout to reverse direction.
roundabout_exitExit a roundabout; to_street carries the exit number and street.

from_sid is 0 on the very first maneuver of a route (no inbound segment). For roundabout exits, to_street follows the format "Nth exit — Street Name".

Block - Signs always present

Traffic-control elements and POIs along the route in driving order: stops, signals, give-way, crossings, speed cameras, mini-roundabouts, turning circles and loops, traffic calming, hazards and city limits. Pre-filtered for the actual driving direction (an inbound stop on a side street is shown; the same node on the through-road is suppressed). Each item carries the sid of the segment it belongs to.

JSON "signs": { "items": [ { "type": "all_way_stop", "sid": 8842711, "location": { "lat": 34.0541, "lon": -118.2390 } }, { "type": "speed_camera", "sid": 8842715, "location": { "lat": 34.0688, "lon": -118.2160 }, "on_way_position": 0.42, "cam_enforcement": "maxspeed", "cam_maxspeed": 55 }, { "type": "crossing", "sid": 8842720, "location": { "lat": 34.0810, "lon": -118.1950 }, "crossing_type": "marked" } ] }

Sign type values

TypeDescription
stopStop sign.
all_way_stopAll-way stop.
traffic_signalsTraffic light.
give_wayYield / give-way.
crossingPedestrian crossing (see crossing_type).
mini_roundaboutMini-roundabout.
turning_circleWidened turning circle at a road end.
turning_loopTurning loop (circular dead-end).
traffic_calmingTraffic-calming feature (see calming_type).
speed_cameraSpeed / red-light camera (see cam_enforcement, cam_maxspeed).
hazardRoad hazard (see hazard_type).
city_limitCity-limit boundary.

Optional sub-fields appear only when meaningful for the sign type: on_way_position (0..1 along the segment, for mid-segment signs like cameras), cam_enforcement + cam_maxspeed (cameras), crossing_type, hazard_type, calming_type.

Block - Weather default-on · opt-out

Returned by default; drop it with "exclude": ["weather"]. ETA-aware weather forecast for points sampled along the route. Each point's reading is projected to the time the driver is expected to arrive there - not "now". Forecast horizon is 36 hours.

In-house risk alerts are included on every point: rain and snow intensity, slippery road, strong wind, glare, low visibility, high UV, tornado risk. Each alert carries a severity mark 1..4 (informational → extreme) and a value - the physical magnitude behind the alert in its natural unit (see below).

JSON "weather": { "points": [ { "location": { "lat": 34.0522, "lon": -118.2437 }, "eta_s": 0, "forecast_unix": 1761408000, "temp_c": 18.4, "feels_like_c": 17.9, "wind_speed_ms": 3.2, "wind_gusts_ms": 5.1, "clouds_pct": 40, "visibility_m": 10000, "uvi": 3.5, "pop": 0.10, "condition_code": 803, "condition_main": "Clouds", "condition_description": "broken clouds", "alerts": [] }, { "location": { "lat": 34.0900, "lon": -118.1850 }, "eta_s": 1086, "forecast_unix": 1761409086, "temp_c": 17.1, "wind_speed_ms": 8.4, "wind_gusts_ms": 14.2, "rain_mm_h": 2.1, "condition_code": 501, "condition_main": "Rain", "condition_description": "moderate rain", "alerts": [ { "type": "rain", "mark": 2, "value": 2.1 }, { "type": "strong_wind", "mark": 3, "value": 14.2 } ] } ], "upon_arrival": { "temp_c": 17.1, "feels_like_c": 16.4, "humidity_pct": 78, "wind_speed_ms": 8.4, "wind_gusts_ms": 14.2, "visibility_m": 9000, "pop": 0.62, "rain_mm_h": 2.1, "condition_code": 501, "condition_main": "Rain", "condition_description": "moderate rain" } }

upon_arrival - a driver-focused summary of the forecast at the destination, projected to the arrival ETA: temperature, feels-like, humidity, wind & gusts, visibility, probability of precipitation and the condition (with rain_mm_h / snow_mm_h when present). Omitted when the weather layer is excluded or no forecast was available.

Alert type values

TypevalueDescription
rainmm/hRain intensity along the route.
snowmm/hSnowfall rate.
strong_windm/sStrong wind (max of sustained and gust).
slippery_road0Black ice, freezing rain or aquaplaning risk.
glare_risk0Low-sun glare risk shortly after sunrise / before sunset under clear sky.
low_visibilitymetersReduced visibility (fog and similar).
high_uvUV indexHigh UV exposure.
tornado0Severe convective / tornado risk.

Where value is 0 there is no single physical magnitude for the alert - its importance is carried by mark.

Alert mark (severity)

MarkDescription
1Informational.
2Moderate.
3Severe.
4Extreme.

condition_main values

ValueDescription
ClearClear sky.
CloudsCloud cover (few to overcast).
DrizzleDrizzle.
RainRain, including freezing rain.
SnowSnow.
FogFog.
ThunderstormThunderstorm.
TornadoSevere convective (mapped from extreme hail / convective).

Block - Quality default-on · opt-out

Returned by default; drop it with "exclude": ["quality"]. Three parallel arrays:

  • segments[] - per-SID aggregate scores. Join onto segments[] in the top-level response by sid.
  • potholes[] - discrete point hits (pothole / sharp bump), each a single coordinate carrying its owning sid.
  • anomalies[] - short bad stretches, each an ordered points[] list you draw a polyline through.

Scores are a road-surface quality coefficient clamped to 0–2.0 - higher is smoother, with ≈1.0 as the reference baseline. See the colour scale below. Only segments with measured telemetry appear in segments[] - if a segment's sid from the top-level segments[] array is missing here, no quality data exists for it yet. potholes[] and anomalies[] are independent and may still carry items even when no per-segment row is returned for the surrounding sids.

Colouravg_scoreRating
≥ 0.90Excellent
0.70 – 0.90Good
0.56 – 0.70Moderate
< 0.56Poor

The same buckets the app uses to tint route segments - the green-to-red palette matches the live route overlay, and potholes[] / anomalies[] scores use the same 0–2.0 scale (lower = worse).

JSON "quality": { "segments": [ { "sid": 8842711, "avg_score": 1.45, "altitude_m": 124.5 }, { "sid": 8842712, "avg_score": 0.62 } ], "potholes": [ { "lat": 34.0541, "lon": -118.2390, "score": 0.21, "speed": 12.4, "azimuth": 1.83, "sid": 8842713 } ], "anomalies": [ { "points": [ { "lat": 34.0550, "lon": -118.2378 }, { "lat": 34.0557, "lon": -118.2365 } ], "score": 0.25, "azimuth": 1.79, "sid": 8842714 } ] }

Viscar-derived from sensor analysis - not upstream-attributed.

Attribution & Licensing

Every successful response includes an attribution block. When required is true, displaying the attribution to your end users is a license obligation, not a UI preference - show the text field (or compose your own from providers[]) somewhere visible in your product.

JSON - full response "attribution": { "required": true, "text": "Underlying road geometry: © OpenStreetMap contributors (ODbL 1.0). Weather data: Open-Meteo (CC BY 4.0).", "providers": [ { "name": "OpenStreetMap", "license": "ODbL 1.0", "url": "https://www.openstreetmap.org/copyright", "scope": ["segments"], "note": "Source of the underlying road network geometry used to compute the route. Maneuvers and sign placements along the route are produced by Viscar's own algorithms." }, { "name": "Open-Meteo", "license": "CC BY 4.0", "url": "https://open-meteo.com", "scope": ["weather"] } ] }

How providers map to response layers

ProviderTriggered byLicenseWhy
OpenStreetMapsegmentsODbL 1.0Underlying road network used to compute the polyline - the scope entry names the segments layer whose geometry is OSM-derived. Not triggered by maneuvers or signs alone - those are produced by Viscar's algorithms, even though their inputs originate in OSM data.
Open-MeteoweatherCC BY 4.0Forecast data shipped largely as-is from the upstream provider.

When required: false

On the build endpoint required is always true - segments[] (OSM-derived geometry) is part of the always-on core and cannot be excluded. The false state is reserved for future endpoints returning only Viscar-original data: there providers is empty, the text field is omitted, and no attribution display is required.

Recommended UI placement

Put it somewhere visible: the bottom-right corner of the map view, a small footer line below an itinerary panel, or a popover on a tap target.

Errors

Every failure response uses the same RFC 9457 application/problem+json envelope - request validation, routing, and API-key / quota / plan checks all share it. Branch on the machine code in the body, not on the HTTP status or the human-readable detail.

Request errors - problem+json envelope

Returned as application/problem+json (RFC 9457) for 400 / 404 / 500. Branch on the machine code - a stable extension member - rather than on the HTTP status or the human-readable detail (which may evolve). status mirrors the HTTP status code.

application/problem+json { "type": "https://viscarapp.com/errors/INVALID_EXCLUDE_OPTION", "status": 400, "detail": "Unknown 'exclude' option(s) for build: tempurature. Allowed: weather, quality.", "code": "INVALID_EXCLUDE_OPTION", "details": { "unknown": ["tempurature"] } }
HTTPCodeWhen
400INVALID_REQUESTMissing or malformed request body.
400MISSING_ORIGINField origin not provided.
400MISSING_DESTINATIONField destination not provided.
400INVALID_COORDINATElat/lon out of range or non-finite.
400INVALID_EXCLUDE_OPTIONUnknown value in exclude array, or an always-on layer listed there.
400INVALID_AVOID_OPTIONUnknown value in avoid array.
400INVALID_PROFILEUnknown value in profile.
400INVALID_COURSEField course not in [0, 360].
404ROUTE_NOT_FOUNDOrigin and destination are valid but no drivable route connects them.
404REGION_NOT_SUPPORTEDOrigin or destination is outside the supported map area (no road within range). details.point is origin or destination.
500ROUTE_BUILD_FAILEDInternal routing error. Retry once before escalating.
500INTERNAL_ERRORUnexpected failure. Safe to retry; if it persists, report it.

Authentication & quota errors

Returned for 401 / 403 / 429 / 503 by the API-key / plan-gating layer, in the same problem+json shape - branch on code. Plan rejections add details.current_plan + details.required_plan.

application/problem+json - 403 plan rejection { "type": "https://viscarapp.com/errors/PLAN_UPGRADE_REQUIRED", "status": 403, "detail": "Your plan tier does not include this endpoint.", "code": "PLAN_UPGRADE_REQUIRED", "details": { "current_plan": "free", "required_plan": "developer" } }
HTTPCodeWhen
401API_KEY_MISSINGNo API key on a plan-gated endpoint.
401API_KEY_INVALIDKey is malformed, unknown, disabled, or revoked.
403PLAN_UPGRADE_REQUIREDPlan tier below the endpoint's requirement. details carries current_plan + required_plan.
429DAILY_LIMIT_EXCEEDEDDaily request quota for your plan exhausted. Resets daily.
503AUTH_SERVICE_UNAVAILABLETransient - the auth service is unreachable. Retry with backoff.
Welcome back
Sign in to your Viscar account
Create account
Start navigating smarter today
Min 8 characters, uppercase, number, symbol
Verify your email
We sent a 6-digit code to your email
Didn't get the code?
Reset password
Enter your email to receive a reset code
Enter reset code
Check your email for the 6-digit code
Didn't get the code?
New password
Choose a strong password for your account
Min 8 characters, uppercase, number, symbol