# Endpoint reference > Every route live under /v1/memory. Field names match the JSON the API sends and accepts. Base URL: `https://api.harny.ai/v1/memory`. Send `Authorization: Bearer $HARNY_APP_KEY` and `X-Harny-User: ` on every call, or a user key (`fsk_`) with no `X-Harny-User`. Every error body is `{code, status, debugMessage, data}` and every response carries `X-Request-Id`. [Errors](/docs/memory-api/errors) lists the codes. ### POST /v1/memory/ingest Adds one data point to a user's memory in an app. | Field | Type | Notes | |---|---|---| | `content` | string | Required. Not blank, no U+0000, at most 500,000 characters. | | `label` | string | At most 200 characters. | | `id` | string | No `/`, at most 512 characters. Defaults to the lowercase SHA-256 of `content`. | | `timestamp` | string | ISO 8601 with a time zone. Defaults to the stored value, else now. | | `metadata` | object | JSON object, at most 64 KiB serialized. | | `audience` | string | Omit it or send `project`. Other values return `audience-unknown`. | | `identity` | string | At most 200 characters. Must equal the acting user if sent. | | `idempotencyKey` | string | At most 200 characters. | Send the `X-Harny-User` header with an app key. A user key needs no `X-Harny-User`. An unknown field returns `invalid-ingestion`. Response `202`: `{projectId, ingestionId, sourceId, status: "queued", unchanged: null}`. `projectId` carries the app id. Response `200` with `status: "completed"` and `unchanged: true` when the stored data point is identical. Nothing is processed again in that case. Sending the same `id` with a change creates a new revision. Harny deletes the memories built on the old revision, then ingests the new text. Fields you leave out of the resend keep their stored values. If the new text is refused, the data point has no memories until you send it again, and that resend counts as a change. Error codes: `400` `content-required`, `id-invalid`, `nul-character`, `invalid-ingestion`, `audience-unknown`, `identity-required`; `401` `unauthorized`; `403` `identity-mismatch`; `409` `idempotency-conflict`, `data-point-deleted`, `data-point-processing`; `413` `content-too-large`; `429` `project-cap-reached` (or bodyless `429` for the per-minute limit); `503` `pipeline-disabled`, `organization-not-ready`. ```bash tab=cURL curl -X POST "https://api.harny.ai/v1/memory/ingest" \ -H "Authorization: Bearer $HARNY_APP_KEY" \ -H "X-Harny-User: user-123" \ -H "Content-Type: application/json" \ -d '{"content": "Met the client on Monday.", "label": "meeting note"}' ``` ```python tab=Python import os import requests resp = requests.post( "https://api.harny.ai/v1/memory/ingest", headers={ "Authorization": f"Bearer {os.environ['HARNY_APP_KEY']}", "X-Harny-User": "user-123", }, json={"content": "Met the client on Monday.", "label": "meeting note"}, ) print(resp.status_code, resp.json()) ``` ```typescript tab=TypeScript const resp = await fetch("https://api.harny.ai/v1/memory/ingest", { method: "POST", headers: { Authorization: `Bearer ${process.env.HARNY_APP_KEY}`, "X-Harny-User": "user-123", "Content-Type": "application/json", }, body: JSON.stringify({ content: "Met the client on Monday.", label: "meeting note" }), }); console.log(resp.status, await resp.json()); ``` ### POST /v1/memory/ingest/batch Adds up to 1,000 data points in one request. | Field | Type | Notes | |---|---|---| | `items` | array | 1 to 1,000 single-ingest objects, without `idempotencyKey`. An `id` may appear once per batch. | | `idempotencyKey` | string | At most 200 characters. | The whole batch is validated before any write. The batch is at most 16 MiB of UTF-8 in total. Each item is then written separately, so a quota refusal at item k keeps items before k. Response `202`: `{projectId, ingestionId, status: "queued", unchanged: null, items: [{ordinal, sourceId, status, unchanged}]}`. Response `200` with `completed` and `unchanged: true` when every item is unchanged. On a partial refusal, the error body includes `data: {ingestionId, accepted: k}`. Retry with the same `idempotencyKey` and body to resume at k. Error codes: `400` `invalid-ingestion` (empty, repeated id, or over 1,000 items), `413` `content-too-large`, plus the quota and availability codes from the single ingest route. ### GET /v1/memory/ingest/{ingestionId} Returns the processing status of one send. | Field | Type | Notes | |---|---|---| | `ingestionId` | path | The id returned by ingest. | Response: `{status, settled, blocked, readiness: {raw, comprehension, consolidation, failed}, failure}`. - `status` is `processing`, `failed`, or `completed`. - `readiness` counts the user's whole memory space, not one send. - `settled` is true when `raw`, `comprehension`, `consolidation` and `failed` are zero. - `blocked` is true when `raw`, `comprehension` and `consolidation` are zero and `failed` is above zero. - `failure` is null or `{code, message}`, where `code` is `content-rejected`, `model-unavailable`, `index-unavailable`, or `internal`. Error codes: `404` `not-found`. The status limiter applies. ### DELETE /v1/memory/ingest/{ingestionId} Deletes all data points that a send created. No request body. Response `204` with no body. Repeating the call also returns `204`. Error codes: `404` `not-found`; `409` `data-point-processing`, `memory-ids-unknown`. The memories the send created are deleted. If a send created none, existing memories are left in place. A data point still in the retry queue has its pending job removed. The id stays recorded as deleted, so sending it again returns `409 data-point-deleted`. ### DELETE /v1/memory/data-points/{sourceId} Deletes one data point and the memories built on it. This is the call that removes one piece of text you sent. To remove everything one send created, use `DELETE /v1/memory/ingest/{ingestionId}`. | Field | Type | Notes | |---|---|---| | `sourceId` | path | The data point's source id: the `id` you gave it when you sent it. | Response `202`: `{operationId, ids: [sourceId]}`. The delete has finished when the call returns. `operationId` identifies the delete. Error codes: `404` `data-point-not-found`; `409` `data-point-processing`, `memory-ids-unknown`. ### POST /v1/memory/data-points/delete Deletes up to 1,000 data points in one request. | Field | Type | Notes | |---|---|---| | `ids` | array | 1 to 1,000 source ids. A repeated id counts once. | Response `202`: `{operationId, ids}`. Nothing is deleted when any id is missing or any id is mid-delivery. Error codes: `400` `invalid-deletion`; `404` `data-point-not-found` with `data: {ids: [missing]}`; `409` `data-point-processing`, `memory-ids-unknown`. This route is free. ### POST /v1/memory/recall Returns the memories that match a query for one user. | Field | Type | Notes | |---|---|---| | `query` | string | Required. Error `recall-query-required` if missing. | | `identity` | string | Required. The user whose memory is read. A body `identity` must match the acting user, else `403 identity-mismatch`. | | `limit` | integer | 1 to 200. Default 100. | | `level` | string | `low` (8,000 tokens, default), `medium` (16,000), `high` (24,000), `extra-high` (32,000). Do not send with `maxTokens`. | | `maxTokens` | integer | Floor 256. Do not send with `level`. | | `queryTimestamp` | string | ISO 8601 with zone. Drops results whose `occurredAt` is later. Unknown times are kept. | | `occurredFrom` | string | Inclusive start of the `occurredAt` window. Unknown times are dropped. | | `occurredTo` | string | Inclusive end of the window. | | `sort` | string | `relevance` (default) or `chronological` (`occurredAt` ascending, unknown last). | Response `200`: ```json { "asOf": "", "usedEvidenceTokens": 0, "results": [ { "id": "", "rank": 1, "occurredAt": "", "content": "", "artifact": {"id": "", "kind": "observation", "occurredAt": ""}, "sources": [ {"sourceId": "", "occurredAt": "", "label": "