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.
| Subject | Use it for | For example |
|---|---|---|
memory_type: user | What 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: agent | What 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.
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/emailcurl -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/emailcurl -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.
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-tokencurl -fsS https://pfmem-api.packagefactory.dk/api/v2/account/csrf-token \ -b cookies.txtPOST /api/v2/account/master-keycurl -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"}'The secret is shown exactly once
The response carries the key's secret. It is hashed server-side the moment it is issued and cannot be retrieved again, by us or by you. Save it in a password manager before you close the terminal.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.
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/tenantscurl -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.
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/keyscurl -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.
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/credentialscurl -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.
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 initcurl -fsSL https://pfmem-api.packagefactory.dk/install.sh | sh cat key.txt | pfmem initThe 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.
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 addpfmem 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.
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 searchpfmem 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 getpfmem get --memory-type user --user-id u_42 --limit 50Read 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.