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

# API Surface

> Every service a custom integration calls, the scope each method needs, and the limits that apply

The methods a custom integration uses, grouped by what they do. Full generated reference:
[gRPC API](/api-reference/api), [API Scopes](/api-reference/scopes),
[OpenAPI spec](https://github.com/getsynq/api/blob/main/openapi.yaml).

All services are under `synq.entities.*`. REST paths are given for the transcoded HTTP API;
the same methods are available over gRPC.

## Write

### Types

`synq.entities.custom.v1.TypesService`

| Method       | REST                                      | Scope             |
| ------------ | ----------------------------------------- | ----------------- |
| `UpsertType` | `POST /api/entities/custom/v1/types`      | Edit Entity Types |
| `DeleteType` | `DELETE /api/entities/custom/v1/types`    | Edit Entity Types |
| `ListTypes`  | `POST /api/entities/custom/v1/types/list` | Read Entity Types |

`Type` carries `type_id` (`1`–`1000`), `name` (max 100 chars), `svg_icon` (max 1 MB) and
`traits`.

### Entities

`synq.entities.custom.v1.EntitiesService`

| Method         | REST                                      | Scope         |
| -------------- | ----------------------------------------- | ------------- |
| `UpsertEntity` | `POST /api/entities/custom/v1/entities`   | Edit Entities |
| `DeleteEntity` | `DELETE /api/entities/custom/v1/entities` | Edit Entities |
| `ListEntities` | `GET /api/entities/custom/v1/entities`    | Read Entities |

`Entity` carries `id`, `type_id`, `name` (max 500 chars), `description` (max 10 000 chars,
Markdown) and up to 20 `annotations`. Each annotation has a name (max 50 chars) and up to 20
values (max 100 chars each).

`synq_path`, `synq_catalog_url`, `ids` and `synq_paths` are returned, never sent.

### Features

`synq.entities.custom.v1.FeaturesService`

| Method                | REST                                         | Scope         |
| --------------------- | -------------------------------------------- | ------------- |
| `UpsertEntityFeature` | `POST /api/entities/custom/v1/features`      | Edit Entities |
| `DeleteEntityFeature` | `DELETE /api/entities/custom/v1/features`    | Edit Entities |
| `ListEntityFeatures`  | `POST /api/entities/custom/v1/features/list` | Read Entities |

A `Feature` is `entity_id` + `feature_id` (max 255 chars, **stable**) + one of:

| Feature              | Per entity | Limits                                                                                |
| -------------------- | ---------- | ------------------------------------------------------------------------------------- |
| `sql_definition`     | one        | `sql` max 1 000 000 chars, `references` max 200                                       |
| `schema`             | one        | `columns` max 1000, nested `fields` max 100 per column                                |
| `column_lineage`     | one        | `edges` max 10 000, column names max 255 chars                                        |
| `check_category`     | one        | `package` and `kind` max 50 chars each                                                |
| `code`               | many       | `content` max 100 000 chars, `name` max 50 chars                                      |
| `git_file_reference` | many       | `repository_url` and `file_path` max 255 chars, `branch_name` required, max 244 bytes |

`ListEntityFeatures` with `entity_id` unset returns the features of every custom entity in
the workspace.

### Groups

`synq.entities.custom.v1.GroupsService`

| Method                | REST                                       | Scope         |
| --------------------- | ------------------------------------------ | ------------- |
| `UpsertEntitiesGroup` | `POST /api/entities/custom/v1/groups`      | Edit Entities |
| `DeleteEntitiesGroup` | `DELETE /api/entities/custom/v1/groups`    | Edit Entities |
| `ListEntityGroups`    | `POST /api/entities/custom/v1/groups/list` | Read Entities |

`UpsertEntitiesGroup` replaces the group's membership whole and returns `deleted_ids` — the
entities that were in the group and are not in this write.

### Relationships

`synq.entities.custom.v1.RelationshipsService`

| Method                | REST                                              | Scope        |
| --------------------- | ------------------------------------------------- | ------------ |
| `UpsertRelationships` | `POST /api/entities/custom/v1/relationships`      | Edit Lineage |
| `DeleteRelationships` | `DELETE /api/entities/custom/v1/relationships`    | Edit Lineage |
| `ListRelationships`   | `POST /api/entities/custom/v1/relationships/list` | Read Lineage |

Both writes return one `RelationshipWriteResult` per requested relationship, in request
order, each with the resolved endpoints and an outcome of `CREATED`, `UPDATED`, `DELETED` or
`NOT_FOUND`.

At least one endpoint must be a custom entity.

### Check relationships

`synq.entities.custom.v1.ChecksRelationshipsService`

| Method                     | REST                                                    | Scope         |
| -------------------------- | ------------------------------------------------------- | ------------- |
| `UpsertCheckRelationships` | `POST /api/entities/custom/v1/check-relationships`      | Edit Entities |
| `DeleteCheckRelationships` | `DELETE /api/entities/custom/v1/check-relationships`    | Edit Entities |
| `ListCheckRelationships`   | `POST /api/entities/custom/v1/check-relationships/list` | Read Entities |

A `CheckRelationship` is `check` + `checked` + optional `checked_columns`. The pair is the
identity; restating it with a different column list replaces the stored one.

### Executions

`synq.entities.custom.v1.EntityExecutionsService`

| Method            | REST                                                | Scope                                  |
| ----------------- | --------------------------------------------------- | -------------------------------------- |
| `UpsertExecution` | `POST /api/entities/custom/executions/v1`           | Edit Executions                        |
| `UpsertLogEntry`  | `POST /api/entities/custom/executions/v1/log-entry` | Edit Executions or Edit Execution Logs |

`Execution` requires `id`, `status` and `created_at`. Status is one of `EXECUTION_STATUS_OK`,
`_WARN`, `_ERROR`, `_CRITICAL`. `created_at` must be in the past and later than 2022-01-01.
`extras` can carry the executed SQL.

## Read

Reading back is how you verify a write landed. These are the methods worth calling.

| Method                      | Service                                                   | REST                                               | Scope                              |
| --------------------------- | --------------------------------------------------------- | -------------------------------------------------- | ---------------------------------- |
| `BatchResolveIdentifiers`   | `synq.entities.resolve.v1.IdentifierResolveService`       | `POST /api/resolve/v1/by-id`                       | Read Entities                      |
| `GetLineage`                | `synq.entities.lineage.v1.LineageService`                 | `POST /api/lineage/v1`                             | Read Lineage                       |
| `GetImpact`                 | `synq.entities.impact.v1.ImpactService`                   | `POST /api/impact/v1`                              | Read Lineage                       |
| `GetSchema`                 | `synq.entities.schemas.v1.SchemasService`                 | `GET /api/schema/v1`                               | Read Entities                      |
| `SearchEntities`            | `synq.entities.entities.v1.EntitiesService`               | `GET /api/entities/v1/search`                      | Read Entities                      |
| `BatchGetEntities`          | `synq.entities.entities.v1.EntitiesService`               | `POST /api/entities/v1/by-id`                      | Read Entities                      |
| `BatchGetEntityAnnotations` | `synq.entities.annotations.v1.AnnotationsService`         | `POST /api/entities/annotations/v1/batchGet`       | Read Entities                      |
| `ListEntityTypeDefs`        | `synq.entities.typedefs.v1.TypeDefsService`               | `POST /api/entities/typedefs/v1/entity-types/list` | Read Entities or Read Entity Types |
| `GetLatestExecutions`       | `synq.entities.executions.v2.EntityExecutionsService`     | see [API reference](/api-reference/api)            | Read Executions                    |
| `BatchIdsByCoordinates`     | `synq.entities.coordinates.v1.DatabaseCoordinatesService` | `POST /api/coordinates/v1/database/by-fqn`         | Read Entities                      |

`BatchIdsByCoordinates` is how you turn a warehouse name your tool reports into the entity
id Coalesce Quality holds for it — useful when your tool names tables as strings.

`BatchResolveIdentifiers` turns an identifier of **any** shape into its `entity_id`, and with
`check_existence: true` reports whether the entity exists. It also returns the entity's whole
**identity group** — the other identifiers naming the same entity, such as a dbt model and the
warehouse table it builds.

Run it over the upstreams your integration is about to name. A **custom** entity that does not
exist is rejected at the write, so you find that one either way; an entity on a **connected
platform** is not checked and binds on next ingestion, so a typo'd warehouse name and a
not-yet-crawled table look identical afterwards. Resolving separates them.

The read methods above are meant to be driven from your integration as input, not only used
to verify a write. Resolving coordinates, schemas and type numbers at run time is what keeps a
mapping from drifting against a hardcoded table of ids.

## Identifiers

A custom entity is named by `CustomIdentifier`:

```json theme={null}
{ "custom": { "id": "metabase::question::87" } }
```

The `id` is a **slug**, not a label: keep it to a lowercase `A-Z a-z 0-9 : _ . -` string built
from the source system's stable id, and put everything human-readable in `name` and
`description`. On write it is truncated at 200 bytes with a hash tail if longer, non-ASCII is
replaced by its literal escape text, and every other character is folded to `_` — so two ids
differing only in folded characters are **the same entity**, while ids differing only in case
are **two**. The stored id is `custom-<slug>`.

Entities from connected platforms are named by their platform's own variant —
`bigquery_table`, `snowflake_table`, `databricks_table`, `postgres_table`, `mysql_table`,
`clickhouse_table`, `redshift_table`, `trino_table`, `mssql_table`, `oracle_table`,
`athena_table`, `fabric_table`, `dbt_core_node`, `dbt_cloud_node`, `sql_mesh_model`,
`sql_mesh_audit`, `airflow_dag`, `airflow_task`, `monitor`, `dataproduct`, `saved_view`.
Prefer these wherever one exists for the platform.

`synq_path` is the fallback variant, for a platform with no structured form — Looker,
Tableau, Coalesce Catalog and anything added since — or for passing through an `entity_id` you
already hold.

Every identifier the API **returns** also carries `entity_id`, the entity's opaque id, and it
is the `/catalog/<entity_id>` segment of the entity's app URL. It is output only: on a request
it is ignored, and the structured variant is what resolves. To name an entity by opaque id on
a request, put it in the `synq_path` variant.

Ways to obtain an `entity_id`: the app URL after `/catalog/`, the
[MCP read tools](/scout/mcp), the `entity_url` in a [webhook payload](/api-reference/webhook),
or `BatchIdsByCoordinates`.

## Enums

**`SqlDialect`** — `BIGQUERY`, `CLICKHOUSE`, `DATABRICKS`, `MYSQL`, `POSTGRESQL`, `REDSHIFT`,
`SNOWFLAKE`, `DUCKDB`, `TRINO`, `MSSQL`, `ORACLE`, `ATHENA`, `FABRIC`.

**`CodeType`** — `SQL`, `PYTHON`, `JSON`, `YAML`, `DBT`, `SQLMESH`, `LOOKML`, `JAVASCRIPT`,
`BASH`, `DAX`, `JAVA`, `SCALA`, `TEXT`, `MARKDOWN`.

## Pagination

The `List*` methods on the custom services take an optional `Pagination`. Omitting it, or
sending a zero `page_size`, returns everything in one response — there is no server-side
default page size. To page, set `page_size` and pass the previous response's
`page_info.last_id` as `pagination.cursor`, until `last_id` comes back empty.

Not every list paginates: `ListEntityTypeDefs`, `ListExecutionTypeDefs` and `ListPlatformDefs`
take no pagination at all and always return the full set.

## Idempotency

Every `Upsert*` and `Delete*` method here is idempotent in **effect**: re-sending the same
state converges on the same result, and deleting something that does not exist succeeds.

It is not a no-op on the wire. An upsert always writes, restamping `updated_at` and
re-triggering the fan-out that write implies, so a re-sync is not free — send it on a schedule,
not in a tight loop.
