Skip to content
GIMLabsDocs

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