Skip to main content

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 304 refreshes.
  • Get the parcel outline (GeoJSON or a [lat, lng] ring), and KML / GPX / DXF exports.
  • 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=) GET /v1/parcels/{ref} All references (see coverage)
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 Figures in FR, IT and DE (NRW); a note without figures elsewhere
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

Examples

in_warsaw = client.parcels.at_point(52.2297, 21.0122)
in_warsaw["referenciaCatastral"], in_warsaw.get("pais")

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"]

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_units follows nextCursor for 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 as 304 Not Modified, cost nothing and are reused; changed tells 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's quota.

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; 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.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for catastrogps 2.0.0
File Size Uploaded Provenance
catastrogps-2.0.0.tar.gz 19.6 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for catastrogps 2.0.0
File Interpreter ABI Platform
catastrogps-2.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 35.5 kB

Release files / catastrogps-2.0.0.tar.gz

Download URL catastrogps-2.0.0.tar.gz
Size 19.6 kB
Tags Source
SHA-256 checksum
How to use checksums
ab2cd3348daaccb102d9f82c5329e947251fa3949ad818a2e06536f87c6f3b72
BLAKE2b-256 checksum
How to use checksums
3449891a104881fa02a130b6ce750d70cf3914d3bbbaeebcc72b0d12b1eebe9f
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.0.0-py3-none-any.whl

Download URL catastrogps-2.0.0-py3-none-any.whl
Size 15.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0b28af660f439f70983c8dba65559976dee85e818ffd3e0946d12e3761e8f8d4
BLAKE2b-256 checksum
How to use checksums
bf72f5fc294800ea6a002f0180c8894b6d6778c755e0da28e89af4a0f62ee653
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.0.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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page