Skip to content

Quickstart

From an empty account to a memory you can search

Eight steps, about five minutes, and one of them is irreversible. Step two shows a secret exactly once — read it before you run it.

Before you start: what a memory is

Two decisions shape everything you store — whose memory it is, and which conversation it came from. Both are easier to get right now than to change later.

You never write a memory directly. You send conversation messages; they are kept verbatim, and extraction reads them and writes the durable facts it finds. That is why nothing is searchable the instant a write returns, and why the messages you send are worth sending in full — a fact that was never said cannot be extracted.

What a key writes is unapproved until a person approves it. The page under Review — Flagged for review — is where that happens, and it is the only place in the product where it can: a key proves which credential wrote something, never that a person meant it. Until then an agent reading the item is told so by a marker in front of the text.

A workspace holds a second thing beside memories, and it is worth knowing about before you start. A memory is a fact the extraction found in a conversation. A procedure is a how-to you write on purpose — how you ship, how you triage, the steps somebody would otherwise work out again — and it is versioned from creation, shareable with the workspace, and read back whole. Your agent reads one before doing the work rather than rediscovering it. The API reference has the endpoints and the rules about who may change one; the command reference has the commands.

The two kinds of subject a memory belongs to
SubjectUse it forFor example
memory_type: userWhat you learn about a person — preferences, constraints, history, anything that should still be true the next time they talk to any of your agents.Prefers Danish. Allergic to shellfish. Works in Aarhus. Wants short answers.
memory_type: agentWhat an agent learns for itself — decisions it made, approaches that failed, conventions it settled on. Knowledge that belongs to the worker, not the person it served.Retries this API twice, never three times. This customer's exports need the legacy format.

Pick one per call: a user memory needs a user id, an agent memory needs an agent id, and a mismatch is refused rather than quietly answered from the wrong side. Reads work the same way — and on a read you may name no subject at all, which spans every subject the key can already see. If you are unsure which a fact is, ask who would be wrong if it were forgotten: the person, or the agent.

In practice you rarely type an id. pfmem init asks once — a person or an agent, and which id, offered from the address your git config already carries — and records the answer, so the subject flags are optional from then on. A key can also be bound to a subject when it is minted; then the server decides and the recorded id is ignored, and POST /api/v2/me tells you what the key you are holding resolves to. On a read, --all-subjects widens back out to every subject that key is allowed to see.

A conversation id groups the messages of one exchange. Extraction works per conversation, so the id decides what gets read together — and reusing one id for unrelated exchanges blurs them into a single context. Use a fresh id per conversation, and keep it stable for as long as that conversation runs.

Writes are asynchronous and return a task id. Poll it, or force the work with a flush when you need results immediately. A task that fails can be run again — from the dashboard or from the command line — and re-running is safe: extraction only processes what has not been processed, so a repeat does not duplicate anything.

Worth doing from the start

  • Send a batch at the end of a turn or a conversation, not one call per message. Up to 100 messages per call.
  • An id only has to be stable and spelled the same way everywhere you call from. An email address is a fine choice, and it is what pfmem init suggests, because most people have one and it rarely changes; what actually costs you recall is calling the same person two different things.
  • One conversation id per conversation, stable while it runs, never reused for a different one.
  • Call flush at a conversation boundary. It costs nothing when there is nothing to extract, and it is the difference between a memory being available now and being available shortly.
  • Narrow reads with an app or project id rather than filtering afterwards. Narrowing is free; it can never widen what a key may see.
  • Give each agent its own key with the narrowest role that works. A key is where permission lives, so a key with less reach is the cheapest safety you can buy.
  • Watch failed tasks. A failure marked not retryable means the cause is in the request or the configuration, and running it again unchanged will fail the same way.
  1. Create an account

    Sign up with an email, a password and a name. Signing up takes a personal invite: use the address it was sent to, or open the link in the invite email and sign up in the browser. The address has to be confirmed before the account can be used, so this call returns no session — it sends a link instead. Open the link, then sign in; that second call is what sets the cookie the next step needs.

    POST /api/auth/sign-up/email
    curl -fsS -X POST https://pfmem-api.packagefactory.dk/api/auth/sign-up/email \
      -H 'content-type: application/json' \
      -c cookies.txt \
      -d '{"email":"[email protected]","password":"a-long-passphrase","name":"Your Name"}'

    Until the address is confirmed, sign-in answers 403 with EMAIL_NOT_VERIFIED. The link is valid for one hour, and the page it lands on can send another.

    POST /api/auth/sign-in/email
    curl -fsS -X POST https://pfmem-api.packagefactory.dk/api/auth/sign-in/email \
      -H 'content-type: application/json' \
      -c cookies.txt \
      -d '{"email":"[email protected]","password":"a-long-passphrase"}'

    You can do this in the browser instead. The result is the same session cookie. Signing up with an address that already exists gets the same answer as a new one — the response is deliberately not a way to ask which addresses are registered.

  2. Mint the master key

    This one call creates the account record and issues its first master key. It authenticates with the session cookie rather than a bearer key, and — because a cookie rides along on its own — it must echo this session's CSRF token in a header. Read the token first, then mint.

    GET /api/v2/account/csrf-token
    curl -fsS https://pfmem-api.packagefactory.dk/api/v2/account/csrf-token \
      -b cookies.txt
    POST /api/v2/account/master-key
    curl -fsS -X POST https://pfmem-api.packagefactory.dk/api/v2/account/master-key \
      -H 'content-type: application/json' \
      -H 'x-csrf-token: <the token from the call above>' \
      -b cookies.txt \
      -d '{"label":"bootstrap master key","account_name":"Acme"}'
    200 OK
    {
      "request_id": "23ba77f0c6474ccb8dc5d65d1a44d069",
      "data": {
        "key": {
          "key_id": "key_01HQ",
          "tier": "master",
          "label": "bootstrap master key",
          "prefix": "mk_live_",
          "tenant_slug": null,
          "role": null,
          "verbs": ["read", "write", "delete"],
          "created_at": 1755640000000
        },
        "secret": "mk_live_<shown here and nowhere else, ever>"
      }
    }

    Calling this a second time is refused, because an account holds one first master key and there is nothing to re-issue. If the secret is gone, the way back is rotation, not a retry.

  3. Create a workspace

    A workspace is a boundary around one set of memories — one per customer, per environment, or per project. Creating one gives it a private space of its own, with nothing else in it and nothing shared with any other workspace. The slug is yours to choose within the allowed shape; the internal name it resolves to is derived from it and returned to you, never sent.

    POST /api/v2/admin/tenants
    curl -fsS -X POST https://pfmem-api.packagefactory.dk/api/v2/admin/tenants \
      -H 'authorization: Bearer mk_live_...' \
      -H 'content-type: application/json' \
      -d '{"slug":"acme_prod","display_name":"Acme (production)",
          "active_embedding_version":"external_v1"}'

    Authenticate with the master key as a bearer token from here on. A slug is lowercase letters, digits and underscores, up to forty characters — a hyphen is a validation error, not a slug with a hyphen in it. The embedding version names an entry that already exists in the registry; it is not a label you invent per workspace.

  4. Mint a key scoped to that workspace

    Master keys are for administration. Day-to-day traffic should carry the narrowest credential that still works, so mint a workspace-scoped key and give that to the client. The tier and the slug in the body below are what narrow it.

    POST /api/v2/admin/keys
    curl -fsS -X POST https://pfmem-api.packagefactory.dk/api/v2/admin/keys \
      -H 'authorization: Bearer mk_live_...' \
      -H 'content-type: application/json' \
      -d '{"tier":"tenant","label":"acme_prod client","tenant_slug":"acme_prod"}'

    The request body is validated strictly: an unrecognised field is a validation error rather than a silently ignored one, so a misspelt name fails loudly instead of widening nothing.

  5. Add your model keys

    MemoryAgent runs on your own models: one reads conversations and pulls the durable facts out (extraction), one makes them searchable (embeddings). Nothing is extracted or found until both are added. Saved without a workspace, as below, a key serves every workspace, including ones created later. You can also add them on the Model keys page.

    POST /api/v2/admin/credentials
    curl -fsS -X POST https://pfmem-api.packagefactory.dk/api/v2/admin/credentials \
      -H 'authorization: Bearer mk_live_...' \
      -H 'content-type: application/json' \
      -d '{"provider":"deepseek","api_key":"sk-...","uses":[{"purpose":"extraction","model":"deepseek-v4-flash"}]}'
    
    curl -fsS -X POST https://pfmem-api.packagefactory.dk/api/v2/admin/credentials \
      -H 'authorization: Bearer mk_live_...' \
      -H 'content-type: application/json' \
      -d '{"provider":"cohere","api_key":"...","uses":[{"purpose":"embedding","model":"embed-v5.0-fast","dimension":1536}]}'

    Each key is checked before it is stored: an extraction key with a one-token call, an embedding key by embedding one short text at the size you name. A model the provider does not serve, or a size it does not produce, is refused. Any OpenAI-compatible endpoint works, with a bearer token, a custom header, basic auth or OAuth client credentials.

  6. Install the client and store the key

    Pipe the key in on standard input rather than passing it as an argument. An argument shows up in the process list for every user on the machine and is written to your shell history; standard input is neither.

    pfmem init
    curl -fsSL https://pfmem-api.packagefactory.dk/install.sh | sh
    cat key.txt | pfmem init

    The stored config file is created with owner-only permissions, and the key it holds is only ever sent to an https origin or to loopback.

  7. Store a conversation

    Ingestion is asynchronous. The call returns as soon as the messages are durable and hands back a task id; extraction into searchable memories happens behind it. Nothing is searchable the instant this returns, which is what the next step is for.

    pfmem add
    pfmem add --mode chat --user-id u_42 --session-id s_1 \
      --idempotency-key batch-2026-08-18-01 \
      --messages '[{"role":"user","content":"I moved to Berlin"}]'

    The idempotency key makes a retry after a timeout safe: a replay returns the first result instead of ingesting the same batch twice.

  8. Read it back

    Force the pending extraction rather than waiting for the background pipeline, then ask a question. The flush blocks for as long as you allow it; the search matches your question against what the workspace holds — by meaning as well as by exact wording — and returns a ranked set of facts, not a transcript.

    pfmem flush · pfmem search
    pfmem flush --session-id s_1 --wait-ms 10000
    pfmem search --memory-type user --user-id u_42 --query "where does he live now?"

    Use the ranked search when you have a question. Use the plain listing when you want everything held about a subject, in a stable order, with real pagination.

    pfmem get
    pfmem get --memory-type user --user-id u_42 --limit 50

    Read that back: content nobody has approved arrives with a marker in front of it, and anything withheld from this key is reported as a count instead of a result. Approving happens in the dashboard, under Review.

Where to go from here

The command reference covers every flag, the exit codes and the log file. The API reference covers the envelope, the status codes and the endpoints these commands call. Both cover the procedures a workspace can keep beside its memories.