> ## 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.

# Connect API conventions

> Base URL, authentication, idempotency, version checks, pending operations and error format shared by the Connect API content routes

This page describes the rules that all Connect API content routes share. The pages for [Service Locations](/connect-api/content/locations), [Fleets](/connect-api/content/fleets), [Vehicles](/connect-api/content/vehicles), [Services](/connect-api/content/services) and [Documents](/connect-api/content/documents) link here instead of repeating them.

<Note>
  The Connect API is a different service from GNet core. It does not use the `core.grdd.net` host. The older core routes (`/api/content/{griddid}`) and the `/api/partner/posts` routes are not interchangeable with the routes in this section. See [Content API overview](/connect-api/content/overview).
</Note>

## Base URL

The public base URL of the Connect API is not published in these docs. Ask GRiDD support ([support@grdd.net](mailto:support@grdd.net)) for the Connect API base URL for your environment.

The examples in this section use a shell variable:

```bash theme={null}
export CONNECT_API_BASE_URL="<base URL from GRiDD support>"
curl "$CONNECT_API_BASE_URL/api/fleets" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```

Paths in these docs are the route paths of the Connect API service. They have no version segment.

## Authentication

Send a core-issued access token on every request:

```http theme={null}
Authorization: Bearer YOUR_ACCESS_TOKEN
```

To get a token, see [Get Token](/connect-api/get-token).

The Connect API does not read the token itself. It calls core `GET /api/token` with the token in the `token` header and waits up to 5 seconds for the answer. The token must carry a company identifier (`aud` or `griddid`) and a `username`.

| Result | HTTP status | Problem `code` |
| - | - | - |
| No `Authorization: Bearer` header, or an empty token | `401` | `unauthenticated` |
| Core rejects the token, the token has expired, or the core answer is not valid | `401` | `unauthenticated` |
| Core cannot be reached, or answers with a 5xx status | `503` | `temporarily_unavailable` (`retryable: true`) |

A missing token, an invalid token and an expired token all return the same problem `code`. Request a new token and try again.

### Company scope

An owner route acts on the company in your token. It does not accept a company identifier in the path, the query or the body. If you send a `griddid` field in a write body, the request fails with `400 invalid_request`, because write bodies reject unknown keys.

A record that belongs to another company is not returned to you.

Reads of another company's published content use separate `public/{griddid}` routes. Each type page lists them.

### Roles and subscription

The owner routes in the Fleet, Vehicle, Service Location and Document pages do not check a role in the token. Creating a record can fail on a subscription check. Each type page states what applies.

The Service routes are the exception. Service writes need the role `ADMIN`, `MANAGER` or `VENDOR`.

A token with the role `gateway` is classed as a service credential. Some routes outside this section refuse it. The routes documented here do not check for it.

<Warning>
  Some routes use a `Service` authorization header, or need an administrator role. They are for GRiDD's own systems, such as the public website renderer. They are not part of the external API. The type pages list them only to say that they are not for external use.
</Warning>

## Request rules

* Send `Content-Type: application/json` on JSON bodies. Other media types return `415 unsupported_media_type`.
* The body limit is 1 MiB. A larger body returns `413 payload_too_large`.
* An empty body or a body that is not valid JSON returns `400 invalid_json`.
* Bodies and query strings are strict. An unknown key, a repeated query parameter or a wrong type returns `400 invalid_request`. The problem body has a `fieldErrors` object that names each field.
* `locale` and `force` are query parameters on most reads. `locale` is one of `en`, `es`, `de`, `fr`. `force=true` reads from the authoritative store instead of the cache.

### Content languages

Fleet, Service Location, Service and Document records can hold more than one language version.

| Where | Field | Meaning |
| - | - | - |
| Create body | `contentLanguage: { "intent": "create_record", "language": "fr" }` | Create the record in this language. It takes priority over `locale`. |
| Update body | `contentLanguage: { "intent": "update_version" \| "add_version", "language": "fr" }` | Edit an existing language version, or add a new one. |
| Detail read | `languageVersion=auto\|en\|es\|de\|fr` | Return one stored language version with its `contentLanguage` state. |
| Delete | `languageVersion=en\|es\|de\|fr` | Remove one language version. Without it, the whole record is deleted. |

If a language intent cannot be applied, the API returns `409 conflict` with one of these `reason` values: `content_language_version_missing`, `content_language_version_exists`, `content_language_final_version`, `content_language_record_exists`.

Vehicles use `locale` only.

## Idempotency

Create requests need an `Idempotency-Key` header. Many update requests need it too. Each type page lists which requests need it.

```http theme={null}
Idempotency-Key: 5d3f2c8e-6b0a-4a3f-9d44-1c2a7e9b0f11
```

* The value is 8 to 128 printable ASCII characters, without spaces. A UUID works well.
* If the header is required and missing, the API returns `428 idempotency_key_required`. If the value is not valid, it returns `400 invalid_request`.
* The API stores the key together with your company, the operation and a hash of the request. The record is kept for 24 hours.
* **Same key, same request:** the API returns the stored result and does not write again.
* **Same key, different request:** the API returns `409 idempotency_conflict`.
* **Same key while the first request is still running:** Fleet, Vehicle, Service Location and Document requests return `202 partial_success` with an `operationId`. Service requests return `409 idempotency_conflict`.

Use a new key for each new change. Reuse a key only to retry the same request.

## Version checks

Single-record reads, creates and updates return an `ETag` header with the record version:

```http theme={null}
ETag: "v2"
```

Update and delete requests send that version back in `If-Match`:

```http theme={null}
If-Match: "v2"
```

| Situation | Response |
| - | - |
| `If-Match` is missing | `428` with `code: "version_required"` |
| `If-Match` is not in the form `"v<number>"` (for example `*` or a weak validator) | `400` with `code: "invalid_request"` |
| The version is older than the stored version | `412` with `code: "version_conflict"` and `currentVersion` |

On `412`, read the record again, apply your change to the new data, and send the update with the new version. The API does not change the record when the version is stale.

`POST` create requests do not use `If-Match`.

## Pending operations

A write can succeed but leave follow-up work unfinished. The follow-up work includes cache refresh, linked records and projections. The API does not report this as a failure. It records an operation and returns `202`.

### Pending response body

A `202` write response has `status: "pending"`, an `operationId` and an `effects` list. This delete example is from the Fleet fixture `fleet-delete-pending.json`:

```json theme={null}
{
  "data": null,
  "invalidated": [],
  "operationId": "fleet-00000000-0000-4000-8000-000000000043",
  "status": "pending",
  "effects": [
    {
      "name": "vehicle_links",
      "status": "failed",
      "retryable": true,
      "problemCode": "partial_success"
    }
  ]
}
```

Each effect has a `name`, a `status` (`completed`, `pending` or `failed`) and a `retryable` flag. A failed effect can also have a `problemCode`.

For a pending create or update, the same `202` response also has the record in `data`.

### Resume a pending operation

Send `POST` to the `operations` route of the same content type. The request has no body:

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

| Content type | Route |
| - | - |
| Fleets | `POST /api/fleets/operations/{operationId}` |
| Vehicles | `POST /api/vehicles/operations/{operationId}` |
| Service Locations | `POST /api/locations/operations/{operationId}` |
| Services | `POST /api/services/operations/{operationId}` |
| Documents | `POST /api/documents/operations/{operationId}` |

The route retries only the effects that are not complete. It returns:

* `200` when the operation is `completed`.
* `202` when it is still `pending`. Wait, then call it again.
* `404 not_found` when the operation does not exist or belongs to another company.

The response body has `operationId`, `status` and `effects`. There is no `GET` route for operations.

<Note>
  Services use the status values `completed`, `pending` and `partial`. A Service operation that has not committed its main write returns `409 conflict` when you try to resume it.
</Note>

## Errors

Error responses use the media type `application/problem+json`. Every response, including a success, carries an `X-Correlation-Id` header. If you send a valid UUID in `X-Correlation-Id`, the API returns it. Otherwise the API creates one. Include this value when you contact support.

```json theme={null}
{
  "code": "version_conflict",
  "message": "The resource version is stale",
  "correlationId": "05f953af-c79d-49b4-b013-f451354b2725",
  "retryable": false,
  "currentVersion": 4
}
```

| Field | Type | Notes |
| - | - | - |
| `code` | string | One of the codes in the table below. |
| `message` | string | Short English text. Do not parse it. Use `code`. |
| `correlationId` | string (UUID) | Same value as the `X-Correlation-Id` header. |
| `retryable` | boolean | `true` if the same request may succeed later. |
| `reason` | string, optional | A more specific reason for some refusals. |
| `fieldErrors` | object, optional | Field name to list of messages. |
| `currentVersion` | integer, optional | Present on `version_conflict`. |
| `retryAfterSeconds` | integer, optional | Also sent as the `Retry-After` header. |
| `operationId` | string, optional | Present on `partial_success`. |

| `code` | HTTP status | Meaning |
| - | - | - |
| `invalid_json` | 400 | The body is empty or is not valid JSON. |
| `invalid_request` | 400 | The request failed validation. See `fieldErrors`. |
| `unsupported_media_type` | 415 | The `Content-Type` is not `application/json`. |
| `payload_too_large` | 413 | The body is too large. |
| `unauthenticated` | 401 | The token is missing, invalid or expired. |
| `forbidden` | 403 | The operation is not permitted for this caller. |
| `entitlement_denied` | 403 | The company's subscription does not allow the operation. |
| `not_found` | 404 | The record does not exist for your company. |
| `conflict` | 409 | The request conflicts with the current state. |
| `domain_rejected` | 422 | The request is valid but a content rule rejects it. |
| `verification_guard` | 409 | Fleet verification state could not be changed safely. Retryable. |
| `version_required` | 428 | `If-Match` is missing. |
| `version_conflict` | 412 | `If-Match` is stale. |
| `idempotency_key_required` | 428 | `Idempotency-Key` is missing. |
| `idempotency_conflict` | 409 | The key was used for a different request. |
| `rate_limited` | 429 | Too many requests. |
| `partial_success` | 202 | An operation is pending. See `operationId`. |
| `downstream_failure` | 502 | A service that the API depends on failed. |
| `temporarily_unavailable` | 503 | The API or a dependency is not available. Retry later. |
| `stored_data_invalid` | 503 | A stored record does not match its contract. Retrying does not help. |
| `deadline_exceeded` | 504 | A downstream request timed out. |
| `internal_error` | 500 | An unexpected error. |

Each type page lists the codes that its routes actually return.

## Lists

List responses return `items`. Most also return a `meta` object:

```json theme={null}
{
  "items": [],
  "meta": { "partial": false, "paginationComplete": true, "rejectedCount": 0, "rejected": [] }
}
```

`meta.partial` is `true` when the API skipped stored rows that did not match the contract, or when it could not read every page. In that case the response also has the header `X-Content-Partial: true`. `meta.rejected` lists up to 50 skipped rows with their `id` and the invalid `paths`.

## Image URLs

Image fields accept URLs only. An uploaded file is not a request field.

* A URL under `https://asset.grdd.dev/` or `https://devasset.grdd.dev/` must use `https`, have no query string, fragment or credentials, and must not contain the path segments `external`, `provider`, `providers`, `sign`, `signed`, `signature`, `upload` or `uploads`, or a `/cdn-cgi/image/` path.
* A URL on another host must be an `https` URL with a real host name (it needs a dot and a top-level domain). The API keeps it as sent.
* A gallery list must not contain duplicates, and must not repeat the primary image.
* Fleet and Vehicle writes also check that the URL belongs to your company. See the [Fleets](/connect-api/content/fleets#image-rules) and [Vehicles](/connect-api/content/vehicles#image-rules) pages.

An invalid image URL returns `400 invalid_request`. Image objects, media IDs and fields such as `photo` or `imageUrl` in Fleet and Vehicle records are not part of the contract.

## Visibility

The `visibility` field is one of `private`, `partners`, `gnet`, `unlisted` or `public`. On create, an omitted value becomes `public` for Fleets, Service Locations and Services. Documents get a default from their `documentType`. An unknown document type defaults to `private`.


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