Recipes
Worked examples that do something useful, rather than one call at a time.
The endpoint reference tells you what each call returns. This page shows what to do with them.
Summarise the ground across a project
Every factual report needs the table that says which strata were found, in how many holes, and over what depths. It is usually built by opening every log and tallying by hand.
Find the project
curl -H "Authorization: Bearer $KEY" \
"https://api.gimlabs.io/v1/projects?limit=200"Take the id of the one you want. Your own reference, like GL-2201, is in
projectId.
Check it has geology
curl -H "Authorization: Bearer $KEY" \
"https://api.gimlabs.io/v1/projects/6512a4b8c9d0e1f234567801"The tables array tells you what the project holds, so you can stop early if
there is no GEOL:
"tables": [
{ "code": "LOCA", "rows": 18, "name": "Location Details" },
{ "code": "GEOL", "rows": 132, "name": "Field Geological Descriptions" }
]Pull the geology and group it
curl -H "Authorization: Bearer $KEY" \
"https://api.gimlabs.io/v1/projects/6512a4b8c9d0e1f234567801/tables/GEOL?limit=200"Every row carries the borehole it came from, so no second lookup is needed:
{
"id": "6512a4b8c9d0e1f2345678ab",
"table": "GEOL",
"locationId": "BH01",
"data": { "GEOL_TOP": "0.00", "GEOL_BASE": "0.35",
"GEOL_GEOL": "MADE GROUND", "GEOL_DESC": "..." }
}The whole thing
No SDK and no dependencies in any of these. Each uses whatever HTTP and JSON its standard library already ships.
import json, os, statistics, urllib.parse, urllib.request
BASE = "https://api.gimlabs.io/v1"
KEY = os.environ["GIMLABS_API_KEY"]
def get(path, **params):
url = BASE + path + ("?" + urllib.parse.urlencode(params) if params else "")
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {KEY}"})
with urllib.request.urlopen(req) as resp:
return json.load(resp)
def paged(path, **params):
"""Follow meta.nextCursor until it comes back null."""
cursor = None
while True:
body = get(path, limit=200, cursor=cursor, **params)
yield from body["data"]
cursor = body["meta"].get("nextCursor")
if not cursor:
return
def ground_model(project_id):
strata, unnamed = {}, 0
for row in paged(f"/projects/{project_id}/tables/GEOL"):
d = row["data"]
try:
top, base = float(d["GEOL_TOP"]), float(d["GEOL_BASE"])
except (KeyError, ValueError):
continue
if base <= top:
continue
# Correlate on the named formation only.
name = (d.get("GEOL_GEOL") or "").strip()
if not name:
unnamed += 1
continue
s = strata.setdefault(name, {"holes": set(), "tops": [], "thick": []})
s["holes"].add(row["locationId"])
s["tops"].append(top)
s["thick"].append(base - top)
# Down the page in the order you would meet them.
for name, s in sorted(strata.items(), key=lambda kv: statistics.median(kv[1]["tops"])):
print(f"{name:<30}{len(s['holes']):>4} holes"
f" top {min(s['tops']):>6.2f}-{max(s['tops']):<6.2f}"
f" thickness {min(s['thick']):>5.2f}-{max(s['thick']):<5.2f}")
if unnamed:
print(f"{unnamed} beds had no stratum named and are not included.")What it gives you
MADE GROUND 18 holes top 0.00-0.30 thickness 0.60-3.20
ALLUVIUM 6 holes top 0.90-2.80 thickness 0.80-2.40
RIVER TERRACE DEPOSITS 16 holes top 1.40-4.10 thickness 2.20-4.60
LONDON CLAY FORMATION 18 holes top 4.00-8.60 thickness 4.10-12.60
23 beds had no stratum named and are not included.Which is the paragraph, already written for you: Made Ground was encountered in all 18 holes to between 0.60 m and 3.20 m; Alluvium was proved in 6 holes only, so it is channelled rather than continuous; and London Clay was reached in every hole, at between 4.00 m and 8.60 m. The terrace gravel sits between the two, and its base is the London Clay surface - which is the number the foundation design turns on.
Getting it right
Group on GEOL_GEOL, not GEOL_DESC. The formation is a controlled value;
the description is prose. Two loggers describing the same clay write different
sentences, so grouping on the description gives you a long list of strata that
each appear once and correlate with nothing.
Say how many beds you left out. Not every bed carries a formation. In the run above 23 of 132 did not, and on some projects it is closer to half - a summary that quietly drops them looks more confident than it deserves.
Summarise one site at a time. Geology is spatial. Averaging strata across several sites mixes ground that may be a hundred kilometres apart, and the result reads like insight without being any.
Last updated 27 August 2026