> ## 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.

# Quickstart

> Sign up, grab your API key, and run the hero demo in 60 seconds.

This walks through the hero curl from the marketing site — create a person, declare a new schema attribute, then write the attribute on that person, and watch enum validation reject a bad value.

## 1. Get an API key

Sign up at `$SALTY_WEB/signup` — see [Introduction → Hosts at a glance](/docs/introduction#hosts-at-a-glance) for the env vars. After email verification you land on `/api` with your first `sk_live_…` key shown ONCE — store it as `$SALTY_API_KEY`:

```bash theme={null}
export SALTY_API_KEY=sk_live_...
```

(For purely scripted setups, hit `POST $SALTY_API/workspaces/bootstrap` with a Supabase JWT to provision a workspace + first key idempotently.)

<Note>
  Only the first 16 characters (`sk_live_xxxxxxxx`) are stored on the server, hashed with argon2. If you lose the full key, revoke it and create a new one.
</Note>

## 2. Create a person

<CodeGroup>
  ```bash curl theme={null}
  curl $SALTY_API/people \
    -H "Authorization: Bearer $SALTY_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "email": "jane@acme.com",
      "first_name": "Jane",
      "last_name": "Doe"
    }'
  ```

  ```ts SDK theme={null}
  import { Salty } from '@salty/sdk';

  const salty = new Salty({ apiKey: process.env.SALTY_API_KEY! });

  const { data, error } = await salty.people.postPeople({
    body: {
      email: 'jane@acme.com',
      first_name: 'Jane',
      last_name: 'Doe',
    },
  });
  ```
</CodeGroup>

Response:

```json theme={null}
{
  "id": "f30057e8-8f1e-49c3-a06f-481d6f03256a",
  "email": "jane@acme.com",
  "first_name": "Jane",
  "last_name": "Doe",
  "primary_company_id": null,
  "custom_attributes": {},
  "created_at": "2026-05-24T02:45:46.417Z",
  "updated_at": "2026-05-24T02:45:46.417Z"
}
```

## 3. Add a custom attribute to the People schema

Define a `lifecycle_stage` enum on the `person` object. Now every person write that includes `custom_attributes.lifecycle_stage` is validated against this enum.

<CodeGroup>
  ```bash curl theme={null}
  curl $SALTY_API/schema/person/attributes \
    -H "Authorization: Bearer $SALTY_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "attribute_key": "lifecycle_stage",
      "display_name": "Lifecycle Stage",
      "data_type": "enum",
      "enum_values": ["lead", "customer", "churned"]
    }'
  ```

  ```ts SDK theme={null}
  await salty.schema.postSchemaByObjectTypeAttributes({
    path: { object_type: 'person' },
    body: {
      attribute_key: 'lifecycle_stage',
      display_name: 'Lifecycle Stage',
      data_type: 'enum',
      enum_values: ['lead', 'customer', 'churned'],
    },
  });
  ```
</CodeGroup>

## 4. Use the new attribute

```bash theme={null}
curl -X PATCH $SALTY_API/people/<id> \
  -H "Authorization: Bearer $SALTY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"custom_attributes":{"lifecycle_stage":"customer"}}'
```

Returns 200 with the updated person.

## 5. Watch validation reject a bad value

```bash theme={null}
curl -X PATCH $SALTY_API/people/<id> \
  -H "Authorization: Bearer $SALTY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"custom_attributes":{"lifecycle_stage":"vip"}}'
```

Returns **400** with the precise error envelope:

```json theme={null}
{
  "error": {
    "type": "invalid_request",
    "code": "attribute_enum_invalid",
    "message": "\"vip\" is not a valid value for \"lifecycle_stage\"; expected one of: lead, customer, churned",
    "param": "lifecycle_stage"
  }
}
```

## Where to go next

* [Concepts → Schema engine](/docs/concepts/schema-engine) — what validates, what doesn't
* [API Reference → People](/docs/api-reference/people) — full CRUD + search + expand
* [API Reference → Custom Objects](/docs/api-reference/custom-objects) — define `invoices`, `projects`, etc.
