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

# How the schema works

> Objects, properties, records, and fields: the four ideas the whole API is built on.

Custral has no fixed data model. Your workspace defines its own, and the API reflects whatever you've built. Four ideas cover all of it:

<CardGroup cols={2}>
  <Card title="Object" icon="table-columns">
    A **table**. Contacts, Companies, Deals, or something only your workspace has.
  </Card>

  <Card title="Property" icon="list">
    A **column** on an object. Has a type, which decides what its values look like.
  </Card>

  <Card title="Record" icon="table-rows">
    A **row** in an object. One contact, one deal.
  </Card>

  <Card title="Field" icon="input-text">
    One **value**. A record's answer for one property.
  </Card>
</CardGroup>

So: an object has properties; a record belongs to an object and has a field per property.

```
Contacts (object)
├── email     (property, type: email)
├── company   (property, type: relation)
└── rec_3Ab…  (record)
    └── fields: {email: "ada@example.com", company: "rec_7Cd…"}
```

## Fields are keyed by property key

A record's `fields` is a flat map keyed by each property's **`key`**, a stable, human-readable identifier like `email` or `annual_revenue`:

```json theme={null}
{
  "id": "rec_3Ab9xK2mQ7",
  "object": "contacts",
  "name": "Ada Lovelace",
  "fields": {
    "email": "ada@example.com",
    "company": "rec_7Cd1yL8nR2"
  }
}
```

The same keys work when writing, so **what you send is what you get back**:

```bash theme={null}
curl -X POST https://api.custral.com/v1/records/contacts \
  -H "Authorization: Bearer sk_live_..." \
  -d '{"fields": {"email": "ada@example.com"}}'
```

Writes are more forgiving than reads: on the way in, a key may be a property's `key`, its **display name** (case-insensitive), or its **id**, so you can post the shape your own system already has. Reads always come back keyed by `key`.

<Note>
  Every record in a list has the **same** `fields` keys. A property with no value
  is `null`, never missing, so you can read `record.fields.email` without
  guarding for its absence.
</Note>

## Discover the schema before you write

Fetch the object to learn which properties exist and what type each one is:

```bash theme={null}
curl https://api.custral.com/v1/objects/obj_3Ab9xK2mQ7 \
  -H "Authorization: Bearer sk_live_..."
```

The `type` is what decides the value you send. Getting it wrong is the most common integration bug: a `select` wants one of its declared options, a `relation` wants a record id, a `number` wants a number rather than a numeric string.

## Property types

The types you'll meet most, and what a value looks like on the wire:

| Type                          | Value                                                         |
| ----------------------------- | ------------------------------------------------------------- |
| `text`                        | A string.                                                     |
| `number`                      | A number, `42`, not `"42"`.                                   |
| `currency`                    | A number; the currency itself lives in the property's config. |
| `date` · `time`               | An ISO 8601 string.                                           |
| `email` · `phone` · `website` | A string, validated for shape.                                |
| `select` · `status`           | The **id of one of the property's options**, not its label.   |
| `user`                        | A user id.                                                    |
| `relation`                    | The **id of a record** in the related object.                 |
| `file` · `image`              | A resource id.                                                |
| `address`                     | An object with the address parts.                             |
| `id`                          | The record's own identifier, read-only.                       |
| `formula` · `rollup`          | **Computed. Read-only**. See below.                           |

The full set is larger (`array`, `notes`, `whiteboard`, `spreadsheet`, `presentation`, `channel`, `conversation`, and several internal ones), but those back in-app surfaces rather than integration data.

## Computed properties are read-only

A `formula` or `rollup` property has **no stored value**. It's recalculated every time you read the record, from the other fields it references. So:

* You can read it like any other field.
* Sending a value for one is ignored. It will be recomputed on the next read.
* Its value reflects the moment you asked, not the moment the record was written.

## Property versions

Each property carries a `version` that increments when its definition changes materially. You don't need to send or track it. It exists so the search index knows when a column's stored values are stale. It's visible on the property because it's part of the honest shape, not because integrations need it.

## What's next

<CardGroup cols={2}>
  <Card title="Objects" icon="table-columns" href="/dev/api-reference/objects">
    Read the schema, objects and their properties.
  </Card>

  <Card title="Records" icon="table-rows" href="/dev/api-reference/records">
    Read and create the rows.
  </Card>
</CardGroup>


## Related topics

- [Records](/dev/webhooks/events/records.md)
- [Requests & responses](/dev/api-reference/requests.md)
- [Developer Overview](/dev/overview.md)
- [The Record object](/dev/api-reference/records.md)
- [Overview](/data/properties/general/overview.md)
