> ## 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.

# Context Receipts

> A per-response manifest of what entered the prompt, what was cut, and why, retrievable after the fact.

`retrieve` (and the drop-in proxy) return a ranked list, but a lot happens between
"the engine found this" and "the model saw this": near-duplicates get collapsed,
low-relevance hits get dropped, results past your `limit` get cut, and, if you
rendered a prompt-ready block, whole memories can get dropped again to fit the
token budget. A context receipt is that decision, written down: what survived,
what didn't, and which reason removed it.

A receipt covers two families of cut. By default it reports the ones made after
retrieval returned, which is the fast path and costs nothing. Ask for
`receipt_detail: "full"` and it also reports the ones the search pipeline made
internally, so a memory that never made it out of ranking is accounted for
instead of simply absent.

> [!NOTE]
>
> **What a receipt does not prove.** A receipt reports what retrieval *returned*
> and what assembly *cut*. It does not prove retrieval was correct. It cannot
> tell you a relevant memory was missing from the candidates in the first place,
> and it is not an isolation guarantee. Treat it as a debugging tool first, an
> audit trail second, not a certificate that the right memories were found.

> [!NOTE]
>
> **Best-effort, never load-bearing.** Asking for a receipt never changes what a
> call returns or how it's billed, and a request succeeds whether or not its
> receipt was actually stored. If you ask for one and it can't be built or saved
> for any reason, the call still completes normally; you just won't have a
> receipt to fetch afterward.

## How it works

Every `retrieve` call already builds and stores a receipt, whether or not you
ask for one. That's what makes a receipt fetchable later for a request
nobody flagged in advance, which is the actual debugging case: grab the
request id out of your own logs (`X-Request-ID`) after the fact and fetch it,
even for a call that never set `receipt`. Passing `receipt: true` changes one
thing only: that call's own response hands you the id back directly, so you
don't have to go dig it out of your logs.

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

```json theme={null}
{
  "space_id": "customer-support-bot",
  "query": "What plan is this customer on?",
  "receipt": true
}
```

```bash cURL theme={null}
curl -X POST https://api.anonalabs.com/v1/retrieve \
  -H "Authorization: Bearer anona_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "space_id": "customer-support-bot",
    "query": "What plan is this customer on?",
    "receipt": true
  }'
```

```python Python theme={null}
result = client.retrieve_receipt(
    space_id="customer-support-bot",
    query="What plan is this customer on?",
)
print(result.receipt_id)
for r in result:          # iterates the results
    print(r["content"])
```

```typescript TypeScript theme={null}
const result = await anona.retrieveReceipt({
  spaceId: "customer-support-bot",
  query: "What plan is this customer on?",
});
console.log(result.receipt_id, result.memories.length);
```

The SDKs set `receipt: true` for you — `retrieve` has nowhere to carry the id, so
asking for it is a separate method rather than a flag.

The response is unchanged except for one added field:

```json theme={null}
{
  "results": [ "…unchanged…" ],
  "usage": null,
  "context": null,
  "token_estimate": null,
  "receipt_id": "3f9a2b7c-1e4d-4a6b-9c3e-8d2f1a0b5c7e"
}
```

`receipt_id` only appears when you set `receipt: true` and a receipt was
actually built. Omitting `receipt` (the default) leaves the response exactly
as it was before this existed, field for field.

### Asking for the search pipeline's own cuts

`receipt_detail` controls how deep the receipt goes. It is independent of
`receipt`: one decides whether this response hands you the id, the other
decides what the receipt contains.

| Value | What the receipt reports |
| - | - |
| `"basic"` (default) | Cuts made after search returned: `dedup`, `min_score`, `limit`, `budget`. Costs nothing extra. |
| `"full"` | The above, plus the cuts search made internally: `candidate_cap`, `rerank_rank`, `engine_budget`. |

```bash cURL theme={null}
curl -X POST https://api.anonalabs.com/v1/retrieve \
  -H "Authorization: Bearer anona_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "space_id": "customer-support-bot",
    "query": "What plan is this customer on?",
    "receipt": true,
    "receipt_detail": "full"
  }'
```

```python Python theme={null}
result = client.retrieve_receipt(
    space_id="customer-support-bot",
    query="What plan is this customer on?",
    receipt_detail="full",
)
```

```typescript TypeScript theme={null}
const result = await anona.retrieveReceipt({
  spaceId: "customer-support-bot",
  query: "What plan is this customer on?",
  receiptDetail: "full",
});
```

> [!NOTE]
>
> **`"full"` can cost you a little latency.** Repeated searches are served from a
> cache, and the pipeline's internal decisions are not part of a cached answer.
> So the first `"full"` call for a given query may re-run the search to collect
> them, even when a `"basic"` call would have been served from cache. Later calls
> reuse that richer entry. Results are identical either way; only the timing
> differs. Leave it on `"basic"` for production traffic and reach for `"full"`
> when you're actually investigating something.

> [!NOTE]
>
> `receipt_id` is the same value as the `X-Request-ID` header every response
> already carries. There's nothing extra to capture. If you log request ids
> today, you already have what you need to fetch the receipt later.

## Fetch a receipt

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

```bash cURL theme={null}
curl "https://api.anonalabs.com/v1/receipts/3f9a2b7c-1e4d-4a6b-9c3e-8d2f1a0b5c7e" \
  -H "Authorization: Bearer anona_live_YOUR_KEY"
```

```python Python theme={null}
receipt = client.get_receipt("3f9a2b7c-1e4d-4a6b-9c3e-8d2f1a0b5c7e")
print(receipt["included"], receipt["excluded"])
```

```typescript TypeScript theme={null}
const receipt = await anona.getReceipt("3f9a2b7c-1e4d-4a6b-9c3e-8d2f1a0b5c7e");
console.log(receipt.included, receipt.excluded);
```

**Response** `200 OK`

```json theme={null}
{
  "request_id": "3f9a2b7c-1e4d-4a6b-9c3e-8d2f1a0b5c7e",
  "space_id": "customer-support-bot",
  "included": [
    {
      "memory_id": "mem_abc123def456",
      "relevance_score": 1.0974,
      "token_estimate": 14
    }
  ],
  "excluded": [
    {
      "memory_id": "mem_9f21e2",
      "reason": "dedup",
      "detail": "covered by mem_abc123def456"
    },
    {
      "memory_id": "mem_7a10c4",
      "reason": "min_score",
      "detail": "score=0.00012 floor=0.5"
    },
    {
      "memory_id": "mem_5b88e0",
      "reason": "limit",
      "detail": "rank=11"
    }
  ],
  "excluded_truncated": {},
  "token_accounting": {},
  "engine_stages": false
}
```

| Field | Type | Description |
| - | - | - |
| `request_id` | string | The id of the call this receipt was built for. Matches `receipt_id` and the `X-Request-ID` response header on that call. |
| `space_id` | string | The space the call searched. |
| `included` | array | What survived every cut, in result order. Each item is `{memory_id, relevance_score, token_estimate}`, with no memory content. |
| `excluded` | array | What was cut, in the order it could have been cut: the search pipeline's own cuts first (only when `receipt_detail` was `"full"`), then `dedup`, `min_score`, `limit`, and `budget`. Each item is `{memory_id, reason, detail}`. Capped at 50 entries per reason. |
| `excluded_truncated` | object | `{}` unless a reason above hit the 50-entry cap. When one did: `{reason: count}`, how many additional cuts for that reason didn't fit in `excluded`. Always accurate, never silently dropped: if the manifest was cut, this says so and by how much. |
| `engine_stages` | boolean | Whether this call asked the search pipeline to account for its own cuts (`receipt_detail: "full"`). When `false`, `excluded` covers only what happened after search returned, and its silence about a memory says nothing about whether search considered it. |
| `token_accounting` | object | `{}` unless the call rendered a prompt-ready block (`format: "block"` on retrieve, or a drop-in proxy call with memory injection on); there's no block to account for otherwise. When one was rendered: `{block_tokens, budget}`, where `block_tokens` is the rendered block's estimated size and `budget` is the ceiling it was rendered against (`null` if you didn't set one). |

> [!NOTE]
>
> **Why cap it.** The engine's own fan-out is bounded by tokens, not result
> count, so a space of short facts combined with a high `limit` and a
> `min_score` floor can drop several hundred results in one call, far more
> than a debugging manifest needs to be useful. `excluded` keeps the first 50
> per reason; `excluded_truncated` always reports how many more there were, so
> a capped list never reads as a complete one. The cap never affects `results`
> itself, only how much of the *cut* list gets written down.

Receipts are kept for about an hour after the call that produced them, then
expire. There is no way to list receipts; fetch each one by the id its call
returned.

## The exclusion reasons

### After search returned (always available)

| `reason` | What it means |
| - | - |
| `dedup` | A raw fact was dropped because a consolidated memory already returned covers it. `detail` names the covering memory's id. |
| `min_score` | The result fell below the `min_score` floor you set. `detail` shows the score and the floor. Expect this reason to account for most of a filtered call: in the default `accurate` mode `relevance_score` is close to two-valued — a near-verbatim match scores around **1.1**, and everything else, including a correct answer phrased differently from the query, scores around **0.0001 or less**. Very little lands in between, so any floor between those two values keeps near-verbatim matches only. See [`relevance_score`](https://docs.memory.anonalabs.com/api-reference/retrieve#response). |
| `limit` | The result ranked outside your requested `limit` (or `top_k`). `detail` shows its rank. |
| `budget` | Rendering a prompt-ready block (`format: "block"`, or the drop-in proxy's memory injection) had to drop the whole memory to fit `context_max_tokens`. `detail` shows its rank. Only ever appears when a block was actually rendered. |

`dedup`, `min_score`, and `limit` happen during retrieval itself, in that
order, so each stage only ever sees what survived the one before it, so a given
memory appears in `excluded` at most once. `budget` happens afterward, during
rendering, and only applies to memories that made it past all three.

### Inside search (`receipt_detail: "full"` only)

Search ranks far more candidates than it returns. These three reasons account
for memories that were considered and dropped before the reasons above ever
applied.

| `reason` | What it means |
| - | - |
| `candidate_cap` | The memory was found, but ranked too low to enter the final scoring round. `detail` shows the rank it reached and the cap. Usually means the query didn't match it strongly enough, not that anything is wrong. |
| `rerank_rank` | It reached final scoring and placed below your `limit`. `detail` shows its position. Raising `limit` would bring it back. |
| `engine_budget` | It scored well enough to be returned, but was passed over to fit the response's own token budget: something ranked below it came back and this didn't. `detail` shows its position. |

> [!NOTE]
>
> The two families never overlap. The reasons above describe memories search
> never returned; the ones in the previous table describe memories it did return
> and something afterward removed. A memory appears under exactly one reason, so
> the counts in `excluded_truncated` stay accurate.

## Account for one specific memory

The receipt tells you what was cut. It can't tell you about a memory that no
stage mentions at all, and that's usually the memory you're asking about:
you know it's in the space, and it isn't in your prompt.

`explain` answers exactly that, for one memory id, against one earlier call.

```http theme={null}
GET /v1/receipts/{request_id}/explain?memory_id=mem_abc123def456
Authorization: Bearer anona_live_YOUR_KEY
```

```bash cURL theme={null}
curl "https://api.anonalabs.com/v1/receipts/3f9a2b7c-1e4d-4a6b-9c3e-8d2f1a0b5c7e/explain?memory_id=mem_abc123def456" \
  -H "Authorization: Bearer anona_live_YOUR_KEY"
```

```python Python theme={null}
print(client.explain(
    "3f9a2b7c-1e4d-4a6b-9c3e-8d2f1a0b5c7e",
    "mem_abc123def456",
))
```

```typescript TypeScript theme={null}
console.log(await anona.explain(
  "3f9a2b7c-1e4d-4a6b-9c3e-8d2f1a0b5c7e",
  "mem_abc123def456",
));
```

**Response** `200 OK`

```json theme={null}
{
  "memory_id": "mem_abc123def456",
  "outcome": "excluded",
  "stage": "fusion",
  "detail": "fusion rank 812, cap 300",
  "arms": {
    "semantic": 812,
    "keyword": null,
    "related": null,
    "time": null
  },
  "scores": { "fusion": 0.031 }
}
```

| Field | Type | Description |
| - | - | - |
| `memory_id` | string | The memory you asked about. |
| `outcome` | string | `included` (it came back on that call), `excluded` (search considered it and dropped it), or `not_retrieved` (no stage of search ever saw it). |
| `stage` | string | Where it was dropped: `retrieval`, `fusion`, or `rerank`. `null` unless `outcome` is `excluded`. |
| `detail` | string | A short plain-language account of that drop. |
| `arms` | object | How each search method ranked it, or `null` for a method that didn't find it at all. Empty when nothing found it. |
| `scores` | object | The scores it earned at the stage it reached. |

### Reading the result

`not_retrieved` is the one worth acting on. It means no part of search matched
the memory, so raising `limit` or lowering `min_score` will not help. Either
the wording is too far from your query, or the memory isn't scoped where you're
searching. Check `user_id`, `agent_id`, `session_id`, and any `tags` on the
original call first.

`arms` tells you which kind of matching worked. A memory found only by
`keyword` and not by `semantic` usually means your query and the stored memory
share words but not meaning, and vice versa. `related` means it was reached
through another memory it's connected to, and `time` means it matched a date
range in the query.

> [!NOTE]
>
> **It replays the original search.** `explain` runs the same query again, pinned
> to the moment the original call ran, so it reports the pipeline as it actually
> behaved rather than as it would behave now. Memories written since then don't
> change the answer. This works on any call, including ones that never set
> `receipt` or `receipt_detail`, which is the point: the call you need to explain
> is rarely the one you flagged in advance.

> [!NOTE]
>
> **Free, but gated.** `explain` costs no credits. It does run a real search
> under the hood, so it counts against your rate limit like any other call, and
> an organization that has run out of credits can't call it, the same answer
> `retrieve` would give.

## On the drop-in proxy

The same tunable works on `/v1/chat/completions`, `/v1/responses`, and
`/v1/messages`. Set `receipt: true` in the request body, or send it once as a
header so a stock SDK never has to touch its request bodies:

```
X-Anona-Receipt: true
```

Body wins when both are set. A malformed header value comes back as `400
invalid_tunable`, never a server error.

Instead of a body field, the id comes back as a response header:

```
X-Anona-Receipt-Id: 3f9a2b7c-1e4d-4a6b-9c3e-8d2f1a0b5c7e
```

Fetch it the same way, from the same `GET /v1/receipts/{request_id}` endpoint.
On the proxy, `token_accounting` reflects what was actually injected into the
model call, the same rendering the proxy uses to build its own prompt, not a
separate estimate.

> [!NOTE]
>
> `receipt_detail` is retrieve only. Proxy receipts are always `"basic"`, so they
> report the cuts made after search returned and set `engine_stages` to `false`.
> `explain` still works on them: it replays the memory search the proxy ran, so
> you can account for a specific memory that never made it into a completion.

## Isolation

A receipt can only be fetched by the organization whose API key made the
original call. A request for another organization's receipt id gets the same
`404` as an unknown or expired one; there's no way to distinguish "not yours"
from "doesn't exist."

The same rule holds one level down, for a space with per-space membership
enforced: only a member of the space the receipt names can fetch it, even
from inside the right organization. A colleague in the same org who isn't on
that space gets the same `404`, not a `403`, which would itself confirm the
receipt exists for a space they can't see.

## Pricing

Free. `GET /v1/receipts/{request_id}` reads something already computed and
already paid for at the time of the original call, so fetching it again costs
nothing.

## Error responses

| Status | Code | Cause |
| - | - | - |
| 401 | `missing_auth` / `invalid_api_key` | Missing or invalid API key. |
| 404 | `receipt_not_found` | No receipt with that id for your organization: never built, expired, or belongs to someone else. On `explain`, also returned for a receipt old enough to predate the endpoint, which has nothing to replay. |

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

## Next steps

> **[Retrieve API](https://docs.memory.anonalabs.com/api-reference/retrieve.md)**
>
> Rank a space's memories against a query.

> **[Chat completions](https://docs.memory.anonalabs.com/api-reference/chat.md)**
>
> The drop-in proxy that can build receipts for injected memory too.


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