Data API
Push GeoJSON or CSV datasets directly into a project from an external pipeline — no UI clicks, no manual import. A single HTTP request creates or replaces a layer; subsequent calls with the same name overwrite in place.
The Data API is intended for automated systems (CI jobs, scheduled scripts, monitoring pipelines) that need to keep a project's data in sync with an external source. For one-off uploads, use the in-app import flow instead.
Endpoint
POST /functions/v1/collections — create or replace a layer
GET /functions/v1/collections — list API-managed layers in the project
The base URL is your Supabase project URL — e.g. https://<project-ref>.supabase.co/functions/v1/collections.
Authentication
Every request needs an opaque bearer token:
Authorization: Bearer gp_<your-token>
Tokens start with the literal prefix gp_ (GeoPlanning) followed by 32 random bytes encoded as base64url. They are stored as a SHA-256 hash on the server — the raw value is shown to you once, when the token is created. If you lose it, generate a new one and revoke the old one.
Generating a token
Tokens are org-scoped: a single token can push data into any project in your organisation. Specify which project a request targets by including project_id in the request body (see Org-scoped tokens below).
To generate a token:
-
Open your Organisation Settings and click the Data API tab.
-
Click New token.

-
Give it a label (e.g.
nightly-build), pick a scope (writefor pushing data,readfor listing only), and an optional expiry date. -
Click Generate token.

-
Copy the token value now — it will not be shown again.
Tokens are visible to org owners and admins only. Plain org members, and project editors/contributors who are not org owners or admins, cannot view or manage Data API tokens.
Revoking a token
In the same settings panel, click Revoke next to the token. Any pipeline using it will start receiving 401 ERR_INVALID_TOKEN immediately.
Pushing data — POST /collections
Two body shapes are supported. Both require a name field — the layer is identified in the project by that name, and subsequent calls with the same name upsert in place.
GeoJSON body
curl -X POST https://<project-ref>.supabase.co/functions/v1/collections \
-H "Authorization: Bearer gp_<your-token>" \
-H "Content-Type: application/json" \
-d '{
"name": "Parking sensors",
"geojson": {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": { "type": "Point", "coordinates": [10.75, 59.91] },
"properties": { "id": "p-001", "occupied": true }
}
]
}
}'
The geojson value must be a FeatureCollection with at least one Feature. Each feature must have a geometry.
CSV body
curl -X POST https://<project-ref>.supabase.co/functions/v1/collections \
-H "Authorization: Bearer gp_<your-token>" \
-H "Content-Type: application/json" \
-d '{
"name": "City offices",
"format": "csv",
"data": "lat,lon,name\n59.91,10.75,Oslo\n55.67,12.56,Copenhagen"
}'
The CSV must have a header row. The Data API automatically detects the coordinate columns by header name (case-insensitive):
- Latitude:
lat,latitude,y,northing - Longitude:
lon,lng,longitude,x,easting
Rows where lat or lon cannot be parsed as a number are silently skipped. All other columns become feature properties. Quoted fields with embedded commas are supported (e.g. "Smith, John"); double quotes inside a quoted field are escaped as "".
Targeting a project
Every token is org-scoped, so include project_id in the body to tell the API which project the layer belongs to:
curl -X POST https://<project-ref>.supabase.co/functions/v1/collections \
-H "Authorization: Bearer gp_<your-token>" \
-H "Content-Type: application/json" \
-d '{
"name": "Air quality stations",
"project_id": "00000000-0000-0000-0000-000000000000",
"geojson": { "type": "FeatureCollection", "features": [...] }
}'
The project must belong to the same organisation as the token, otherwise the request fails with 403 ERR_PROJECT_NOT_IN_ORG.
Response
{
"success": true,
"layer_id": "00000000-0000-0000-0000-000000000000",
"created": true
}
| Field | Meaning |
|---|---|
layer_id | Stable UUID of the layer. Same value on subsequent updates of the same name. |
created | true if a new layer was created, false if an existing layer was replaced. |
Status code is 201 on create, 200 on replace.
Listing layers — GET /collections
curl -X GET https://<project-ref>.supabase.co/functions/v1/collections \
-H "Authorization: Bearer gp_<your-token>"
For org-scoped tokens, pass the project ID as a query parameter:
curl -X GET "https://<project-ref>.supabase.co/functions/v1/collections?project_id=<uuid>" \
-H "Authorization: Bearer gp_<your-token>"
Response:
{
"success": true,
"items": [
{
"id": "00000000-0000-0000-0000-000000000000",
"name": "Parking sensors",
"type": "geojson",
"created_at": "2026-04-29T08:31:00Z",
"updated_at": "2026-04-29T10:14:00Z"
}
]
}
Only layers created via the Data API are returned — manually-imported layers are excluded.
Error codes
All errors return JSON of shape { "success": false, "message": "ERR_*" } with an HTTP status:
| Status | Code | Meaning |
|---|---|---|
| 400 | ERR_NAME_REQUIRED | name field missing or empty. |
| 400 | ERR_INVALID_BODY | Request body is not valid JSON. |
| 400 | ERR_PROJECT_REQUIRED | Org-scoped token without project_id in body or query. |
| 400 | ERR_GEOJSON_NULL | geojson is null or not an object. |
| 400 | ERR_GEOJSON_MISSING_TYPE | GeoJSON has no type field. |
| 400 | ERR_GEOJSON_INVALID_TYPE | GeoJSON type is not FeatureCollection. |
| 400 | ERR_GEOJSON_MISSING_FEATURES | No features array. |
| 400 | ERR_GEOJSON_EMPTY_FEATURES | Empty features array. |
| 400 | ERR_GEOJSON_FEATURE_MISSING_TYPE | A feature has no type. |
| 400 | ERR_GEOJSON_FEATURE_INVALID_TYPE | A feature's type is not Feature. |
| 400 | ERR_GEOJSON_FEATURE_MISSING_GEOMETRY | A feature has no geometry. |
| 400 | ERR_CSV_NO_COORDINATES | CSV has no detectable lat/lon columns. |
| 401 | ERR_INVALID_TOKEN | Token missing, malformed, not found, or expired. |
| 403 | ERR_FORBIDDEN | Token has read scope only (POST requires write). |
| 403 | ERR_PROJECT_NOT_IN_ORG | Org-scoped token, but project_id belongs to a different org. |
| 413 | ERR_PAYLOAD_TOO_LARGE | Request body exceeds 25 MB. |
| 500 | ERR_INTERNAL / ERR_STORAGE_UPLOAD / ERR_INSERT / ERR_UPDATE | Server-side failure — retry safe. |
Limits
| Limit | Value |
|---|---|
| Maximum request body size | 25 MB |
| Rate limiting | None in v1 — please be reasonable. May be added later. |
| Layer name length | Same as the in-app layers panel (no specific cap enforced by the API itself). |
| Token expiry | Optional. If set, requests with the token after the expiry date return 401 ERR_INVALID_TOKEN. |
Migration from the old /public/v1/collections API
If you are coming from the previous platform's RS256 JWT-based collections API:
| Old | New | |
|---|---|---|
| Endpoint | POST /public/v1/collections | POST /functions/v1/collections |
| Token | RS256 JWT with ws + scope claims | Opaque gp_<token> |
| Token source | Backend's JwtGeneratorService | Organisation Settings → Data API tab |
| Workspace binding | ws claim in JWT | Encoded in the token record |
| Name conflict | 400 ERR_NAME_TAKEN | Upserts in place — no error |
| List endpoint | GET /v1/assets/datasets | GET /functions/v1/collections |
| List response key | items[].publicId | items[].id |
The Authorization: Bearer … header pattern and the request body shape ({ name, geojson } and { name, format, data }) are unchanged. Typical migration is a 3-line diff: new endpoint URL, new token, drop any workspace_id field from the body.