> ## Documentation Index
> Fetch the complete documentation index at: https://docs.grdd.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Service Locations

> Create, read, update and delete your business and service-area locations with the Connect API

A **service location** is a business or service-area location of your company, for example an airport office or a depot. It has an address, tags and airport codes. It is shown on your company profile.

<Note>
  This page is about business locations. For GPS tracking, see [Location overview](/connect-api/location/overview).
</Note>

All routes on this page belong to the Connect API service. Read [Connect API conventions](/connect-api/content/api-conventions) first. It explains the base URL, authentication, `Idempotency-Key`, `If-Match`, pending operations and the error format.

This page uses `$CONNECT_API_BASE_URL` for the base URL. Ask GRiDD support ([support@grdd.net](mailto:support@grdd.net)) for the Connect API base URL for your environment.

## What is available

Service location routes are implemented as owner routes (your own company) and as one authenticated read of another company's published locations. The source does not mark this type as incomplete. The Connect API README names only Fleets and Vehicles as complete production implementations. This page describes the route contracts in the source and does not claim more.

## Endpoints

### Owner routes

These routes act on the company in your token.

| Method | Path | Purpose | `Idempotency-Key` | `If-Match` |
| - | - | - | - | - |
| `GET` | `/api/locations` | List your locations. | – | – |
| `POST` | `/api/locations` | Create a location. | Required | – |
| `GET` | `/api/locations/{slug}` | Read one location by slug. Returns an `ETag`. | – | – |
| `PATCH` | `/api/locations/{slug}` | Update one location. | Required | Required |
| `DELETE` | `/api/locations/{slug}` | Delete one location, or one language version. | – | Required |
| `POST` | `/api/locations/operations/{operationId}` | Resume a pending operation. See [Pending operations](/connect-api/content/api-conventions#pending-operations). | – | – |
| `GET` | `/api/locations/tags` | List your location tags. | – | – |
| `PUT` | `/api/locations/tags` | Save your location tags. | – | – |
| `GET` | `/api/locations/airport-codes?address=` | Suggest airport codes for an address. | – | – |
| `GET` | `/api/locations/documents` | List links between your locations and your documents. | – | – |

### Read another company's published locations

| Method | Path | Purpose |
| - | - | - |
| `GET` | `/api/locations/public/{griddid}` | Read the published locations of another company with your Bearer token. |

This route returns a reduced record (`PublicLocation`). It returns only active locations that your relationship with the company allows. A confirmed partner of the company can also see `partners` locations. If the partnership cannot be confirmed, the API returns only `public` and `gnet` locations. If the owning company's plan limits the published items that a viewer can see, the response holds only the first location and has `lockedCount` and `lockedSlugs`. Use `lockedSlugs` for matching only. Do not show them.

### Not for external use

These routes exist in the service. They need a `Service` credential, an administrator role or another internal identity. Do not call them.

* `GET /api/locations/published`, `GET /api/locations/published/{griddid}`, `GET /api/locations/published/radius` and `GET /api/locations/pages/{slug}` use the public website renderer credential.
* `GET /api/locations/system/{griddid}` is for administrators.
* `POST /api/locations/index` rebuilds the search index for all companies. It needs an administrator role or a service credential.

`GET /api/locations/pages` is not described here because its response is an untyped list of page records.

The location search helpers `POST /api/location-resolutions` and `GET /api/location-suggestions` are part of search. They are not part of the location write flow. They are not described on this page.

## Authentication and access

* Send `Authorization: Bearer YOUR_ACCESS_TOKEN`. See [Authentication](/connect-api/content/api-conventions#authentication).
* The owner routes do not check a role in the token.
* **Location cap.** Creating a location, and changing an inactive location to `active`, counts against a plan limit on **active locations**. The limits in the subscription contract in source are: Io 1, Basic 1, Professional 2, Premium 6, Enterprise unlimited. An active trial lifts the limit. Updates that do not activate a location, deleting, and deactivating are not limited, so an account over the limit can still clean up.
* When the limit is reached, or the subscription is lapsed, the API returns `403 entitlement_denied`. If the plan cannot be determined, it returns `503 temporarily_unavailable`.

## Location fields

### Read record

| Field | Type | Notes |
| - | - | - |
| `id` | integer | |
| `slug` | string | Identifies the location in the path. |
| `griddid` | string | Your company. |
| `name` | string | |
| `address` | string | |
| `location` | string or null | Coordinates as a string. See [Coordinates](#coordinates). |
| `display_order` | number or null | |
| `image_url` | string or null | See [Image URLs](/connect-api/content/api-conventions#image-urls). |
| `extra_photos` | array | Items are `{ "photo": id or object, "id": string }`. |
| `airport_codes` | array | Items are `{ "airport_code": string, "id": string }`. |
| `tags` | array | Items are `{ "tag": string, "id": string }`. |
| `page` | integer, string, object or null | A linked page record. |
| `status` | `active` or `inactive` | |
| `visibility` | `private`, `partners`, `gnet`, `unlisted`, `public` | |
| `country`, `state`, `city`, `cities` | string or null | `cities` is a single string. |
| `description` | string or null | |
| `documents` | array | Items are `{ "document": id or object, "id": string }`. |
| `version` | integer | Use it in `If-Match`. |
| `createdAt`, `updatedAt` | string (ISO 8601) | |

### Write body

All write bodies are strict. Unknown keys return `400 invalid_request`.

**Create** (`POST /api/locations`):

| Field | Type | Required | Notes |
| - | - | - | - |
| `location` | object | Yes | The fields below. |
| `locale` | `en`, `es`, `de`, `fr` | No | Language of the new record. |
| `contentLanguage` | object | No | `{ "intent": "create_record", "language": "fr" }`. Takes priority over `locale`. |

Fields in `location`:

| Field | Type | Create | Notes |
| - | - | - | - |
| `name` | string | **Required** | Trimmed. At least 1 character. |
| `address` | string | **Required** | |
| `visibility` | enum | Optional | Default `public`. |
| `slug` | string | Optional | No `/`. If you omit it, the API creates one from your company ID and the name. If the slug exists, the API returns `409 conflict`. |
| `location` | string or null | Optional | |
| `display_order` | number or null | Optional | If you omit it or send `0`, the API puts the new location last. |
| `image_url` | URL or null | Optional | |
| `extra_photos` | array | Optional | Each item is an ID, or `{ "photo": id }`. |
| `airport_codes` | array | Optional | Items are `{ "airport_code": "MIA" }`. |
| `tags` | array | Optional | Items are `{ "tag": "airport" }`. |
| `page` | integer, string or null | Optional | |
| `status` | `active`, `inactive` or null | Optional | |
| `country`, `state`, `city`, `cities`, `description` | string or null | Optional | |
| `documents` | array | Optional | Each item is a document ID, or `{ "document": id }`. |

**Update** (`PATCH /api/locations/{slug}`): the same body shape, but `location` can hold any subset of the fields above and none is required. `contentLanguage` must be `update_version` or `add_version`. Send `If-Match` and `Idempotency-Key`.

**Delete** (`DELETE /api/locations/{slug}`): optional query `locale` and `languageVersion`. Send `If-Match`.

### Coordinates

The `location` field is a free string in the contract. The service reads it as `"longitude,latitude"`, for example `"-80.2870,25.7959"`. This is the same form that the legacy core content route returns.

### Airport codes

If you send `airport_codes` and a `location` with usable coordinates, the API checks the codes. A code that is more than 161 km (100 statute miles) from the location is rejected. The API skips codes that are not in its airport list. It does not check a location at `0,0`.

The response is `422` with `code: "domain_rejected"`, `message: "airport_out_of_range"` and the codes in `fieldErrors.airport_codes`. No record is written.

## Create a location

```bash theme={null}
curl -X POST "$CONNECT_API_BASE_URL/api/locations" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7c1e4b52-2d0f-4b1c-9f55-3d9d2b6a8e10" \
  -d '{
    "location": {
      "name": "Airport Office",
      "address": "1 Terminal Way, Miami, FL",
      "status": "active",
      "tags": [{ "tag": "airport" }]
    },
    "locale": "en"
  }'
```

`201 Created` with the header `ETag: "v1"`:

```json theme={null}
{
  "data": {
    "id": 7,
    "name": "Airport Office",
    "griddid": "demo-company",
    "address": "1 Terminal Way, Miami, FL",
    "display_order": 1,
    "image_url": null,
    "extra_photos": [],
    "airport_codes": [],
    "tags": [{ "tag": "airport", "id": "tag-1" }],
    "page": null,
    "status": "active",
    "visibility": "public",
    "documents": [],
    "slug": "demo-company-airport-office",
    "version": 1,
    "updatedAt": "2026-08-29T12:00:00.000Z",
    "createdAt": "2026-08-29T12:00:00.000Z"
  },
  "invalidated": [],
  "status": "completed"
}
```

If follow-up work is not finished, the response is `202`. It has `status: "pending"`, an `operationId` and an `effects` list. See [Pending operations](/connect-api/content/api-conventions#pending-operations).

## Read your locations

```bash theme={null}
curl "$CONNECT_API_BASE_URL/api/locations?locale=en" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

Response `200`. The record below is based on the fixture `location-record.json`. The fixture writes `location` as `"25.7959,-80.2870"`. This page uses the `"longitude,latitude"` order that the service reads. See [Coordinates](#coordinates).

```json theme={null}
{
  "items": [
    {
      "id": 7,
      "name": "Airport Office",
      "griddid": "demo-company",
      "address": "1 Terminal Way, Miami, FL",
      "location": "-80.2870,25.7959",
      "display_order": 1,
      "image_url": "https://asset.grdd.dev/location/demo-company/airport.webp",
      "extra_photos": [],
      "airport_codes": [{ "airport_code": "MIA", "id": "airport-1" }],
      "tags": [{ "tag": "airport", "id": "tag-1" }],
      "page": null,
      "status": "active",
      "visibility": "public",
      "country": "US",
      "state": "FL",
      "city": "Miami",
      "cities": "Miami Beach",
      "description": "Airport transportation office",
      "documents": [],
      "slug": "airport-office",
      "version": 2,
      "updatedAt": "2026-08-29T12:00:00.000Z",
      "createdAt": "2026-08-28T12:00:00.000Z"
    }
  ],
  "meta": { "partial": false, "paginationComplete": true, "rejectedCount": 0, "rejected": [] }
}
```

The list has no pagination parameters. `GET /api/locations/{slug}` returns one record and the header `ETag: "v2"`. If the slug does not belong to your company, the response is `404 not_found`.

## Update a location

Send the version you read in `If-Match`:

```bash theme={null}
curl -X PATCH "$CONNECT_API_BASE_URL/api/locations/airport-office" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "If-Match: \"v2\"" \
  -H "Idempotency-Key: 0b8a9f3e-71c4-4d0a-8a3e-52f1c6d7e9a2" \
  -d '{ "location": { "name": "Airport Office Updated", "tags": [] } }'
```

The response is `200` with the same shape as the create response (`data`, `invalidated`, `status`) and a new `ETag`. A pending update returns `202`. A stale `If-Match` returns `412 version_conflict`.

<Note>
  `tags: []` removes all tags. When you omit a field, the API does not change it. When you send `null` or an empty list for a field that allows it, the API clears the field.
</Note>

## Delete a location

```bash theme={null}
curl -X DELETE "$CONNECT_API_BASE_URL/api/locations/airport-office" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "If-Match: \"v3\""
```

* `204 No Content` when the delete is complete.
* `202` with `status: "pending"` and an `operationId` when follow-up work is not finished. The body has `data: null`, `invalidated`, `status: "pending"`, `operationId` and `effects`.

Resume a pending operation:

```bash theme={null}
curl -X POST \
  "$CONNECT_API_BASE_URL/api/locations/operations/location-00000000-0000-4000-8000-000000000001" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

```json theme={null}
{
  "operationId": "location-00000000-0000-4000-8000-000000000001",
  "status": "pending",
  "effects": [
    {
      "name": "location_cache",
      "status": "failed",
      "retryable": true,
      "problemCode": "downstream_failure"
    }
  ]
}
```

## Errors

Error bodies use the [problem format](/connect-api/content/api-conventions#errors). These codes come from the location routes.

| Status | `code` | When |
| - | - | - |
| 400 | `invalid_json`, `invalid_request` | The body or query is not valid. Includes a bad `If-Match` or `Idempotency-Key` value. |
| 401 | `unauthenticated` | The token is missing, invalid or expired. |
| 403 | `entitlement_denied` | The location cap is reached, or the subscription is lapsed. |
| 403 | `forbidden` | The operation is not permitted for your company. |
| 404 | `not_found` | No location with this slug for your company, or no such operation. |
| 409 | `conflict` | The slug already exists, or a content language rule refused the change. |
| 409 | `idempotency_conflict` | The key was used for a different request. |
| 412 | `version_conflict` | `If-Match` is stale. The body has `currentVersion`. |
| 413, 415 | `payload_too_large`, `unsupported_media_type` | Body size or media type. |
| 422 | `domain_rejected` | Airport codes are out of range (`message: "airport_out_of_range"`). |
| 428 | `version_required`, `idempotency_key_required` | A required header is missing. |
| 202 | `partial_success` | An idempotent retry found the first request still running. Use `operationId`. |
| 503 | `temporarily_unavailable` | Core, the cache, or the API data store is not available. |
| 500 | `internal_error` | Unexpected error. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.