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

# Spaces

> Create and manage memory spaces, the isolation boundary for memories within your organization.

## Create a space

```http theme={null}
POST /v1/spaces/
Authorization: Bearer anona_live_YOUR_KEY
Content-Type: application/json
```

```json theme={null}
{
  "name": "customer-support-bot",
  "description": "Memories for the customer support chatbot",
  "space_type": "customer_support"
}
```

| Parameter | Type | Required | Description |
| - | - | - | - |
| `name` | string | Yes | The name of the space. It also becomes the `space_id`. |
| `description` | string | No | What this space is for. |
| `space_type` | string | No | Sets the new space up for a job: a mission, extraction guidance, disposition dials and two starting memory models. One of `customer_support`, `coding_agent`, `personal_assistant`, `sales_account`, `blank`. Defaults to `blank`, which configures nothing. See [Space types](#space-types). |

`space_id` is not a settable field. Sending one is rejected with `422`. So is a
`space_type` that is not on the list above — a request asking for a type Anona does not
have is refused rather than quietly creating a plain space.

**Response** `201 Created`

```json theme={null}
{
  "space_type": "customer_support",
  "space_id": "customer-support-bot",
  "name": "customer-support-bot",
  "description": "Memories for the customer support chatbot",
  "created_at": "2026-07-14T12:34:56Z"
}
```

**`space_type` in the response is what was applied, not what you asked for.** It is
`null` when nothing was applied: when you sent nothing, when you sent `blank`, and also
when a type was requested and could not be applied. The space is created and usable
either way — applying a type never fails the create — so this field is how you tell a
seeded space from a plain one without a second call. If it comes back `null` after you
asked for a type, either set the pieces yourself or delete the space and create it
again.

```bash cURL theme={null}
curl -X POST https://api.anonalabs.com/v1/spaces/ \
  -H "Authorization: Bearer anona_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "customer-support-bot",
    "description": "Memories for the customer support chatbot"
  }'
```

```python Python theme={null}
space = client.create_space(
    "customer-support-bot",
    description="Memories for the customer support chatbot",
)
```

```typescript TypeScript theme={null}
const space = await anona.createSpace({
  name: "customer-support-bot",
  description: "Memories for the customer support chatbot",
});
```

**The `space_id` is the name you chose.** There is no separate generated id. The
name you pass on create is the identifier every other endpoint takes, so pick it
deliberately. Every memory operation requires it.

> [!WARNING]
>
> A name containing characters that are not URL-safe produces a `space_id` containing
> them too. That is legal, but the id then has to be percent-encoded everywhere it
> appears in a path: `my space` becomes `/v1/spaces/my%20space`. Sticking to letters,
> digits, `-` and `_` avoids the problem entirely.

## Space types

A new space is an empty box. A **space type** fills some of it in at creation: what the
space is for, what its extraction pass should keep, how it weighs what it is told, and
two standing questions worth keeping an answer to.

Pass one as `space_type` on create. That is the only moment a type can be applied —
there is no endpoint that converts an existing space, because everything a type sets is
already settable on its own.

| `space_type` | What it is for | Dials it moves |
| - | - | - |
| `customer_support` | Conversations with customers. Keeps who they are and what was promised to them. | skepticism 2, empathy 5 |
| `coding_agent` | An agent working in a codebase. Keeps decisions, constraints and the conventions it must not break. | literalism 4, empathy 2 |
| `personal_assistant` | One person's assistant. Keeps their preferences, their people and what they are waiting on. | empathy 4 |
| `sales_account` | One account through a deal. Keeps the people, the objections and where it stands. | skepticism 4, empathy 4 |
| `blank` | An empty space. Nothing is configured for you. This is the default. | none |

A dial a type does not move stays at its default of 3. The dials are moved only where
the use case really differs, and they change how a space weighs what it is told when it
synthesizes — never what a plain retrieve returns. See
[space profile](https://docs.memory.anonalabs.com/api-reference/models.md#space-profile).

### What a type sets, and how to change it

Three things, all of which stay editable afterwards.

| What | Where it lands | How to change it later |
| - | - | - |
| Mission and disposition | The space profile | `PUT /v1/spaces/{space_id}/profile` — see [space profile](https://docs.memory.anonalabs.com/api-reference/models.md#space-profile) |
| Extraction guidance | The space's `guidance`, added to the standard extraction rules | `PUT /v1/spaces/{space_id}/extraction-settings` — see [extraction settings](https://docs.memory.anonalabs.com/api-reference/extraction-settings.md) |
| Two memory models | Standing questions the space keeps a current answer to | `POST`, `PATCH` and `DELETE /v1/spaces/{space_id}/models` — see [memory models](https://docs.memory.anonalabs.com/api-reference/models.md) |

A space type is a starting point, not a mode. Nothing is locked, nothing behaves differently
later because of the type you chose, and a space you seeded is the same kind of space as
one you configured by hand. It writes configuration only: no memories are created, and
your own writes are unaffected.

`space_type` is also not stored as a property of the space, and it is not a field of
the read responses at all: `GET /v1/spaces/{space_id}` and `GET /v1/spaces` omit it
rather than reporting it null, because there would be nothing to report — what a
type did is the mission, the guidance and the models it wrote, and each of those reads
back on its own endpoint.

> [!WARNING]
>
> Both of those `PUT`s are full replaces. Read the current settings and send them back
> with your edit to keep what the type wrote; sending only the one field you care about
> clears the rest.

### The starting models are empty on purpose

> [!NOTE]
>
> A seeded space's two models have **no content** until the space has memories worth
> reading. This is expected, not a failed apply.

Each one is created with `mode: "delta"` and `refresh_on_new_memories: true`, so it
writes itself the first time newly recorded memories are consolidated, and rewrites
itself as the space learns more. Until then `GET /v1/spaces/{space_id}/models` lists
them with `content: null`.

Generating an answer at creation would have been worse than empty. The space is seconds
old and holds nothing, so the pass would have spent your credits to store "I have no
information about this" as the model's content — and that sentence then reads as the
answer, to you and to any agent reading the model, until something replaces it. An empty
model is honest about what the space knows; a confident empty one is not.

Record something and let it consolidate, then read the models back. To fill one sooner,
`POST /v1/spaces/{space_id}/models/{model_id}/refresh` writes it now; that call is
billed like [reason](https://docs.memory.anonalabs.com/api-reference/reason.md).

### What each type starts with

Model ids are stable, so a seeded model can be refreshed, edited or deleted by id.

| Type | Model | The question it answers |
| - | - | - |
| `customer_support` | Customer profile and preferences (`customer-profile`) | Who is this customer, what do they use us for, and how do they prefer to be dealt with? |
| `customer_support` | Open issues and commitments (`open-commitments`) | What is still unresolved for this customer, and what have we promised them that has not happened yet? |
| `coding_agent` | Project context (`project-context`) | What is this project, how is it structured, and what constraints govern changes to it? |
| `coding_agent` | Decisions and what was rejected (`decisions-and-rejections`) | What has been decided about how this system works, and what approaches were tried and rejected, and why? |
| `personal_assistant` | Preferences and working style (`how-they-work`) | How does this person prefer to work and be communicated with? |
| `personal_assistant` | Open threads (`open-threads`) | What is this person waiting on, and what have they committed to that is not done yet? |
| `sales_account` | Account and stakeholders (`account-map`) | Who is involved in this account, what does each of them care about, and who decides? |
| `sales_account` | Deal state and objections (`deal-state`) | Where does this deal stand, what objections have been raised, and what is the agreed next step? |

### List the space types

```http theme={null}
GET /v1/space-types
```

No API key and no credits: it is a static catalogue and it names no customer data. Read
it rather than hardcoding the list, which goes stale the first time a type is added or
reworded.

```bash theme={null}
curl https://api.anonalabs.com/v1/space-types
```

**Response** `200 OK`

```json theme={null}
{
  "items": [
    {
      "space_type": "customer_support",
      "name": "Customer support",
      "summary": "For conversations with customers. Keeps who they are and what was promised to them.",
      "mission": "This space supports customer conversations. Answers are read by support agents who need to pick up a thread without reading its history, so say who the customer is, what they asked for, and what was promised to them.",
      "disposition": { "skepticism": 2, "empathy": 5 },
      "models": [
        {
          "name": "Customer profile and preferences",
          "question": "Who is this customer, what do they use us for, and how do they prefer to be dealt with?"
        },
        {
          "name": "Open issues and commitments",
          "question": "What is still unresolved for this customer, and what have we promised them that has not happened yet?"
        }
      ]
    }
  ],
  "total": 5
}
```

One entry per type, in the order a picker should show them, with `blank` last. `mission`
is the exact text the type writes to the space profile — worth reading before you choose,
and worth editing afterwards if it is nearly right. `disposition` carries only the dials
that type moves, and `models` only their names and questions; the answers are written in
the space itself.

## List spaces

```http theme={null}
GET /v1/spaces/
Authorization: Bearer anona_live_YOUR_KEY
```

```bash cURL theme={null}
curl https://api.anonalabs.com/v1/spaces/ \
  -H "Authorization: Bearer anona_live_YOUR_KEY"
```

```python Python theme={null}
for space in client.list_spaces():
    print(space["space_id"], space["name"])
```

```typescript TypeScript theme={null}
for (const space of await anona.listSpaces()) {
  console.log(space.space_id, space.name);
}
```

**Response** `200 OK`

```json theme={null}
{
  "spaces": [
    {
      "space_id": "customer-support-bot",
      "name": "customer-support-bot",
      "created_at": "2026-07-14T12:34:56Z"
    }
  ],
  "total": 1
}
```

The list holds your organization's spaces you can open, and every space another
organization has shared with you. A shared one carries `shared_by` (the owning
organization's name) and `qualified_id` (`{owner_slug}:{space_name}`); both are
`null` for a space you own. With an API key, "you" is the member who created the
key: a key lists, and reaches, what that person can. A key created before keys
recorded their creator lists every space in the organization and no shares.

## Get space details

```http theme={null}
GET /v1/spaces/{space_id}
Authorization: Bearer anona_live_YOUR_KEY
```

```bash cURL theme={null}
curl https://api.anonalabs.com/v1/spaces/customer-support-bot \
  -H "Authorization: Bearer anona_live_YOUR_KEY"
```

```python Python theme={null}
space = client.get_space("customer-support-bot")
```

```typescript TypeScript theme={null}
const space = await anona.getSpace("customer-support-bot");
```

**Response** `200 OK`

```json theme={null}
{
  "space_id": "customer-support-bot",
  "name": "customer-support-bot",
  "description": "Memories for the customer support chatbot",
  "created_at": "2026-07-14T12:34:56Z"
}
```

`space_id` may be a shared space's `qualified_id` as well as a bare name. A share
is read from the owning organization and comes back with `shared_by` and
`qualified_id` set; a bare name that matches both your own space and a share is
`409 space_ambiguous`.

## Delete a space

```http theme={null}
DELETE /v1/spaces/{space_id}
Authorization: Bearer anona_live_YOUR_KEY
```

```bash cURL theme={null}
curl -X DELETE https://api.anonalabs.com/v1/spaces/customer-support-bot \
  -H "Authorization: Bearer anona_live_YOUR_KEY"
```

```python Python theme={null}
client.delete_space("customer-support-bot")
```

```typescript TypeScript theme={null}
await anona.deleteSpace("customer-support-bot");
```

**Response** `204 No Content`. A space that does not exist is `404 space_not_found`,
so a mistyped id is reported rather than read as a successful delete.

> [!WARNING]
>
> Deleting a space permanently destroys the space and every memory in it. This cannot be
> undone.

## Space members

Spaces can optionally have named members within your organization.

> [!NOTE]
>
> The membership endpoints are control-plane calls: they need a **dashboard session**
> with the owner or admin role, not an API key. An API key gets `401 session_required`;
> a developer or viewer session gets `403 insufficient_role`. The `GET` listing also
> accepts the token `anona login --cli` stores, with the same role check.

| Method and path | What it does |
| - | - |
| `GET /v1/spaces/{space_id}/members` | Lists the members of a space. |
| `POST /v1/spaces/{space_id}/members/invite` | Invites a member to the space. |
| `DELETE /v1/spaces/{space_id}/members/{member_id}` | Removes a member from the space. |
| `GET /v1/spaces/{space_id}/slack-invites` | Lists everyone a bound Slack channel offered this space to who has not accepted. |

| State | Who can reach the space |
| - | - |
| No member rows | Anyone in the organization. The space is treated as org-wide. |
| One or more member rows | Only the listed members. Access is enforced. |

The org-wide state exists for spaces that pre-date membership: creating a space
through the API or dashboard automatically writes a member row for the creator, so
a new space is member-only from the moment it exists.

### Invited over Slack

A space bound to a [Slack channel](https://docs.memory.anonalabs.com/slack.md) also has people who were offered it
over Slack and have not taken it. They have no Anona account yet, so they are
listed separately rather than as half-built members:

```http theme={null}
GET /v1/spaces/{space_id}/slack-invites
```

**Response** `200 OK`

```json theme={null}
{
  "items": [
    {
      "id": "7c1f0a2b-1d4e-4a5f-9b31-0c2d8e6f1a44",
      "slack_user_id": "U086JQ2AB",
      "display_name": "Alice Ng",
      "channel_id": "C07ABCDEF",
      "channel_name": "general",
      "role": "viewer",
      "state": "pending",
      "invited_at": "2026-10-01T09:00:00Z",
      "expires_at": "2026-10-15T09:00:00Z"
    }
  ]
}
```

`state` is `pending` (asked, no answer yet), `declined` (said no) or `expired`
(fourteen days passed). Anyone who accepted is absent, because they hold a real
membership row and the member listing above already names them.

There is no email field and there will not be one: the Slack roster is read
through `users:read`, never `users:read.email`. `display_name` is null for a
roster synced before names were stored.

## Rules

A **rule** is a standing instruction the space must follow whenever it answers a
question. It is not a preference and it is not a tone setting — those are the
disposition dials on the [space profile](https://docs.memory.anonalabs.com/api-reference/models.md#space-profile),
which only shift how an answer is worded. A rule applies to **every** answer the
space gives, and it overrides what the memories say:

```text A rule theme={null}
Never describe revenue as committed unless a memory records a signed document.
Name the stage the deal is actually at instead.
```

That holds even when a memory says the deal is done.

Rules shape every answer Anona synthesizes for you: [reason](https://docs.memory.anonalabs.com/api-reference/reason.md),
[asking about one end user](https://docs.memory.anonalabs.com/api-reference/user-profiles.md#ask-about-a-user), and
the answers [memory models](https://docs.memory.anonalabs.com/api-reference/models.md) keep.

They do not touch [retrieve](https://docs.memory.anonalabs.com/api-reference/retrieve.md) or the
[drop-in proxy](https://docs.memory.anonalabs.com/api-reference/chat.md): a search returns the memories it matched,
and the proxy hands your own prompt to your own model with memories added to it.
Whatever shapes those answers is yours.

> [!NOTE]
>
> Rules are **owner-only** and **free**. They consume no credits, deliberately — an
> organization out of credits must still be able to switch off a rule that is
> producing bad answers.

### List the rules on a space

```http theme={null}
GET /v1/spaces/{space_id}/rules
Authorization: Bearer anona_live_YOUR_KEY
```

**Response** `200 OK`

```json theme={null}
{
  "items": [
    {
      "id": "rl_7c1f0a2b",
      "space_id": "accounts",
      "name": "Never call revenue committed",
      "content": "Never describe revenue as committed unless a memory records a signed document. Name the stage the deal is actually at instead.",
      "priority": 100,
      "is_active": true,
      "tags": [],
      "created_at": "2026-09-18T09:12:00Z",
      "updated_at": "2026-09-18T09:12:00Z"
    }
  ],
  "total": 1
}
```

Highest priority first. Inactive rules are listed too: they are stored and still
editable, they are simply not applied.

### Add a rule

```http theme={null}
POST /v1/spaces/{space_id}/rules
Authorization: Bearer anona_live_YOUR_KEY
Content-Type: application/json
```

```json theme={null}
{
  "name": "Never call revenue committed",
  "content": "Never describe revenue as committed unless a memory records a signed document. Name the stage the deal is actually at instead.",
  "priority": 100
}
```

| Parameter | Type | Required | Description |
| - | - | - | - |
| `name` | string | Yes | Up to 120 characters. The label an answer reports back in `rules_applied`, so write it for whoever reads that answer. |
| `content` | string | Yes | Up to 2,000 characters. The rule itself, in plain language. |
| `priority` | integer | No | 0–1000, default 0. Rules apply highest first; equal priorities break on recency, newest first. |
| `is_active` | boolean | No | Default `true`. A rule that is off is stored and editable but never applied. |
| `tags` | string\[] | No | Up to 20. Narrows when the rule applies — see [Scoping a rule](#scoping-a-rule). Most rules want none. |

**Response** `201 Created`, carrying the stored rule in the shape above.

### How many rules a space can have

A space may have **25 active** rules at once. A create or an activation that
would make it 26 is refused with `422 too_many_active_rules`.

The cap counts *active* rules only, so switching one off always frees its slot
without deleting anything:

```http theme={null}
PATCH /v1/spaces/{space_id}/rules/{rule_id}
```

```json theme={null}
{ "is_active": false }
```

Every active rule is written into every answer the space composes, so the cap is
what stops a space from spending more of an answer on its own rules than on its
memories. Well under it is the better place to be: a rule that is not worth the
room is worth switching off.

### Edit or remove a rule

`PATCH` is a partial edit — only the fields you send change, and the rest are
left exactly as they are. That is deliberate: switching a rule off or lifting its
priority should never mean resending the text of something enforced on every
answer, where a typo is expensive.

A `PATCH` that names no field is refused with `422 empty_update` rather than
treated as a no-op.

```http theme={null}
DELETE /v1/spaces/{space_id}/rules/{rule_id}
```

**Response** `204 No Content`. Answers already given are unchanged; later ones
stop obeying it.

### Which rules shaped an answer

[`POST /v1/reason`](https://docs.memory.anonalabs.com/api-reference/reason#which-rules-shaped-the-answer) returns
`rules_applied`, naming the rules that actually applied to that answer:

```json theme={null}
"rules_applied": [
  { "id": "rl_7c1f0a2b", "name": "Never call revenue committed" }
]
```

It carries the id and the name, and **deliberately not the rule's text**. The
text is your own configuration — you can read it once from
`GET /v1/spaces/{space_id}/rules` — while this rides every answer, and a space
may hold 25 active rules of 2,000 characters each.

An empty list is a real finding, not a missing field: it says no rule applied.
That is what separates *the memories are wrong* from *a rule said otherwise*,
which need opposite fixes.

### Scoping a rule

`tags` narrow when a rule applies. Most rules want none.

* A rule with **no tags** applies to every question the space is asked. This is
  the usual case, and the one to reach for.
* A **tagged** rule applies only to a question filtered to matching tags with
  [`tag_groups`](https://docs.memory.anonalabs.com/api-reference/retrieve#compound-tag-filters). A question that
  names no filter is answered under the untagged rules alone, so a tagged rule
  can never reach a question that did not ask for it.
* A question scoped to one end user (`user_id`, `agent_id`, `session_id`) is
  likewise answered under the untagged rules alone.

A tag starting with `anona:` is refused with `422 reserved_tag`. That namespace
carries scope, and a rule able to claim it could pin itself to one end user's
questions.

### From the SDKs

```python Python theme={null}
client.create_rule(
    "accounts",
    name="Never call revenue committed",
    content=(
        "Never describe revenue as committed unless a memory records a "
        "signed document. Name the stage the deal is actually at instead."
    ),
    priority=100,
)

for rule in client.list_rules("accounts"):
    state = "active" if rule["is_active"] else "off"
    print(rule["priority"], rule["name"], state)

client.update_rule("accounts", "rl_7c1f0a2b", is_active=False)
client.delete_rule("accounts", "rl_7c1f0a2b")
```

```typescript TypeScript theme={null}
await anona.createRule({
  spaceId: "accounts",
  name: "Never call revenue committed",
  content:
    "Never describe revenue as committed unless a memory records a signed " +
    "document. Name the stage the deal is actually at instead.",
  priority: 100,
});

for (const rule of await anona.listRules("accounts")) {
  console.log(rule.priority, rule.name, rule.is_active ? "active" : "off");
}

await anona.updateRule({
  spaceId: "accounts",
  ruleId: "rl_7c1f0a2b",
  isActive: false,
});
await anona.deleteRule({ spaceId: "accounts", ruleId: "rl_7c1f0a2b" });
```

Both clients have `async_` twins in Python and are promise-based in TypeScript.
`update_rule` / `updateRule` send only the fields you pass.

## In the SDKs

```python Python theme={null}
spaces = client.list_spaces()
for s in spaces:
    print(s["space_id"], s["name"])

for t in client.list_space_types():
    print(t["space_type"], "-", t["summary"])

space = client.create_space(
    "support-bot",
    description="Customer support memory",
    space_type="customer_support",
)
# None means nothing was applied, whatever you asked for.
print(space["space_type"])

client.delete_space("support-bot")
```

```typescript TypeScript theme={null}
const spaces = await anona.listSpaces();
for (const s of spaces) {
  console.log(s.space_id, s.name);
}

for (const t of await anona.listSpaceTypes()) {
  console.log(t.space_type, "-", t.summary);
}

const space = await anona.createSpace({
  name: "support-bot",
  description: "Customer support memory",
  spaceType: "customer_support",
});
// null means nothing was applied, whatever you asked for.
console.log(space.space_type);

await anona.deleteSpace("support-bot");
```

Listing, listing types, creating, fetching and deleting a space are wrapped in both
clients (Python also ships `async_` variants). Membership management is
dashboard/session-only, so it is deliberately in neither API-key SDK.

## Error responses

| Status | Code | Cause |
| - | - | - |
| 401 | `missing_auth` / `invalid_api_key` | Missing or invalid API key. |
| 403 | `space_access_denied` | You are not a member of this space. |
| 403 | `space_limit_reached` | Your plan's space limit is used up. |
| 403 | `space_read_only` | A read-only member attempted a write. |
| 403 | `space_owner_only` | Only the owning organization can read or change a space's rules. |
| 403 | `space_unavailable` | The owner of a shared space can't be reached: the owning organization has revoked its API keys and holds no live one, or was deleted. A share is served while the owner holds a live key, and also when it has never created a key or holds only expired ones. |
| 404 | `space_not_found` | The space does not exist. |
| 404 | `rule_error` | No rule with that id on this space. |
| 409 | `space_ambiguous` | A bare name matches both your own space and one shared with you — use the `qualified_id`. |
| 409 | `space_exists` | Create was called with a name you already hold. A space's id is its name, so re-creating is never a fresh space — fetch or delete the existing one instead. |
| 422 | `validation_error` | Missing or invalid `name`, a `space_id` field, or a `space_type` that is not one of the five. |
| 422 | `too_many_active_rules` | A space may have at most 25 active rules. Switch one off or remove it first. |
| 422 | `empty_update` | A rule `PATCH` that names no field to change. |
| 422 | `reserved_tag` | A rule tag starting with `anona:`, which is reserved for scope. |

There is no `429` here. The space endpoints are control-plane calls: they spend no
credits and are not rate-limited, so they carry none of the `X-RateLimit-*` headers
either. See [Usage](https://docs.memory.anonalabs.com/api-reference/usage.md#response-headers).

See the full [error reference](https://docs.memory.anonalabs.com/api-reference/errors.md).

## Next steps

> **[Memories API](https://docs.memory.anonalabs.com/api-reference/memories.md)**
>
> Store and manage memories.

> **[Errors](https://docs.memory.anonalabs.com/api-reference/errors.md)**
>
> The complete error code reference.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.