---
title: "Update a memory"
method: PUT
path: "/v1/projects/{project_id}/memories/{memory_id}"
tags: ["Memories"]
---

# Update a memory

`PUT /v1/projects/{project_id}/memories/{memory_id}`

## Request

- Base URL: `https://api.naturali.ai`
- URL: `https://api.naturali.ai/v1/projects/{project_id}/memories/{memory_id}`
- Auth: one of:
  - HTTP bearer
  - OAuth 2 (authorization code, authorization URL `https://api.naturali.ai/authorize`, token URL `https://api.naturali.ai/token`, scopes: `mcp:access`)

Updates an existing memory. Regenerates the embedding if content changes.

## Path parameters

- `memory_id` string, required

## Headers

- `If-Match` string

## Request body

- object
  - `content` string: Updated text content
  - `tags` NullableTagBag, nullable: A `TagBag` on a request field where `null` is meaningful: a full-replacement update that clears the bag. A record whose bag is unset leaves the field out.
  - `metadata` NullableMetadataBag, nullable: A `MetadataBag` on a request field where `null` is meaningful: a full-replacement update that clears the bag. A record whose bag is unset leaves the field out.
  - `expected_version` integer, nullable: The version the caller believes the resource holds. When the resource is at any other version the write is refused with `409 VERSION_CONFLICT` and nothing is written; `meta.current_version` on that response names the version in force. Omit it to write unconditionally. Omitting it does not make the write unordered: two writes that reach the server together are still serialized, and the one whose version was taken first is refused the same way. What the field adds is refusing a write whose author read the resource some time ago and has not seen what happened since. The `If-Match` header carries the same precondition for a client that prefers the HTTP spelling. Sending both with different versions is `400 VALIDATION_FAILED`.

## Response `200`

Memory updated

- Memory
  - `id` string
  - `memory_store_id` string
  - `content` string: The text this memory currently holds. Stored once per distinct text per store and shared with every assertion that stated it, so a memory carries no copy of its own.
  - `source_type` 'manual' | 'conversation': Whether there is a source to point at. `conversation` means `source_id` names the conversation this fact was learned in; `manual` means there is nothing to point at — a direct API write, or an agent write made outside a conversation.
  - `source_id` string, nullable: The conversation this fact was learned in, when `source_type` is `conversation`; null when it is `manual`. Deliberately a loose pointer rather than a foreign key, so deleting the conversation leaves the id in place — the record of where the fact came from outlives its source.
  - `tags` TagBag: Key-value labels on a resource. A flat object of string values — an array, a nested object or a number is rejected with `400 VALIDATION_FAILED`, never coerced. Keys are opaque and stored verbatim, so `cost_center` and `costCenter` are two different tags. Matched by JSONB containment wherever tags are read: the `?tags=` filter and knowledge search. Keys beginning `system.` are reserved: the platform writes them to record which conversation, actor, agent and role a row came from, and a write naming one is refused with `400 RESERVED_TAG_KEY`. They are read and filtered like any other tag. The bag is bounded, because every pair reaches the IAM context of every access check on the resource: at most 50 keys, each key at most 128 characters and each value at most 256. A write past a bound — including a merge that would grow the stored bag past the key count — is `400 VALIDATION_FAILED` with `meta.limit` naming the bound it crossed. `system.*` keys are the platform's and do not count against the 50.
  - `metadata` MetadataBag: Caller-owned annotations on a resource, stored as the object they were written as: the types a value was written with are the types a read returns, so a filter can ask an ordering question about a number. Unlike other body fields, keys are stored and returned verbatim in the casing supplied — they are not converted between snake_case and camelCase. No key is reserved, and that is the point: every piece of state the platform owns lives in its own typed column, so nothing written here reaches platform state. The platform never reads the bag — it is not an IAM context, not a policy input and not part of a prompt — which is what separates it from a tag bag.
  - `invalidated_at` string, date-time, nullable: When the memory stopped holding, whether a supersede replaced it or a retraction withdrew it. Null means the memory is currently valid. Invalidated memories are excluded from listing, from write deduplication, and from knowledge search, but remain readable by ID — with their original text — for audit.
  - `superseded_by_memory_id` string, nullable: The memory that replaced this one, when it was superseded. Null for a valid memory and for a retracted one, which nothing replaced.
  - `version` integer: The memory's write version, starting at 1 and incremented on every update. States a precondition against it with `expected_version` or `If-Match` on `PUT /memories/{memory_id}`.
  - `created_at` string, date-time
  - `updated_at` string, date-time

## Other responses

- `401`: Unauthorized
- `403`: Forbidden
- `404`: Memory store or memory not found
- `409`: The resource has moved past the version this write read (`VERSION_CONFLICT`): a stated `expected_version` or `If-Match` no longer matches, or a concurrent write took the version first. `meta.current_version` names the version in force. Nothing is written.
- `500`: Internal server error

---

[API](https://skmtc.dev/naturali/apis/naturali-ai-api.md) · [All operations](https://skmtc.dev/naturali/apis/naturali-ai-api/llms.txt) · [OpenAPI document](https://skmtc.dev/naturali/apis/naturali-ai-api/revisions/e4905b78f422?raw)
