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.
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.
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
Everyretrieve 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.
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.
"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.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
200 OK
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.The exclusion reasons
After search returned (always available)
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.
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.
200 OK
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.
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.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:
400 invalid_tunable, never a server error.
Instead of a body field, the id comes back as a response header:
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.
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 same404 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
See the full error reference.
Next steps
Retrieve API
Rank a space’s memories against a query.
Chat completions
The drop-in proxy that can build receipts for injected memory too.