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

# Services

> Create, read, update, order and delete the services of your company with the Connect API

A **service** is something your company offers, for example "Airport Transfer". It has a title, a description, tags, images and a display order. Services appear on your company profile.

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

The Service routes are implemented as owner routes (your own company) and as authenticated reads of another company's published services. The Connect API README names only Fleets and Vehicles as complete production implementations, and says not to infer completeness for other content types. 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/services` | List your services, with a cursor. | – | – |
| `POST` | `/api/services` | Create a service. | Required | – |
| `GET` | `/api/services/{serviceId}` | Read one service. Returns an `ETag`. | – | – |
| `PATCH` | `/api/services/{serviceId}` | Update one service. | Required | Required |
| `DELETE` | `/api/services/{serviceId}` | Delete one service, or one language version. | – | Required |
| `POST` | `/api/services/reorderings` | Set the display order of several services. | Required | – (versions are in the body) |
| `POST` | `/api/services/operations/{operationId}` | Resume a pending operation. See [Pending operations](/connect-api/content/api-conventions#pending-operations). | – | – |
| `GET` | `/api/services/tag-catalog` | Read the tag catalog: preset tags and your custom tags. Returns an `ETag`. | – | – |
| `PUT` | `/api/services/tag-catalog` | Replace your custom tags. | Required | Required |

`{serviceId}` is the numeric service ID.

### Read another company's published services

| Method | Path | Purpose |
| - | - | - |
| `GET` | `/api/services/public/{griddid}` | List the published services of another company with your Bearer token. |
| `GET` | `/api/services/public/{griddid}/{slug}` | Read one published service by slug. |

These routes return a reduced record (`id`, `slug`, `title`, `description`, `icon`, `imageUrl`, `tags`, `displayOrder`, `locale`, `resolvedLocale`, `updatedAt`, and on the detail route `extraImageUrls`). They return only active services. `public` services are always returned. `gnet` services are returned to any signed-in caller. `partners` services are returned only if core confirms that you are a partner of the company. `private` and `unlisted` services are not returned.

The list route takes the query `locale` (default `en`), `cursor` and `limit` (1 to 100, default 50). The response has `items`, `nextCursor`, and can have `meta` and `lockedCount`. If `lockedCount` is present, the company's plan limits the published services that a viewer can see, and `items` holds only the first service.

### Not for external use

These routes need the `Service` credential of the public website renderer or an administrator role. Do not call them.

* `GET /api/services/catalog`
* `GET /api/services/published-index`
* `GET /api/services/published/{griddid}` and `GET /api/services/published/{griddid}/{slug}`
* `GET /api/services/system/{griddid}` (administrators)

## Authentication and access

* Send `Authorization: Bearer YOUR_ACCESS_TOKEN`. See [Authentication](/connect-api/content/api-conventions#authentication).
* **Role.** Create, update, delete, reorder and tag-catalog replace need a token with the role `ADMIN`, `MANAGER` or `VENDOR`. Otherwise the API returns `403 forbidden`. Reads need no role.
* **Subscription.** The service write path in source has no subscription check. The subscription contract sets no limit for services.

## Service fields

### Read record

| Field | Type | Notes |
| - | - | - |
| `id` | integer | |
| `slug` | string | |
| `griddid` | string | Your company. |
| `title` | string | 2 to 100 characters. |
| `description` | string or null | Up to 5000 characters. |
| `icon` | string or null | |
| `priceRange` | string or null | |
| `imageUrl` | string or null | See [Image URLs](/connect-api/content/api-conventions#image-urls). |
| `extraImageUrls` | array of strings | No duplicates. Must not contain `imageUrl`. |
| `status` | `active` or `inactive` | |
| `visibility` | `private`, `partners`, `gnet`, `unlisted`, `public` | |
| `displayOrder` | string | A decimal number as a string, for example `"1.5"`. |
| `tags` | array of strings | 1 to 50 characters each. No duplicates. |
| `pageId` | integer or null | |
| `documentIds` | array of integers | No duplicates. |
| `locale` | `en`, `es`, `de`, `fr` | The language you asked for. |
| `resolvedLocale` | `en`, `es`, `de`, `fr` | The language of the returned text. |
| `version` | integer | Use it in `If-Match`. |
| `createdAt`, `updatedAt` | string (ISO 8601) | |

### Write body

Service write bodies are flat. They have no wrapper object. They are strict: unknown keys, such as `id`, `griddid` or `version`, return `400 invalid_request`.

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

| Field | Type | Required | Notes |
| - | - | - | - |
| `title` | string | **Yes** | 2 to 100 characters. |
| `status` | `active` or `inactive` | **Yes** | |
| `visibility` | enum | No | Default `public`. |
| `description` | string or null | No | Up to 5000 characters. |
| `icon`, `priceRange` | string or null | No | |
| `imageUrl` | URL or null | No | |
| `extraImageUrls` | array of URLs, or null | No | No duplicates. Must not contain `imageUrl`. |
| `displayOrder` | string | No | A decimal string. If you omit it, the API assigns one. |
| `tags` | array of strings | No | |
| `pageId` | integer or null | No | |
| `documentIds` | array of integers, or null | No | |
| `locale` | `en`, `es`, `de`, `fr` | No | Default `en`. |
| `contentLanguage` | object | No | `{ "intent": "create_record", "language": "fr" }`. |

**Update** (`PATCH /api/services/{serviceId}`): the same fields, all optional, but at least one field is required. `title`, `status` and `visibility` are optional. `locale` is optional, and `contentLanguage` must be `update_version` or `add_version`. Send `If-Match` and `Idempotency-Key`.

**Delete** (`DELETE /api/services/{serviceId}`): send `If-Match`. Optional query `locale` and `languageVersion`.

**Reorder** (`POST /api/services/reorderings`):

| Field | Type | Required | Notes |
| - | - | - | - |
| `items` | array | Yes | 2 to 100 items. Each `serviceId` must be unique. |
| `items[].serviceId` | integer | Yes | |
| `items[].version` | integer | Yes | The version you read. A stale version returns `412 version_conflict`. |
| `items[].displayOrder` | string | Yes | A decimal string. |
| `locale` | `en`, `es`, `de`, `fr` | No | Default `en`. |

The response has `items` (the updated services), `operationId`, `status` and `effects`.

**Replace the tag catalog** (`PUT /api/services/tag-catalog`): body `{ "customTags": [{ "value": "airport", "label": "Airport" }] }`. At most 1000 tags. `value` is trimmed, lower-cased, 1 to 50 characters, and unique. `label` is 1 to 50 characters. Send `If-Match` with the catalog `version` from the last `GET`, and `Idempotency-Key`.

## Create a service

```bash theme={null}
curl -X POST "$CONNECT_API_BASE_URL/api/services" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: c2b7f1a4-83d0-4e65-b1a9-6f0e4d7a2c58" \
  -d '{
    "title": "Airport Transfer",
    "description": "Private airport transportation",
    "status": "active",
    "icon": "plane",
    "imageUrl": "https://asset.grdd.dev/demo-company/services/airport.webp",
    "tags": ["airport"]
  }'
```

`201 Created` with the header `ETag: "v1"`. The `data` object is built from the fixture `service-record.json`:

```json theme={null}
{
  "data": {
    "id": 42,
    "slug": "airport-transfer",
    "griddid": "demo-company",
    "title": "Airport Transfer",
    "description": "Private airport transportation",
    "icon": "plane",
    "priceRange": null,
    "imageUrl": "https://asset.grdd.dev/demo-company/services/airport.webp",
    "extraImageUrls": [],
    "status": "active",
    "visibility": "public",
    "displayOrder": "1.5",
    "tags": ["airport"],
    "pageId": null,
    "documentIds": [],
    "locale": "en",
    "resolvedLocale": "en",
    "version": 1,
    "createdAt": "2026-09-02T12:00:00.000Z",
    "updatedAt": "2026-09-02T12:00:00.000Z"
  },
  "tagCanonicalization": { "status": "not_needed" },
  "operationId": "service-00000000-0000-4000-8000-000000000001",
  "status": "completed",
  "effects": [
    { "name": "owner_read_model", "status": "completed", "retryable": false },
    { "name": "owner_cache", "status": "completed", "retryable": false }
  ]
}
```

The mutation response has `data`, `tagCanonicalization`, `operationId`, `status` and `effects`.

* `status` is `completed`, `pending` or `partial`. A create or update returns `201` or `200` if `completed`, and `202` otherwise.
* `tagCanonicalization.status` is `not_needed`, `completed` or `skipped`. A `skipped` result has a `reason`.
* `effects[].name` is one of `owner_read_model`, `owner_cache`, `document_edges`, `profile_facts`, `feed_projection`, `worker_event`, `published_projection`, `published_index`.

<Note>
  The repository has no create-response fixture for services. The values of `operationId` and `effects` above are examples that follow the contract.
</Note>

## Read services

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

The query parameters are `locale` (default `en`), `cursor`, `limit` (1 to 100, default 50) and `force` (`true` or `false`). Use the `nextCursor` of one response as the `cursor` of the next request. `nextCursor: null` means the last page. The response is the fixture `service-list.json`:

```json theme={null}
{
  "items": [
    {
      "id": 42,
      "slug": "airport-transfer",
      "griddid": "demo-company",
      "title": "Airport Transfer",
      "description": "Private airport transportation",
      "icon": "plane",
      "priceRange": null,
      "imageUrl": "https://asset.grdd.dev/demo-company/services/airport.webp",
      "extraImageUrls": [],
      "status": "active",
      "visibility": "public",
      "displayOrder": "1.5",
      "tags": ["airport"],
      "pageId": null,
      "documentIds": [7],
      "locale": "en",
      "resolvedLocale": "en",
      "version": 1,
      "createdAt": "2026-09-02T12:00:00.000Z",
      "updatedAt": "2026-09-02T12:00:00.000Z"
    }
  ],
  "nextCursor": null
}
```

`GET /api/services/42` returns one service and an `ETag`. A service that does not exist for your company returns `404 not_found`.

## Update a service

```bash theme={null}
curl -X PATCH "$CONNECT_API_BASE_URL/api/services/42" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "If-Match: \"v1\"" \
  -H "Idempotency-Key: 18e5d0b3-2a47-4c91-8f6d-b3a9c0e7d124" \
  -d '{ "priceRange": "$$", "tags": ["airport", "corporate"] }'
```

The response is `200` (or `202` if not completed) with the mutation response shape and a new `ETag`. A stale `If-Match` returns `412 version_conflict`.

## Delete a service

```bash theme={null}
curl -X DELETE "$CONNECT_API_BASE_URL/api/services/42" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "If-Match: \"v2\""
```

`204 No Content` when the delete is complete. Otherwise the response is `202`, with `operationId`, `status` (`pending` or `partial`) and `effects`, and no `data`. Resume it with `POST /api/services/operations/{operationId}`. The resume route returns `200` when the status is `completed`, and `202` otherwise.

## Errors

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

| Status | `code` | When |
| - | - | - |
| 400 | `invalid_json`, `invalid_request` | The body or query is not valid. Includes unknown keys, a bad `If-Match` or `Idempotency-Key` value, an image URL that fails the URL rules, and a service rule violation. |
| 401 | `unauthenticated` | The token is missing, invalid or expired. |
| 403 | `forbidden` | The token has no role `ADMIN`, `MANAGER` or `VENDOR` for a write. |
| 404 | `not_found` | No service with this ID for your company, or no such operation. |
| 409 | `conflict` | An operation cannot be resumed yet, or a content language rule refused the change. |
| 409 | `idempotency_conflict` | The key was used for a different request, or the first request with this key is still running. |
| 412 | `version_conflict` | `If-Match` (or an item version) is stale. The body has `currentVersion`. |
| 413, 415 | `payload_too_large`, `unsupported_media_type` | Body size or media type. |
| 428 | `version_required`, `idempotency_key_required` | A required header is missing. |
| 503 | `temporarily_unavailable` | Core, the cache or the data store is not available. |
| 503 | `stored_data_invalid` | A stored service does not match its contract. Retrying does not help. |
| 500 | `internal_error` | Unexpected error. |


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