PretCarburant.ro Public API — REST v1

Programmatic access to aggregated Romanian fuel price data. Same data we use on the site, refreshed several times a day, no authentication required. Licensed Creative Commons BY 4.0 — free to use with attribution.

Need more than the free quota? Commercial plans from €19/month — 10,000–1,000,000 requests/month, 3-day trial. See plans →

Quickstart: from zero to your first call

  1. Get a key — or start without one. Every endpoint answers without authentication, with a reduced data set or quota. For the full data set and a monthly quota, subscribe at /en/fuel-price-api — the key (format pcro_live_...) arrives by email within minutes and every plan starts with a 3-day free trial.
  2. Make your first call. No key needed for this one:
    curl -s https://pretcarburant.ro/api/v1/preturi
  3. Read what you got. The response is JSON: status: "ok", today's date in data, current minimum prices per city in rezultate (in RON per liter — benzina is petrol, motorina is diesel, gpl is LPG) and national network averages in retele. Without a key you get the top 20 cities and a nota field saying so; with a key in the X-Api-Key header the same call returns every city with at least one price (252 right now). A missing price is null, never 0 — see data conventions.

That is the whole loop: one GET, one JSON body, field names in Romanian (they are part of the stable contract — we do not rename them). Everything below is detail.

Authentication

Authentication is a single header, sent on every request: X-Api-Key. There is no OAuth, no token exchange, no expiry dance.

curl -s -H "X-Api-Key: pcro_live_YOUR_KEY" https://pretcarburant.ro/api/v1/preturi

Without a key the request does not fail — it succeeds with less: /api/v1/preturi returns the top 20 cities instead of all 252 cities with prices, /api/v1/statii returns a 150-record sample instead of all ~6,000, and the endpoints marked below accept 1 request per week per IP. A valid key removes the truncation and replaces the weekly cap with your plan's monthly quota, plus X-RateLimit-* headers on every response.

A key that is present but invalid or revoked is an explicit 401, not a silent downgrade — a paying client must never be quietly served the public subset. Real response:

{"status": "error", "message": "Invalid or revoked API key."}

Keep the key server-side. Never ship it in browser JavaScript or a mobile app binary — anyone can read it there. If a key leaks, write to contact@pretcarburant.ro and we rotate it.

Available Endpoints

Base URL: https://pretcarburant.ro/api/v1/. All responses are JSON; public read endpoints send Access-Control-Allow-Origin: *. Every response carries Cache-Control: no-store: no intermediate cache (CDN, proxy, browser) keeps it, so every request reaches the server — if you need a cache, keep it on your side; prices change several times a day and the actualizat_la field tells you how fresh they are. A wrong path under /api/ answers 404 with a JSON body, not an HTML page. Each row links to the detailed section for that endpoint.

REST v1 Endpoints
EndpointDescriptionLimit without a key
GET /api/v1/preturi City-aggregated prices + network averages (filter ?judet=CJ) None (top-20 subset)
GET /api/v1/preturi/minime National min/avg/max prices per fuel type None
GET /api/v1/judete/<judet> County-level average prices (e.g. /api/v1/judete/CJ) 1 request/week per IP
GET /api/v1/retele Monitored networks with brand-aggregated national averages 1 request/week per IP
GET /api/v1/statii Station-level records with coordinates, brand, price (filters: ?brand=, ?tip=, geo ?lat=&lon=&raza=); stable per-station id 1 request/week per IP (150-record sample)
GET /api/v1/statie/<station_id>/istoric Daily price history for one station (up to 90 days) 1 request/week per IP
GET /api/v1/geocode City-name autocomplete (?q=cluj, min 2 characters) 1 request/week per IP
POST /api/v1/traseu/custom Stations along an A→B route (JSON body: start, end, tip, raza) 1 request/week per IP
GET /api/v1/cheie Your own key's plan, quota and usage (requires X-Api-Key) — (key required; does not consume quota)
GET /api/v1/reviews/<slug> Public reviews and average rating of one station None
POST /api/v1/review Submit a star rating or a review for a station Per-IP anti-spam limit
GET /api/v1/de-statii Live station prices in Germany (EUR; separate data channel) 30 requests/min per IP
GET /api/v1/push/vapid-key
POST /api/v1/push/subscribe
Web-push plumbing for this site's own service worker None (not meant for integrations)
GET /api/v1/health Data health: freshness and number of records with a price None (no cache)
GET /openapi.json Machine-readable OpenAPI 3.1 spec (alias /api/v1/openapi.json) None (5-min cache, on /openapi.json only)

OpenAPI 3.1 Specification

The whole v1 API is formally described in an OpenAPI 3.1 specification, available at https://pretcarburant.ro/openapi.json (alias: /api/v1/openapi.json). It covers every endpoint, parameter, response shape and error code — import it straight into Postman or Insomnia, generate a client in your language with openapi-generator, or hand it to an AI coding assistant to write the integration for you. Served without rate limiting and with open CORS; /openapi.json carries Cache-Control: public, max-age=300 (5 minutes), the /api/v1/openapi.json alias no-store like the rest of the API.

curl -s https://pretcarburant.ro/openapi.json | jq '.info.version'

Endpoint Reference

One section per endpoint: what it is for, its parameters with real constraints, a runnable request, and the actual response captured from the API (long lists trimmed to their first element — the shape is untouched). Prices are RON per liter unless stated otherwise.

GET /api/v1/preturi — prices per city + network averages

The main endpoint: current minimum prices in each monitored city, plus the national average of each network — everything an app needs to answer "how much is fuel where my user is". No weekly cap (it also feeds the embeddable widget); like the whole API, responses carry Cache-Control: no-store.

ParameterTypeRequiredDefaultConstraints
judetstringoptional—County filter: full name (with or without diacritics) or plate code, e.g. cluj or CJ
zerosstringoptional—1/true/yes/da restores the legacy 0 sentinel for missing prices (see missing prices)
curl -s "https://pretcarburant.ro/api/v1/preturi?judet=B"

Real response without a key (lists trimmed to the first element):

{
  "status": "ok",
  "data": "2026-09-02",
  "total": 20,
  "preturi_lipsa": "null",
  "nota": "Acces public: top 20 orase. Setul complet (252 orase cu preturi) necesita o cheie API: https://pretcarburant.ro/api-preturi-carburanti",
  "rezultate": [
    {
      "oras": "Bucuresti",
      "slug": "bucuresti",
      "judet": "B",
      "lat": 44.42527,
      "lng": 26.01369,
      "benzina": 8.92,
      "benzina_premium": 9.4,
      "motorina": 9.67,
      "motorina_premium": 10.49,
      "gpl": 4.23
    }
  ],
  "retele": [
    {
      "nume": "Petrom",
      "slug": "petrom",
      "culoare": "#E31937",
      "logo": "/static/img/brands/petrom.svg",
      "benzina": 8.92,
      "benzina_premium": 9.4,
      "motorina": 9.73,
      "motorina_premium": 10.5,
      "gpl": 3.72
    }
  ]
}
FieldTypeMeaning
statusstringAlways "ok" on success
datastringThe server's calendar day when it answered, ISO 8601 (YYYY-MM-DD) — not the date of the prices: after midnight, or while a source is down, it still shows today
actualizat_lastring or nullMoment of the last successful price-data update, ISO 8601 with offset — same computation and value as in /health. Added on 2026-09-25 (absent from the captured example above)
totalintegerNumber of cities in rezultate
rezultate[]arrayCities: oras (name), slug, judet (county plate code), lat/lng (WGS84), and five price fields (number or null): benzina, benzina_premium, motorina, motorina_premium, gpl
retele[]arrayNetworks: nume, slug, culoare (brand hex color), logo (relative path), plus the same five price fields as national averages
preturi_lipsastringMissing-price convention of this response: "null" or "zero"
notastringOnly without a key: explains the top-20 truncation

With a key: the same request returns every city with at least one price ("total": 252 right now; cities where we only have stations without a price are left out), the nota field disappears, and the response carries the X-RateLimit-* headers.

GET /api/v1/preturi/minime — national min / avg / max

The cheapest, average and most expensive price in the country for each fuel type — the numbers behind a "prices today" headline. Fully public: no key, no weekly cap. Only the zeros parameter applies. Real response, complete:

curl -s https://pretcarburant.ro/api/v1/preturi/minime
{
  "status": "ok",
  "data": "2026-09-02",
  "preturi_lipsa": "null",
  "preturi": {
    "benzina_standard": {"min": 8.69, "mediu": 8.98, "max": 9.42},
    "benzina_premium": {"min": 8.88, "mediu": 9.6, "max": 9.9},
    "motorina_standard": {"min": 9.27, "mediu": 9.74, "max": 10.49},
    "motorina_premium": {"min": 10.2, "mediu": 10.53, "max": 10.89},
    "gpl": {"min": 3.61, "mediu": 4.34, "max": 5.0}
  }
}
FieldTypeMeaning
preturiobjectKey = fuel type (benzina_standard, benzina_premium, motorina_standard, motorina_premium, gpl)
preturi.*.min / mediu / maxnumber or nullNational minimum / average / maximum, RON per liter
data, actualizat_la—As on /preturi: the server's day and the moment of the data

GET /api/v1/judete/<judet> — county averages

One number per fuel for a whole county — for fleet tools and regional dashboards that do not care about individual cities. Averages over all cities of the county. Weekly public cap without a key.

ParameterTypeRequiredDefaultConstraints
judet (path)stringrequired—County name (with or without diacritics) or plate code: cluj, CJ, Timis…
zerosstringoptional—Legacy 0 sentinel, see missing prices
curl -s https://pretcarburant.ro/api/v1/judete/CJ

Real response, complete:

{
  "status": "ok",
  "data": "2026-09-02",
  "judet": "CJ",
  "nr_orase": 15,
  "benzina_standard": 8.92,
  "benzina_premium": 9.55,
  "motorina_standard": 9.65,
  "motorina_premium": 10.52,
  "gpl": 4.35,
  "preturi_lipsa": "null"
}
FieldTypeMeaning
judetstringNormalized county plate code
nr_oraseintegerHow many cities the average covers
benzina_standard … gplnumber or nullCounty average per fuel type; null when no city had that price
data, actualizat_la—As on /preturi: the server's day and the moment of the data

An unknown county is an explicit 404: {"status": "error", "message": "Judet necunoscut: xx"}.

GET /api/v1/retele — network averages

The current national average of each monitored fuel network — for brand comparisons. Same shape as the retele array of /preturi, without the city data, plus data and actualizat_la with the same meaning. Weekly public cap without a key; only the zeros parameter applies.

curl -s https://pretcarburant.ro/api/v1/retele

Real response (trimmed to two of the 8 networks — note the honest null where OMV does not sell LPG):

{
  "status": "ok",
  "data": "2026-09-02",
  "preturi_lipsa": "null",
  "retele": [
    {
      "nume": "Petrom",
      "slug": "petrom",
      "culoare": "#E31937",
      "logo": "/static/img/brands/petrom.svg",
      "benzina": 8.92,
      "benzina_premium": 9.4,
      "motorina": 9.73,
      "motorina_premium": 10.5,
      "gpl": 3.72
    },
    {
      "nume": "OMV",
      "slug": "omv",
      "culoare": "#003B7E",
      "logo": "/static/img/brands/omv.svg",
      "benzina": 9.02,
      "benzina_premium": 9.65,
      "motorina": 9.82,
      "motorina_premium": 10.64,
      "gpl": null
    }
  ]
}

GET /api/v1/statii — station-level records

The granular data set: one record per station per fuel type (a physical station selling four fuels appears four times). This is what you want for maps, "cheapest near me" features and price monitoring.

ParameterTypeRequiredDefaultConstraints
brandstringoptional—Case-insensitive brand filter, e.g. petrom, omv
tipstringoptional—One of benzina_standard, benzina_premium, motorina_standard, motorina_premium, gpl, adblue (AdBlue, at some stations only)
latnumberoptional—Geo filter latitude — takes effect together with lon
lonnumberoptional—Geo filter longitude; lng is accepted as an alias
razanumberoptional10Geo radius in km, clamped to 0.1–50
curl -s -H "X-Api-Key: pcro_live_YOUR_KEY" \
  "https://pretcarburant.ro/api/v1/statii?brand=petrom&tip=motorina_standard"

Real response with a key (trimmed to the first of 397 records):

{
  "status": "ok",
  "data": "2026-09-02",
  "total": 397,
  "statii": [
    {
      "id": "2f4185850d36",
      "brand": "Petrom",
      "oras": "Campeni",
      "judet": "Alba",
      "adresa": "Str. Libertatii 19A, 515500",
      "lat": 46.3618,
      "lng": 23.04885,
      "tip": "motorina_standard",
      "pret": 9.73
    }
  ]
}

With the geo filter (?lat=46.77&lon=23.62&raza=5) each record gains distanta_km (distance from your point, km) and the list is sorted by it. Without a key the response is a sample capped at 150 records, plus two extra fields captured here for real: "total_disponibil": 6189 (how many records exist) and a nota explaining the sample. With a key you get all of them.

FieldTypeMeaning
idstringStable station identifier — feed it to /statie/<id>/istoric. A price-less pin no longer repeats next to the priced record of the same id + tip pair
brand, oras, judet, adresastringBrand, city, county, street address (judet may be an empty string when the source omits it)
lat, lngnumberWGS84 coordinates
tipstringFuel type of this record (present on every record; adblue at some stations)
pretnumber or nullRON per liter; null = currently no price
pret_expiratbooleanPresent (true) when the price was withdrawn because its source stopped publishing for over 24 hours
nesigurbooleanPresent (true) when the price is served but the site does not rely on it (stuck, orphaned or stale): it is left out of minimums and rankings, and for blocat and observatie_veche also flagged "unreliable price" on the station page. A minimum you compute over the response should exclude these records; the site's national minimum also skips isolated or aberrant prices
motiv_nesigurstringOnly with nesigur: blocat (unchanged for at least 14 days while the rest of the brand's network moved), observatie_veche (the source has not reported it for at least 7 days) or orfan_anpc (Monitorul Prețurilor no longer sends the row)
nesigur_dinstring or nullOnly with nesigur: the day the price has been unchanged since (blocat) or the day of the last observation (observatie_veche); null for orfan_anpc. ANPC dates a price by its reporting day, so observat_la can be recent even on a stuck price
pret_masurat_lastringWhen the source gives it: the measurement date exactly as the source sends it (ANPC day, SOCAR page timestamp). For the age of the served price use observat_la
pret_incoerentbooleanPresent (true) when a premium price was withdrawn for being below standard at the same pump (pret is null)
nume, franciza, nota_pret, program, servicii, telefonstring / booleanWhen the source gives them: the name of manually added stations; SOCAR franchise stations, whose price the network does not publish, with a note (in Romanian); opening hours, services and phone
coord_suspecta, approximate_coordsbooleanPresent (true) when the network's coordinates fall in another county than the declared one, or are approximate
observat_lastring or nullWhen the price was observed, ISO 8601: for Monitorul Prețurilor (ANPC) prices we publish only the reporting day, without the time ("2026-09-22"); for SOCAR's page and manual reports, a timestamp with offset; null = the source does not say, or there is no price. Never the time we downloaded the source
distanta_kmnumberOnly with the geo filter: distance from the requested point

The response also carries actualizat_la next to data: the moment of the last successful price update, ISO 8601 with offset, the same value as in /api/v1/health. Since 2026-09-28 it no longer contains price-less pins left next to a priced record of the same station and fuel, nor records without tip.

GET /api/v1/statie/<station_id>/istoric — price history

Daily price series of one station — for trend charts and "did this station just raise prices?" logic. The station_id is the id field from /statii; nothing else is accepted.

ParameterTypeRequiredDefaultConstraints
station_id (path)stringrequired—The id field of a /statii record
daysintegeroptional30History window in days, capped at 90
curl -s "https://pretcarburant.ro/api/v1/statie/2f4185850d36/istoric?days=7"

Real response, complete:

{
  "status": "ok",
  "station_id": "2f4185850d36",
  "labels": ["27.08", "28.08", "29.08", "30.08", "31.08", "01.09", "02.09"],
  "series": {
    "benzina_standard": [9.51, 9.51, 9.51, 9.51, 9.51, 9.51, 9.55],
    "benzina_premium": [9.99, 9.99, 9.99, 9.99, 9.99, 9.99, 10.03],
    "motorina_standard": [10.14, 10.14, 10.14, 10.14, 10.14, 9.97, 9.97],
    "motorina_premium": [10.91, 10.91, 10.91, 10.91, 10.91, 10.74, 10.74]
  },
  "variatie": {
    "benzina_standard": 0.04,
    "benzina_premium": 0.04,
    "motorina_standard": 0.0,
    "motorina_premium": 0.0
  }
}
FieldTypeMeaning
labels[]array of stringDay labels in DD.MM format (display labels, not ISO 8601), one per data point, oldest first
seriesobjectKey = fuel type, value = list of prices aligned index-by-index with labels; only fuels the station sells appear
variatieobjectPrice change over the requested window, per fuel type (RON; positive = increase)

An unknown id — or a station with no recorded history — is a 404 with a hint in the body: {"status": "error", "message": "Statie necunoscuta sau fara istoric. Foloseste campul `id` din /api/v1/statii."}.

GET /api/v1/geocode — city autocomplete

Turns a partial city name into full names for a search box. At most 10 matches; fewer than 2 characters returns an empty list rather than an error.

ParameterTypeRequiredDefaultConstraints
qstringrequired—Fragment of a city name, minimum 2 characters
curl -s "https://pretcarburant.ro/api/v1/geocode?q=cluj"

Real response, complete: {"results": ["Cluj-Napoca"]}. Note this endpoint has no status field — just results, an array of strings.

POST /api/v1/traseu/custom — stations along a route

Geocodes two place names, computes the driving route between them and returns the fuel stations within a corridor along it — the backend of a "where do I refuel on the way" feature. This is the only v1 endpoint that takes a JSON body, and the only one whose errors use {"ok": false, "msg": "..."} instead of the usual status/message shape — handle it separately.

Body fieldTypeRequiredDefaultConstraints
startstringrequired—Departure place name, e.g. "Bucuresti"
endstringrequired—Destination place name, e.g. "Brasov"
tipstringoptionalbenzina_standardFuel type (same five values as everywhere)
razanumberoptional5Corridor half-width in km, a positive JSON number, capped at 15. A string, null or a non-finite number is a 400
curl -s -X POST https://pretcarburant.ro/api/v1/traseu/custom \
  -H "Content-Type: application/json" \
  -d '{"start": "Bucuresti", "end": "Brasov", "tip": "motorina_standard"}'

Real response (of 153 stations found, statii trimmed to the first — the API itself returns at most 50 — and waypoints to the first two of ~200):

{
  "ok": true,
  "start": {"name": "Bucuresti", "lat": 44.4268, "lng": 26.1025},
  "end": {"name": "Brasov", "lat": 45.655, "lng": 25.611},
  "km": 182.0,
  "duration_min": 165,
  "tip": "motorina_standard",
  "total_statii": 153,
  "statii": [
    {
      "brand": "Rompetrol",
      "oras": "Brasov",
      "adresa": "Str. Fagarasului 2",
      "lat": 45.662296,
      "lng": 25.574169,
      "tip": "motorina_standard",
      "pret": 9.67,
      "dist_ruta": 4.0
    }
  ],
  "waypoints": [[44.425874, 26.102435], [44.428773, 26.103985]]
}
FieldTypeMeaning
okbooleantrue on success, false on error (with msg)
start / endobjectGeocoded endpoints: name, lat, lng
km, duration_minnumberRoute length (km) and driving time (minutes)
statii[]arrayStations in the corridor, at most 50, each with dist_ruta (km from the route)
total_statiiintegerTotal found before the 50-record cap
waypoints[]array of [lat, lng]Downsampled route geometry (~200 points) for drawing on a map

A place that cannot be geocoded, an impossible route or an invalid body (not a JSON object, start/end/tip not strings, raza not a positive number) is a 400, in the same {ok, msg} shape: {"ok": false, "msg": "Nu am gasit locatia"}.

GET /api/v1/cheie — your key's status

Self-service introspection for the key you send: plan, quota, consumption, daily breakdown. It never consumes quota and has no public cap — checking your own usage must not cost anything. Requires X-Api-Key; without a valid key it is a 401 (body: {"status": "error", "message": "Cheie invalida sau revocata."}).

curl -s -H "X-Api-Key: pcro_live_YOUR_KEY" https://pretcarburant.ro/api/v1/cheie

Real response (zilnic trimmed):

{
  "status": "ok",
  "plan": "starter",
  "plan_nume": "Starter",
  "limita_lunara": 10000,
  "consum_luna": 9,
  "total_cereri": 9,
  "creata": "2026-09-02T20:33:07+00:00",
  "ultima_folosire": "2026-09-02T20:33:07+00:00",
  "abonament": false,
  "zilnic": [
    {"zi": "2026-09-02", "count": 9}
  ]
}
FieldTypeMeaning
plan / plan_numestringPlan slug and display name (Dedicat for custom keys)
limita_lunaraintegerMonthly quota (a per-key custom limit overrides the plan's)
consum_lunaintegerRequests used this UTC calendar month
total_cereriintegerLifetime request count
creata / ultima_folosirestring or nullKey creation / last use, ISO 8601 UTC
abonamentbooleanWhether the key is tied to an active subscription
zilnic[]arrayLast 30 days: zi (ISO date) and count

GET /api/v1/reviews/<slug> — station reviews

Public rating and up to 20 approved reviews of one station. The slug is the station's URL slug from the site's /statie/... pages. No key, no cap. A slug with no reviews is still a 200 — real response, complete:

curl -s https://pretcarburant.ro/api/v1/reviews/petrom-campeni-1
{
  "status": "ok",
  "avg_rating": 0,
  "review_count": 0,
  "reviews": []
}

Each element of reviews[] carries the reviewer's nickname, the star rating (1–5), the comment text and the submission date.

POST /api/v1/review — submit a rating

Adds a star rating, optionally with a text review, to a station. A rating alone (stars only) is accepted directly; any free text (a comment, or a custom nickname) additionally requires a reCAPTCHA v3 token and goes through moderation before it appears.

Body fieldTypeRequiredDefaultConstraints
statie_slugstringrequired—Station slug (as in /reviews/<slug>)
ratingintegerrequired—1 to 5
nicknamestringoptionalAnonimMax 50 characters
commentstringoptional—Max 500 characters
recaptcha_tokenstringconditional—Required whenever free text is present
curl -s -X POST https://pretcarburant.ro/api/v1/review \
  -H "Content-Type: application/json" \
  -d '{"statie_slug": "petrom-campeni-1", "rating": 5}'

Real response, complete:

{
  "status": "ok",
  "message": "Multumim pentru rating!",
  "avg_rating": 5.0,
  "review_count": 1
}

Errors: 400 for a missing slug, an invalid rating, a body that is not a JSON object or text fields of another type (e.g. {"status": "error", "message": "Slug statie lipsa."} — captured for real), 403 when reCAPTCHA verification fails, 429 when the same IP submits too many reviews.

GET /api/v1/de-statii — stations in Germany

Live station prices in Germany, for the Romanian diaspora — a proxy over Tankerkönig/MTS-K open data (CC BY 4.0). This is a completely separate channel from the Romanian data: prices are in EUR per liter and never mix into the national aggregates. Limited to 30 requests per minute per IP; no weekly cap and no key needed.

ParameterTypeRequiredDefaultConstraints
latnumberrequired—47.0–55.2 (inside Germany)
lngnumberrequired—5.5–15.5 (inside Germany)
radnumberoptional15Search radius in km, clamped to 1–25
curl -s "https://pretcarburant.ro/api/v1/de-statii?lat=52.52&lng=13.40&rad=2"

Real response (trimmed to the first of 3 stations):

{
  "status": "ok",
  "tara": "DE",
  "moneda": "EUR",
  "sursa": "Tankerkönig / MTS-K (CC BY 4.0)",
  "atribuire": "https://www.tankerkoenig.de",
  "lat": 52.52,
  "lng": 13.4,
  "rad_km": 2.0,
  "nr_statii": 3,
  "generat_la": "2026-09-02T23:35+03:00",
  "statii": [
    {
      "nume": "TotalEnergies Berlin",
      "brand": "TotalEnergies",
      "lat": 52.528899,
      "lng": 13.41808,
      "dist_km": 1.6,
      "benzina": 2.249,
      "e10": 2.189,
      "motorina": 2.269,
      "deschis": true,
      "adresa": "Prenzlauer Allee 1-4",
      "oras": "Berlin",
      "cod_postal": 10405
    }
  ]
}

Fields: benzina is E5, e10 is E10, motorina is diesel (all EUR/liter, null when the station does not sell that fuel); deschis = open right now; dist_km = distance from your point. Responses are kept for 10 minutes on the server, keyed on coordinates rounded to 0.01 degrees and the radius rounded to the km — lat/lng may be those of the request that filled the cache, and generat_la says when. Errors: 400 for missing, non-finite (nan, inf) or out-of-Germany coordinates ({"status": "error", "message": "Coordonatele trebuie să fie în Germania."}), 429 over 30 requests/minute (with a Retry-After header), 502 when the upstream source does not answer, 503 when the service is temporarily unavailable.

GET /api/v1/push/vapid-key and POST /api/v1/push/subscribe — web push

These two exist for this site's own service worker (browser price alerts) and are documented for completeness — they are not meant for commercial integrations. GET /api/v1/push/vapid-key returns the public VAPID key (real response: {"status": "ok", "publicKey": "BE7ueOZzVNlQLqeFLGAb8Oq6rLc2sIhf8_j9gcl5FTgYwo9eB2YX8u23O0HaIO9NXJGDLM-ClmPHqeyz3mIAHrQ"}). POST /api/v1/push/subscribe registers a browser push subscription: required body fields endpoint, p256dh, auth; optional pagina — the path of the page the visitor subscribed on, from which the server derives the city and fuel of the subscription (without it, or on a page with no city, the subscription is national). The old fields tip_carburant, prag_pret and oras are accepted but ignored since 2026-09-23. The response returns the derived oras and tip_carburant (null when there is none). Missing required fields → 400 (real body: {"status": "error", "message": "Missing endpoint, p256dh or auth"}).

cURL Examples

City-aggregated prices

curl -s https://pretcarburant.ro/api/v1/preturi | jq '.rezultate[0]'

Filter by county

curl -s "https://pretcarburant.ro/api/v1/preturi?judet=CLUJ" | jq '.rezultate'

National minimum prices

curl -s https://pretcarburant.ro/api/v1/preturi/minime | jq '.preturi'

Petrom stations selling diesel

curl -s "https://pretcarburant.ro/api/v1/statii?brand=petrom&tip=motorina_standard" | jq '.statii | length'

Stations within 10 km (lat/lon)

curl -s "https://pretcarburant.ro/api/v1/statii?lat=46.77&lon=23.62&raza=10" | jq '.total'

County average price

curl -s https://pretcarburant.ro/api/v1/judete/CLUJ | jq

Code Samples

The same call in four languages: county-filtered prices with an API key, with error handling and quota-header reading — not just the happy path. Replace pcro_live_YOUR_KEY with your key.

curl

curl -s -w '\nHTTP %{http_code}\n' \
  -H "X-Api-Key: pcro_live_YOUR_KEY" \
  "https://pretcarburant.ro/api/v1/preturi?judet=CJ"

Python (requests)

import requests

resp = requests.get(
    "https://pretcarburant.ro/api/v1/preturi",
    params={"judet": "CJ"},
    headers={"X-Api-Key": "pcro_live_YOUR_KEY"},
    timeout=10,
)

remaining = resp.headers.get("X-RateLimit-Remaining")
if remaining is not None and int(remaining) < 100:
    print(f"Warning: only {remaining} requests left this month")

if resp.status_code == 401:
    raise SystemExit("API key invalid or revoked")
if resp.status_code == 429:
    retry_after = int(resp.headers.get("Retry-After", "3600"))
    raise SystemExit(f"Quota exceeded, retry in {retry_after} s")
resp.raise_for_status()

body = resp.json()
for city in body["rezultate"]:
    # A missing price is None -- skip it, never treat it as 0
    if city["motorina"] is not None:
        print(city["oras"], city["motorina"], "RON/L")

JavaScript (fetch, Node 18+)

const resp = await fetch(
  "https://pretcarburant.ro/api/v1/preturi?judet=CJ",
  { headers: { "X-Api-Key": "pcro_live_YOUR_KEY" } }
);

const remaining = resp.headers.get("x-ratelimit-remaining");
if (remaining !== null && Number(remaining) < 100) {
  console.warn(`Only ${remaining} requests left this month`);
}

if (resp.status === 401) throw new Error("API key invalid or revoked");
if (resp.status === 429) {
  const retryAfter = resp.headers.get("retry-after");
  throw new Error(`Quota exceeded, retry in ${retryAfter} s`);
}
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);

const body = await resp.json();
for (const city of body.rezultate) {
  // A missing price is null -- skip it, never treat it as 0
  if (city.motorina !== null) {
    console.log(city.oras, city.motorina, "RON/L");
  }
}

PHP (curl)

<?php
$ch = curl_init("https://pretcarburant.ro/api/v1/preturi?judet=CJ");
$headers = [];
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 10,
    CURLOPT_HTTPHEADER => ["X-Api-Key: pcro_live_YOUR_KEY"],
    CURLOPT_HEADERFUNCTION => function ($ch, $line) use (&$headers) {
        $parts = explode(":", $line, 2);
        if (count($parts) === 2) {
            $headers[strtolower(trim($parts[0]))] = trim($parts[1]);
        }
        return strlen($line);
    },
]);
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status === 401) { exit("API key invalid or revoked\n"); }
if ($status === 429) {
    exit("Quota exceeded, retry in " . ($headers["retry-after"] ?? "?") . " s\n");
}
if ($status !== 200) { exit("HTTP $status\n"); }

if (isset($headers["x-ratelimit-remaining"]) && (int)$headers["x-ratelimit-remaining"] < 100) {
    fwrite(STDERR, "Only {$headers['x-ratelimit-remaining']} requests left this month\n");
}

$body = json_decode($raw, true);
foreach ($body["rezultate"] as $city) {
    // A missing price is null -- skip it, never treat it as 0
    if ($city["motorina"] !== null) {
        echo $city["oras"] . " " . $city["motorina"] . " RON/L\n";
    }
}

Error Catalog

Every error body below was captured from the API, verbatim (messages are in Romanian — they are part of the observed contract). Route on the HTTP status code, not on message text.

HTTP error codes
CodeWhen it happensWhat your client should do
401An X-Api-Key header was sent, but the key is invalid or revoked (a missing key is never a 401 — you get the public subset instead)Do not retry. Check the key for typos or truncation; if it was working yesterday, contact us — it may have been rotated.
404Unknown county on /judete/<judet>, unknown/history-less station id on /statie/<id>/istoric, or a path that does not exist under /api/ (JSON body {status, message, message_en, docs}, not an HTML page)Fix the identifier: county from name or plate code, station id from the id field of /statii. Do not retry unchanged.
429 (plan quota)Valid key, but the plan's monthly quota is used up — the body has limit and upgrade_urlRead Retry-After (seconds to the monthly reset) and back off, or upgrade the plan. Do not hammer: the counter only resets at the start of the next UTC month.
429 (public quota)No key, on a weekly-capped endpoint, second request within 7 days — the body has next_allowed and retry_after_secondsWait until next_allowed (or the Retry-After header), or get a key — this cap exists precisely so the free tier stays free. The plans are linked from upgrade_url_en and from the Link header with rel="payment".
400Malformed input: unroutable places on /traseu/custom, missing/invalid review fields, coordinates outside Germany on /de-statii, a non-finite number (nan, inf) in a parameter, a JSON body that is not an object, or a text field of another typeFix the request body or parameters; the message says which field.
503/health when a price source in service is older than 24 h, none is left, or no prices are served; /de-statii when that service is unavailableTreat our data as unavailable and fall back or alert; poll /health (it is uncached and free) until it returns 200.
503 (with a key)You sent X-Api-Key, but key verification is temporarily unavailable on our side — the key is not declared invalidRetry after Retry-After seconds (same number as retry_after_seconds in the body).

401 — invalid or revoked key

{"status": "error", "message": "Invalid or revoked API key."}

404 — unknown county / unknown station

{"status": "error", "message": "Judet necunoscut: xx"}
{"status": "error", "message": "Statie necunoscuta sau fara istoric. Foloseste campul `id` din /api/v1/statii."}

429 — plan quota exceeded (with a key)

Comes with X-RateLimit-Limit, X-RateLimit-Remaining: 0, X-RateLimit-Reset and Retry-After headers:

{
  "status": "error",
  "message": "Cvota lunara a planului (10000 cereri) a fost depasita.",
  "limit": 10000,
  "upgrade_url": "https://pretcarburant.ro/api-preturi-carburanti"
}

429 — public weekly quota (without a key)

Comes with a Retry-After header and a Link header with rel="payment" pointing to the plans:

{
  "message": "Rate limit: 1 cerere pe saptamana. Urmatoarea cerere permisa peste 6z 23h.",
  "message_en": "Rate limit: 1 request per week per IP for this endpoint without an API key. Next request allowed in 6d 23h.",
  "next_allowed": "2026-09-09T23:33:07.255254+03:00",
  "retry_after_seconds": 604799,
  "status": "error",
  "upgrade_mesaj": "O cheie API inlocuieste limita saptamanala cu cota lunara a planului (de la 10.000 cereri/luna).",
  "upgrade_message": "An API key replaces the weekly limit with a monthly plan quota (from 10,000 requests/month).",
  "upgrade_url": "https://pretcarburant.ro/api-preturi-carburanti",
  "upgrade_url_en": "https://pretcarburant.ro/en/fuel-price-api"
}

503 — degraded data on /health

The body has the same 12 fields as on 200 (see health), with status: "degraded".

503 — key verification temporarily unavailable (with a key)

Exact body; comes with a Retry-After: 30 header:

{
  "message": "Verificarea cheii API e temporar indisponibila. Reincearca peste 30 de secunde.",
  "message_en": "API key verification is temporarily unavailable. Retry in 30 seconds.",
  "retry_after_seconds": 30,
  "status": "error"
}

404 — unknown path under /api/

Exact body; 410 has the same shape:

{
  "docs": "https://pretcarburant.ro/api",
  "message": "Endpoint inexistent.",
  "message_en": "Not found.",
  "status": "error"
}

One deliberate exception: /traseu/custom reports its errors as {"ok": false, "msg": "Nu am gasit locatia"} — different shape, same 400 status.

Data Conventions

Rate Limits and AI Bot Policy

Anonymous users hit 1 request/week/IP on rate-limited endpoints (/judete, /retele, /statii, /istoric, /geocode, /traseu/custom). The /preturi and /preturi/minime endpoints are rate-limit-free, and so are /health, /reviews and /openapi.json. A valid API key replaces the weekly cap with your plan's monthly quota on all of them.

AI assistants (ChatGPT, Claude, Perplexity, Gemini, Bing Copilot, etc.) are bypassed via an explicit User-Agent whitelist. They get a 60 req/min/IP cap instead (shared by all server processes; over it, a 429 with Retry-After), so they can cite our data directly in answers. Whitelisted UAs include: GPTBot, ChatGPT-User, OAI-SearchBot, ClaudeBot, anthropic-ai, PerplexityBot, Google-Extended, Googlebot, Applebot-Extended, Bytespider, Meta-ExternalAgent, CCBot, MistralAI, cohere-ai, YouBot, DiffBot, Bravebot and others.

The X-RateLimit-* Quota Headers

Every request carrying a valid API key (X-Api-Key header) gets three response headers telling you exactly where you stand with your monthly usage — on every endpoint, public ones included:

The headers also come on the quota-exceeded 429 response — exactly when you need them most — together with Retry-After: the number of seconds until the quota resets, computed for real, not a fixed value. Reading them:

curl -s -D - -o /dev/null -H "X-Api-Key: YOUR_KEY" \
  https://pretcarburant.ro/api/v1/statii

HTTP/2 200
X-RateLimit-Limit: 10000
X-RateLimit-Remaining: 9977
X-RateLimit-Reset: 1790812800

1790812800 means 1 October 2026, 00:00 UTC. When X-RateLimit-Remaining approaches zero, it is time to move to a bigger plan — after the reset the counter starts again from the full quota.

Response Format

All responses share this shape:

{
  "status": "ok",
  "data": "2026-05-07",
  "preturi_lipsa": "null",
  "rezultate": []
}

On error:

{
  "status": "error",
  "message": "Rate limit: 1 request per week...",
  "retry_after_seconds": 543210
}

Missing prices: null, not 0

When we do not have a fuel price for a city or a network, the field comes back as null. No fuel costs zero RON — the real ranges are 5.50–12.00 RON/L for petrol, 5.50–13.00 for diesel and 2.50–6.00 for LPG — so a 0 would not be a measurement but a made-up value that drags down any average computed over our response. The rule applies to /preturi (both rezultate[] and retele[]), /preturi/minime (the min, mediu, max fields), /judete/<judet> and /retele.

Only price fields are converted. A 0 in total, nr_orase, lat or lng stays 0, because there zero is a real answer, not a missing value. The pret field in /statii was already null before this change.

{
  "oras": "Turda", "slug": "turda", "judet": "CJ",
  "lat": 46.5667, "lng": 23.7833,
  "benzina": 7.15, "benzina_premium": null,
  "motorina": 7.40, "motorina_premium": null, "gpl": null
}

For integrations that cannot be updated alongside us, ?zeros=1 (?zeros=true also works) restores the exact previous behaviour, with 0 instead of null:

curl -s "https://pretcarburant.ro/api/v1/preturi?zeros=1" | jq '.rezultate[0]'

Every response declares its own convention in the preturi_lipsa field: "null" (default) or "zero" (with ?zeros=1). Read it in code instead of assuming — it is the only reliable way to know what you got. The conversion only goes one way: a null never becomes 0, not even with ?zeros=1, because /judete/<judet> already answered null when it had nothing to average.

Health Check: /api/v1/health

For monitoring before and after going to production: GET /api/v1/health answers without authentication, without rate limiting and with Cache-Control: no-store — a cached health check would hide exactly the incident you are trying to detect.

Real response (HTTP 200, 2026-09-25):

{
  "actualizat_la": "2026-09-24T21:15:07.097831+00:00",
  "inregistrari_total": 6656,
  "observatie_cea_mai_veche": "2026-09-21",
  "preturi_disponibile": 5481,
  "preturi_fara_data_observatie": 0,
  "statii_cu_pret": 5481,
  "statii_cu_pret_unice": 1348,
  "statii_total": 1705,
  "status": "ok",
  "vechime_maxima_descarcare_secunde": 19324,
  "vechime_maxima_secunde": 19324,
  "vechime_secunde": 2816
}

The HTTP status code tells the whole story: 200 = every price source in service wrote within the last 24 hours and prices are being served; 503 (with status: "degraded") = at least one price source in service is older than 24 hours (one live source does not hide a dead one), no price source is in service any more, or no prices are being served. Sources retired on purpose are not judged. A monitor that only checks the status code is enough; on 503 the body keeps the same 12 fields.

License and Attribution

Data is published under Creative Commons BY 4.0. Free to use commercially or non-commercially with visible attribution: "Source: PretCarburant.ro (https://pretcarburant.ro)".

The license covers every /api/v1 response, with or without an API key: a subscription pays for access (the full dataset and the monthly quota), not for a different license. Every response also states it in the Link header with rel="license".

The full dataset is also published on Zenodo with DOI: 10.5281/zenodo.19560194. Organization identity on Wikidata: Q139285387.

Commercial / high-volume access

The free CC-BY 4.0 tier stays unchanged: 1 request/week per IP on rate-limited endpoints, with visible attribution. We are not closing or capping it — public data stays public.

For apps, fleets, newsrooms and any high-volume integration, there are commercial plans with instant activation — your API key arrives by email within minutes of subscribing:

  • Starter — €19/month, 10,000 requests/month
  • Pro — €49/month, 100,000 requests/month
  • Business — €149/month, 1,000,000 requests/month, priority support

All plans include a 3-day free trial, access to every endpoint without the weekly quota, and X-RateLimit-* headers for usage monitoring. See plans and subscribe →

Special needs (contractual SLA, bulk export, historical snapshots, extended rights without attribution)? Write to us for a dedicated quote.

Versioning and Compatibility

What we promise — and only what we can actually keep:

Version History

Contact and Bug Reports

Email: contact@pretcarburant.ro. For technical bugs or feature requests, prefix the subject with „[API]". We reply within 48 business hours.

Responsible disclosure for security issues: see /.well-known/security.txt.