Building on KAPEX
Checked against the code October 12, 2026
If you write the code, this page is for you. It has every call an app would make to KAPEX and what each one sends back, including the parts that aren’t built. There’s nowhere to send any of them yet, so it starts with what runs today.
What runs today
KAPEX is a container that runs beside your app and talks to it over REST, or through its MCP server. The Python and TypeScript clients are in our code repository, which is private, and aren’t published to PyPI or npm yet. The image isn’t on a registry either, so today this is the shape of it, not something you can pull.
There is no public endpoint and no hosted sandbox. The code on this page won’t run for you yet.
The loop, around each reply
An integration is four steps around the model call your app already makes. Two of those steps are calls to KAPEX.
- Before your model answers, ask KAPEX for the context for this person and this message.
- Put what comes back in your system prompt.
- Call your model as you do now.
- After the reply, send the turn to KAPEX.
Your model stays the same, and your prompt stays yours. What comes back is plain sentences, and you choose where in the prompt they go.
/api/v1. On /api/b2c/memory the same loop uses POST /retrieve and POST /turn.Without KAPEX
def answer(user_id, message):
system = APP_PROMPT
reply = call_model(system, message)
return reply
With KAPEX
def answer(user_id, message):
items = requests.post(
f"{KAPEX_URL}/api/v1/context/items",
headers={"X-API-Key": KAPEX_KEY},
json={"user_id": user_id, "message": message},
).json()["items"]
system = APP_PROMPT
system += "\n\nBackground on this person:\n"
system += "\n".join(item["sentence"] for item in items)
reply = call_model(system, message)
# after the reply is out
run_later(send_turn, user_id, message, reply)
return reply
def send_turn(user_id, message, reply):
for speaker, text in (("user", message), ("assistant", reply)):
requests.post(
f"{KAPEX_URL}/api/v1/ingest/v2",
headers={"X-API-Key": KAPEX_KEY},
json={"user_id": user_id, "speaker": speaker, "message": text},
)
The lines with a teal rule are new. The call to your model is the same line as before.
As curl
# Before your model answers: ask what matters about this person for this message
curl -X POST "$KAPEX_URL/api/v1/context/items" \
-H "X-API-Key: $KAPEX_KEY" \
-H "Content-Type: application/json" \
-d '{"user_id": "ana", "message": "Can you help me plan the birthday dinner?"}'
# Write the sentence from each item into your system prompt, then call your model
# After the reply: send the turn, one message per call
curl -X POST "$KAPEX_URL/api/v1/ingest/v2" \
-H "X-API-Key: $KAPEX_KEY" \
-H "Content-Type: application/json" \
-d '{"user_id": "ana", "speaker": "user",
"message": "Can you help me plan the birthday dinner?"}'
curl -X POST "$KAPEX_URL/api/v1/ingest/v2" \
-H "X-API-Key: $KAPEX_KEY" \
-H "Content-Type: application/json" \
-d '{"user_id": "ana", "speaker": "assistant",
"message": "Happy to. Is it still Saturday, and how many are coming?"}'
On /api/v1 the key goes in an X-API-Key header, and each message is its own call to /ingest/v2, with speaker set to user, assistant or other. The user_id is your app’s own id for that person.
The other set of routes, /api/b2c/memory, runs the same loop with two calls, and hands back the note as one block of text:
# Before your model answers
curl -X POST "$KAPEX_URL/api/b2c/memory/retrieve" \
-H "Authorization: Bearer $KAPEX_KEY" \
-H "Content-Type: application/json" \
-d '{"user_id": "ana", "query": "Can you plan me an easy week of runs?"}'
# Put the context_block from the answer in your system prompt, then call your model
# After the reply: send the turn
curl -X POST "$KAPEX_URL/api/b2c/memory/turn" \
-H "Authorization: Bearer $KAPEX_KEY" \
-H "Content-Type: application/json" \
-d '{"user_id": "ana",
"user_message": "Can you plan me an easy week of runs?",
"assistant_response": "Here is a gentle week, mostly short runs and one rest day."}'
On that set the key goes in an Authorization: Bearer header, or in an X-API-Key header. /retrieve also takes an optional top_k, which caps how many memories go into the note.
One turn, traced
-
Ana writes
“Can you help me plan the birthday dinner?”
-
Your app asks KAPEX
POST /api/v1/context/items {"user_id": "ana", "message": "Can you help me plan the birthday dinner?"} -
What comes back
{"items": [ {"node_id": "…", "about": "birthday dinner", "sentence": "Cooking for a friend's birthday on Saturday.", "date": "…"} ]} -
What goes in your system prompt
Background on this person: Cooking for a friend's birthday on Saturday. Check a date with the person before you rely on it.
The last line is your app’s, not KAPEX’s.
-
Your model replies
“Happy to. Is it still Saturday, and how many are coming?”
-
Your app sends the turn
Two
POST /api/v1/ingest/v2calls, withspeakerset touserand then toassistant.
Written to show the intent, not captured from a running system.
What your app has to do
KAPEX holds the memory and writes what goes to the model. These parts are yours, and they are what a person using your app will judge it by.
- Tell people the app remembers them, and ask before it does. KAPEX doesn’t ask.
- Draw the list of what is held about them, with keep, hide, fix and delete on every line.
- When someone asks in the conversation for something to be left alone, make the call that does it.
- Offer the export.
- Erase the person when they close their account.
- Put the providers KAPEX calls on its own in your data review.
- Keep protected health information out of it.
The control page shows each of these from the person’s side.
Ways in
Below are the ways an app can reach KAPEX. The repository also holds an OpenAPI 3.1 description, but it and both clients were written for an older set of routes than the two on this page. Where they disagree, go by this page.
- REST
- JSON over HTTP, on the two sets of routes in the next section.
- MCP
- An MCP server that speaks stdio, SSE or HTTP is in the repository. It isn’t lined up with either set of routes below yet, so use REST for now.
- Python and TypeScript
- Clients in the repository, not on PyPI or npm.
Two sets of routes today
If you’re starting now, start on /api/v1. It’s the set the control page describes, because that set does everything it promises a person. Whoever runs KAPEX makes its keys with an admin call; keys for the b2c set came from a signup that’s closed.
KAPEX answers on two sets of routes that this page covers. An older third set, under /v1, is still in the code, and the OpenAPI file, both clients and part of the MCP server call it. Don’t build on it.
One memory goes by several names here: a node on /api/b2c/memory, an item on /api/v1, and a line on the control page.
The two sets don’t share memory. Each takes its own kind of key and files a person under its own id, so an app picks one set and stays on it.
| What it does | On /api/v1 | On /api/b2c/memory |
|---|---|---|
| Before the reply | POST /context/items, which returns items: what each one is about, a sentence and a date |
POST /retrieve, which returns a context_block of plain text |
| How sure it is | Not marked. Each item is a plain sentence, so any caution has to come from your prompt. | Marked in the text. What it is less sure of comes back as something to check or ask about. |
| After the reply | POST /ingest/v2 |
POST /turn |
| List what it holds for a person | GET /memory/v2/items, all of it, archived lines marked and replaced wordings left out |
GET /nodes, the first page only by default, archived lines left out |
| Leave alone, or undo | POST or DELETE /memory/v2/suppress |
POST or DELETE /suppress |
| Keep, or undo | POST or DELETE /memory/v2/pin |
POST or DELETE /pin |
| Fix | PATCH /memory/v2/nodes/<id> |
PATCH /nodes/<id> |
| Delete one memory | DELETE /memory/v2/nodes/<id>, which writes the ledger record first and takes the memory’s details with it |
DELETE /nodes/<id>, which writes no ledger record and leaves the memory’s details behind |
| See what was deleted | GET /memory/v2/ledger |
GET /users/<id>/deletion, for an erase |
| Export a person | GET /memory/v2/export, with archived lines, replaced wordings and the ledger |
POST /users/<id>/export, with archived lines left out |
| Erase a person | DELETE /memory/v2/users/<id> |
DELETE /users/<id> |
| Key header | X-API-Key |
Authorization: Bearer, or X-API-Key |
Three of the b2c calls do less than the control page promises a person. A single delete writes no ledger record and leaves the memory’s details behind, the list returns only its first page by default and leaves out archived memories, and the export leaves archived memories out. For a person’s controls as the control page describes them, build on the /api/v1 set.
What comes back
Each turn you send is kept in the words that were used, filed under each person and topic KAPEX finds in it. What comes back to you is a few sentences drawn from that store, not the store itself.
From /context/items you get items: for each one, what it is about, a sentence and a date, with no scores. You write them into your prompt yourself.
{"user_id": "ana",
"items": [{"node_id": "…",
"about": "apartment",
"sentence": "Moved into a new apartment and is unpacking the kitchen first.",
"date": "2026-10-03"}],
"count": 1}
From /retrieve, the part you hand your model is context_block: plain text, in order, sized to a budget so it doesn’t crowd out the rest of your prompt.
What it is sure of, it states. What it is less sure of comes back worded as something to check or ask about, so your model isn’t handed a guess as if it were a fact. How it works shows an example note.
Items from /context/items carry no such wording. If you want your model to check before it leans on an item, say so in your prompt.
Anything the person asked it to leave alone is taken out before the rest is chosen.
It ties a name to the person, whatever words were used. If someone says “my dad” one day and “Father” the next, both land on the same person.
Flagging two memories that disagree is in the code, but it’s switched off by default, and it isn’t part of what /retrieve returns.
The part we keep to ourselves
How KAPEX ranks one memory against another isn’t published. Everything on this page is about what goes in and what comes out.
Models and keys
The context is plain text, so it can go in front of whichever model your app calls. You bring your own model key. In the loop above, your app makes its own model call with that key, the way it does today.
KAPEX also makes model calls of its own while it reads a turn. By default they go to a Claude Haiku model on Amazon Bedrock, or to Anthropic directly if the Bedrock call fails and an Anthropic key is set. Embeddings come from Amazon Titan on Bedrock, and nothing else stands in for them in the code today. These calls use the credentials set up where KAPEX runs, so those providers belong in your cost and data reviews.
Deleting and exporting
On the /api/v1 set, deleting one memory writes the ledger record first, then removes the memory and the details stored with it. Notes the engine files under the person instead of the memory, such as patterns across several memories and copies of what was said, survive a single delete, and so does the wording a fix replaced. Erasing the person removes both.
On the /api/b2c/memory set, deleting one memory takes that memory’s links to other memories with it, and nothing more.
Erasing a person is one call on either set. On the b2c set it opens a deletion record before any work starts. When the work is done, it counts what is left for that person, which should be nothing, and writes the count to the record.
A large erase carries on in the background, so the call answers straight away with a request id, and GET /users/<id>/deletion tells you where it stands. The record holds the id you sent, the counts and the times, and none of what the person said.
Export returns a person’s memories as JSON. On the /api/v1 set it also carries archived memories, replaced wordings and the ledger. On the b2c set it leaves archived memories out. Neither export carries the copies of what was said that are filed under the person.
When a call fails
On the b2c set, a call that fails answers with JSON: a sentence in error, and a short code you can match on, such as MISSING_USER_ID or NOT_FOUND.
A missing or unrecognised key gets the sentence without a code. The /api/v1 set answers with the sentence alone.
Questions builders ask
Can we run it ourselves, and on what?
The intended setup is the KAPEX container in your own environment, beside a PostgreSQL database and Redis, and the repository has compose files for a bundled or an external database. It also needs the models for its own calls, so out of the box that means an AWS account with Bedrock access. That’s the design, not something to take on trust while nothing is running.
Where does the key come from?
On the /api/v1 set, whoever runs KAPEX creates a key for each app with an admin call. Keys for the b2c set came from the self-service signup, which is closed.
Does it slow the reply?
The call for context comes before your model call, so it adds to the wait. The call that sends the turn can go after the reply is out. There’s no published timing figure.
Can we load past conversations?
Yes. POST /ingest on the b2c set takes a list of messages, each with the time it happened, and POST /ingest/v2 takes an event_time with each message.
How to judge it yourself
When there is something to run, run the same conversation twice with the same model: once with the memory you use now, even if that is none, and once with KAPEX. Read what each one hands the model, turn by turn.
Four things are worth checking on a conversation of your own.
- Ask for a topic to be left alone, then check that the lines held on it stop reaching your app.
- Call someone “my dad” and later “Father”, then check that the list shows one person.
- Fix a line, then check that your prompt gets the new wording.
- Delete a line, then check that the ledger shows it.
Use a conversation of your own, not one of ours, and read the context before the replies. If what KAPEX hands over isn’t better for the people using your app, don’t use it.
If you want to talk through a test like that before there is anything to run, write to us about integrating.
Get in touch
If you’d build on it anyway, say what you’re making in the box on the waitlist.