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

# User data

> Read, merge, patch, delete, and list the per-end-user record an agent keeps, and look up an end user by email or phone

An agent keeps one JSON record per end user. These routes read and write that record for the caller or for a named end user, list every record on an agent, and resolve an email address or phone number to an end user. They are the REST twin of the [`User`](/reference/sdk/user) runtime object.

*Verified against lua-cli 3.33.0.*

## Base URL and authentication

Reads need `knowledge:read` and writes `knowledge:write` on the agent; the host, the bearer header, and the error envelope are on the [REST API overview](/reference/rest/overview). The two profile lookups are authorized by the handler as the key's owner, which a scoped key cannot stand in for: call them with a user session or a legacy key. Records are not partitioned by environment; sandbox and production share them.

## The record

A record is `{ userId, agentId, data, createdAt, updatedAt }`, with `data` the JSON bag your tools wrote and timestamps as Unix time in milliseconds. Reads that find the end user's Lua profile add `_luaProfile`, a read-only object the platform maintains; it is never part of `data`. The routes under `/developer/user/data/...` unwrap the record and answer `data` itself with `userId` added, so a field of yours named `userId` is overwritten in that answer.

Writes merge at the top level: `PUT` shallow-merges the body's keys into `data` (incoming wins, `null` is stored as a value, nothing is deleted) and creates the record when it is missing; `PATCH` sets and unsets named top-level keys atomically and needs an existing record.

## Endpoints

### GET /developer/user/data/agent/:agentId

Returns the caller's own record on the agent, unwrapped.

<ParamField path="agentId" type="string" required>The agent.</ParamField>

**Response**

`200` with `data`'s fields, plus `userId` and, when available, `_luaProfile`. A missing record answers `{ "userId" }` alone; nothing is created by a read.

Equivalent: `User.get()` inside a tool.

<CodeGroup>
  ```ts TypeScript theme={null}
  const response = await fetch('https://api.heylua.ai/developer/user/data/agent/<<YOUR_AGENT_ID>>', {
    headers: { Authorization: 'Bearer <<YOUR_API_KEY>>' },
  });
  const record: { userId: string; [field: string]: unknown } = await response.json();
  console.log(record.userId, record['plan']);
  ```

  ```bash cURL theme={null}
  curl "https://api.heylua.ai/developer/user/data/agent/<<YOUR_AGENT_ID>>" \
    -H "Authorization: Bearer <<YOUR_API_KEY>>"
  ```
</CodeGroup>

### GET /developer/user/data/agent/:agentId/user/:userId

Returns a named end user's record on the agent, unwrapped, in the same shape.

<ParamField path="agentId" type="string" required>The agent.</ParamField>
<ParamField path="userId" type="string" required>The end user.</ParamField>

Equivalent: `User.get('<userId>')`.

### PUT /developer/user/data/agent/:agentId

Merges the body into the caller's record, creating it when missing.

<ParamField path="agentId" type="string" required>The agent.</ParamField>

<ParamField body="*" type="object" required>
  Any JSON object. Top-level keys are shallow-merged into `data`; nested objects are replaced whole.
</ParamField>

**Response**

`200` with the merged `data` plus `userId`.

**Errors**

| Status | Code or message   | Meaning       | Fix          |
| ------ | ----------------- | ------------- | ------------ |
| `400`  | `Agent not found` | No such agent | Check the id |

Equivalent: `user.update({ plan: 'pro' })`.

<CodeGroup>
  ```ts TypeScript theme={null}
  const response = await fetch('https://api.heylua.ai/developer/user/data/agent/<<YOUR_AGENT_ID>>', {
    method: 'PUT',
    headers: { Authorization: 'Bearer <<YOUR_API_KEY>>', 'Content-Type': 'application/json' },
    body: JSON.stringify({ plan: 'pro', preferences: { locale: 'en-GB' } }),
  });
  const merged: { userId: string; plan?: string } = await response.json();
  console.log(merged.plan);
  ```

  ```bash cURL theme={null}
  curl -X PUT "https://api.heylua.ai/developer/user/data/agent/<<YOUR_AGENT_ID>>" \
    -H "Authorization: Bearer <<YOUR_API_KEY>>" \
    -H "Content-Type: application/json" \
    -d '{ "plan": "pro", "preferences": { "locale": "en-GB" } }'
  ```
</CodeGroup>

### PUT /developer/user/data/agent/:agentId/user/:userId

Merges the body into a named end user's record, with the same body, response, and errors.

### PATCH /developer/user/data/agent/:agentId

Sets and unsets top-level keys of the caller's record in one atomic write.

<ParamField path="agentId" type="string" required>The agent.</ParamField>
<ParamField body="set" type="object">Keys to write; `null` is stored as a value.</ParamField>
<ParamField body="unset" type="string[]">Keys to remove.</ParamField>

**Response**

`200` with the resulting `data` plus `userId`.

**Errors**

| Status | Code or message       | Meaning                        | Fix                   |
| ------ | --------------------- | ------------------------------ | --------------------- |
| `400`  | `Agent not found`     | No such agent                  | Check the id          |
| `404`  | `No agent data found` | The end user has no record yet | Create one with `PUT` |

Equivalent: `user.patch({ set: { tier: 'gold' }, unset: ['trialEndsAt'] })`.

<CodeGroup>
  ```ts TypeScript theme={null}
  const response = await fetch('https://api.heylua.ai/developer/user/data/agent/<<YOUR_AGENT_ID>>', {
    method: 'PATCH',
    headers: { Authorization: 'Bearer <<YOUR_API_KEY>>', 'Content-Type': 'application/json' },
    body: JSON.stringify({ set: { tier: 'gold' }, unset: ['trialEndsAt'] }),
  });
  const patched: { userId: string; tier?: string } = await response.json();
  console.log(patched.tier);
  ```

  ```bash cURL theme={null}
  curl -X PATCH "https://api.heylua.ai/developer/user/data/agent/<<YOUR_AGENT_ID>>" \
    -H "Authorization: Bearer <<YOUR_API_KEY>>" \
    -H "Content-Type: application/json" \
    -d '{ "set": { "tier": "gold" }, "unset": ["trialEndsAt"] }'
  ```
</CodeGroup>

### PATCH /developer/user/data/agent/:agentId/user/:userId

Same mutation on a named end user's record. `PATCH /developer/agents/:agentId/user-data/:userId` is an alias with identical behavior.

### DELETE /developer/user/data/agent/:agentId

Deletes the caller's record on the agent.

<ParamField path="agentId" type="string" required>The agent.</ParamField>

**Response**

`200` with an empty body when a record was deleted, or with `{ "message": "Agent data deleted successfully" }` when no record existed; both are success.

Equivalent: `user.clear()`.

<CodeGroup>
  ```ts TypeScript theme={null}
  const response = await fetch('https://api.heylua.ai/developer/user/data/agent/<<YOUR_AGENT_ID>>', {
    method: 'DELETE',
    headers: { Authorization: 'Bearer <<YOUR_API_KEY>>' },
  });
  console.log(response.status);
  ```

  ```bash cURL theme={null}
  curl -X DELETE "https://api.heylua.ai/developer/user/data/agent/<<YOUR_AGENT_ID>>" \
    -H "Authorization: Bearer <<YOUR_API_KEY>>"
  ```
</CodeGroup>

### DELETE /developer/user/data/agent/:agentId/user/:userId

Deletes a named end user's record. `DELETE /developer/agents/:agentId/user-data/:userId` is an alias.

### GET /developer/agents/:agentId/user-data

Lists the agent's records, most recently updated first, with the profile of each end user when known.

<ParamField path="agentId" type="string" required>The agent.</ParamField>
<ParamField query="page" type="integer" default="1">Page number from 1.</ParamField>
<ParamField query="limit" type="integer" default="20">From 1 to 100.</ParamField>

<ParamField query="search" type="string">
  Matches end-user ids and profile fields. When the profile search hits its cap the answer carries `searchTruncated: true`.
</ParamField>

**Response**

`200` with `{ "data": [...], "pagination": { ... }, "searchTruncated"? }`; each item is a full record with `_luaProfile` when known, and `pagination` carries `currentPage`, `totalPages`, `totalCount`, `limit`, `hasNextPage`, and `hasPrevPage`.

**Errors**

| Status | Code or message                                                                                      | Meaning                            | Fix                                         |
| ------ | ---------------------------------------------------------------------------------------------------- | ---------------------------------- | ------------------------------------------- |
| `400`  | `page must be a positive integer`, `limit must be an integer between 1 and 100`, `page is too large` | A paging parameter is out of range | Use a page from 1 and a limit from 1 to 100 |

<CodeGroup>
  ```ts TypeScript theme={null}
  const params = new URLSearchParams({ page: '1', limit: '50', search: 'example.com' });
  const response = await fetch(`https://api.heylua.ai/developer/agents/<<YOUR_AGENT_ID>>/user-data?${params}`, {
    headers: { Authorization: 'Bearer <<YOUR_API_KEY>>' },
  });
  const page: { data: Array<{ userId: string; data: Record<string, unknown> }>; pagination: { totalCount: number } } =
    await response.json();
  console.log(page.pagination.totalCount);
  ```

  ```bash cURL theme={null}
  curl "https://api.heylua.ai/developer/agents/<<YOUR_AGENT_ID>>/user-data?page=1&limit=50&search=example.com" \
    -H "Authorization: Bearer <<YOUR_API_KEY>>"
  ```
</CodeGroup>

### GET /developer/agents/:agentId/user-data/capabilities

Tells a client whether the caller may write user data on this agent, so a UI can hide its editor.

**Response**

`200` with `{ "canWrite": true | false }`.

### GET /developer/agents/:agentId/user-data/:userId

Returns one record wrapped, `{ userId, agentId, data, createdAt, updatedAt, _luaProfile? }`.

**Errors**

| Status | Code or message       | Meaning                                  | Fix                   |
| ------ | --------------------- | ---------------------------------------- | --------------------- |
| `404`  | `No agent data found` | The end user has no record on this agent | Create one with `PUT` |

### GET /developer/user/profile/email/:email

Resolves an email address to an end user who has talked to one of your agents.

<ParamField path="email" type="string" required>The address, URL-encoded; matching is case-insensitive.</ParamField>
<ParamField query="agentId" type="string">Limit the lookup to one agent you can access.</ParamField>

**Response**

`200` with the end user's profile, including `id`.&#x20;

**Errors**

| Status | Code or message                     | Meaning                                                                                                                             | Fix                                 |
| ------ | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- |
| `403`  | `Insufficient permissions`          | The caller is a scoped key                                                                                                          | Use a user session or a legacy key  |
| `404`  | `User with email <email> not found` | Nobody has that address, or the end user has never talked to an agent you can access; the two cases are indistinguishable by design | Check the address and the `agentId` |

Equivalent: `User.get({ email: 'customer@example.com' })`.

<CodeGroup>
  ```ts TypeScript theme={null}
  const email = encodeURIComponent('customer@example.com');
  const response = await fetch(`https://api.heylua.ai/developer/user/profile/email/${email}?agentId=<<YOUR_AGENT_ID>>`, {
    headers: { Authorization: 'Bearer <<YOUR_API_KEY>>' },
  });
  if (response.status === 404) throw new Error('No end user with that address');
  const profile: { id: string } = await response.json();
  console.log(profile.id);
  ```

  ```bash cURL theme={null}
  curl "https://api.heylua.ai/developer/user/profile/email/customer%40example.com?agentId=<<YOUR_AGENT_ID>>" \
    -H "Authorization: Bearer <<YOUR_API_KEY>>"
  ```
</CodeGroup>

### GET /developer/user/profile/phone/:phone

Resolves a phone number the same way. A leading `+` is optional.

Equivalent: `User.get({ phone: '+15551234567' })`.

## See also

* [`User`](/reference/sdk/user) — the same record from agent code
* [Identify users](/build/identify-users) — how an end user gets an id on each channel
* [Custom data](/reference/rest/custom-data) — agent-wide collections instead of per-user records
* [REST API overview](/reference/rest/overview) — authentication, scopes, and the error envelope
