Get started

Python examples

Call the GeotechOS API from Python with requests. A small client that waits on 429, then a client, a project and a job.

This page shows the API in Python. It uses the requests library. The same steps work in any language that can send HTTPS requests.

Before you start

pip install requests
  • Set the address and the key as environment variables. Do not write the key in your code.
export GEOTECHOS_URL="https://au.geotechos.com/api/integration/v1"
export GEOTECHOS_API_KEY="<your API key>"

Check the key

import os
import requests

URL = os.environ["GEOTECHOS_URL"]
HEADERS = {
    "Authorization": f"Bearer {os.environ['GEOTECHOS_API_KEY']}",
    "Accept": "application/json",
}

answer = requests.get(f"{URL}/whoami", headers=HEADERS, timeout=20)
answer.raise_for_status()
me = answer.json()
print(me["integration"]["name"], "in", me["workspace"]["name"])
print("Can write:", ", ".join(me["writable_features"]))

A small client

This client sends the headers, waits when GeotechOS says to wait, and stops with a clear message on a refusal. Save it as geotechos.py.

"""A small client for the GeotechOS integration API."""

import os
import time

import requests


class GeotechOSError(Exception):
    """GeotechOS refused a request. `status`, `errors` and `request_id` say why."""

    def __init__(self, response):
        body = response.json() if response.content else {}
        self.status = response.status_code
        self.errors = body.get("errors", {})
        self.request_id = response.headers.get("X-Request-Id")
        super().__init__(f"{self.status}: {body.get('message', response.reason)} (request {self.request_id})")


class GeotechOS:
    def __init__(self, url=None, key=None):
        self.url = (url or os.environ["GEOTECHOS_URL"]).rstrip("/")
        self.session = requests.Session()
        self.session.headers.update({
            "Authorization": f"Bearer {key or os.environ['GEOTECHOS_API_KEY']}",
            "Accept": "application/json",
        })

    def request(self, method, path, body=None, tries=4):
        for attempt in range(1, tries + 1):
            response = self.session.request(method, f"{self.url}{path}", json=body, timeout=30)
            if response.status_code == 429 and attempt < tries:
                # Over the limit: wait as long as GeotechOS says.
                time.sleep(int(response.headers.get("Retry-After", "5")))
                continue
            if response.status_code >= 500 and attempt < tries:
                # A problem in GeotechOS: wait longer each time.
                time.sleep(2 ** attempt)
                continue
            if not response.ok:
                raise GeotechOSError(response)
            return response.json()

    def put(self, resource, external_id, body):
        """Make the record, or update it: safe to send again."""
        return self.request("PUT", f"/{resource}/ext:{external_id}", body)

    def get(self, resource, name):
        """Read a record by ext:…, uuid:… or number:…."""
        return self.request("GET", f"/{resource}/{name}")

Send a client, a project and a job

Send the parents first. Each record uses the id it has in your system.

from geotechos import GeotechOS, GeotechOSError

api = GeotechOS()

try:
    client = api.put("clients", "C-19", {
        "legal_name": "Example Developments Pty Ltd",
        "email": "[email protected]",
    })
    if client.get("adopted"):
        print("Attached to a client that was already in GeotechOS")

    project = api.put("projects", "P-500", {
        "name": "Example Estate Stage 2",
        "address": "14 Sample Road",
        "city": "Parramatta",
        "state": "NSW",
        "client": {"ext": "C-19"},
    })
    print("Project", project["data"]["number"])

    job = api.put("jobs", "J-88421", {
        "title": "Compaction testing, lot 3",
        "project": {"ext": "P-500"},
        "scheduled_start_at": "2026-11-03T07:00:00+11:00",
        "scheduled_end_at": "2026-11-03T12:00:00+11:00",
    })
    print("Job", job["data"]["number"], "starts", job["data"]["scheduled_start_at"])
except GeotechOSError as error:
    print("Refused:", error)
    for field, problems in error.errors.items():
        print(f"  {field}: {problems[0]}")

Run it twice. The second time, every put updates the record that the first run made.

Times from your own database

Send every time with its offset. If your database keeps local times with no zone, give them your office's zone first:

from datetime import datetime
from zoneinfo import ZoneInfo

local = datetime(2026, 11, 3, 7, 0, tzinfo=ZoneInfo("Australia/Sydney"))
print(local.isoformat())  # 2026-11-03T07:00:00+11:00

Cancel a job

There is no delete. When a job goes in your system, send the time it was cancelled and why:

from datetime import datetime, timezone

api.request("PATCH", "/jobs/ext:J-88421", {
    "cancelled_at": datetime.now(timezone.utc).isoformat(),
    "cancellation_reason": "Client postponed the pour.",
})

Send many records

Each key can send 300 requests in a minute. The client above waits when it gets a 429, so a loop over many records needs no extra code:

for row in rows_from_your_system():
    try:
        api.put("jobs", row["id"], to_geotechos_job(row))
    except GeotechOSError as error:
        # Keep it for a person to look at. Do not stop the whole run.
        log_failed(row["id"], error.status, str(error))