catastrogps
Official Python client for the Catastro GPS API: cadastral parcels from 29 European countries (including the Basque Country and Navarre foral cadastres), with one API key: by cadastral reference in 27, by coordinates in all 29.
- Look up a parcel by its official cadastral reference or by coordinates, with the country detected for you.
- Turn a postal address into parcels in Spain and 26 more countries, ranked by confidence.
- Import every unit of a Spanish building (dwellings, shops, garages) with paging and free
304refreshes. - Get the parcel outline (GeoJSON or a
[lat, lng]ring), KML / GPX exports and the adjoining parcels. - Crops and land use of each part of a parcel (Spain with the foral territories, Austria, Estonia, Greece), with crop names in 10 languages and a free crop code catalogue.
- Spanish provinces, municipalities and streets to build a structured address search.
- Relief, Natura 2000 and climate (Copernicus DEM, EEA, ERA5-Land) and ground motion (Copernicus EGMS, mm/year) for any covered country.
- Solar, agriculture, market, score, value history and comparison for Spain, Portugal, France, Italy and Germany.
- Quota, prepaid balance and per-minute limit of every response, typed exceptions for every API code.
- Two small dependencies (
httpx,typing_extensions), typed, Python 3.9+.
Free tier: 250 calls a month, forever. Failed lookups are not charged. Get a key at parcelgps.com/developers.
Install
pip install catastrogps
Quick start
from catastrogps import CatastroGPS
client = CatastroGPS("pk_live_your_key_here")
parcel = client.parcels.get("9872023VH5797S0001WX")
print(parcel["municipality"], parcel.get("parcelArea"), parcel["latitude"], parcel["longitude"])
CatastroGPS() with no arguments reads CATASTROGPS_API_KEY from the environment. Use it as a context manager to close the connection pool:
with CatastroGPS() as client:
...
Methods
| Method | Endpoint | Countries |
|---|---|---|
parcels.get(ref, country=, lang=) |
GET /v1/parcels/{ref} |
All references (see coverage). Carries subparcelCount and cropSummary |
parcels.subparcels(ref, country=, lang=) |
GET /v1/parcels/{ref}/subparcels |
ES (with PV/NA), AT, EE, GR. 1 unit; 0 when the parcel has none |
parcels.neighbors(ref, country=) |
GET /v1/parcels/{ref}/neighbors |
Spain, and countries where we hold a copy; supported: false (free) elsewhere |
parcels.at_point(lat, lng, country=) |
GET /v1/search/coordinates |
All, including HR, SE and UK (Scotland) |
search_address_candidates(q, street=, number=, municipality=, postcode=, country=, limit=) |
GET /v1/search/address/candidates |
Spain (with Basque Country and Navarre) and 26 more; not SE or HR |
parcels.find_by_address(text) |
POST /v1/search/address/parse |
Spain, central Catastro, live (prefer the candidates search) |
get_units(ref14, country=, previous=), iter_unit_pages |
GET /v1/parcels/{ref14}/units |
ES, PV, NA |
parcels.geometry(ref, country=) |
GET /v1/parcels/{ref}/polygon |
All but UK |
parcels.terrain(ref, country=, lat=, lng=) |
GET /v1/parcels/{ref}/terrain |
All; lat/lng required outside ES, PV, NA, PT, FR, IT, DE |
parcels.ground_motion(ref, country=, lat=, lng=) |
GET /v1/parcels/{ref}/ground-motion |
Same as terrain, inside EGMS coverage |
parcels.solar, parcels.agriculture, parcels.score |
/solar, /agriculture, /score |
ES, PV, NA, PT, FR, IT, DE |
parcels.market(ref, country=) |
/market |
Sales figures in FR, IT and DE (NRW); zonePrices (official province prices) in ES, PV, NA; PT available: false |
parcels.value_history(ref, country=) |
/value-history |
ES, PV, NA, PT, FR, IT, DE, AT |
parcels.compare([(ref, country), ...]) |
POST /v1/parcels/compare |
2 or 3 parcels in ES, PT, FR, IT, DE |
export.file(ref, "kml" | "gpx", country=) |
GET /v1/export/{format} |
Where a geometry exists |
resolve(text, hint=) |
GET /v1/resolve |
Free, no quota: which country and kind a text is |
crop_catalog(lang=) |
GET /v1/reference/crops |
Free, no quota: every cropCode with its name in the 10 languages |
locations.provinces(), locations.municipalities(region), locations.streets(region, municipality, name=) |
GET /v1/locations/... |
Spain, common territory |
Examples
in_warsaw = client.parcels.at_point(52.2297, 21.0122)
in_warsaw["reference"], in_warsaw.get("country")
found = client.search_address_candidates("Damrak 1, 1012 LG Amsterdam", country="NL")
found["candidates"][0]["reference"], found["candidates"][0]["confidence"]
terrain = client.parcels.terrain("10194A00110004")
terrain["relief"]["slope"]["class"], terrain["protectedAreas"]["natura2000"], terrain.get("climate")
brussels = client.parcels.terrain("21004C0123/00", country="BE", lat=50.848139, lng=4.353613)
motion = client.parcels.ground_motion("0745901TG4304N0002KH")
motion["status"], motion.get("groundMotion", {}).get("vertical", {}).get("meanMmYear")
comparison = client.parcels.compare([("9872023VH5797S0001WX", "ES"), ("750560000AB0001", "FR")])
kml_bytes = client.export.file("9872023VH5797S0001WX", "kml")
guess = client.resolve("05102200100005")
guess["candidates"]
crops = client.parcels.subparcels("46063A00100121", lang="en")
for row in crops["subparcels"]:
print(row["cropCode"], row.get("crop"), row.get("cropOriginal"), row["area"], row.get("useClass"))
catalog = client.crop_catalog(lang="en")
names = {crop["code"]: crop["name"] for crop in catalog["crops"]}
around = client.parcels.neighbors("9872023VH5797S")
[n["reference"] for n in around["neighbors"]]
provinces = client.locations.provinces()["provinces"]
streets = client.locations.streets("CIUDAD REAL", "AGUDO", name="GLORIA")["streets"]
Crop names follow lang= (es, ca, en, pt, fr, it, de, pl, nl, cs; see CROP_LANGUAGES), then the Accept-Language header, then English; cropOriginal always keeps the label as the source writes it. The full rows with geometry come from parcels.subparcels; parcels.get only carries subparcelCount and a cropSummary (share of each crop). If the source does not answer, subparcels raises ServiceUnavailableError with code SUB_001 and nothing is charged.
terrain and ground_motion carry a source.attribution string that must be shown next to the data. A ground motion answer with status: "no_data" and reason: "no_reflectors" means the satellite had nothing stable to measure (fields, forest, water): there is no figure, not a zero.
Responses are the data object of the English v1 API (https://api.parcelgps.com/v1) as a dict: reference, municipality, parcelArea, latitude, longitude… Values that come from an official source (land use, addresses) stay in its language. See the API reference and the OpenAPI spec at https://api.parcelgps.com/v1/openapi.json. Version 2 of this SDK talks only to /v1; 1.x used the deprecated Spanish-named /api routes.
Import a building (comunidad de propietarios, Spain)
Address, then finca (14-character reference), then every unit with use, area, participation coefficient, stair, floor and door.
found = client.search_address_candidates("Avenida José Rodríguez de la Borbolla Camoyán 10, Dos Hermanas")
finca = found["candidates"][0]
comunidad = client.get_units(finca["reference"], country=finca["country"])
comunidad["propertyUnitCount"]
for u in comunidad["units"]:
print(u["reference"], u["use"], u["area"], u.get("ownershipShare"), u["staircase"], u["floor"], u["door"])
comunidad["dataSource"], comunidad["attribution"]
refreshed = client.get_units(finca["reference"], previous=comunidad)
refreshed["changed"]
get_unitsfollowsnextCursorfor you (up to 200 units per page).iter_unit_pages(ref)yields page by page.- Cost: one quota unit per unit served in each page, minimum one per page; a page is cut to the quota you have left. Pass the previous result as
previous=to re-import: unchanged pages come back as304 Not Modified, cost nothing and are reused;changedtells you if anything moved. - Basque Country and Navarre (
country="PV"or"NA") are served from our copy of each foral cadastre: pass the foral reference of any unit or the finca key as the reference. The foral open data carry no participation coefficient. - Organisations with a per-finca cap agreed by contract (Startup and Growth) pay at most that cap per finca and period: see
client.last_quota["finca_cap"]({"cap", "used"}), also in each page'squota.
Quota, prepaid balance and limits
client.last_quota is read from the headers of the last response; conditional_request also returns the quota of that exact response.
| Key | Header | Meaning |
|---|---|---|
plan, limit, remaining, resets_at |
X-Quota-Tier, X-Quota-Limit, X-Quota-Remaining, X-Quota-Reset |
Monthly quota of your plan, after this response |
overage: balance, unit_price, remaining_units |
X-Quota-Overage, X-Quota-Balance, X-Quota-Overage-Unit-Price, X-Quota-Overage-Remaining |
Prepaid pay-as-you-go balance in euros, used once the monthly quota is spent |
rate_limit: limit, remaining, reset_seconds |
X-RateLimit-* |
Per-minute burst limit of your key (Free 10, Developer 60, Startup 120, Growth 300) |
finca_cap: cap, used |
X-Quota-Finca-Cap, X-Quota-Finca-Cap-Used |
Only on /units, only with a contract cap |
When the monthly quota is spent and there is no prepaid balance left, the API answers 429 KEY_AUTH_004 (never 402): the client raises QuotaExceededError with the quota of that response, and prepaid_balance_empty is true when your organisation has the prepaid overage on (top up at https://parcelgps.com/app/developer). Only 2xx responses spend quota; 300, 304, 4xx and 5xx are free, and so are 2xx answers without data.
Errors
Every error is a CatastroGPSError with status, code and details:
from catastrogps import (
AmbiguousReferenceError,
CoverageError,
NotFoundError,
PlaceNameError,
QuotaExceededError,
)
try:
client.parcels.get("05102200100005")
except AmbiguousReferenceError as error:
client.parcels.get("05102200100005", country=error.candidates[0]["country"])
except PlaceNameError as error:
print("A place name, not a reference", error.location)
except CoverageError as error:
print(error.country, error.feature, error.supported_countries)
except NotFoundError as error:
print(error.message)
except QuotaExceededError as error:
print("Quota spent", error.quota, error.prepaid_balance_empty)
| Exception | API code / status |
|---|---|
AmbiguousReferenceError |
300 CNV_AMBIGUOUS: bare digits of several countries; candidates lists them |
CoverageError |
422 CNV_COVERAGE: country not covered, or an analysis outside its countries |
PlaceNameError (a ValidationError) |
422 CNV_PLACE_NAME: a place name, with the geocoded location when found |
ValidationError |
400 VALIDATION_ERROR, other 422 |
AuthenticationError / ForbiddenError |
401 KEY_AUTH_001..003, 403 |
NotFoundError |
404 NOT_FOUND |
QuotaExceededError |
429 KEY_AUTH_004 |
RateLimitError |
429 KEY_RATE_002, RATE_LIMIT_EXCEEDED; retry_after |
ServiceUnavailableError |
503 SERVICE_UNAVAILABLE, SUB_001 (subparcels source down, not charged); retry_after |
ServerError, TimeoutError, NetworkError |
5xx, client timeout, network failure |
Timeouts, network errors, per-minute 429s and 502/503/504 are retried up to max_retries times (default 2) with exponential backoff, waiting Retry-After when the API sends it (up to 60 s). An exhausted monthly quota is never retried.
Options
| Argument | Default | |
|---|---|---|
api_key |
CATASTROGPS_API_KEY env var |
Required |
base_url |
https://api.parcelgps.com |
Paths are under /v1 |
timeout |
30.0 seconds |
Official cadastres can be slow |
max_retries |
2 |
|
http_client |
a new httpx.Client |
Inject your own for proxies or tests (httpx.MockTransport) |
Coverage
Measured against production on 1 October 2026. 29 European countries, including the Basque Country and Navarre foral cadastres: by cadastral reference in 27, by coordinates in all 29; in Croatia, Sweden and the United Kingdom the source is partial, as the table shows.
| Code | Country / region | Reference | Coordinates | Address | Notes |
|---|---|---|---|---|---|
ES |
Spain | Yes | Yes | Yes | Units per building |
PV · NA |
Basque Country · Navarre | Yes | Yes | With country="ES" |
Foral cadastres; units per building |
FR · IT |
France · Italy | Yes | Yes | Yes | |
DE |
Germany | Partial | Partial | Partial | 15 of 16 Länder: no Bavaria |
PT |
Portugal | Yes | Partial | Partial | The cadastre does not cover Lisbon, Porto or Coimbra |
AT NL BE PL CZ DK NO FI EE |
Austria, Netherlands, Belgium, Poland, Czechia, Denmark, Norway, Finland, Estonia | Yes | Yes | Yes | Belgium is slow (3-15 s) |
CH |
Switzerland | Yes | Partial | Yes | Not in cantons that publish no parcels (e.g. Vaud) |
LT SI SK BG LU LI IS CY |
Lithuania, Slovenia, Slovakia, Bulgaria, Luxembourg, Liechtenstein, Iceland, Cyprus | Yes | Yes | Yes | |
GR · LV |
Greece · Latvia | With the country set | Yes | Yes | |
IE |
Ireland | With country="IE" |
Partial | Partial | Numeric SP_ID |
SE |
Sweden | Agricultural blocks | Agricultural blocks | No | Not property units |
HR |
Croatia | No | ARKOD only | No | Agricultural parcels of the Paying Agency, not the cadastre |
UK |
United Kingdom | No | Scotland | Scotland |
Not covered: Hungary, Romania and the rest (CoverageError). Terrain and ground motion work wherever a parcel outline exists, with lat/lng outside the seven analysis countries. Data comes from each official source, so availability follows theirs.
Pricing
| Plan | Price | Calls / month | Prepaid overage per 1,000 |
|---|---|---|---|
| Free | €0, forever | 250 | €10 |
| Developer | €19 / month | 5,000 | €5 |
| Startup | €49 / month | 15,000 | €4 |
| Growth | €99 / month | 50,000 | €2.50 |
Overage prices are net (VAT apart) and are charged from a prepaid balance you top up in the portal; with the balance at zero the API answers 429, never debt.
The same key works with the JavaScript SDK (npm install catastrogps), the Go SDK and the MCP server for AI agents (catastro-gps-mcp).
License
MIT
Metadata
Release files for catastrogps 2.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| catastrogps-2.1.0.tar.gz | 24.9 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| catastrogps-2.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 42.4 kB
Release files / catastrogps-2.1.0.tar.gz
| Download URL | catastrogps-2.1.0.tar.gz |
|---|---|
| Size | 24.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bc252dd0d889605c0754d11040f18e20ec51a370732df787f052c91c768bee76
|
|
BLAKE2b-256 checksum How to use checksums |
575fb1ce1cae2f817160d905aad78f273c0b1dca1ebd24036fb00ff5d200afa8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.4
|
Release files / catastrogps-2.1.0-py3-none-any.whl
| Download URL | catastrogps-2.1.0-py3-none-any.whl |
|---|---|
| Size | 17.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
54d26ea4d1ed64fbfc2cfbc9acb1c5321486bf61161386fe944e02fb280f080e
|
|
BLAKE2b-256 checksum How to use checksums |
511f1647990f3d8a1ba18ca22a75191fb09b9b0f789117c0b1955f6be66def98
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.4
|
Security for catastrogps 2.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.