ChangelogOpen dashboard

Locations

5 endpoints

Places a campaign can target — a circle or a polygon. A location is only ever created by importing a GeoJSON FeatureCollection (see the import endpoint); this resource lists, reads, updates and deletes ones already imported.

List locations#

GET/locations
Needs locations:readNo token needed

Newest first. Each comes back with its shape as GeoJSON — a Point with a radius_meters for a circle, a Polygon otherwise — and the names of any location groups it belongs to.

Query parameters

cursor
string

An opaque value from a previous page's meta.next_cursor. Omit for the first page.

limit
integer

How many to return, up to 100.

Default: 20
updated_since
string (date-time)

Only locations updated at or after this ISO 8601 timestamp.

Responses

200
200 response
{
  "data": [
    {
      "id": "0192f0a4-6b1e-7c3a-9d2f-5e8b1a4d3001",
      "name": "Harbour Coffee, Quay Street",
      "external_id": "store-014",
      "type": "circle",
      "geometry": {
        "type": "Point",
        "coordinates": [
          -2.5879,
          51.4545
        ]
      },
      "radius_meters": 150,
      "groups": [
        "Bristol stores"
      ],
      "created_at": "2026-09-20T09:12:00Z",
      "updated_at": "2026-09-27T14:03:00Z"
    }
  ],
  "meta": {
    "next_cursor": "eyJpZCI6IjAxOTJmMGE0In0"
  }
}
Response fields
data
LocationResource[]
required
data[].id
string
required
data[].external_id
string | null
required
data[].name
string
required
data[].address
string | null
required
data[].type
string
required
data[].geometry
any[] | null
required
data[].radius_meters
integer | null
required
data[].groups
any[]
data[].created_at
string | null
required
data[].updated_at
string | null
required
meta
object
required
meta.next_cursor
string | null
required
401

The token is missing, expired or revoked.

unauthenticated
401 response
{
  "error": {
    "code": "string",
    "message": "string",
    "fields": {}
  }
}
403

The token can't do this.

missing_abilityplatform_api_not_enabledwrong_environment
403 response
{
  "error": {
    "code": "string",
    "message": "string",
    "fields": {}
  }
}
422
422 response
{
  "error": [
    null
  ]
}
429

Too many requests. Wait for the number of seconds in Retry-After.

rate_limited
429 response
{
  "error": {
    "code": "string",
    "message": "string",
    "fields": {}
  }
}
Request
curl 'https://api.bubbl.tech/platform/v1/locations?limit=20' \
  -H 'Accept: application/json'
Request
const response = await fetch('https://api.bubbl.tech/platform/v1/locations?limit=20', {
  method: 'GET',
  headers: {
    Accept: 'application/json',
  },
});

const data = await response.json();
Request
import requests

response = requests.request(
    'GET',
    'https://api.bubbl.tech/platform/v1/locations?limit=20',
    headers={
        'Accept': 'application/json',
    },
)
response.raise_for_status()
data = response.json()
Request
<?php

$client = new \GuzzleHttp\Client();

$response = $client->request('GET', 'https://api.bubbl.tech/platform/v1/locations?limit=20', [
    'headers' => [
        'Accept' => 'application/json',
    ],
]);

$data = json_decode((string) $response->getBody(), true);

Get a location#

GET/locations/{location}
Needs locations:readNo token needed

Path parameters

location
string (uuid)
required

The location ID

Responses

200
200 response
{
  "data": {
    "id": "0192f0a4-6b1e-7c3a-9d2f-5e8b1a4d3001",
    "name": "Harbour Coffee, Quay Street",
    "external_id": "store-014",
    "type": "circle",
    "geometry": {
      "type": "Point",
      "coordinates": [
        -2.5879,
        51.4545
      ]
    },
    "radius_meters": 150,
    "groups": [
      "Bristol stores"
    ],
    "created_at": "2026-09-20T09:12:00Z",
    "updated_at": "2026-09-27T14:03:00Z"
  }
}
Response fields
data
object
required
data.id
string
required
data.external_id
string | null
required
data.name
string
required
data.address
string | null
required
data.type
string
required
data.geometry
any[] | null
required
data.radius_meters
integer | null
required
data.groups
any[]
required
data.created_at
string | null
required
data.updated_at
string | null
required
401

The token is missing, expired or revoked.

unauthenticated
401 response
{
  "error": {
    "code": "string",
    "message": "string",
    "fields": {}
  }
}
403

The token can't do this.

missing_abilityplatform_api_not_enabledwrong_environment
403 response
{
  "error": {
    "code": "string",
    "message": "string",
    "fields": {}
  }
}
404

Nothing with that ID in this workspace.

not_found
404 response
{
  "error": {
    "code": "string",
    "message": "string",
    "fields": {}
  }
}
429

Too many requests. Wait for the number of seconds in Retry-After.

rate_limited
429 response
{
  "error": {
    "code": "string",
    "message": "string",
    "fields": {}
  }
}
Request
curl 'https://api.bubbl.tech/platform/v1/locations/{location}' \
  -H 'Accept: application/json'
Request
const response = await fetch('https://api.bubbl.tech/platform/v1/locations/{location}', {
  method: 'GET',
  headers: {
    Accept: 'application/json',
  },
});

const data = await response.json();
Request
import requests

response = requests.request(
    'GET',
    'https://api.bubbl.tech/platform/v1/locations/{location}',
    headers={
        'Accept': 'application/json',
    },
)
response.raise_for_status()
data = response.json()
Request
<?php

$client = new \GuzzleHttp\Client();

$response = $client->request('GET', 'https://api.bubbl.tech/platform/v1/locations/{location}', [
    'headers' => [
        'Accept' => 'application/json',
    ],
]);

$data = json_decode((string) $response->getBody(), true);

Update a location#

PATCH/locations/{location}
Needs locations:writeNo token needed

Only its name, address and external_id — not its shape. To change a location's shape, re-import it with the same properties.external_id (see the import endpoint).

Path parameters

location
string (uuid)
required

The location ID

Request body

application/jsonUpdateLocationRequest
name
string
address
string | null
external_id
string | null

Responses

200
200 response
{
  "data": {
    "id": "0192f0a4-6b1e-7c3a-9d2f-5e8b1a4d3001",
    "name": "Harbour Coffee, Quay Street",
    "external_id": "store-014",
    "type": "circle",
    "geometry": {
      "type": "Point",
      "coordinates": [
        -2.5879,
        51.4545
      ]
    },
    "radius_meters": 150,
    "groups": [
      "Bristol stores"
    ],
    "created_at": "2026-09-20T09:12:00Z",
    "updated_at": "2026-09-27T14:03:00Z"
  }
}
Response fields
data
object
required
data.id
string
required
data.external_id
string | null
required
data.name
string
required
data.address
string | null
required
data.type
string
required
data.geometry
any[] | null
required
data.radius_meters
integer | null
required
data.groups
any[]
required
data.created_at
string | null
required
data.updated_at
string | null
required
401

The token is missing, expired or revoked.

unauthenticated
401 response
{
  "error": {
    "code": "string",
    "message": "string",
    "fields": {}
  }
}
403

The token can't do this.

missing_abilityplatform_api_not_enabledwrong_environment
403 response
{
  "error": {
    "code": "string",
    "message": "string",
    "fields": {}
  }
}
404

Nothing with that ID in this workspace.

not_found
404 response
{
  "error": {
    "code": "string",
    "message": "string",
    "fields": {}
  }
}
422

Some fields aren't valid. fields says which, with the dashboard's wording.

validation_failed
422 response
{
  "error": {
    "code": "string",
    "message": "string",
    "fields": {}
  }
}
429

Too many requests. Wait for the number of seconds in Retry-After.

rate_limited
429 response
{
  "error": {
    "code": "string",
    "message": "string",
    "fields": {}
  }
}
Request
curl -X PATCH 'https://api.bubbl.tech/platform/v1/locations/{location}' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  --data '{
  "name": "Harbour Coffee, Quay St",
  "radius": 200
}'
Request
const response = await fetch('https://api.bubbl.tech/platform/v1/locations/{location}', {
  method: 'PATCH',
  headers: {
    Accept: 'application/json',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'Harbour Coffee, Quay St',
    radius: 200,
  }),
});

const data = await response.json();
Request
import requests

response = requests.request(
    'PATCH',
    'https://api.bubbl.tech/platform/v1/locations/{location}',
    headers={
        'Accept': 'application/json',
        'Content-Type': 'application/json',
    },
    json={
        'name': 'Harbour Coffee, Quay St',
        'radius': 200,
    },
)
response.raise_for_status()
data = response.json()
Request
<?php

$client = new \GuzzleHttp\Client();

$response = $client->request('PATCH', 'https://api.bubbl.tech/platform/v1/locations/{location}', [
    'headers' => [
        'Accept' => 'application/json',
        'Content-Type' => 'application/json',
    ],
    'json' => [
        'name' => 'Harbour Coffee, Quay St',
        'radius' => 200,
    ],
]);

$data = json_decode((string) $response->getBody(), true);

Delete a location#

DELETE/locations/{location}
Needs locations:writeNo token needed

Path parameters

location
string (uuid)
required

The location ID

Responses

200
200 response
{
  "data": {
    "id": "0192f0a4-6b1e-7c3a-9d2f-5e8b1a4d3001",
    "deleted": true
  }
}
Response fields
data
object
required
data.id
string
required
data.deleted
boolean
required
401

The token is missing, expired or revoked.

unauthenticated
401 response
{
  "error": {
    "code": "string",
    "message": "string",
    "fields": {}
  }
}
403

The token can't do this.

missing_abilityplatform_api_not_enabledwrong_environment
403 response
{
  "error": {
    "code": "string",
    "message": "string",
    "fields": {}
  }
}
404

Nothing with that ID in this workspace.

not_found
404 response
{
  "error": {
    "code": "string",
    "message": "string",
    "fields": {}
  }
}
429

Too many requests. Wait for the number of seconds in Retry-After.

rate_limited
429 response
{
  "error": {
    "code": "string",
    "message": "string",
    "fields": {}
  }
}
Request
curl -X DELETE 'https://api.bubbl.tech/platform/v1/locations/{location}' \
  -H 'Accept: application/json'
Request
const response = await fetch('https://api.bubbl.tech/platform/v1/locations/{location}', {
  method: 'DELETE',
  headers: {
    Accept: 'application/json',
  },
});

const data = await response.json();
Request
import requests

response = requests.request(
    'DELETE',
    'https://api.bubbl.tech/platform/v1/locations/{location}',
    headers={
        'Accept': 'application/json',
    },
)
response.raise_for_status()
data = response.json()
Request
<?php

$client = new \GuzzleHttp\Client();

$response = $client->request('DELETE', 'https://api.bubbl.tech/platform/v1/locations/{location}', [
    'headers' => [
        'Accept' => 'application/json',
    ],
]);

$data = json_decode((string) $response->getBody(), true);

Import locations#

POST/locations/import
Needs locations:writeNo token needed

A GeoJSON FeatureCollection. Each feature needs properties.name; a Point geometry also needs properties.radius (metres, 50–1000) and becomes a circle, a Polygon becomes a polygon. properties.external_id makes a re-import update the same location instead of creating a new one; properties.group adds it to a location group (created if it doesn't exist).

The response gives one result per feature — created, updated, skipped (over the plan's location limit) or failed, with reasons for the latter two. Send ?dry_run=1 to see what would happen without writing anything, and an Idempotency-Key header so a retried request after a lost response doesn't import the same features twice.

Request body

application/jsonImportLocationsRequestrequired
type
string
required
One of: FeatureCollection
features
string[]
required
dry_run
boolean

Responses

200
200 response
{
  "data": {
    "dry_run": false,
    "results": [
      {
        "index": 0,
        "status": "created",
        "name": "Riverside Store",
        "id": "0192f0a4-6b1e-7c3a-9d2f-5e8b1a4d2001",
        "reasons": null
      },
      {
        "index": 1,
        "status": "failed",
        "name": null,
        "reasons": [
          "Name is missing (properties.name)"
        ]
      }
    ],
    "summary": {
      "created": 1,
      "updated": 0,
      "skipped": 0,
      "failed": 1
    }
  }
}
Response fields
data
object
required
data.dry_run
boolean
required
data.results
object[]
required
data.results[].index
string
required
data.results[].status
"failed" | "skipped" | string
required
One of: updated, created
data.results[].name
string
required
data.results[].id
any | null | string | null | string
required
data.results[].reasons
any[] | any | null
required
data.summary
object
required
data.summary.created
integer
required
data.summary.updated
integer
required
data.summary.skipped
integer
required
data.summary.failed
integer
required
401

The token is missing, expired or revoked.

unauthenticated
401 response
{
  "error": {
    "code": "string",
    "message": "string",
    "fields": {}
  }
}
403

The token can't do this.

missing_abilityplatform_api_not_enabledwrong_environment
403 response
{
  "error": {
    "code": "string",
    "message": "string",
    "fields": {}
  }
}
422

Some fields aren't valid. fields says which, with the dashboard's wording.

validation_failed
422 response
{
  "error": {
    "code": "string",
    "message": "string",
    "fields": {}
  }
}
429

Too many requests. Wait for the number of seconds in Retry-After.

rate_limited
429 response
{
  "error": {
    "code": "string",
    "message": "string",
    "fields": {}
  }
}
Request
curl -X POST 'https://api.bubbl.tech/platform/v1/locations/import' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  --data '{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": {
        "type": "Point",
        "coordinates": [
          -2.5879,
          51.4545
        ]
      },
      "properties": {
        "name": "Harbour Coffee, Quay Street",
        "radius": 150,
        "external_id": "store-014",
        "group": "Bristol stores"
      }
    }
  ]
}'
Request
const response = await fetch('https://api.bubbl.tech/platform/v1/locations/import', {
  method: 'POST',
  headers: {
    Accept: 'application/json',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    type: 'FeatureCollection',
    features: [
      {
        type: 'Feature',
        geometry: {
          type: 'Point',
          coordinates: [
            -2.5879,
            51.4545,
          ],
        },
        properties: {
          name: 'Harbour Coffee, Quay Street',
          radius: 150,
          external_id: 'store-014',
          group: 'Bristol stores',
        },
      },
    ],
  }),
});

const data = await response.json();
Request
import requests

response = requests.request(
    'POST',
    'https://api.bubbl.tech/platform/v1/locations/import',
    headers={
        'Accept': 'application/json',
        'Content-Type': 'application/json',
    },
    json={
        'type': 'FeatureCollection',
        'features': [
            {
                'type': 'Feature',
                'geometry': {
                    'type': 'Point',
                    'coordinates': [
                        -2.5879,
                        51.4545,
                    ],
                },
                'properties': {
                    'name': 'Harbour Coffee, Quay Street',
                    'radius': 150,
                    'external_id': 'store-014',
                    'group': 'Bristol stores',
                },
            },
        ],
    },
)
response.raise_for_status()
data = response.json()
Request
<?php

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://api.bubbl.tech/platform/v1/locations/import', [
    'headers' => [
        'Accept' => 'application/json',
        'Content-Type' => 'application/json',
    ],
    'json' => [
        'type' => 'FeatureCollection',
        'features' => [
            [
                'type' => 'Feature',
                'geometry' => [
                    'type' => 'Point',
                    'coordinates' => [
                        -2.5879,
                        51.4545,
                    ],
                ],
                'properties' => [
                    'name' => 'Harbour Coffee, Quay Street',
                    'radius' => 150,
                    'external_id' => 'store-014',
                    'group' => 'Bristol stores',
                ],
            ],
        ],
    ],
]);

$data = json_decode((string) $response->getBody(), true);