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

# Custom Objects

> Define new object types like invoices, projects, properties — with records and schema.

Custom Objects let you model anything that isn't a Person, Company, or Deal — without waiting for us to ship native support. Each custom object has a slug, a display name, and records. Records can have their own [schema](/docs/api-reference/schema) (the engine works the same way as for native objects — pass the custom object's `id` as `:object_type` when defining attributes).

## Endpoints

| Method   | Path                                | Purpose                                                                                 |
| -------- | ----------------------------------- | --------------------------------------------------------------------------------------- |
| `GET`    | `/custom-objects`                   | List definitions                                                                        |
| `POST`   | `/custom-objects`                   | Define a new custom object                                                              |
| `GET`    | `/custom-objects/:slug`             | Get one definition                                                                      |
| `GET`    | `/custom-objects/:slug/records`     | List records, paginated                                                                 |
| `POST`   | `/custom-objects/:slug/records`     | Create a record                                                                         |
| `PATCH`  | `/custom-objects/:slug/records/:id` | Update a record                                                                         |
| `DELETE` | `/custom-objects/:slug/records/:id` | Soft-delete a record (30-day trash; restore via POST /trash/custom-objects/:id/restore) |

## Define a custom object

```bash theme={null}
curl -X POST $SALTY_API/custom-objects \
  -H "Authorization: Bearer $SALTY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "slug": "invoices",
    "display_name": "Invoice",
    "display_name_plural": "Invoices"
  }'
```

Slug rules: lowercase, snake\_case, starts with a letter, max 64 chars. Duplicate slug → `409 custom_object_exists`.

## Add a schema for the new object

The schema endpoints work on custom objects too — pass the custom object's **UUID** (not its slug) as `:object_type`:

```bash theme={null}
curl -X POST $SALTY_API/schema/<custom-object-id>/attributes \
  -H "Authorization: Bearer $SALTY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "attribute_key": "status",
    "display_name": "Status",
    "data_type": "enum",
    "enum_values": ["unpaid", "paid"]
  }'
```

## Create a record

```bash theme={null}
curl -X POST $SALTY_API/custom-objects/invoices/records \
  -H "Authorization: Bearer $SALTY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "data": {
      "amount": 12500,
      "currency": "USD",
      "status": "paid"
    }
  }'
```

`data` is validated against any attribute definitions you've created for the custom object's UUID. Unknown keys pass through (lenient).

If you defined `status` as an enum and try `"status": "overdue"`:

```json theme={null}
{
  "error": {
    "type": "invalid_request",
    "code": "attribute_enum_invalid",
    "message": "\"overdue\" is not a valid value for \"status\"; expected one of: unpaid, paid",
    "param": "status"
  }
}
```

## List records

```bash theme={null}
curl "$SALTY_API/custom-objects/invoices/records?limit=50" \
  -H "Authorization: Bearer $SALTY_API_KEY"
```

Standard `{ data, next_cursor }` envelope. See [Pagination](/docs/concepts/pagination).
