ÜcretliYol API Documentation

The ÜcretliYol API supports two working models: (1) pricing a route you already have — you build the route, send us its geometry and vehicle details, and we return the highway/bridge/toll cost (POST /toll); (2) pricing by building a route — you send only a start and end point and we build the route server-side (POST /route). Whichever endpoint you use, the cost is always computed over the real itinerary — either the one you sent or the one we built. POST /toll today covers all 7 countries: Turkey, Greece, Bulgaria, Albania, North Macedonia, Serbia and Montenegro — all in the same simple request/response model. POST /classify also produces a class for all 7 of them.

How to get an API key? Sign in and open the API Service tab to request access. Once enabled, you can generate your own key. After the 14-day trial, contact us to move to a paid plan.
📌 Release note (2026-09-08): POST /toll now produces real pricing for Albania (AL), North Macedonia (MK) and Serbia (RS) as well — previously only BG/GR/TR were supported. API Pro and Enterprise plans now default to access for all countries (see the Limits section). If your integration hard-codes a country list, check GET /countries to pick up new countries automatically.
📌 Release note (2026-09-10): Montenegro (ME) added. POST /toll, /site-hesapla and /site-maliyet now produce real pricing for Montenegro — see the "MONTENEGRO" section below. It shows as status: "development" in GET /countries (a temporary label pending a live-route verification — the endpoint is usable today since capabilities.toll: true).
📌 Release note (2026-09-11): POST /classify now produces a class for Montenegro (ME) too — it computes Montenegro's official 5-tier tariff category (ME_CAT_1..ME_CAT_5) from your physical vehicle profile (see the "Classification summary" table below). GET /countries now shows capabilities.classify: true for ME as well.
📌 Release note (2026-09-11): On POST /toll, sinif is now conditionally required for Albania (AL), North Macedonia (MK), Serbia (RS) and Montenegro (ME) — send a valid country-native category (me_kategori / rs_kategori / mk_kategori, or for Albania BOTH al_kategori AND al_kategori5) and you no longer need to send sinif at all. Existing integrations that only ever send sinif are completely unaffected.

Authentication

Send your API key in the X-API-Key header on every request:

X-API-Key: wt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json

Your key is valid only for the countries you selected (or that were later approved by an admin). Requesting an unauthorized country returns ulke_izni_yok. Your key must also be active, not expired, and within its hourly/daily/monthly usage limits.

An account can have only one active key at a time. Clicking "Generate New Key" in the panel immediately invalidates your previous key and issues a new one — this is a key rotation, not a reset. Your remaining hourly/daily/monthly usage allowance is not lost: limits are counted per account, not per key, so rotating a key never resets your usage counters. Cancelling a key deactivates it instantly on both the panel and the server.
⚠️ Designed for server-to-server use only. Keep your API key on your own server, never exposed in a browser or mobile client, and issue the request from there. Also, CORS (browser Origin) is only open for ucretliyol.com — calling directly from your own domain's browser JavaScript will hit a CORS error; issue the request from your own backend instead.

Endpoints

Base URL for all endpoints below:

Base URLhttps://api.ucretliyol.com
FormatJSON (request and response, application/json; charset=utf-8)

Endpoints that don't require a key

EndpointMethodDescription
/GETAPI name, version and endpoint list
/statusGETHealth of the database and routing service
/countriesGETCountry list and which endpoints each one supports today (details below)

Endpoints that require an API key

EndpointMethodUse
/tollPOSTPrices a route you send — the focus of this document. TR, GR, BG, AL, MK, RS, ME
/routePOSTBuilds a route between two points and returns the Turkish (and, if the route enters Greece, Greek) toll cost
/classifyPOSTProduces each country's own vehicle class from a single physical profile

/site-hesapla and /site-maliyet also technically work with the same key (for fuel/logistics cost and a rich screen that returns every country in one call), but they are not designed for general third-party integration and can change without notice. Since /toll alone now covers all 7 countries, we recommend /toll, /route and /classify for your own integration.

GET /countries — countries and capabilities

No key required. Returns each country's overall maturity (status) and which endpoints actually work for it today (capabilities). status: "development" does not mean a country doesn't work at all — some capabilities can be true while status is still "development"; always rely on capabilities to decide whether an endpoint is usable.

{
  "countries": [
    { "code": "TR", "name": "Türkiye", "status": "active",
      "capabilities": { "toll": true, "site_hesapla": true, "site_maliyet": true, "classify": true } },
    { "code": "GR", "name": "Greece", "status": "active",
      "capabilities": { "toll": true, "site_hesapla": true, "site_maliyet": true, "classify": true } },
    { "code": "BG", "name": "Bulgaria", "status": "development",
      "capabilities": { "toll": true, "site_hesapla": true, "site_maliyet": true, "classify": true } },
    { "code": "AL", "name": "Albania", "status": "active",
      "capabilities": { "toll": true, "site_hesapla": true, "site_maliyet": true, "classify": true } },
    { "code": "MK", "name": "North Macedonia", "status": "active",
      "capabilities": { "toll": true, "site_hesapla": true, "site_maliyet": true, "classify": true } },
    { "code": "RS", "name": "Serbia", "status": "active",
      "capabilities": { "toll": true, "site_hesapla": true, "site_maliyet": true, "classify": true } },
    { "code": "ME", "name": "Montenegro", "status": "development",
      "capabilities": { "toll": true, "site_hesapla": true, "site_maliyet": true, "classify": true } }
  ]
}
FieldMeaning
capabilities.tolltrue means this country is accepted by POST /toll
capabilities.classifytrue means POST /classify can produce a class for this country
capabilities.site_hesapla / site_maliyetWhether this country produces real pricing in ÜcretliYol's own internal formats (not a reference for third-party integration)
plannedAn optional field for capabilities not active today but planned for the future (all 7 countries currently have every capability active, so this field doesn't appear for any of them today — it may surface for a new country before it goes fully live)
We recommend polling /countries periodically instead of hard-coding this list — you'll pick up a newly supported country without changing your integration. Romania, Croatia, Hungary and Bosnia-Herzegovina are in progress / on our roadmap — when one goes live it will appear here first, with capabilities.toll:true.

GET /status — health check

No key required. A simple health endpoint you can use in your monitoring/ uptime tooling; confirms the database and routing service (Valhalla) are reachable.

GET /status

{ "status": "ok", "database": true, "valhalla": true }

database is always true in a response (if the database were unreachable the API would already answer every request with 503 — so if you got this far, the database is up). valhalla:false means the routing service is temporarily unresponsive; this can affect endpoints where the server DRAWS the route (/route, /site-hesapla, /site-maliyet) — /toll isn't affected since it never draws a route (you supply it).

Route Submission

Send your route in one of two forms:

koordinatlar (recommended)

Your route points as [longitude, latitude]. Longitude first!

"koordinatlar": [[27.9624, 43.2180], [27.9700, 43.2200], [28.5582, 43.7360]]
geometri (advanced)

An encoded polyline produced with the same algorithm Google popularized, at precision 6 (1e6).

"geometri": "quwdI..._encoded_polyline_p6_..."
⚠️ Don't paste the overview_polyline or steps[].polyline value you get from Google's Directions/Maps API directly into geometri. Google's own API encodes its polyline at precision 5 (1e5) by default; this API expects precision 6 (1e6). A precision mismatch silently decodes to wrong (completely unrelated) coordinates — it doesn't error, the route just maps to the wrong place. If you're working with a route from Google, either re-encode it at precision 6 on your side, or (much simpler and safer) decode the route points and send them as a koordinatlar array instead — this sidesteps the precision issue entirely.
Coordinates are WGS84 decimal degrees and a route must contain at least two points. Denser route points give more accurate results — ideal: a point every 50–100 m. The system looks for toll/segment intersections with exact geometry, WITHOUT a proximity tolerance; a very sparse route can miss a toll booth or bridge line.
⚠️ If koordinatlar and geometri are sent together in the same request, which one is used can vary by country (the encoded geometry is used for Bulgaria, koordinatlar for the others). If the two fields don't represent the same route, different countries can produce different results — it's safest to send only one of them per request.

Use a decimal point per the JSON standard for numbers ("co2_sinifi": 1, 380.5); comma-decimal strings ("380,5") are not a reliable contract.

How should you submit a route your own app already built?

If your own navigation/mapping stack already produces a route (Google Directions, Mapbox Directions, HERE, your own OSRM/Valhalla server, GPS logs from a fleet-tracking system, etc.), POST /toll is built exactly for this — the API does NOT draw its own route; it prices the itinerary you send. What to do:

  1. From your route source (map SDK, directions API, GPS log), extract the ordered list of points that represent the actual road the route follows — not just the start and end point, but the whole itinerary in between. For example, decode Google Directions' overview_polyline into its [lat, lon] points.
  2. Convert every point to [longitude, latitude] order (most map SDKs give you [lat, lon] — ÜcretliYol expects the opposite order, matching GeoJSON).
  3. Send that array as koordinatlar, together with the vehicle fields for whichever country you want priced, to POST /toll.
  4. If the route crosses more than one country (e.g. it leaves Turkey and enters Greece), send the same coordinate array in a separate request per country, with that country's own ulke and arac fields — /toll only prices one country per request.
If you don't have your own route (only a start/end point), we can build it for you server-side — see POST /route below. Use /toll when you already HAVE a route, /route when you DON'T; don't mix the two up.

Physical Vehicle Profile

For consistent integration across countries, we recommend storing a vehicle's physical data in your system as a single profile — this is what the /classify endpoint consumes:

FieldTypeDescription
vehicle_typestringmotorcycle, car, van, bus, truck
axle_countintegerTotal axle count
front_axle_height_cmnumberFront axle height (cm) — do not confuse with total vehicle height
total_height_cmnumberTotal vehicle height (cm)
max_weight_tonsnumberMaximum weight (tons)
seat_countintegerSeat count, used to distinguish bus/minibus
emission_classstringe.g. euro_3, euro_4, euro_5, euro_6
Important rule: If you don't know a physical field that affects the toll class, don't send a guessed value — leave it out and, if needed, ask the user. Don't copy one country's class number (e.g. Turkey's sinif) directly into another country's field; each country has its own classification logic (see below).

Currency and Exchange Rates

Which currency you get back varies by country. The table below shows which currency field(s) appear in each country's response — there is no "choose a currency" parameter, every country always returns ALL of the fields its engine produces; you decide which one to use.

CountryNative currencyResponse field(s)TL equivalent
Turkey (TR)TRYtoll.tryalready TL, no separate field
Greece (GR)EURtoll.eurtoll.tl
Bulgaria (BG)EURtoll.eurtoll.tl
Albania (AL)ALL (Lek)toll.eur and toll.lek (both, always)toll.tl
North Macedonia (MK)MKD (Denar)toll.eur and toll.denar (both, always)toll.tl
Serbia (RS)RSD (Dinar)toll.eur and toll.rsd (both, always)toll.tl
Montenegro (ME)EURtoll.eurtoll.tl
Don't want EUR — just the local currency? For Albania, North Macedonia and Serbia, the response always contains both EUR (toll.eur) and that country's own local currency (toll.lek / toll.denar / toll.rsd) — no extra parameter is needed, just read the field you want. Greece, Bulgaria and Montenegro (Montenegro is officially euroized) don't have a separate "local currency" field because their local currency already IS EUR (toll.eur is already local for all three). Turkey's result is already in TL.
⚠️ If a crossing/facility's eur field is 0 but its local currency field is greater than 0 — this is not a bug: that specific toll/facility's EUR tariff simply hasn't been entered into our database yet. For that record, the local-currency field (lek/denar/rsd) is the authoritative value — use it instead of EUR. If you notice this, let us know and we'll complete that tariff row.

The exchange-rate fields (eur_kur, lek_kur, denar_kur, rsd_kur) tell you the exact rate used to compute the TL equivalent in that specific response. The rate is refreshed periodically from the Central Bank of Turkey's (TCMB) daily bulletin (for EUR and other major currencies) and a supplementary source (for ALL/MKD/RSD, which aren't in the TCMB bulletin, as a cross-rate); if a fresh rate can't be fetched, the system falls back to the last known good rate — so tl is almost never left empty. Don't cache a rate on your side — read each response's own *_kur field, since it can vary slightly between requests.

Required Fields and Response Fields by Country

For POST /toll, each country prices tolls differently. Send the vehicle fields required by the country you're requesting. If something is missing, the API does not calculate — it returns which field is missing and the valid values, so no wrong toll is produced.

BG Bulgaria — distance-based toll road (heavy vehicles)

The toll is computed from the actual road distance travelled and the emission class.

FieldRequiredValues
kategoriYeskamyon_3_5_12, kamyon_12_2_3aks, kamyon_12_4aks, otobus_3_5_12, otobus_12
emisyonYeseuro_0_i_ii, euro_iii_iv, euro_v, euro_vi_eev, zev
co2_sinifiOptional1–4 (only matters for euro_vi_eev); zev automatically uses 5; a missing/out-of-range value falls back to 1

Tonnage/axle info is embedded in the category — don't send it separately. Category names must be sent verbatim as text; don't send Turkey's sinif number to Bulgaria.

// Request
{
  "ulke": "BG",
  "koordinatlar": [[27.9624, 43.2180], [28.1000, 43.4000], [28.5582, 43.7360]],
  "arac": { "kategori": "kamyon_12_4aks", "emisyon": "euro_vi_eev", "co2_sinifi": 1 }
}

// Response
{
  "ok": true,
  "ulke": "BG",
  "toll": {
    "eur": 22.96,
    "tl": 1287.72,
    "toll_km": 109.33,
    "ucretsiz_km": 1.29,
    "dagilim_km": { "otoyol_am": 109.33, "frc1": 0.0, "frc2": 0.0 },
    "birim_eur_km": [0.17, 0.15, 0.14]
  },
  "eur_kur": 56.08,
  "kaynak": "wayid"
}
Response fieldDescription
toll.eur / toll.tlComputed toll (EUR and its TL equivalent at the current rate)
toll.toll_km / toll.ucretsiz_kmTolled classified km and toll-free km
toll.dagilim_kmKm breakdown across motorway (AM/FRC0), FRC1, FRC2
toll.birim_eur_kmUnit price array, fixed order [AM/FRC0, FRC1, FRC2]
eur_kurCurrent EUR rate used for the TL equivalent
kaynakUsually wayid; can be segment or eski_motor if that service is unreachable

For cars and motorcycles, BG's /toll accepts the heavy-vehicle categories above; the passenger-car vignette model is not part of this general API contract.

GR Greece — Highway + Bridge

Highway passes need kategori. If the route crosses a multi-category special facility (e.g. the Rio–Antirrio bridge), extra vehicle details may be required to resolve the right category.

FieldRequiredValues / Notes
kategoriYes1 = Motorcycle · 2 = Car · 3 = Van/minibus/3-axle · 4 = Truck (4+ axles)
arac_tipiIf special facilitybinek, motosiklet, kamyon, otobus — the facility database's canonical type
koltuk_sayisiIf bus/minibusReal seat count — the bridge fare can depend on it
aks_sayisiIf truck2, 3, 4, 5…
yukseklik_cmMay be neededTotal vehicle height, in centimetres (the API converts to metres internally — sending 3.80 is read as "3.80 cm")
romork_varMay be needed0 / 1
engelliOptionaltrue / false — for cars, requests the disabled/blue-card category

If arac_tipi is omitted, the system makes a rough guess from the Turkish class or axle count; sending it explicitly is recommended for the correct facility category.

// Highway request
{
  "ulke": "GR",
  "koordinatlar": [[21.3200, 38.3200], [21.7800, 38.2500]],
  "arac": { "kategori": 3 }
}

// Special facility (bridge): full vehicle profile
{
  "ulke": "GR",
  "koordinatlar": [[21.7600, 38.3000], [21.7800, 38.2500]],
  "arac": {
    "kategori": 3, "arac_tipi": "otobus", "koltuk_sayisi": 50,
    "aks_sayisi": 2, "yukseklik_cm": 380, "romork_var": 0
  }
}
⚠️ Important — Greek bridges: Some bridges such as Rio–Antirrio split fares into up to 9 categories (by vehicle type, seat count, axles, height, trailer) — this list depends on the facility database's content and can change, don't assume it's fixed. If information is missing you get a response like this:
{
  "ok": false,
  "hata": "kopru_bilgi_eksik",
  "ulke": "GR",
  "eksik": ["koltuk_sayisi"],
  "mesaj": "Seat count is required for the Rio-Antirrio bridge.",
  "kopru": "Rio-Antirrio",
  "secenekler": [
    { "etiket": "Bus 20-50 seats", "deger": "20", "ucret": 18.0, "kategori_id": 101 },
    { "etiket": "Bus 51+ seats", "deger": "51", "ucret": 25.0, "kategori_id": 102 }
  ]
}

secenekler is generated from the facility database's real active categories. Flow: (1) ask the user for the field in eksik, (2) put the real physical value into arac, (3) resend the same request with the full profile. Don't add secenekler[].ucret to your total yourself — the authoritative result is the toll.koprular field of a successful request.

For cars, the system never asks a follow-up question: if you omit engelli (or it's unknown), the standard category is applied automatically; send engelli:true only when the vehicle genuinely has a disabled/blue-card permit to get the correct (discounted) category.

// Successful response
{
  "ok": true, "ulke": "GR", "arac": { "kategori": 3 },
  "toll": {
    "eur": 46.5, "tl": 2607.12, "gecis_sayisi": 3,
    "gecisler": [
      { "otoyol": "Olympia Odos", "tip": "cift", "giris": "Toll A", "cikis": "Toll B", "normal": 28.5, "pass": 25.0 }
    ],
    "koprular": [
      { "tesis_id": 12, "tesis_adi": "Rio-Antirrio", "kategori": "Bus 20-50 seats", "ucret": 18.0, "para_birimi": "EUR", "ucret_eur": 18.0 }
    ]
  },
  "eur_kur": 56.067, "kaynak": "gr_kavsak"
}
gecisler[].tipDescription
singleSingle-point fare
ciftDistance fare from a matched entry/exit toll pair
eksikOnly one normal toll was found, no entry/exit pair formed; fare returns 0

On a route with no tolled crossing, you get ok:true, gecis_sayisi:0, empty gecisler/koprular arrays and an informational mesaj — this is not an error.

TR Turkey — Toll booth (class-based)

The HGS/OGS class is required.

FieldRequiredValues
sinifYes1 = Car · 2 = Minibus · 3 = 2-3 axles · 4 = 4-5 axles · 5 = 6+ axles · 6 = Motorcycle
// Request
{
  "ulke": "TR",
  "koordinatlar": [[29.0000, 40.9500], [29.1000, 41.0000]],
  "arac": { "sinif": 1 }
}

// Response
{
  "ok": true, "ulke": "TR", "arac": { "sinif": 1 },
  "toll": {
    "try": 185.5, "gise_sayisi": 2,
    "giseler": [
      { "tip": "GIRIS_CIKIS", "otoyol": "Highway name", "giris_gise": "Entry toll", "cikis_gise": "Exit toll", "ucret": 160.0, "aciklama": "" },
      { "tip": "TEK_NOKTA", "otoyol": "Bridge or tunnel", "giris_gise": "Crossing name", "cikis_gise": null, "ucret": 25.5, "aciklama": "" }
    ]
  },
  "kaynak": "tr_gise"
}
giseler[].tipMeaningIncluded in total?
GIRIS_CIKISEntry and exit toll matchedYes
TEK_NOKTASingle-point/bridge/tunnel fareYes
TARIFE_YOKToll crossing detected but no active fare defineducret is null — do not treat as free

Turkey's total is in toll.try, currency TRY.

AL Albania — Toll/facility (single point)

The category is derived from Turkey's sinif — the engine detects, for each toll/facility it crosses, whether that specific facility's tariff row uses a 4-class or 5-class scheme (e.g. stand-alone facilities like the Llogara Tunnel) and picks the right category itself. For manual control, you can send al_kategori (1-4) and/or al_kategori5 (1-5) — send both, valid, and sinif isn't needed at all (whichever scheme a facility on the route turns out to use, the category is determined directly from these two; sending only one still requires sinif, since the route could cross a facility on the other scheme).

FieldRequiredValues
sinifConditional*1 = Car · 2 = Minibus · 3 = 2-3 axles · 4 = 4-5 axles · 5 = 6+ axles · 6 = Motorcycle (same classification as Turkey — the engine derives Albania's own category from this)
al_kategoriOptional1-4 — manual category selection for a facility with a 4-class tariff
al_kategori5Optional1-5 — manual category selection for a facility with a 5-class tariff (e.g. the Llogara Tunnel)

* sinif is not required only if BOTH al_kategori AND al_kategori5 are sent and valid; otherwise it's required.

// Request
{
  "ulke": "AL",
  "koordinatlar": [[19.9200, 40.2700], [19.7300, 40.1900]],
  "arac": { "sinif": 3 }
}

// Response
{
  "ok": true, "ulke": "AL",
  "arac": { "sinif": 3, "kullanilan_kategori": 2, "kategori_sema": 5 },
  "toll": {
    "eur": 8.0,
    "lek": 985.0,
    "tl": 448.8,
    "gecis_sayisi": 1,
    "gecisler": [
      { "tesis_adi": "Llogara Tunnel", "kategori": 2, "sema": 5, "eur": 8.0, "lek": 985.0 }
    ]
  },
  "eur_kur": 56.1, "lek_kur": 0.61, "kaynak": "al_gise"
}
Response fieldDescription
toll.eur / toll.lekTotal toll; both are always populated (see "Currency and Exchange Rates" above)
toll.tlTL equivalent, computed from EUR if present, otherwise from Lek
arac.kullanilan_kategori / kategori_semaThe category and scheme the engine auto-picked (or your manual override) — a route can cross facilities on different schemes, check each gecisler[] item's own fields for details
eur_kur / lek_kurCurrent rates used for the TL equivalent

On a route with no tolled crossing, you get ok:true, toll.eur/lek/tl:0, an empty gecisler array and an informational mesaj — this is not an error.

MK North Macedonia — Toll booth (single point, exact match)

The category is derived from Turkey's sinif (the official axle + front-axle-height rule isn't captured in today's vehicle profile yet). For manual control, send mk_kategori — send a valid one and sinif isn't needed at all.

FieldRequiredValues
sinifConditional*1 = Car · 2 = Minibus · 3 = 2-3 axles · 4 = 4-5 axles · 5 = 6+ axles · 6 = Motorcycle
mk_kategoriOptional1-5 (number) or North Macedonia's official plate scheme "1a"/"1b"/"2"/"3"/"4" (string)

* sinif is not required if a valid mk_kategori is sent; otherwise it's required.

// Request
{
  "ulke": "MK",
  "koordinatlar": [[22.5479, 41.1352], [21.4233, 42.0151]],
  "arac": { "sinif": 3 }
}

// Response
{
  "ok": true, "ulke": "MK",
  "arac": { "sinif": 3, "kullanilan_kategori": "2" },
  "toll": {
    "eur": 4.5,
    "denar": 277.0,
    "tl": 252.5,
    "gecis_sayisi": 2,
    "gecisler": [
      { "tesis_adi": "Kumanovo", "kategori": "2", "eur": 2.2, "denar": 135.0 },
      { "tesis_adi": "Miladinovci", "kategori": "2", "eur": 2.3, "denar": 142.0 }
    ]
  },
  "eur_kur": 56.1, "denar_kur": 0.91, "kaynak": "mk_gise"
}
Response fieldDescription
toll.eur / toll.denarTotal toll; both are always populated
toll.tlTL equivalent, computed from EUR if present, otherwise from Denar
eur_kur / denar_kurCurrent rates used for the TL equivalent

On a route with no tolled crossing, you get ok:true, toll.eur/denar/tl:0, an empty gecisler array and an informational mesaj — this is not an error.

RS Serbia — Toll booth (entry-exit, distance-based)

The category is derived from Turkey's sinif (the official axle + front-axle-height + trailer rule isn't captured in today's vehicle profile yet). For manual control, send rs_kategori — send a valid one and sinif isn't needed at all.

FieldRequiredValues
sinifConditional*1 = Car · 2 = Minibus · 3 = 2-3 axles · 4 = 4-5 axles · 5 = 6+ axles · 6 = Motorcycle
rs_kategoriOptional1-5 (number) or Serbia's official plate scheme "1a"/"1"/"2"/"3"/"4" (string)

* sinif is not required if a valid rs_kategori is sent; otherwise it's required.

// Request
{
  "ulke": "RS",
  "koordinatlar": [[22.81005, 43.00236], [21.92940, 43.35077]],
  "arac": { "sinif": 1 }
}

// Response
{
  "ok": true, "ulke": "RS",
  "arac": { "sinif": 1, "kullanilan_kategori": "1" },
  "toll": {
    "eur": 6.8,
    "rsd": 797.0,
    "tl": 381.5,
    "gecis_sayisi": 1,
    "gecisler": [
      { "giris": "Dimitrovgrad", "cikis": "Niš Istok", "kategori": "1", "eur": 6.8, "rsd": 797.0 }
    ]
  },
  "eur_kur": 56.1, "rsd_kur": 0.47, "kaynak": "rs_gise"
}
Response fieldDescription
toll.eur / toll.rsdTotal toll; both are always populated. EUR is Serbia's official/trusted source
toll.tlTL equivalent, computed from EUR if present, otherwise from RSD
eur_kur / rsd_kurCurrent rates used for the TL equivalent

On a route with no tolled crossing, you get ok:true, toll.eur/rsd/tl:0, an empty gecisler array and an informational mesaj — this is not an error.

ME Montenegro — Toll booth (single point and entry-exit, mixed model)

The category is derived from Turkey's sinif (the official axle + front-axle-height + trailer rule isn't captured in today's vehicle profile yet). For manual control, send me_kategori — send a valid one and sinif isn't needed at all. Montenegro is already EUR-based, so the response has no separate local currency field (see "Currency and Exchange Rates" above).

FieldRequiredValues
sinifConditional*1 = Car · 2 = Minibus · 3 = 2-3 axles · 4 = 4-5 axles · 5 = 6+ axles · 6 = Motorcycle
me_kategoriOptional1-5 (number or string) — Montenegro's official 5-tier tariff category

* sinif is not required if a valid me_kategori is sent; otherwise it's required.

// Request
{
  "ulke": "ME",
  "koordinatlar": [[19.0904, 42.7731], [19.0187, 42.4304]],
  "arac": { "sinif": 1 }
}

// Response
{
  "ok": true, "ulke": "ME",
  "arac": { "sinif": 1, "kullanilan_kategori": "1" },
  "toll": {
    "eur": 3.5,
    "tl": 196.4,
    "gecis_sayisi": 1,
    "gecisler": [
      { "otoyol": "Bar–Boljare", "tip": "cift", "giris": "Smokovac", "cikis": "Mateševo", "kategori": "1", "eur": 3.5 }
    ]
  },
  "eur_kur": 56.1, "kaynak": "me_gise"
}
Response fieldDescription
toll.eurTotal toll (EUR) — Montenegro's single, official currency
toll.tlTL equivalent, computed with eur_kur
eur_kurCurrent rate used for the TL equivalent
gecisler[].tipsingle: a single-point crossing (e.g. the Sozina Tunnel) · cift: an entry-exit zoned distance model (e.g. the zones on the Bar-Boljare highway)

On a route with no tolled crossing, you get ok:true, toll.eur/tl:0, an empty gecisler array and an informational mesaj — this is not an error.

For AL/MK/RS/ME, sinif isn't a literal copy of the category — if you send it, the engine automatically translates it into that country's own official category scheme (Albania's 4-class/5-class, North Macedonia's 1a/1b/2/3/4, Serbia's 1a/1/2/3/4, Montenegro's 1-5). This is a best-effort estimate and won't always match the real local plate class exactly. If you know the vehicle's real local category (e.g. it's already recorded in your fleet system), use the al_kategori / al_kategori5 / mk_kategori / rs_kategori / me_kategori override fields for better accuracy — send a valid override and sinif isn't needed at all (for Albania, only when BOTH al_kategori AND al_kategori5 are sent; the other three countries need just one field). This way an integration that already knows that country's own category system never has to learn Turkey's classification at all.

POST /classify — universal vehicle classification

Send the "Physical Vehicle Profile" above and get every country's own class in a single request. Useful for integrations that want to convert one vehicle profile into country-specific classes.

// Request
{
  "vehicle_type": "truck", "axle_count": 5, "front_axle_height_cm": 135,
  "total_height_cm": 400, "max_weight_tons": 40, "seat_count": 2,
  "emission_class": "euro_5"
}

// Response
{
  "vehicle": { "vehicle_type": "truck", "axle_count": 5, "front_axle_height_cm": 135,
               "total_height_cm": 400, "max_weight_tons": 40, "seat_count": 2, "emission_class": "euro_5" },
  "classes": {
    "TR": { "durum": "tamam", "sinif": "CLASS_4", "eksik": [] },
    "BG": { "durum": "tamam", "sinif": "BG_TRUCK_4AXLE_PLUS_EURO_5", "eksik": [] },
    "GR": { "durum": "tamam", "sinif": "GR_CAT_4", "eksik": [] },
    "AL": { "durum": "tamam", "sinif": "AL_CAT_5", "eksik": [] },
    "MK": { "durum": "tamam", "sinif": "MK_CAT_4", "eksik": [] },
    "RS": { "durum": "tamam", "sinif": "RS_CAT_4", "eksik": [] },
    "ME": { "durum": "tamam", "sinif": "ME_CAT_5", "eksik": [] }
  }
}

classes contains only the countries your API key is authorized for. durum: "tamam" means the class could be resolved for that country; a non-empty eksik list means the result is not reliable without those fields.

CountryClassification summary
TurkeyMotorcycle→CLASS_6 · 2 axles + front axle ≥130cm→CLASS_2 · 2 axles + unknown/<130cm→CLASS_1 · 3 axles→CLASS_3 · 4-5 axles→CLASS_4 · 6+ axles→CLASS_5
BulgariaMotorcycle→exempt · ≤3.5t→passenger vignette · >3.5t bus→BG_BUS_<EURO> · other heavy vehicle 2/3/4+ axles→BG_TRUCK_<N>AXLE_<EURO>. If emission is unknown the resolver assumes EURO_6 — always send the real emission explicitly to /toll for pricing.
GreeceMotorcycle→GR_CAT_1 · ≤3.5t and height ≤220cm→GR_CAT_2 · 2-3 axles→GR_CAT_3 · 4+ axles→GR_CAT_4
AlbaniaMotorcycle→AL_CAT_1 · car ≤3.5t→AL_CAT_2 · van/bus ≤23 seats→AL_CAT_3 · >23 seats or 2-axle truck→AL_CAT_4 · other 3+ axle heavy vehicle→AL_CAT_5
North MacedoniaApproximate mapping to the official 1a/1b/2/3/4 plate scheme from weight and height thresholds (falls back to weight thresholds if disambiguating info is missing)
SerbiaApproximate mapping to the official 1a/1/2/3/4 plate scheme from weight, height and trailer info
MontenegroMotorcycle→ME_CAT_1 · >3.5t, 2-3 axles→ME_CAT_4 · >3.5t, 4+ axles→ME_CAT_5 · ≤3.5t (or unknown), 3+ axles→ME_CAT_3 · ≤3.5t, 2 axles, front axle <130cm (or total height ≤190cm)→ME_CAT_2 · ≤3.5t, 2 axles, front axle ≥130cm (or total height >190cm)→ME_CAT_3. Matches the official Autoput Bar-Boljare 5-tier tariff exactly (see monteput.me/cjenovnik/).

Sending fields explicitly is the most accurate approach; for backward compatibility, a missing vehicle_type defaults to car and a missing/<2 axle_count defaults to 2 — so a result from an incomplete profile does not guarantee physical accuracy. Require a complete profile before classifying in production.

POST /route — route and Turkish toll between two points

Use this to build a route and compute the Turkish toll cost by giving only a start/end point, without sending ready-made geometry. If you don't have your own route (only two coordinates), this is exactly what you need — we build it with Valhalla/OSRM.

// Request
{
  "from": { "lat": 41.0082, "lon": 28.9784 },
  "to":   { "lat": 38.4237, "lon": 27.1428 },
  "vehicle_class": 1
}

// Response
{
  "distance_km": 485.2, "duration_min": 321, "vehicle_class": 1,
  "tolls": [
    { "tip": "GIRIS_CIKIS", "otoyol": "Highway name", "giris_gise": "Entry", "cikis_gise": "Exit", "ucret": 250.0, "aciklama": "" }
  ],
  "total": 250.0, "currency": "TRY"
}

vehicle_class is clamped to 1–6; if omitted, 1 is used. The route is built with an automobile (auto) profile — it does not reflect heavy-vehicle routing restrictions, so for heavy vehicles it's more accurate to build your own real route and send it to /toll. If your key has GR access and the route enters Greece, an extra greece section may be added to the response; if your key lacks TR access, it may return tr_izin_yok:true. /route only prices Turkey (+ optional Greece) — for Bulgaria/Albania/North Macedonia/Serbia, build your own route and issue a separate /toll request per country.

Code Examples

curl -X POST https://api.ucretliyol.com/toll \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "ulke": "BG",
    "koordinatlar": [[27.9624,43.2180],[28.5582,43.7360]],
    "arac": {"kategori":"kamyon_12_4aks","emisyon":"euro_vi_eev","co2_sinifi":1}
  }'

Java: a reusable client that submits your own route

The class below takes a route produced by your own navigation/mapping stack (Google Directions, Mapbox, HERE, your own OSRM/Valhalla server, fleet GPS logs, etc.) and prices it for any of the seven countries. You fill in the arac map per country (see the "Required Fields by Country" tables above).

import org.json.JSONArray;
import org.json.JSONObject;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.List;
import java.util.Map;

public class UcretliYolClient {

    private final String baseUrl;
    private final String apiKey;
    private final HttpClient http = HttpClient.newHttpClient();

    public UcretliYolClient(String baseUrl, String apiKey) {
        this.baseUrl = baseUrl;
        this.apiKey = apiKey;
    }

    /**
     * Prices a route built by your own application. "route" is the ORDERED list
     * of points the route follows — not just the start/end, but the actual
     * itinerary. Each point is {longitude, latitude} (note: most map SDKs give
     * you latitude/longitude order — this API expects the OPPOSITE order).
     *
     * @param ulke  "TR" | "GR" | "BG" | "AL" | "MK" | "RS"
     * @param route Route points, each {longitude, latitude}
     * @param arac  Vehicle info, which fields depend on the country (e.g. "sinif"
     *              for TR, "kategori"+"emisyon" for BG)
     */
    public JSONObject priceToll(String ulke, List route, Map arac) throws Exception {
        JSONArray koordinatlar = new JSONArray();
        for (double[] point : route) {
            koordinatlar.put(new JSONArray(new double[]{point[0], point[1]}));
        }

        JSONObject body = new JSONObject()
                .put("ulke", ulke)
                .put("koordinatlar", koordinatlar)
                .put("arac", new JSONObject(arac));

        HttpRequest req = HttpRequest.newBuilder()
                .uri(URI.create(baseUrl + "/toll"))
                .header("Content-Type", "application/json")
                .header("X-API-Key", apiKey)
                .POST(HttpRequest.BodyPublishers.ofString(body.toString()))
                .build();

        HttpResponse res = http.send(req, HttpResponse.BodyHandlers.ofString());
        JSONObject data = new JSONObject(res.body());

        if (res.statusCode() != 200 || !data.optBoolean("ok", false)) {
            // On 400/401/403/422 check data's "hata"/"eksik"/"gecerli_degerler"
            // fields — don't blindly retry (see HTTP Status Codes above).
            throw new RuntimeException("ÜcretliYol error (" + res.statusCode() + "): " + data);
        }
        return data;
    }
}

// ---- Usage ----
UcretliYolClient client = new UcretliYolClient("https://api.ucretliyol.com", "YOUR_API_KEY");

// Points from your own navigation SDK, representing the road the route follows
List route = List.of(
    new double[]{27.9624, 43.2180},
    new double[]{28.1000, 43.4000},
    new double[]{28.5582, 43.7360}
);

// TR — sinif required
JSONObject tr = client.priceToll("TR", route, Map.of("sinif", 1));

// GR — kategori required (1-4)
JSONObject gr = client.priceToll("GR", route, Map.of("kategori", 3));

// BG — kategori + emisyon required
JSONObject bg = client.priceToll("BG", route, Map.of(
    "kategori", "kamyon_12_4aks", "emisyon", "euro_vi_eev", "co2_sinifi", 1));

// AL — sinif required; response always includes EUR AND Lek together
JSONObject al = client.priceToll("AL", route, Map.of("sinif", 3));
System.out.println("Albania: " + al.getJSONObject("toll").getDouble("eur") + " EUR / "
    + al.getJSONObject("toll").getDouble("lek") + " Lek");

// MK — sinif required; response always includes EUR AND Denar together
JSONObject mk = client.priceToll("MK", route, Map.of("sinif", 3));

// RS — sinif required; response always includes EUR AND RSD together
JSONObject rs = client.priceToll("RS", route, Map.of("sinif", 1));

HTTP Status Codes and Error Handling

HTTPMeaningWhat to do
200Successful calculation (may still find no tolled crossing)Check the response fields
204CORS OPTIONS preflightDon't expect a body
400Request/field/category/geometry errorShow hata, eksik, gecerli_degerler
401Missing or invalid API keyCheck your key
403Key not authorized for this countryHave the country added to your key
404Endpoint or route not foundCheck the URL and route status
422The pricing engine could not compute a resultLog it; don't blindly retry the same request
429Hourly/daily/monthly limit reachedWait for that window to reset
502Routing service not respondingRetry with controlled backoff
503Database/service temporarily unavailableRetry with controlled backoff
⚠️ The response body shape differs by endpoint — they do not all share one contract: /toll and /classify return {"ok":false,"hata":"..."} on error; /route returns only {"error":"..."} (no ok or hata field); /site-hesapla and /site-maliyet return only {"hata":"..."} (no ok or error field). Write your error parsing accordingly — don't write it for one endpoint and copy it to the others.
CodeerrorMeaning
400eksik_parametreA required field is missing (response lists missing fields + valid values)
400kopru_bilgi_eksikMulti-category bridge needs more vehicle detail (seats/axles/height)
400gecersiz_kategoriInvalid category
400gecersiz_sinifClass outside the 1–6 range
400gecersiz_emisyonInvalid emission
400geometri_eksikNeither coordinates nor polyline sent
400gecersiz_ulkeulke is a value other than TR/GR/BG/AL/MK/RS
401—Invalid or missing API key
403ulke_izni_yokKey not authorized for this country
422hesaplama_hatasiThe engine/map/data calculation failed
429—Hourly request limit reached
429gunluk_limit_asildiDaily request limit reached
429aylik_limit_asildiMonthly request limit reached
{
  "ok": false,
  "hata": "eksik_parametre",
  "ulke": "BG",
  "eksik": ["kategori", "emisyon"],
  "mesaj": "These fields are required for the Bulgaria calculation.",
  "gecerli_degerler": {
    "kategori": ["kamyon_3_5_12", "kamyon_12_2_3aks", "kamyon_12_4aks", "otobus_3_5_12", "otobus_12"],
    "emisyon": ["euro_0_i_ii", "euro_iii_iv", "euro_v", "euro_vi_eev", "zev"]
  }
}
Error vs. "zero cost": HTTP 400/401/403/422/5xx means the operation failed. HTTP 200 + ok:true + eur:0 can mean no toll was found on the route (not an error). In Turkey, giseler[].tip = "TARIFE_YOK" means a crossing was detected but no fare is defined — do not treat it as free. In Greece, gecis_sayisi:0 is a successful result meaning no tolled crossing was found.

Retry strategy: don't retry 400/401/403/422 (repeating without fixing the request body returns the same error); don't retry 429 before the relevant window resets; only retry 502/503 and network timeouts, using controlled (exponential) backoff.

Limits

Each API key is limited by three independent windows: hourly, daily and monthly (a rolling 30 days, not a calendar month). If any window is exceeded you get 429; the response's hata field tells you which window was hit.

PlanHourlyDailyMonthlyCountries available
API Trial (free)1001003,000TR only
API Basic1,500— (no cap)50,000TR, GR (default — other countries can be requested on the signup form, subject to admin approval)
API Pro6,000— (no cap)500,000All countries (TR, GR, BG, AL, MK, RS, ME)
API Enterprise25,000— (no cap)5,000,000All countries (TR, GR, BG, AL, MK, RS, ME)

On paid plans, when the monthly limit is reached the service either stops or continues with an overage fee, depending on the preference you chose on your request form. On the free trial plan, the service always stops when the limit is reached. Rotating your key does not reset these counters — limits are counted per account (see Authentication above). Contact us to raise your limits.

Recommended Integration Flow

  1. Register the vehicle: Store each vehicle's physical fields (see "Physical Vehicle Profile" above) in your own system; derive country classes from real physical data, never from a fixed guess.
  2. Check with /classify: Send the full profile and confirm durum: "tamam" for each country; a non-empty eksik list means that country's class is not reliable yet.
  3. Per-country /toll request: Send the same route once per country, with that country's own fields (TR/AL/MK/RS→sinif, GR→kategori plus bridge fields if needed, BG→kategori+emisyon+co2_sinifi). Never copy a category value from one country into another; for AL/MK/RS, if you know the vehicle's real local category, specify it with the al_kategori/mk_kategori/rs_kategori override fields.
  4. Interpret the response: Check the HTTP status first, then the hata/eksik fields; read the total from the country's own currency field (see "Currency and Exchange Rates" above) and use the TL equivalent only for display; don't treat TARIFE_YOK entries as free; on kopru_bilgi_eksik, ask the user for real physical data, never a guess.
  5. Retries: Only retry 502, 503 and network timeouts, with exponential backoff; never retry 400/401/403/422 or a 429 whose window hasn't reset. The API keeps a written usage record, so don't blindly resend a request if you're unsure whether it was already processed after a timeout.

Frequently Asked Questions

In Bulgaria the toll is based on the actual road distance travelled (unlike the fixed booth/bridge system in Turkey and Greece). ÜcretliYol measures this distance with its own independent engine using real GPS coordinates — it follows the true road geometry instead of approximating with straight lines. Bulgaria's official system may segment the route differently, treat certain junctions/links differently, or simplify the distance. So the two results may not match to the centimetre; the difference is very small and our value is based on the real road length. Turkey and Greece use fixed booth/bridge pricing, so no such difference occurs there — results match exactly.

Whatever your route source is (Google Directions, Mapbox, HERE, your own OSRM/Valhalla server, GPS logs), extract the ordered list of points representing the actual road it follows and send it as koordinatlar in [longitude, latitude] order — not just the start/end, the whole itinerary in between. See "How should you submit a route your own app already built?" and "Code Examples" above for step-by-step guidance and a full Java example. If you don't have your own route, use /route instead and we'll build it.

[longitude, latitude] — longitude first, then latitude. Example: [27.9624, 43.2180]. This matches the GeoJSON standard, but is the OPPOSITE of the [lat, lon] order most map SDKs (Google, Mapbox, etc.) give you — don't forget to convert. Reversing it will map your route to the wrong place, with no error returned.

POST /toll today supports all 7 countries: Turkey (TR), Greece (GR), Bulgaria (BG), Albania (AL), North Macedonia (MK), Serbia (RS), Montenegro (ME). POST /classify also produces a class for all 7 of them. Rather than hard-coding this, we recommend checking GET /countries's capabilities.toll / capabilities.classify fields — you'll pick up a new country or capability automatically (Romania, Croatia, Hungary and Bosnia-Herzegovina are in progress). Specify it via the ulke field on each request; your key works only for the countries it's authorized for.

No extra parameter needed. For Albania, North Macedonia and Serbia, the response always includes both EUR and the local currency together — just read toll.lek (Albania), toll.denar (North Macedonia) or toll.rsd (Serbia) directly and ignore EUR. Greece and Bulgaria's local currency already is EUR. Turkey's result is already TL. See "Currency and Exchange Rates" above for the full breakdown.

No — the toll.eur/toll.lek fields are ALWAYS present in the contract, the engine never omits them. If a specific toll/facility shows 0 EUR but a positive Lek value, that just means that one tariff row's EUR figure hasn't been entered into our database yet — not an engine or API bug, a data gap on that single record. In that case, the Lek value is the authoritative figure — use it; let us know and we'll complete that tariff.

/toll — YOU build the route, we only price it (the API does not draw routes), all 7 countries. /route — you give only a start/end coordinate, we build the route; returns only the Turkish (+ optional Greek) toll cost. /classify — no route needed; it turns a single physical vehicle profile into every country's own class, all 7 of them, usually as a preparation step before deciding what to send to /toll.

No. Generating a new key in the panel immediately invalidates your previous one (an account can only have one active key), but your remaining hourly/daily/monthly usage allowance is not lost — limits are counted per account, not per key. So rotating your key is not a way to reset your limit.

Your key is limited to the countries chosen at signup (or later approved by an admin). Requesting a country outside that scope returns ulke_izni_yok. API Pro and Enterprise plans default to access for all countries. Contact us to add more countries.

When the trial ends your key becomes inactive. For uninterrupted use, higher limits and more countries, move to a paid plan — contact us to reactivate your key.

There are three independent limits: hourly, daily and monthly (see the Limits table above). Whichever one is hit returns 429 with the matching error code (gunluk_limit_asildi / aylik_limit_asildi); once that window rolls over, requests are accepted again. On the free trial plan, the real constraint is the daily cap of 100 requests. You can request higher limits for heavy usage.

Some Greek bridges such as Rio–Antirrio split the fare into up to 9 categories by vehicle type, seat count, axles, height and trailer. We need these to compute the correct bridge fare; if missing, a kopru_bilgi_eksik response tells you which field is required, along with the facility's real active categories (secenekler). Exception: for cars, we never ask a follow-up question — omit engelli to get the standard category automatically, or send engelli:true only when the vehicle genuinely holds a disabled/blue-card permit.

The denser the points, the more accurate the distance. Ideally a point every 50–100 m. Very sparse points (e.g. only turns) can make the distance appear shorter than it is.

It depends on the country: Turkey→TL, Greece/Bulgaria→EUR (+ TL equivalent), Albania/North Macedonia/Serbia→both EUR and the local currency (Lek/Denar/RSD, both always together, + TL equivalent). There is no "choose a currency" parameter — you always get every field that country's engine produces. The current exchange rate is provided in each response's eur_kur field (and lek_kur/denar_kur/rsd_kur where applicable). See "Currency and Exchange Rates" above for the full table.

Upgrading: Test all features during the trial. For permanent access, higher limits and more countries, contact us.