Price estimation
POST /estimate returns a lane's market price as p25/median/p75 with a confidence score, resolved through a four-level cascade. Confidence is computed from data volume and freshness, scaled by the cascade level.
POST /api/v1/estimate returns the market price for a single lane as a percentile
distribution with a confidence score. What the numbers mean conceptually is covered in
APEX › Overview; this page is the exact contract.
Request
curl -X POST https://apex.tovr.eu/api/v1/estimate \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{
"loading_country": "PL",
"loading_postal_code": "50-148",
"unloading_country": "DE",
"unloading_postal_code": "60311",
"distance_km": 700
}'| Field | Type | Required | Description |
|---|---|---|---|
loading_country | string | yes | ISO 3166-1 alpha-2 code (e.g. DE, PL). |
loading_postal_code | string | no | Origin postal code — without it the cascade starts at country level. |
unloading_country | string | yes | ISO 3166-1 alpha-2 code. |
unloading_postal_code | string | no | Destination postal code. |
distance_km | number | yes | Route distance in kilometres; must be > 0. |
Postal codes are optional but valuable: without both, a request can only ever resolve at
country_pair level. With them, the cascade can find exact corridors, reverse corridors, and
postal-prefix regions.
Response
{
"estimated_price_eur": 1168.3,
"estimated_price_per_km": 1.669,
"p25_price_per_km": 1.5935,
"median_price_per_km": 1.669,
"p75_price_per_km": 1.8707,
"confidence_score": 0.87,
"quality_status": "HIGH",
"observation_count": 3496,
"data_freshness_hours": 237,
"match_level": "exact"
}| Field | Type | Description |
|---|---|---|
estimated_price_eur | number | median_price_per_km × distance_km. |
estimated_price_per_km | number | Median EUR per kilometre at the matched level. |
p25_/median_/p75_price_per_km | number | The price distribution at the matched level. |
confidence_score | number | 0.0–1.0; volume + freshness × cascade multiplier (below). |
quality_status | string | HIGH, MEDIUM, LOW, or INSUFFICIENT_DATA. |
observation_count | number | Distinct observations behind the statistics (unweighted). |
data_freshness_hours | number | Hours since the newest observation used. |
match_level | string | exact, reverse, postal_prefix, country_pair, or none. |
The cascade
The estimator resolves each request through four levels, returning the first that has enough data. Confidence is discounted as the match broadens:
| Level | match_level | Min obs | Multiplier | How it matches |
|---|---|---|---|---|
| 1 | exact | 3 | ×1.0 | Both postal codes resolve to a known corridor. |
| 2 | reverse | 3 | ×0.85 | The corridor exists in the opposite direction (B→A proxies A→B). |
| 3 | postal_prefix | 5 | ×0.7 | Aggregates corridors sharing the same 2-digit postal prefixes. |
| 4 | country_pair | 10 | ×0.6 | Average across the whole country pair. |
Every level's statistics use only trusted data: price_per_km > 0, not flagged as an outlier,
and observed prices only (enriched/estimated prices never feed an estimate). Executed-freight
observations are weighted 3× in the averages and percentiles; observation_count reports
distinct observations, unweighted. (Why executed prices weigh more:
Data sources.)
How confidence_score is computed
The estimate's confidence comes from data volume and freshness — not from the corridor aggregate-quality score used elsewhere. For the matched level:
volume = min(observation_count / 100, 1.0) // saturates at 100 observations
freshness = max(1 − data_freshness_hours / 720, 0.1) // penalises data older than 30 days
base = round((volume × 0.6 + freshness × 0.4) × 100) / 100
confidence_score = base × cascade_multiplier // ×1.0 exact … ×0.6 country_pairThe quality_status is derived from confidence_score:
| Status | Condition |
|---|---|
HIGH | confidence_score ≥ 0.5 |
MEDIUM | 0.3 ≤ confidence_score < 0.5 |
LOW | confidence_score < 0.3 |
INSUFFICIENT_DATA | no cascade level met its minimum (see below) |
This is not the corridor aggregate-quality formula (sample size / source diversity / price variance). That score is computed during corridor aggregation and used for analytics — it never reaches an estimate response. The two are compared in Confidence & quality.
INSUFFICIENT_DATA
When no level reaches its minimum observation count, the API still returns 200 OK with all
price fields 0, match_level: "none", and quality_status: "INSUFFICIENT_DATA". This is a
data-coverage statement, not an error — some lanes genuinely have no coverage yet.
Related
- Many lanes at once: Batch estimation.
- The corridor inventory: Corridors.
- Error codes: Errors.
Rate limiting
Each APEX key has a per-minute request budget enforced in a fixed one-minute window. Responses carry the budget state in X-RateLimit headers; exceeding it returns 429 with Retry-After.
Batch estimation
POST /estimate/batch prices up to 500 lanes in one call. Items are deduplicated and all lookups are batched, so a 500-item request is far cheaper than 500 single calls. Items fail independently.