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

# Write memories

> POST /v1/memories. Nodes and edges in one transaction, with an optional backfill declaration.

Two ways in. **Structured**: nodes and edges as they are; the server stamps `captured_at` and lists what it could not write in `refused`. **Natural language**: `messages`, each with the instant it was said or written; the engine extracts the statements as `claim` and `prediction` blocks, the people, assets and organisations as `entity` nodes, and the causal links the speaker actually argued as typed edges between the blocks. A request may carry both.

<Note>
  A block extracted from a message is **asserted when the message says it was** (`observed_at`), never when it was ingested. Add a `backfill` declaration when the message is from the past so the blocks are also *known* then, not today. Without it, a 2024 transcript ingested today is asserted in 2024 but known today, and `as_of=2024-06-01` reads hide it.
</Note>

## Request

<ParamField body="space" type="string" required>
  Whose memory this is. Stamped on every node in the request. Any string; a prefix like `user:`, `agent:`, `tckg:` is recommended.
</ParamField>

<ParamField body="backfill" type="object">
  Declares that this data was known before today. The request becomes one batch, and masking uses `declared_captured_at` for every row in it. Fields: `declared_captured_at` (timestamp), `reason` (string). See [Concepts](/concepts#backfill).
</ParamField>

<ParamField body="nodes" type="Node[]" default="[]">
  Nodes to write. One request is one transaction.
</ParamField>

<ParamField body="edges" type="Edge[]" default="[]">
  Edges to write. Endpoints must exist in the same tenant, in this request or earlier.
</ParamField>

<ParamField body="messages" type="Message[]" default="[]">
  Natural language to extract from. Processed in order; each message is one episode. Extraction takes a few seconds per message and runs inside the request.
</ParamField>

### Message

<ParamField body="content" type="string" required>
  The text: a transcript, a note, a conversation turn, an article.
</ParamField>

<ParamField body="observed_at" type="string" required>
  When it was said or written (ISO 8601 date or instant). Every block extracted from it is asserted at this instant, and the extraction knows nothing after it.
</ParamField>

<ParamField body="name" type="string">
  A label for the episode. Defaults to the first words of the content.
</ParamField>

<ParamField body="source_description" type="string">
  Where it came from: a channel, a document, a meeting.
</ParamField>

What comes out of a message:

| In the text | Becomes | How to read it back |
| - | - | - |
| A checkable statement about the past or present | `claim` node | `GET /v1/memories?kind=claim` |
| A forward-looking statement, a buy/sell/avoid recommendation | `prediction` node | `kind=prediction` |
| A company, a person, an asset, an institution | `entity` node, linked from the blocks that mention it by `MENTIONS` | `kind=entity`, `/why` |
| "X, so Y", "X is why Y" between two of the speaker's statements | `CAUSES`, `CONTRIBUTING_FACTOR`, `TRIGGERS`, `PREVENTS`, `SUPPORTS`, `CONTRADICTS`, `CONCURRENT_SIGNAL` edge | `/why`, `/search` |
| The message itself | `episode` node, `MENTIONS` every node extracted from it | `kind=episode` |

The same statement said again in a later message resolves onto the existing block (no duplicate); a statement the engine judges to contradict an earlier one leaves the earlier block in place and adds a `SUPERSEDES` edge from the newer one.

### Node

<ParamField body="id" type="string" required>
  Unique within the tenant. The same id may be written again only with a non-overlapping `valid` interval.
</ParamField>

<ParamField body="kind" type="string" required>
  One of `entity`, `claim`, `prediction`, `factor`, `timeseries`, `episode`.
</ParamField>

<ParamField body="statement" type="string">
  The text that search indexes.
</ParamField>

<ParamField body="category" type="string" />

<ParamField body="payload" type="object">
  Free-form. Conventions: `about` (the period the content is about), `source`, `factblock_id`.
</ParamField>

<ParamField body="asserted_at" type="timestamp" required>
  When the statement was made.
</ParamField>

<ParamField body="valid_from" type="timestamp">
  Start of the validity interval.
</ParamField>

<ParamField body="valid_to" type="timestamp">
  End of the validity interval. Open when omitted.
</ParamField>

<ParamField body="fact_key" type="string">
  For `resolve`. If the key is not declared yet the row is written with an `undeclared_fact` warning.
</ParamField>

<ParamField body="fact_value" type="any">
  The value `resolve` returns for this candidate.
</ParamField>

<ParamField body="embedding" type="number[]">
  A vector you computed. Pass a query vector from the same model to `search`.
</ParamField>

<ParamField body="captured_at / known_at" type="forbidden">
  Sending either refuses the row with `captured_at_supplied`. The server owns knowledge time.
</ParamField>

### Edge

<ParamField body="source_id" type="string" required />

<ParamField body="target_id" type="string" required>
  Both must be nodes in the same tenant. Otherwise the edge is refused with `missing_endpoint`.
</ParamField>

<ParamField body="edge_type" type="string" required>
  One of the 13 types in [Concepts](/concepts#edge-types).
</ParamField>

<ParamField body="confidence" type="number">
  0 to 1.
</ParamField>

<ParamField body="lag" type="string">
  A PostgreSQL interval such as `"20 days"` or `"3 mons"`. A value that does not parse refuses the row.
</ParamField>

<ParamField body="mechanism" type="string">
  One sentence on why.
</ParamField>

<ParamField body="properties" type="object">
  Free-form.
</ParamField>

<ParamField body="asserted_at" type="timestamp" required />

<ParamField body="valid_from / valid_to" type="timestamp">
  As for nodes.
</ParamField>

## Response

<ResponseField name="accepted" type="integer">
  Rows written (nodes plus edges).
</ResponseField>

<ResponseField name="refused" type="object[]">
  Rows not written. Each has the row's ids, a stable `reason`, and `why` with the original error text.
  Reasons: `captured_at_supplied`, `missing_endpoint`, `cross_tenant`, `overlapping_valid` (same id, overlapping `valid`), `missing_required_field`, `unparseable_value` (`lag`, timestamps).
</ResponseField>

<ResponseField name="warned" type="object[]">
  Rows handled with a note. `already_remembered` (an identical row exists; nothing written), `undeclared_fact`.
</ResponseField>

<ResponseField name="captured_at" type="timestamp">
  The instant the server learned this request. The first `as_of` at which the rows are visible, unless a backfill was declared.
</ResponseField>

<ResponseField name="backfill_batch" type="uuid or null">
  The batch id when `backfill` was given.
</ResponseField>

<ResponseField name="episodes" type="object[]">
  One entry per message, in order. `id` is the episode node. `nodes` lists what the message produced (`id`, `kind`, `statement`); existing blocks the message resolved onto appear here with their existing ids. `edges` lists the relations (`source_id`, `target_id`, `edge_type`, `fact`), with `as` saying how each was stored: `edge` for a typed link between two blocks, `claim` for a relation that became a block of its own (a statement about two entities). `accepted` counts them.
</ResponseField>

<CodeGroup>
  ```bash Request theme={null}
  curl -s -X POST $TCKG/v1/memories -H 'content-type: application/json' -d '{
    "space": "user:randy",
    "backfill": {"declared_captured_at": "2024-04-15T00:00:00Z", "reason": "import from notes"},
    "nodes": [{"id": "c2", "kind": "claim", "statement": "Bond yields rise after the rate hike",
               "asserted_at": "2024-04-10T00:00:00Z", "valid_from": "2024-04-10T00:00:00Z"}],
    "edges": [{"source_id": "c2", "target_id": "c1", "edge_type": "CONTRIBUTING_FACTOR",
               "asserted_at": "2024-04-10T00:00:00Z", "valid_from": "2024-04-10T00:00:00Z"}]
  }'
  ```

  ```json Response theme={null}
  {"accepted": 2, "refused": [], "warned": [],
   "captured_at": "2026-10-03T07:20:03.410219+00:00",
   "backfill_batch": "6c1f0b2e-3f3a-4b2d-9a1e-0d9c1f6a7b11"}
  ```

  ```json Response with a refusal theme={null}
  {"accepted": 0,
   "refused": [{"id": "x", "reason": "captured_at_supplied",
                "why": "captured_at is stamped by the server; remove it from the row"}],
   "warned": [], "captured_at": "2026-10-03T07:21:44.002117+00:00", "backfill_batch": null}
  ```
</CodeGroup>


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