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.

  1. Before your model answers, ask KAPEX for the context for this person and this message.
  2. Put what comes back in your system prompt.
  3. Call your model as you do now.
  4. 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.

The loop, with its two calls You write to the app. Before the reply, the app calls POST /context/items on KAPEX and gets back items for this person and this message. After the reply, it sends each message of the turn with POST /ingest/v2. You The app The model KAPEX before the reply: POST /context/items after the reply: POST /ingest/v2
The loop, with its two calls You write to the app. Before the reply, the app calls POST /context/items on KAPEX and gets back items for this person and this message. After the reply, it sends each message of the turn with POST /ingest/v2. You The app The model KAPEX before the reply: POST /context/items after the reply: POST /ingest/v2
Both calls are on /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?"}'
Shape only. There is no endpoint to send this to yet.

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

  1. Ana writes

    “Can you help me plan the birthday dinner?”

  2. Your app asks KAPEX

    POST /api/v1/context/items
    {"user_id": "ana",
     "message": "Can you help me plan the birthday dinner?"}
  3. What comes back

    {"items": [
      {"node_id": "…",
       "about": "birthday dinner",
       "sentence":
         "Cooking for a friend's birthday on Saturday.",
       "date": "…"}
    ]}
  4. 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.

  5. Your model replies

    “Happy to. Is it still Saturday, and how many are coming?”

  6. Your app sends the turn

    Two POST /api/v1/ingest/v2 calls, with speaker set to user and then to assistant.

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 doesOn /api/v1On /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.