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.
Quickstart: from zero to your first call
- 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. - Make your first call. No key needed for this one:
curl -s https://pretcarburant.ro/api/v1/preturi - Read what you got. The response is JSON:
status: "ok", today's date indata, current minimum prices per city inrezultate(in RON per liter —benzinais petrol,motorinais diesel,gplis LPG) and national network averages inretele. Without a key you get the top 20 cities and anotafield saying so; with a key in theX-Api-Keyheader the same call returns every city with at least one price (252 right now). A missing price isnull, never0— 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.
| Endpoint | Description | Limit 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-keyPOST /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.
| Parameter | Type | Required | Default | Constraints |
|---|---|---|---|---|
judet | string | optional | — | County filter: full name (with or without diacritics) or plate code, e.g. cluj or CJ |
zeros | string | optional | — | 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
}
]
}
| Field | Type | Meaning |
|---|---|---|
status | string | Always "ok" on success |
data | string | The 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_la | string or null | Moment 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) |
total | integer | Number of cities in rezultate |
rezultate[] | array | Cities: 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[] | array | Networks: nume, slug, culoare (brand hex color), logo (relative path), plus the same five price fields as national averages |
preturi_lipsa | string | Missing-price convention of this response: "null" or "zero" |
nota | string | Only 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}
}
}
| Field | Type | Meaning |
|---|---|---|
preturi | object | Key = fuel type (benzina_standard, benzina_premium, motorina_standard, motorina_premium, gpl) |
preturi.*.min / mediu / max | number or null | National 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.
| Parameter | Type | Required | Default | Constraints |
|---|---|---|---|---|
judet (path) | string | required | — | County name (with or without diacritics) or plate code: cluj, CJ, Timis… |
zeros | string | optional | — | 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"
}
| Field | Type | Meaning |
|---|---|---|
judet | string | Normalized county plate code |
nr_orase | integer | How many cities the average covers |
benzina_standard … gpl | number or null | County 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.
| Parameter | Type | Required | Default | Constraints |
|---|---|---|---|---|
brand | string | optional | — | Case-insensitive brand filter, e.g. petrom, omv |
tip | string | optional | — | One of benzina_standard, benzina_premium, motorina_standard, motorina_premium, gpl, adblue (AdBlue, at some stations only) |
lat | number | optional | — | Geo filter latitude — takes effect together with lon |
lon | number | optional | — | Geo filter longitude; lng is accepted as an alias |
raza | number | optional | 10 | Geo 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.
| Field | Type | Meaning |
|---|---|---|
id | string | Stable 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, adresa | string | Brand, city, county, street address (judet may be an empty string when the source omits it) |
lat, lng | number | WGS84 coordinates |
tip | string | Fuel type of this record (present on every record; adblue at some stations) |
pret | number or null | RON per liter; null = currently no price |
pret_expirat | boolean | Present (true) when the price was withdrawn because its source stopped publishing for over 24 hours |
nesigur | boolean | Present (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_nesigur | string | Only 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_din | string or null | Only 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_la | string | When 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_incoerent | boolean | Present (true) when a premium price was withdrawn for being below standard at the same pump (pret is null) |
nume, franciza, nota_pret, program, servicii, telefon | string / boolean | When 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_coords | boolean | Present (true) when the network's coordinates fall in another county than the declared one, or are approximate |
observat_la | string or null | When 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_km | number | Only 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.
| Parameter | Type | Required | Default | Constraints |
|---|---|---|---|---|
station_id (path) | string | required | — | The id field of a /statii record |
days | integer | optional | 30 | History 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
}
}
| Field | Type | Meaning |
|---|---|---|
labels[] | array of string | Day labels in DD.MM format (display labels, not ISO 8601), one per data point, oldest first |
series | object | Key = fuel type, value = list of prices aligned index-by-index with labels; only fuels the station sells appear |
variatie | object | Price 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.
| Parameter | Type | Required | Default | Constraints |
|---|---|---|---|---|
q | string | required | — | 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 field | Type | Required | Default | Constraints |
|---|---|---|---|---|
start | string | required | — | Departure place name, e.g. "Bucuresti" |
end | string | required | — | Destination place name, e.g. "Brasov" |
tip | string | optional | benzina_standard | Fuel type (same five values as everywhere) |
raza | number | optional | 5 | Corridor 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]]
}
| Field | Type | Meaning |
|---|---|---|
ok | boolean | true on success, false on error (with msg) |
start / end | object | Geocoded endpoints: name, lat, lng |
km, duration_min | number | Route length (km) and driving time (minutes) |
statii[] | array | Stations in the corridor, at most 50, each with dist_ruta (km from the route) |
total_statii | integer | Total 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}
]
}
| Field | Type | Meaning |
|---|---|---|
plan / plan_nume | string | Plan slug and display name (Dedicat for custom keys) |
limita_lunara | integer | Monthly quota (a per-key custom limit overrides the plan's) |
consum_luna | integer | Requests used this UTC calendar month |
total_cereri | integer | Lifetime request count |
creata / ultima_folosire | string or null | Key creation / last use, ISO 8601 UTC |
abonament | boolean | Whether the key is tied to an active subscription |
zilnic[] | array | Last 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 field | Type | Required | Default | Constraints |
|---|---|---|---|---|
statie_slug | string | required | — | Station slug (as in /reviews/<slug>) |
rating | integer | required | — | 1 to 5 |
nickname | string | optional | Anonim | Max 50 characters |
comment | string | optional | — | Max 500 characters |
recaptcha_token | string | conditional | — | 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.
| Parameter | Type | Required | Default | Constraints |
|---|---|---|---|---|
lat | number | required | — | 47.0–55.2 (inside Germany) |
lng | number | required | — | 5.5–15.5 (inside Germany) |
rad | number | optional | 15 | Search 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.
| Code | When it happens | What your client should do |
|---|---|---|
| 401 | An 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. |
| 404 | Unknown 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_url | Read 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_seconds | Wait 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". |
| 400 | Malformed 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 type | Fix 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 unavailable | Treat 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 invalid | Retry 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
- Unit and currency — all Romanian prices are RON per liter; only
/de-statiiis EUR per liter (itsmonedafield says so). - Decimal separator in JSON is always a point (
9.67), regardless of the language of this page — JSON numbers know no locale. Format for display in your own layer. - Missing prices come as
null, never0— details, thepreturi_lipsafield and the?zeros=1escape hatch in the section below. If you compute averages, excludenullvalues first. - Dates and timestamps are ISO 8601 —
dataisYYYY-MM-DD, the server's day when it answered — not the date of the prices (that isactualizat_la); timestamps (actualizat_la,creata,generat_la,next_allowed) carry an explicit offset. One exception: thelabelsof/istoricareDD.MMdisplay labels. - Time zone — quota metering runs on the UTC calendar month, and
X-RateLimit-Resetis a Unix timestamp in UTC. Timestamps in bodies state their own offset; do not assume Romanian local time. - Coordinates — WGS84 decimal degrees, fields
latandlng(query parameterlon, withlngaccepted as alias). - Field names are Romanian and stable —
oras(city),judet(county),retele(networks),statii(stations),pret(price). They are the contract; we do not translate or rename them.
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:
X-RateLimit-Limit— your plan's monthly quota (e.g.10000on Starter).X-RateLimit-Remaining— requests left until the reset (never negative).X-RateLimit-Reset— Unix timestamp (seconds, UTC) of the reset moment: the start of the next UTC calendar month, the actual metering window.
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
}
status—"ok"or"degraded".actualizat_la— the most recent successful write of any price source, ISO 8601 with offset;nullwhen it cannot be determined. Same value asactualizat_laon/preturi,/preturi/minime,/reteleand/judete.vechime_secunde— seconds since that most recent write. Informative only: it does not decide the status.vechime_maxima_secunde— seconds since the oldest successful download among the price sources in service; this one decides the status.vechime_maxima_descarcare_secunde— the same number under its accurate name: it measures downloads, not how old the prices are.observatie_cea_mai_veche— the oldest observation moment among the prices served, as the source publishes it (a date, or date and time);nullwhen no served price has one.preturi_fara_data_observatie— how many served prices carry no observation date.statii_cu_pret— historical field: station×fuel records with a non-empty price, not physical stations.inregistrari_total— all station×fuel records, including missing or expired prices.preturi_disponibile— records with a valid price after expiry;0means no prices are being served.statii_total— distinct physical stations on the map, including those without a price.statii_cu_pret_unice— distinct physical stations with at least one valid price.
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:
/api/v1is stable. Existing fields keep their names, types and meaning. Code written against this page keeps working.- We add without notice. New fields in responses, new optional parameters and new endpoints can appear at any time — parse what you know and ignore what you do not (most JSON libraries already do).
- We do not remove or rename inside v1. A breaking change — removing a field, renaming it, changing its type — would ship as a new version under a new path, with the old one kept running through an announced transition period.
- When behaviour changes compatibly (like missing prices becoming
nullin v1.4 — with the?zeros=1escape hatch), we document it in the version history below, bump theinfo.versionof the OpenAPI spec, and email API subscribers. - No formal SLA on the free tier. We run monitoring and take availability seriously, but uptime percentages are only guaranteed in dedicated enterprise contracts — ask via contact. Use
/healthto observe data freshness yourself.
Version History
- v1.0 (2026-04-01) — initial 5 public endpoints; 5-min cache; open CORS.
- v1.1 (2026-04-14) — bot UA whitelist for AI assistants; per-IP 60 req/min flood cap.
- v1.2 (2026-05-07) — formal public documentation at
/api(RO/EN/HU). - v1.3 (2026-07-26) — self-serve commercial plans with API keys (
X-Api-Keyheader), per-plan monthly quotas and theX-RateLimit-Limit/X-RateLimit-Remainingheaders;/geocodeand/traseu/customendpoints documented. - v1.4 (2026-08-16) — missing prices are returned as
nullinstead of0on/preturi,/preturi/minime,/judeteand/retele; the?zeros=1parameter restores the old format, and thepreturi_lipsafield states which convention each response used. - v1.5 (2026-09-02) — OpenAPI 3.1 specification at
/openapi.json;GET /api/v1/healthhealth-check endpoint; theX-RateLimit-Resetheader, plus quota headers on every keyed response (429 included), withRetry-Aftercomputed to the actual monthly reset. Full developer manual on this page: quickstart, per-endpoint reference with real captured responses, error catalog, code samples in four languages. - v1.6 (2026-09-23) — the public weekly-quota 429 now says where to get a key: additive fields
upgrade_url,upgrade_url_en,message_en,upgrade_mesaj/upgrade_message, plus theRetry-Afterheader and aLinkheader withrel="payment". Existing fields are unchanged. - v1.7 (2026-09-25) — additive
actualizat_lafield on/preturi,/preturi/minime,/reteleand/judete(the moment of the data;datastays the server's day);next_allowedcarries an explicit offset; an unavailable key database answers 503 withRetry-After, not 401; an unknown path under/api/answers a JSON 404; non-finite parameters (nan,inf) and JSON bodies of the wrong type are a 400, not a 500;Retry-Afteron the per-minute 429s. The documentation now states the real caching:no-storeon all of/api/(the "5-min cache" on/preturiwas wrong). - v1.8 (2026-09-28) —
/statii: additivenesigur,motiv_nesigurandnesigur_dinfields on stuck, orphaned or stale prices, which the site keeps out of its minimums;actualizat_lathere too; theadbluevalue oftip, now documented; no more price-less duplicate pins and no records withouttip; internal fields that were not in the specification are no longer sent (app proposals,geo_dedus,geo_incercat,coord_corectata,added_at); approved stations with no price yet no longer appear as a row withouttip. The premium minimum skips premium prices reported equal to the same pump's standard price./traseu/customno longer lists stuck or orphaned prices (merely stale ones stay)./preturi(with a key) and/judetecount only cities with at least one price, and this page states their number from the data, not by hand.
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.