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

# Metadata API

> Manage reusable metadata source and field definitions for API-created records.

Use the Metadata API to define namespaced fields before attaching typed values to a recording or another supported entity. Sources separate fields that may share a name but come from different systems or represent different entities.

All endpoints on this page accept either a user access token or an organization API key. The examples use an organization API key:

```bash theme={null}
X-BuildBetter-Api-Key: ORGANIZATION_API_KEY
```

Base URL:

```text theme={null}
https://api.buildbetter.app/v3/rest
```

## Resource model

A source is an organization-scoped namespace with a public UUID, a `type`, and an `externalEntityName`. Within an organization, the pair of `type` and `externalEntityName` identifies the source definition. Sources of different types can use the same entity name without colliding. A field belongs to exactly one source and is identified by its own public UUID.

For example, separate API sources named `accounts` and `subscriptions` can each define a field named `status` without colliding. Field names need to be unique only within their source.

| Resource        | Important fields                                                              | Ownership                                                                                                                                   |
| --------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| Metadata source | `id`, `externalEntityName`, `type`, `managedBy`, `isCanonicalRecordingSource` | Customer-created API sources are editable. Provider sources and the canonical recordings source cannot be renamed.                          |
| Metadata field  | `id`, `sourceId`, `name`, `type`                                              | Fields under API-typed sources can be created and renamed, including fields under the canonical recordings source. Field type is immutable. |

All `sourceId` and `fieldId` values in this API are public UUIDs. Unknown, inaccessible, and cross-organization UUIDs return `404 Not Found` rather than exposing another organization's resources.

## The recordings source

BuildBetter maintains one canonical source per organization with:

```json theme={null}
{
  "externalEntityName": "recordings",
  "type": "api",
  "managedBy": "buildbetter",
  "isCanonicalRecordingSource": true
}
```

BuildBetter creates this source on demand instead of prepopulating it when the organization is provisioned. `GET /v3/rest/metadata/sources` ensures the source exists before returning results, so you can discover its public UUID and define recording fields before creating the organization's first recording. `POST /v3/rest/recordings` also ensures it exists when creating a recording.

BuildBetter owns this source's name, so it cannot be recreated or renamed. You can create and rename fields beneath it because it is API-typed.

`POST /v3/rest/recordings` always creates one canonical metadata record in this source. Recording create accepts `record.values[].fieldId`, but it does not accept `sourceId`; every supplied field must belong to the canonical `recordings` source.

## Endpoint overview

| Method  | Path                                          | Purpose                              |
| ------- | --------------------------------------------- | ------------------------------------ |
| `GET`   | `/v3/rest/metadata/sources`                   | List visible metadata sources        |
| `POST`  | `/v3/rest/metadata/sources`                   | Create a customer-managed API source |
| `GET`   | `/v3/rest/metadata/sources/{sourceId}`        | Get one source                       |
| `PATCH` | `/v3/rest/metadata/sources/{sourceId}`        | Rename a customer-managed source     |
| `GET`   | `/v3/rest/metadata/sources/{sourceId}/fields` | List fields in a source              |
| `POST`  | `/v3/rest/metadata/sources/{sourceId}/fields` | Create a field in an API source      |
| `GET`   | `/v3/rest/metadata/fields/{fieldId}`          | Get one field                        |
| `PATCH` | `/v3/rest/metadata/fields/{fieldId}`          | Rename a field in an API source      |

Deletion is not exposed. Removing definitions that already have values requires separate lifecycle handling.

## List sources

<Card title="GET /v3/rest/metadata/sources" icon="list">
  ```bash theme={null}
  curl "https://api.buildbetter.app/v3/rest/metadata/sources?page=1&limit=10" \
    -H "X-BuildBetter-Api-Key: ORGANIZATION_API_KEY"
  ```
</Card>

### Query parameters

| Parameter | Required | Description                                            |
| --------- | -------- | ------------------------------------------------------ |
| `page`    | No       | Page number, starting at `1`. Defaults to `1`.         |
| `limit`   | No       | Results per page, from `1` to `100`. Defaults to `10`. |

### Success response

`200 OK`. The endpoint returns an array for the requested page.

```json theme={null}
[
  {
    "id": "0198f3b0-6e2d-7b11-8e12-91b2f91f6401",
    "externalEntityName": "recordings",
    "type": "api",
    "managedBy": "buildbetter",
    "isCanonicalRecordingSource": true,
    "createdAt": "2026-07-28T12:00:00.000Z",
    "updatedAt": "2026-07-28T12:00:00.000Z"
  },
  {
    "id": "0198f3b0-6e2d-7b11-8e12-91b2f91f6701",
    "externalEntityName": "accounts",
    "type": "api",
    "managedBy": "customer",
    "isCanonicalRecordingSource": false,
    "createdAt": "2026-07-28T12:10:00.000Z",
    "updatedAt": "2026-07-28T12:10:00.000Z"
  }
]
```

`managedBy` can be `customer`, `integration`, or `buildbetter`. Provider-managed sources retain their provider `type`, while sources created through this API always have type `api`.

Listing sources creates the canonical API-typed `recordings` source on demand if it does not exist yet. Concurrent list and recording-create requests still resolve to the same organization-scoped source.

Request successive pages until a page contains fewer than `limit` results.

## Create a source

<Card title="POST /v3/rest/metadata/sources" icon="plus">
  Creates an API-typed namespace for an external entity.

  ```bash theme={null}
  curl -X POST "https://api.buildbetter.app/v3/rest/metadata/sources" \
    -H "X-BuildBetter-Api-Key: ORGANIZATION_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "externalEntityName": "accounts"
    }'
  ```
</Card>

`externalEntityName` is required, is trimmed, and must contain from `1` to `255` characters. The server sets `type` to `api`; the request cannot select an integration provider type.

### Success response

`201 Created`

```json theme={null}
{
  "id": "0198f3b0-6e2d-7b11-8e12-91b2f91f6701",
  "externalEntityName": "accounts",
  "type": "api",
  "managedBy": "customer",
  "isCanonicalRecordingSource": false,
  "createdAt": "2026-07-28T12:10:00.000Z",
  "updatedAt": "2026-07-28T12:10:00.000Z"
}
```

Creating an API source whose entity name is already used by another API source in the organization returns `409 Conflict`. A provider source of another `type` may use the same entity name. The name `recordings` is reserved for the canonical BuildBetter-managed API source and also returns `409 Conflict`.

If a create response is lost, list sources and match `externalEntityName` before retrying. Retrying an already successful create returns `409 Conflict` rather than creating a duplicate.

## Get a source

<Card title="GET /v3/rest/metadata/sources/{sourceId}" icon="database">
  ```bash theme={null}
  curl "https://api.buildbetter.app/v3/rest/metadata/sources/0198f3b0-6e2d-7b11-8e12-91b2f91f6701" \
    -H "X-BuildBetter-Api-Key: ORGANIZATION_API_KEY"
  ```
</Card>

Returns `200 OK` with the same source shape returned by create. An unknown or inaccessible `sourceId` returns `404 Not Found`.

## Update a source

<Card title="PATCH /v3/rest/metadata/sources/{sourceId}" icon="pen">
  Renames a customer-managed API source.

  ```bash theme={null}
  curl -X PATCH "https://api.buildbetter.app/v3/rest/metadata/sources/0198f3b0-6e2d-7b11-8e12-91b2f91f6701" \
    -H "X-BuildBetter-Api-Key: ORGANIZATION_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "externalEntityName": "customer_accounts"
    }'
  ```
</Card>

The complete request body is `{ "externalEntityName": string }`. `type`, `managedBy`, and `isCanonicalRecordingSource` are server-owned and cannot be patched.

Returns `200 OK` with the updated source.

| Status             | When it is returned                                                                      |
| ------------------ | ---------------------------------------------------------------------------------------- |
| `400 Bad Request`  | The body is malformed or the name is blank or too long.                                  |
| `401 Unauthorized` | Authentication is missing or invalid.                                                    |
| `403 Forbidden`    | The source is integration-managed or is the canonical `recordings` source.               |
| `404 Not Found`    | The source UUID is unknown or inaccessible.                                              |
| `409 Conflict`     | The new name is already used by another API source or is the reserved `recordings` name. |

## List fields in a source

<Card title="GET /v3/rest/metadata/sources/{sourceId}/fields" icon="list">
  ```bash theme={null}
  curl "https://api.buildbetter.app/v3/rest/metadata/sources/0198f3b0-6e2d-7b11-8e12-91b2f91f6401/fields?page=1&limit=10" \
    -H "X-BuildBetter-Api-Key: ORGANIZATION_API_KEY"
  ```
</Card>

`page` defaults to `1`; `limit` defaults to `10` and can be from `1` to `100`.

### Success response

`200 OK`. The endpoint returns an array for the requested page.

```json theme={null}
[
  {
    "id": "0198f3b0-6e2d-7b11-8e12-91b2f91f6101",
    "sourceId": "0198f3b0-6e2d-7b11-8e12-91b2f91f6401",
    "name": "contract_value",
    "type": "integer",
    "createdAt": "2026-07-28T12:20:00.000Z",
    "updatedAt": "2026-07-28T12:20:00.000Z"
  }
]
```

An unknown or inaccessible source UUID returns `404 Not Found`. Request successive pages until a page contains fewer than `limit` results.

## Create a field

<Card title="POST /v3/rest/metadata/sources/{sourceId}/fields" icon="plus">
  ```bash theme={null}
  curl -X POST "https://api.buildbetter.app/v3/rest/metadata/sources/0198f3b0-6e2d-7b11-8e12-91b2f91f6401/fields" \
    -H "X-BuildBetter-Api-Key: ORGANIZATION_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "name": "contract_value",
      "type": "integer"
    }'
  ```
</Card>

`name` is required, is trimmed, and must contain from `1` to `255` characters. `type` is required and must be one of the field types below.

### Success response

`201 Created`

```json theme={null}
{
  "id": "0198f3b0-6e2d-7b11-8e12-91b2f91f6101",
  "sourceId": "0198f3b0-6e2d-7b11-8e12-91b2f91f6401",
  "name": "contract_value",
  "type": "integer",
  "createdAt": "2026-07-28T12:20:00.000Z",
  "updatedAt": "2026-07-28T12:20:00.000Z"
}
```

Field names must be unique within one source. The same name can be used in a different source without conflict. Retrying an already successful create with the same source and name returns `409 Conflict`.

| Status             | When it is returned                                                   |
| ------------------ | --------------------------------------------------------------------- |
| `400 Bad Request`  | The body is malformed, the name is invalid, or `type` is unsupported. |
| `401 Unauthorized` | Authentication is missing or invalid.                                 |
| `403 Forbidden`    | The source is integration-managed rather than API-typed.              |
| `404 Not Found`    | The source UUID is unknown or inaccessible.                           |
| `409 Conflict`     | A field with the same name already exists in the source.              |

## Get a field

<Card title="GET /v3/rest/metadata/fields/{fieldId}" icon="tag">
  ```bash theme={null}
  curl "https://api.buildbetter.app/v3/rest/metadata/fields/0198f3b0-6e2d-7b11-8e12-91b2f91f6101" \
    -H "X-BuildBetter-Api-Key: ORGANIZATION_API_KEY"
  ```
</Card>

Returns `200 OK` with the same field shape returned by create. An unknown or inaccessible `fieldId` returns `404 Not Found`.

## Update a field

<Card title="PATCH /v3/rest/metadata/fields/{fieldId}" icon="pen">
  Renames a field in an API-typed source.

  ```bash theme={null}
  curl -X PATCH "https://api.buildbetter.app/v3/rest/metadata/fields/0198f3b0-6e2d-7b11-8e12-91b2f91f6101" \
    -H "X-BuildBetter-Api-Key: ORGANIZATION_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "name": "annual_contract_value"
    }'
  ```
</Card>

The complete request body is `{ "name": string }`. A field's `type` and `sourceId` are immutable through this API.

Returns `200 OK` with the updated field.

| Status             | When it is returned                                     |
| ------------------ | ------------------------------------------------------- |
| `400 Bad Request`  | The body is malformed or the name is blank or too long. |
| `401 Unauthorized` | Authentication is missing or invalid.                   |
| `403 Forbidden`    | The field belongs to an integration-managed source.     |
| `404 Not Found`    | The field UUID is unknown or inaccessible.              |
| `409 Conflict`     | The new name already exists in the same source.         |

## Field types and values

Field definitions use one of these immutable types. Values are validated when an owning resource, such as a recording, creates its metadata record.

| Field type | Accepted JSON value                                                                    |
| ---------- | -------------------------------------------------------------------------------------- |
| `string`   | A JSON string.                                                                         |
| `boolean`  | `true` or `false`, not the strings `"true"` or `"false"`.                              |
| `integer`  | A JSON number that is a safe integer.                                                  |
| `float`    | A finite JSON number.                                                                  |
| `date`     | A valid RFC 3339 full-date string such as `"2026-07-28"`.                              |
| `datetime` | An RFC 3339 date-time string with a timezone offset, such as `"2026-07-28T15:00:00Z"`. |
| `json`     | Any JSON-serializable value.                                                           |

Every type also accepts `null`. BuildBetter returns public values in their declared JSON types rather than as storage strings. A value that does not match the field definition returns `422 Unprocessable Entity` from the owning resource endpoint.

## Use recording fields

First find the canonical source by listing sources and selecting the item where `isCanonicalRecordingSource` is `true`. Create or list fields beneath its public `id`, then include their public field UUIDs in recording create:

```json theme={null}
{
  "record": {
    "externalId": "crm-call-4815",
    "values": [
      {
        "fieldId": "0198f3b0-6e2d-7b11-8e12-91b2f91f6101",
        "value": 125000
      }
    ]
  }
}
```

The recording request deliberately omits `sourceId`. BuildBetter resolves the canonical API-typed `recordings` source and rejects a field from any other source with `422 Unprocessable Entity`. See [Create a recording](/pages/api/recordings#create-a-recording) for the complete request.

## Common errors

REST errors include an HTTP status plus the standard error response fields `statusCode`, `timestamp`, `path`, `error`, and, when available, `message`. Handle status codes rather than matching human-readable messages.

Across all metadata endpoints:

* `401 Unauthorized` means authentication is missing or invalid.
* `403 Forbidden` means the definition is visible but read-only for this API operation.
* `404 Not Found` also covers inaccessible and cross-organization UUIDs.
* `409 Conflict` indicates a source or field uniqueness collision.

Invalid requests do not create partial definitions. Correct the request and retry it.
