# List surfaces across catalogs `GET /platform/v1/surfaces` List surfaces of every catalog the caller may see, narrowed by organization and / or tag. To list one catalog's surfaces use `GET /platform/v1/catalogs/{catalog_id}/surfaces`. Results are ordered by catalog_id, then id. ## Request example ```bash curl --request GET \ --url 'https://api.eu1.luigisbox.ai/platform/v1/surfaces?organization_id=lbo_a1b2c3&tag_id=lbt_a1b2c3&page=1&size=20' \ --header 'Authorization: Bearer ' ``` ## Query Parameters | Name | Type | Required | Description | Constraints | |---|---|---|---|---| | `organization_id` | `anyOf(string, null)` | No | Restrict to surfaces of this organization's catalogs. | — | | `tag_id` | `anyOf(string, null)` | No | Restrict to members of this surface-dimension tag. Every tagged surface response carries the `lbt_` id it accepts under `tags[].id`. A tag that does not exist, belongs to another organization, or groups another dimension simply matches nothing. | — | | `page` | `integer` | No | 1-based page number. | >= 1 | | `size` | `integer` | No | Number of items per page. | >= 1; <= 100 | ## Responses | Status | Description | Content | |---|---|---| | `200` | Successful Response | application/json: object | | `401` | Missing or invalid credentials | application/json: object | | `403` | Authenticated but not permitted | application/json: object | | `422` | Request validation failed | application/json: object | ### 200 Successful Response #### application/json Schema: `object` ##### Attributes | Attribute | Type | Required | Description | Constraints | |---|---|---|---|---| | `items` | `Array` | Yes | The items on this page. | — | | `items[].catalog_id` | `string` | Yes | Catalog Id | — | | `items[].created_at` | `string` | Yes | Created At | format: date-time | | `items[].default_version` | `anyOf(integer, null)` | Yes | Default Version | — | | `items[].id` | `string` | Yes | Id | — | | `items[].input_type` | `anyOf(string, null)` | Yes | Catalog object type of the anchor objects this surface is queried with — what a recommender request names in `anchor_ids`. Required for `recommender` surfaces and rejected for every other kind, which take no anchor. | — | | `items[].intent` | `string` | Yes | The kind of discovery this Surface performs — it fixes which Discovery API endpoint serves the Surface and what input a request must supply (`search` a query, `recommender` anchor objects, `collection` a scope). It is the contract, not the algorithm: a `search` Surface may retrieve lexically, semantically or both. Immutable — it is the middle slot of the surface id `lbs__`, so changing it means a new Surface. | allowed values: search, recommender, collection | | `items[].output_types` | `Array` | Yes | Catalog object types this surface may answer with. Non-empty; duplicates collapse and input order is preserved. Acts as an allowlist: a request asking for a type absent from this list is rejected rather than answered with fewer results. It is a property of the surface, not of its kind — a recommender declares whatever it recommends, so a category recommender carries `category`. | — | | `items[].slug` | `string` | Yes | Slug | — | | `items[].tags` | `Array` | Yes | The surface-dimension tags this surface belongs to, ordered by name, each carrying both its id and its name — the `id` is what the `tag_id` filter takes, the `name` is what a person reads, and neither needs a further request. Read-only here: membership is changed through `POST /platform/v1/tags`, or `PATCH /platform/v1/tags/{tag_id}` with `add_members` / `remove_members`, which carry `{catalog_id, surface_id}` pairs. A surface belongs to at most 20 tags. | — | | `items[].tags[].id` | `string` | Yes | The tag's id — what `tag_id` filters and the tag writes take. | — | | `items[].tags[].name` | `string` | Yes | Tag name as `namespace:value`, lowercased on write (`intent:SEARCH` is stored as `intent:search`). The `intent`, `language` and `recommender` namespaces are platform-managed: writing one requires `platform:reserved-tags:write` and deleting one requires `platform:reserved-tags:delete`. | <= 256 characters; /^[a-z][a-z0-9_-]{0,63}:[^\s:]{1,191}$/ | | `items[].updated_at` | `string` | Yes | Updated At | format: date-time | | `page` | `integer` | Yes | 1-based page number. | — | | `size` | `integer` | Yes | Number of items per page. | — | | `total` | `integer` | Yes | Total number of items across all pages. | — | ##### Response example ```json { "items": [ { "catalog_id": "example", "created_at": "2026-04-15T12:00:00Z", "default_version": 1, "id": "example", "input_type": "example", "intent": "search", "output_types": [ "example" ], "slug": "example", "tags": [ { "id": "example", "name": "example" } ], "updated_at": "2026-04-15T12:00:00Z" } ], "page": 1, "size": 1, "total": 1 } ``` ### 401 Missing or invalid credentials #### application/json Schema: `object` ##### Attributes | Attribute | Type | Required | Description | Constraints | |---|---|---|---|---| | `exception_details` | `object` | No | Machine-readable error context; empty object when there is nothing to add. | — | | `exception_details.error_code` | `string` | No | Stable machine code naming why a well-formed request was refused: snake_case, prefixed by its domain — e.g. `filter_unknown_field` or `cursor_mismatch`. Safe to branch on; `reason` is not. Absent when the status code says all there is to say. | — | | `exception_details.validation_errors` | `Array` | No | Every field-level problem found in the request, when validation is what failed. Absent when the rejection is not about a specific value. | — | | `exception_details.validation_errors[].loc` | `Array` | Yes | Path to the offending value from the request root, e.g. `["body", "filters", 0, "operator"]`. | — | | `exception_details.validation_errors[].msg` | `string` | Yes | What is wrong with the value at `loc`. | — | | `exception_details.validation_errors[].type` | `string` | Yes | Stable machine code for the problem, e.g. `missing` or `string_too_short`. | — | | `exception_details.*` | `any` | No | Additional property. | — | | `reason` | `string` | Yes | Human-readable, actionable error message. | — | | `request_id` | `anyOf(string, null)` | No | Request correlation ID (matches the X-Request-Id response header). | — | ##### Response example ```json { "exception_details": { "error_code": "example", "validation_errors": [ { "loc": [ "example" ], "msg": "example", "type": "example" } ] }, "reason": "example", "request_id": "example" } ``` ### 403 Authenticated but not permitted #### application/json Schema: `object` ##### Attributes | Attribute | Type | Required | Description | Constraints | |---|---|---|---|---| | `exception_details` | `object` | No | Machine-readable error context; empty object when there is nothing to add. | — | | `exception_details.error_code` | `string` | No | Stable machine code naming why a well-formed request was refused: snake_case, prefixed by its domain — e.g. `filter_unknown_field` or `cursor_mismatch`. Safe to branch on; `reason` is not. Absent when the status code says all there is to say. | — | | `exception_details.validation_errors` | `Array` | No | Every field-level problem found in the request, when validation is what failed. Absent when the rejection is not about a specific value. | — | | `exception_details.validation_errors[].loc` | `Array` | Yes | Path to the offending value from the request root, e.g. `["body", "filters", 0, "operator"]`. | — | | `exception_details.validation_errors[].msg` | `string` | Yes | What is wrong with the value at `loc`. | — | | `exception_details.validation_errors[].type` | `string` | Yes | Stable machine code for the problem, e.g. `missing` or `string_too_short`. | — | | `exception_details.*` | `any` | No | Additional property. | — | | `reason` | `string` | Yes | Human-readable, actionable error message. | — | | `request_id` | `anyOf(string, null)` | No | Request correlation ID (matches the X-Request-Id response header). | — | ##### Response example ```json { "exception_details": { "error_code": "example", "validation_errors": [ { "loc": [ "example" ], "msg": "example", "type": "example" } ] }, "reason": "example", "request_id": "example" } ``` ### 422 Request validation failed #### application/json Schema: `object` ##### Attributes | Attribute | Type | Required | Description | Constraints | |---|---|---|---|---| | `exception_details` | `object` | No | Machine-readable error context; empty object when there is nothing to add. | — | | `exception_details.error_code` | `string` | No | Stable machine code naming why a well-formed request was refused: snake_case, prefixed by its domain — e.g. `filter_unknown_field` or `cursor_mismatch`. Safe to branch on; `reason` is not. Absent when the status code says all there is to say. | — | | `exception_details.validation_errors` | `Array` | No | Every field-level problem found in the request, when validation is what failed. Absent when the rejection is not about a specific value. | — | | `exception_details.validation_errors[].loc` | `Array` | Yes | Path to the offending value from the request root, e.g. `["body", "filters", 0, "operator"]`. | — | | `exception_details.validation_errors[].msg` | `string` | Yes | What is wrong with the value at `loc`. | — | | `exception_details.validation_errors[].type` | `string` | Yes | Stable machine code for the problem, e.g. `missing` or `string_too_short`. | — | | `exception_details.*` | `any` | No | Additional property. | — | | `reason` | `string` | Yes | Human-readable, actionable error message. | — | | `request_id` | `anyOf(string, null)` | No | Request correlation ID (matches the X-Request-Id response header). | — | ##### Response example ```json { "exception_details": { "error_code": "example", "validation_errors": [ { "loc": [ "example" ], "msg": "example", "type": "example" } ] }, "reason": "example", "request_id": "example" } ```