The GeoidLife API
Coordinate transformation with real datum grids, geoid heights, CRS lookup and geospatial file conversion as one JSON API, backed by PROJ and GDAL. The same engines the browser tools use, without the browser.
x-api-key header. Points are [x, y] or [x, y, z], longitude first.Get a free API key
One key per email address. We send the key by email so the address has to be real; posting the same address again re-sends it.
Quick start
Base URL https://api.geoidlife.com/v1
# WGS 84 lon/lat → UTM zone 11N (NAD83), two points, one with a height curl -s -X POST $BASE/transform \ -H "x-api-key: $KEY" -H "Content-Type: application/json" \ -d '{"from":"EPSG:4326","to":"EPSG:26911", "points":[[-118.2437,34.0522],[-118.4,33.9,100]]}'
import requests BASE = "https://api.geoidlife.com/v1" H = {"x-api-key": "YOUR_KEY"} r = requests.post(f"{BASE}/transform", headers=H, json={ "from": "EPSG:4326", "to": "EPSG:26911", "points": [[-118.2437, 34.0522], [-118.4, 33.9, 100]], }) print(r.json()["points"]) # [[385213.89, 3768641.79], [370556.99, 3751950.49, 100.0]]
const BASE = "https://api.geoidlife.com/v1"; const res = await fetch(`${BASE}/geoid`, { method: "POST", headers: { "x-api-key": "YOUR_KEY", "Content-Type": "application/json" }, body: JSON.stringify({ points: [[-118.2437, 34.0522, 100]] }), // lon, lat, ellipsoidal h }); const { points } = await res.json(); console.log(points[0].H, points[0].N); // 135.1082 (NAVD88), -35.1082 (GEOID18 N)
Response
{
"ok": true,
"from": "OGC:CRS84", "to": "EPSG:26911",
"operation": "Inverse of NAD83 to WGS 84 (54) + UTM zone 11N",
"accuracy_m": 2.0,
"grids": [{"name": "us_noaa_cshpgn.tif", "available": true, "url": "https://cdn.proj.org/us_noaa_cshpgn.tif"}],
"candidates": 3, "count": 2, "failed": 0,
"points": [[385213.8945, 3768641.79], [370556.9895, 3751950.49, 100.0]]
}Every response tells you which PROJ operation ran, its published accuracy and the grid files involved, so the number is auditable. Compare that with a browser library that silently applies a null transform.
Try it here
Paste your key, edit the request, run. The call goes straight from your browser to the API (CORS is on); the key is kept only in this tab.
Response
(nothing yet)
POST/transform
Transform coordinates between any two coordinate reference systems. PROJ picks the most accurate operation it knows for the area your points are in, fetching the grid on demand: NADCON5 and HPGN in the US, NTv2 in Canada, Australia and Europe, geocentric and time-dependent Helmert transformations between modern frames.
| Field | Type | Description |
|---|---|---|
| from * | string | Source CRS: EPSG:4326, 4326, ESRI:102645, WKT or a PROJ string. |
| to * | string | Target CRS, same forms. |
| points * | array | Up to 1,000 points as [x, y] or [x, y, z], or {"x","y","z"} objects. x is easting or longitude, y is northing or latitude, z is metres. Null is returned for a point that cannot be transformed. |
| epoch | number | Decimal year for time-dependent frames, e.g. ITRF2014 → NAD83(2011) at 2020.5. |
| only_best | boolean | Default true: fail with 422 rather than silently fall back to a less accurate operation when the best grid cannot be fetched. |
Request
{
"from": "EPSG:4267", // NAD27
"to": "EPSG:4269", // NAD83
"points": [[-118.2437, 34.0522]]
}Response
{
"ok": true, "from": "OGC:CRS27", "to": "OGC:CRS83",
"operation": "NAD27 to NAD83 (7)",
"accuracy_m": 0.15,
"grids": [{"name": "us_noaa_nadcon5_nad27_nad83_1986_conus.tif", "available": true, …}],
"candidates": 2, "count": 1, "failed": 0,
"points": [[-118.24460641, 34.052212394]]
}That is a 100 m shift the old datum needs in Los Angeles, applied with the NADCON5 grid. The first call for a new datum pair in a cold container may take one to three seconds while the grid tiles are fetched; after that it is tens of milliseconds.
POST/geoid
Turn ellipsoidal heights (what GNSS gives you) into orthometric heights (what plans, benchmarks and flood maps use), or go the other way, and get the geoid undulation N at each point. GEOID18 and GEOID12B over the contiguous US, EGM2008 and EGM96 worldwide.
| Field | Type | Description |
|---|---|---|
| points * | array | Up to 1,000 [lon, lat, height] triples, height in metres. |
| model | string | auto (default: GEOID18 inside CONUS, EGM2008 elsewhere), GEOID18, GEOID12B, EGM2008, EGM96. |
| height | string | ellipsoidal (default) — you send h and get H back, or orthometric — you send H and get h back. |
| input_crs | string | Horizontal frame of your lon/lat. Defaults to NAD83(2011) for the GEOID models and WGS 84 for the EGM models, which is what the models are defined on. |
Request
{ "points": [[-118.2437, 34.0522, 100.0]] }Response
{
"ok": true, "model": "GEOID18", "grid": "us_noaa_g2018u0.tif",
"operation": "NAD83(2011) to NAVD88 height (3)",
"units": "metres", "count": 1, "failed": 0,
"points": [{"lon": -118.2437, "lat": 34.0522,
"h": 100.0, "H": 135.1082, "N": -35.1082}]
}h = ellipsoidal height, H = orthometric height (NAVD88 for the GEOID models, EGM height for the EGM models), N = h − H.
GET/crs/{code}
Describe a coordinate reference system: name, datum, ellipsoid, projection, axis order and units, area of use, and on request the WKT2, ESRI WKT1 and PROJJSON definitions. Handy for populating a CRS picker or validating user input before a transform.
| Parameter | In | Description |
|---|---|---|
| code * | path | 26911, EPSG:26911, ESRI:102645, IGNF:LAMB93… |
| format | query | wkt adds WKT2, wkt1 adds ESRI WKT1, projjson adds PROJJSON, all adds all three. |
GET /crs/2229?format=wkt
{
"ok": true, "code": "EPSG:2229", "name": "NAD83 / California zone 5 (ftUS)",
"type": "Projected CRS", "datum": "North American Datum 1983", "ellipsoid": "GRS 1980",
"projection": "Lambert Conic Conformal (2SP)", "units": "US survey foot",
"axes": [{"name": "Easting", "direction": "east", …}, {"name": "Northing", …}],
"area_of_use": {"name": "United States (USA) - California - counties Kern; Los Angeles; …", "bounds": [-121.42, 32.76, -114.12, 35.81]},
"proj4": "+proj=lcc +lat_0=33.5 +lon_0=-118 +lat_1=35.4666666666667 …",
"wkt2": "PROJCRS[\"NAD83 / California zone 5 (ftUS)\", …"
}POST/convert
Convert a geospatial file with GDAL. KMZ, KML, zipped Shapefile, GeoJSON, GPX, CSV and DBF in; Shapefile, KML, GeoJSON or CSV out. You get a download link to a zip, valid for one hour, plus a GeoJSON preview of the input.
| Field | Type | Description |
|---|---|---|
| file * | string | The file, base64-encoded. Up to 6 MB decoded on the Free plan. |
| filename * | string | Original name with extension; the extension drives auto-detection. |
| conversion | string | auto (default) or one of kmz-to-shp, kml-to-shp, kml-to-geojson, shp-to-kmz, shp-to-geojson, geojson-to-shp, geojson-to-kml, gpx-to-shp, gpx-to-geojson, csv-to-shp, dbf-to-csv. |
# bash: base64 the file inline curl -s -X POST $BASE/convert -H "x-api-key: $KEY" -H "Content-Type: application/json" \ -d "{\"file\":\"$(base64 < parcels.kmz)\",\"filename\":\"parcels.kmz\",\"conversion\":\"kmz-to-shp\"}" { "success": true, "conversion_type": "kmz-to-shp", "output_file": "parcels_shapefile.zip", "download_url": "https://…amazonaws.com/output/d5ac96f6_parcels_shapefile.zip?X-Amz-…", "files_created": ["parcels.shp", "parcels.shx", "parcels.dbf", "parcels.prj"], "preview_type": "map", "preview_data": { "type": "FeatureCollection", … } }
This endpoint shares its engine with converter.geoidlife.com, so its response shape is the converter's (success rather than ok). GeoPackage, FileGDB and raster formats are on the list for v1.1.
POST/keys
What the form at the top of this page calls. No key needed. Send {"email": "...", "name": "...", "use_case": "..."} and the key is emailed; the response is {"ok": true, "status": "created"} or "resent". Keys are never returned in the response body.
Errors, limits and fair use
Every error is JSON with the same shape: {"ok": false, "error": {"code": "...", "message": "..."}}.
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_json · missing_crs · bad_point · unknown_model … | Something in the request body; the message says what. |
| 403 | invalid_api_key | No x-api-key header, or a key that is disabled or not on a plan. |
| 404 | unknown_crs · not_found | CRS code PROJ does not know, or no such route. |
| 413 | too_many_points | More than 1,000 points in one call. Batch them. |
| 422 | no_transformation · transform_failed | PROJ has no operation between those CRS, or the best one needs a grid it could not fetch (set only_best:false to accept a fallback). |
| 429 | quota_exceeded · throttled | Quota used (daily on Free, resets 00:00 UTC; monthly on paid plans, resets on the 1st) or more requests per second than the plan allows. Back off and retry, or move up a plan. |
- Quotas count requests, not points: one call with 1,000 points is one request.
- Payloads up to 10 MB;
/convertfiles up to 6 MB decoded on the Free plan. - Timeouts at 29 seconds (the HTTP gateway's limit). Large conversions will move to an asynchronous job endpoint in a later version.
- Fair use: one key per person or project, no reselling, no bulk scraping of the CRS registry. Keys that hammer the service get disabled with an email first.
- Status and versions:
GET /healthreturns the PROJ and pyproj versions; breaking changes will ship as/v2.
Plans
Monthly plans, paid by card through Stripe. The key for your email moves to the new quota within seconds of checkout (if the address has no key yet, we create one and email it). Prices in USD before tax; cancel any time. Quotas are hard caps for now; metered overage at the schedule rates ($0.005 / $0.003 / $0.002 per extra request) comes next.
- 2,000 requests / month
- 5 requests / s
- 1,000 points per call
- All endpoints
- Email support
- 20,000 requests / month
- 10 requests / s
- 1,000 points per call
- All endpoints
- Priority email support
- 120,000 requests / month
- 25 requests / s
- 1,000 points per call
- All endpoints
- Priority support, named contact
You go to Stripe Checkout to pay; your card never touches our servers. Already on a plan? Use the same email and we will email you a link to switch instead of charging you twice.
Need more than Team, an on-premise container, or a specific grid or datum (NATRF2022 is on the roadmap as soon as NGS publishes it)? Email us.
FAQ
Why would I use this instead of proj4js or a library?
Libraries in the browser do not ship datum grids, so NAD27 → NAD83 or an ellipsoid-to-NAVD88 height silently comes back metres off, with no indication. This API runs full PROJ with the official grids and tells you in every response which operation it used and its accuracy. If you already run PROJ or GDAL server-side with grids, you do not need us.
Which coordinate order do I send?
Always x then y: longitude, latitude for geographic CRS, easting, northing for projected ones, regardless of the CRS's official axis order. Heights are metres.
How accurate are the geoid heights?
As accurate as the model: GEOID18 is quoted by NGS at a few centimetres over most of CONUS, EGM2008 at decimetres globally. We interpolate the official grids with PROJ; the model name and grid file are in every response so you can cite them.
Can I call it from a web page?
Yes, CORS is enabled. Keep in mind that a key in page source is public; for anything beyond a demo, proxy through your own server.
How does billing work?
Paid plans are monthly subscriptions through Stripe; we never see your card. After checkout, a webhook moves the key for your email to the plan's quota within seconds (if the address has no key yet we create one and email it). Manage subscription emails you a link to the Stripe customer portal to change plan (prorated), update the card, download invoices or cancel; a cancelled plan runs to the end of the paid month, then the key drops back to Free. Prices are in USD before tax; where we are registered to collect sales tax or VAT, Stripe adds it at checkout.
Where does my data go?
Transform, geoid and CRS requests are processed in memory and only the route, timing and your key id are logged. Converted files sit in a private bucket behind a one-hour link and are deleted after seven days. Everything runs in AWS us-east-1.
Is there an SLA?
Not yet; paid plans get email support and a written SLA is being drafted. The service runs on AWS Lambda and API Gateway with no single server to fall over, and the browser tools at geoidlife.com are always there as a fallback.
