Conventions
Response shape, paging, incremental sync and rate limits.
Response shape
Every successful response is wrapped:
{ "data": ..., "meta": { "nextCursor": null } }data is an object for a single resource and an array for a list. meta only
appears where there is something to say.
Every failure looks like this, with a stable machine-readable type:
{
"error": {
"type": "insufficient_scope",
"message": "This API key lacks the 'tables:read' scope.",
"docs": "https://docs.gimlabs.io/api/errors#insufficient_scope"
}
}Branch on type, never on message. Messages get reworded; types do not.
Paging
List endpoints are cursor-paged.
| Parameter | Default | Max |
|---|---|---|
limit | 100 | 200 |
cursor | - | - |
Read meta.nextCursor and pass it back as ?cursor=. When it comes back
null, you have everything.
curl -H "Authorization: Bearer $KEY" \
"https://api.gimlabs.io/v1/projects?limit=100&cursor=6512a4b8c9d0e1f234567801"Cursors are stable while data is being added underneath you, which offset paging is not. Do not build a cursor yourself or store one long-term; treat it as opaque.
Incremental sync
Every list endpoint accepts ?updatedSince=<ISO 8601> and returns only rows
changed at or after that moment.
curl -H "Authorization: Bearer $KEY" \
"https://api.gimlabs.io/v1/projects/{id}/tables/GEOL?updatedSince=2026-08-01T00:00:00Z"This is how you keep a warehouse or a dashboard fresh without re-pulling everything. Store the timestamp of your last successful run and pass it next time.
Note
Deleted rows are simply absent; there is no tombstone. If you need to detect deletions, do a full pull periodically alongside your incremental ones.
Rate limits
Limits apply on two axes at once. Per key stops one runaway script. Per company is the real ceiling, since you can mint as many keys as you like.
Endpoints are grouped by how much work they cost us:
| Class | Endpoints | Per key | Per company |
|---|---|---|---|
| Cheap | /me, /projects, /projects/{id} | 120/min | 600/min |
| Read | locations, tables | 60/min | 300/min |
Plus a company-wide ceiling of 100,000 requests per month across all keys.
Every response carries your current state:
RateLimit-Limit: 60
RateLimit-Remaining: 58
RateLimit-Reset: 1787781060RateLimit-Reset is a Unix timestamp in seconds.
When you hit a limit
You get 429 with a Retry-After header in seconds. Wait that long, then
retry. Retry-After accounts for which window you actually blew, so a monthly
breach tells you the truth rather than suggesting you try again in 40 seconds.
Back off rather than retrying in a tight loop. A sensible client watches
RateLimit-Remaining and slows down before it gets refused at all.
Dates
All timestamps are UTC, ISO 8601, e.g. 2026-08-26T09:12:44.000Z. Rate-limit
windows are UTC too, so a monthly allowance resets at midnight UTC on the 1st,
not in your local timezone.
Values are strings
Inside a data object, values come back exactly as stored, as strings. We do
not coerce them, because guessing types is how precision gets quietly lost in
ground investigation data. Convert at your end.
The lifted location fields (easting, northing, latitude, longitude,
groundLevel, finalDepth) are the deliberate exception: numbers, or null
where the location has no such value.
Last updated 26 August 2026