Developer Guide
Build persistent, searchable, encrypted storage into your own projects with the JauMemory REST API
Introduction
Beyond the MCP tools your AI assistant uses, JauMemory exposes a REST API you can call from any backend, script, or service. Use it as instant persistent storage for your own projects: store JSON-tagged notes, events, and documents, and get hybrid keyword + semantic search back without running a database.
- Zero infrastructure: no database to host, no schema migrations. POST content, search it back.
- Automatic classification: memories are typed (error / solution / insight / question / context / task) and importance-scored server-side if you don't set those fields.
- Real search: keyword, semantic, or hybrid, computed on JauMemory's own servers. Your content never touches a third-party AI API.
- Shared with your AI: everything your app stores over REST is the same memory pool your connected AI assistants read via MCP, and vice versa.
Building an AI integration instead?
If your goal is to give an AI assistant memory (rather than your own code), skip REST and use the MCP server: npx -y @jaumemory/mcp-server. See the documentation. This page is for calling JauMemory directly from your own software.
Base URLs
https://mem.jau.app/v1 # REST API (this page)
mem.jau.app:50051 # gRPC over TLS (used by the MCP server)
All REST endpoints below are relative to https://mem.jau.app/v1. All traffic is HTTPS.
Authentication: Bearer Tokens
The memory endpoints authenticate with a JWT access token in the Authorization header. You get one by logging in with your account credentials (accounts are invite-only during the beta · request an invite):
POST /v1/auth/login
Content-Type: application/json
{
"email": "you@example.com",
"password": "your-password",
"mfa_code": "123456" // only if you enabled TOTP 2FA
}
Response (fields abridged):
{
"access_token": "eyJhbGciOi...",
"refresh_token": "…",
"token_type": "Bearer",
"expires_in": 3600,
"requires_mfa": false,
"username": "you"
}
Then send the token on every request:
Authorization: Bearer eyJhbGciOi...
Access tokens are short-lived. Exchange the refresh token at POST /v1/auth/refresh when you get a 401, rather than storing the password. Treat both tokens as secrets: server-side code only, never in client-side JavaScript shipped to browsers you don't control.
Authentication: API Keys
JauMemory also has account-scoped API keys, managed over REST:
| Method | Path | Description |
|---|---|---|
| POST | /v1/api-keys | Create a key. Body: { "name": "my-app", "permissions": ["memories:read:own", "memories:write:own"], "expires_in_days": 90 } (expiry optional). The response includes the full key once; only a prefix is stored after that. |
| GET | /v1/api-keys | List your keys (name, 8-char prefix, permissions, expiry, last used · never the full key). |
| DELETE | /v1/api-keys/:id | Revoke a key immediately. |
Keys look like jm_ followed by 32 random characters, are hashed with Argon2id server-side (the plaintext is never stored), and are sent in an X-API-Key header.
Current limitation: memory endpoints require a Bearer token
Today, requests that carry only X-API-Key pass authentication but are rejected by the memory endpoints themselves, so the store/search loop on this page works with Bearer JWTs only. Create and revoke API keys freely, but don't build the memory loop on them until this note disappears from the docs. Use the login + refresh flow above.
Memory Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /v1/memories | List memories, newest first. Query params: limit (1-1000, default 20), offset (0-10000), memory_type, tag, start_date / end_date (Unix seconds). Returns { data, total, limit, offset }. |
| POST | /v1/memories | Create a memory. Returns 201 with the stored memory, including its server-assigned type and importance. |
| GET | /v1/memories/:id | Fetch one memory by UUID. |
| PATCH | /v1/memories/:id | Partial update: any subset of content, context, memory_type, importance, tags, metadata. |
| DELETE | /v1/memories/:id | Soft delete. Deleted memories sit in a trash you can inspect (GET /v1/memories/trash) and restore (POST /v1/memories/trash/restore) before permanent removal. |
| POST | /v1/memories/search | Search (see below). Returns an array of memory objects. |
| GET | /v1/memories/stats | Counts and distributions for your memory store. |
Consolidation (merging similar memories into insights) is not available over REST; it runs through the MCP consolidate tool.
The Memory Object
Create request fields:
{
"content": "string", // required, 1 to 50,000 characters
"context": "string", // optional free-text context
"memory_type": "task", // optional: error | solution | insight | question | context | task
// (auto-classified from content if omitted)
"importance": 0.8, // optional 0.0-1.0 (auto-scored if omitted)
"tags": ["project-x"], // optional, up to 20 tags
"metadata": { "any": "json" } // optional arbitrary JSON
}
What you get back everywhere:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"content": "…",
"context": "…",
"importance": 0.8,
"memory_type": "task",
"tags": ["project-x"],
"metadata": { "any": "json" },
"created_at": "2026-07-06T18:30:00Z",
"updated_at": "2026-07-06T18:30:00Z"
}
Search
POST /v1/memories/search
Content-Type: application/json
{
"query": "database timeout", // optional: omit for a filters-only search
"mode": "hybrid", // keyword | semantic | hybrid
"limit": 10, // 1-100
"min_similarity": 0.5, // optional semantic threshold, 0.0-1.0
"filters": { // all optional
"memory_types": ["error", "solution"],
"tags": ["project-x"],
"importance_min": 0.5,
"importance_max": 1.0,
"date_range": {
"start": "2026-06-01T00:00:00Z",
"end": "2026-07-01T00:00:00Z"
}
}
}
The response is a plain array of memory objects, best matches first. (Relevance scores and matched-term details are exposed on the gRPC/MCP surface only; the HTTP contract stays thin.)
Credential Vault
Store an API key once, encrypted; your tools, skills, and agents then use it by reference without ever seeing the plaintext again. The vault is what lets a scheduled skill call a third-party API on your behalf while the secret never appears in a chat, a log, or a response body.
| Method | Path | Description |
|---|---|---|
| GET | /v1/credentials | List credentials (masked values only). Query params: limit, offset, provider, credential_type, active_only. |
| POST | /v1/credentials | Store a credential. value is write-only: accepted here, never returned by any endpoint. |
| GET | /v1/credentials/presets | Provider presets (correct auth header/prefix for common providers). |
| GET | /v1/credentials/expiring | Credentials nearing their expiry or rotation reminder. |
| GET | /v1/credentials/:id | One credential, masked. |
| PUT | /v1/credentials/:id | Update metadata (name, description, expiry, scopes...). |
| DELETE | /v1/credentials/:id | Delete a credential. |
| POST | /v1/credentials/:id/rotate | Replace the secret in place: { "new_value": "..." }. The id stays stable, so nothing downstream breaks. |
| POST | /v1/credentials/:id/verify | Test the credential against its provider. |
| GET | /v1/credentials/:id/logs | Access log: when and by what the credential was used. |
Every read returns a masked_value (plus metadata like provider, expiry, last rotation, and scopes); there is no endpoint that returns the plaintext. Same auth constraint as the memory endpoints: these handlers currently accept Bearer JWTs only, so use the login + refresh flow, not X-API-Key.
# Store (value is write-only; the response echoes masked metadata only)
curl -s https://mem.jau.app/v1/credentials \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "openai-prod",
"provider": "openai",
"credential_type": "api_key",
"value": "sk-..."
}'
# → 201 { "id": "...", "name": "openai-prod", "masked_value": "sk-****...", ... }
# Rotate in place when the key changes
curl -s https://mem.jau.app/v1/credentials/CRED_ID/rotate \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "new_value": "sk-newkey..." }'
Know the trade-off
Vault encryption is server-side with your per-user key, not end-to-end; that design is exactly what enables reference-based injection and cross-device use. The full honest picture, including mitigations and when not to use the vault, is in the vault documentation.
Example: curl
# 1. Log in and capture a token (requires jq)
TOKEN=$(curl -s https://mem.jau.app/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"you@example.com","password":"your-password"}' \
| jq -r .access_token)
# 2. Store a memory
curl -s https://mem.jau.app/v1/memories \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"content": "Customer #4521 prefers email contact, timezone UTC+2",
"tags": ["crm", "customer-4521"]
}'
# 3. Search it back
curl -s https://mem.jau.app/v1/memories/search \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"query": "how does customer 4521 want to be contacted", "mode": "hybrid", "limit": 5}'
Example: JavaScript (fetch)
A minimal server-side client (Node 18+, no dependencies):
const BASE = "https://mem.jau.app/v1";
async function login(email, password) {
const res = await fetch(`${BASE}/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password })
});
if (!res.ok) throw new Error(`Login failed: ${res.status}`);
const { access_token } = await res.json();
return access_token;
}
async function remember(token, content, tags = []) {
const res = await fetch(`${BASE}/memories`, {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ content, tags })
});
if (!res.ok) throw new Error((await res.json()).error?.message);
return res.json(); // the stored memory, with id + auto type/importance
}
async function search(token, query, limit = 5) {
const res = await fetch(`${BASE}/memories/search`, {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ query, mode: "hybrid", limit })
});
if (!res.ok) throw new Error((await res.json()).error?.message);
return res.json(); // array of memories, best match first
}
// The core loop
const token = await login(process.env.JM_EMAIL, process.env.JM_PASSWORD);
await remember(token, "Deploy checklist: run migrations before restarting workers", ["ops"]);
const hits = await search(token, "what do I do before restarting workers");
console.log(hits[0].content);
Example: Python (requests)
import os
import requests
BASE = "https://mem.jau.app/v1"
# 1. Log in
resp = requests.post(f"{BASE}/auth/login", json={
"email": os.environ["JM_EMAIL"],
"password": os.environ["JM_PASSWORD"],
})
resp.raise_for_status()
headers = {"Authorization": f"Bearer {resp.json()['access_token']}"}
# 2. Store a memory
memory = requests.post(f"{BASE}/memories", headers=headers, json={
"content": "Sensor batch B-17 calibrated 2026-07-06, drift within 0.2%",
"tags": ["lab", "batch-b17"],
}).json()
print("stored:", memory["id"], "type:", memory["memory_type"])
# 3. Search it back
hits = requests.post(f"{BASE}/memories/search", headers=headers, json={
"query": "when was batch B-17 last calibrated",
"mode": "hybrid",
"limit": 5,
}).json()
print("found:", hits[0]["content"])
Errors
Errors come back as JSON with a stable machine-readable code:
{
"error": {
"code": "RES001",
"message": "Memory not found"
}
}
Validation failures (VAL002) add a details array of { "field", "message" } objects so you can surface per-field errors.
| HTTP | Code | Meaning |
|---|---|---|
| 401 | AUTH001 | Missing/invalid/expired token · refresh or log in again |
| 403 | AUTH003 | Authenticated but not permitted for this resource |
| 404 | RES001 | Resource not found (or not yours · IDs are scoped to your account) |
| 400 | VAL001 / VAL002 | Bad request / field validation failed (see details) |
| 429 | RATE001 | Rate limit exceeded · back off and retry |
| 500 | DB001 / INT001 | Server-side failure · safe to retry with backoff |
Rate Limits
Limits are enforced per account, by plan. All plans are free during the beta.
| Plan | Requests/minute | Memories/month |
|---|---|---|
| Hobbyist | 15 | 1,500 |
| Pro | 120 | 20,000 |
Hitting a limit returns 429. Design for it: batch writes where you can, cache reads where freshness allows, and use exponential backoff on retry.
Security Model
- Encrypted at rest with per-user keys: every memory is encrypted with your account's own AES-256-GCM key before it hits the database. Encryption and decryption happen server-side and transparently; the API speaks plaintext JSON to you over TLS.
- TLS in transit: HTTPS for REST, TLS for gRPC.
- Row-level isolation: PostgreSQL row-level security scopes every query to your user. Other users' IDs 404 for you.
- Hardened auth: Argon2id password and API-key hashing, revocable per-device sessions, optional TOTP 2FA, audit logging on sensitive operations.
- Your data stays yours: GDPR export and account deletion are built in; nothing you store trains AI models.
To be precise about what this is not: it is server-side encryption, not end-to-end encryption. The server holds the per-user keys so it can search and classify your content for you.