Memory API
Long-term memory for AI agents. Ingest conversations and files into per-user memory, then ask questions in natural language and get cited answers back.
The Memory API gives every user a private, persistent memory. Scope everything to a user_id, ingest chat transcripts and files into the same store, then search and get a synthesized answer with citations — not just raw chunks.
parcle(PyPI): the Python SDK for the Memory API, used with apmem_...API key.parcle-sync: a separate repo for local sync workflows — do not install it throughpip install parcle.
Authentication#
Every request requires a pmem_... API key passed as a Bearer token.
Authorization: Bearer pmem_...Sign in at hackathon.parcle.ai, create a Developer Memory API key, and export it as PARCLE_API_KEY.
Quickstart (Python SDK)#
pip install parcle
export PARCLE_API_KEY="pmem_..."from parcle import Parcle
# Reads PARCLE_API_KEY from the environment if api_key is omitted.
client = Parcle(api_key="pmem_...")
# 1. Create the user you'll be storing memory for.
# Call this once per user before ingesting.
client.create_user(user_id="ada", name="Ada Lovelace", timezone="UTC")
# 2. Write a conversation into memory.
# Omit session_id to start a new session; pass one to append.
dialog = client.ingest_dialog(
user_id="ada",
messages=[
{"role": "user", "content": "I'm allergic to peanuts."},
{"role": "assistant", "content": "Got it -- I'll avoid peanuts in suggestions."},
],
tag={"app": "diet-tracker"},
)
# Append more turns to the same session.
client.ingest_dialog(
user_id="ada",
session_id=dialog.session_id,
messages=[
{"role": "user", "content": "Also, I don't eat shellfish."},
],
)
# 3. Ingest a file (PDF, Markdown, text, DOCX, PPTX, XLSX, ...).
file_result = client.ingest_file(
user_id="ada",
file="diet-notes.pdf",
tag={"app": "diet-tracker", "source": "file"},
)
# Ingestion waits until content is searchable by default.
# Pass wait=False to return immediately, then call
# client.wait_until_ready(user_id, event_id) yourself.
# 4. Ask a question -- get an answer with confidence and citations.
result = client.search(
user_id="ada",
query="What food should I avoid?",
tag_filter={"app": "diet-tracker"},
)
print(result.answer) # "You're allergic to peanuts and don't eat shellfish."
print(result.confidence) # 0.92
print(result.citations) # [Citation(type='session', id='ses_...')]
# 5. Delete memory when needed.
client.delete_by_session(user_id="ada", session_id=dialog.session_id)
client.delete_by_file(user_id="ada", file_id=file_result.file_id)REST API Reference#
Base URL: https://api.parcle.ai. All endpoints accept and return JSON except file uploads (multipart form data). Search returns a Server-Sent Events stream.
export PARCLE_API_KEY="pmem_..."
export PARCLE_BASE_URL="https://api.parcle.ai"Create a user#
Create a user namespace. Call once per user before ingesting any data. Omit user_id to have one auto-generated.
| Field | Type | Required | Description |
|---|---|---|---|
user_id | string | No | Your own user identifier. Auto-generated if omitted. |
name | string | No | Display name (for your own readability). |
timezone | string | No | IANA timezone (e.g. "America/New_York"). Used to interpret relative time in searches. Defaults to "UTC". |
curl -X POST "$PARCLE_BASE_URL/v1/users" \
-H "Authorization: Bearer $PARCLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"user_id": "ada",
"name": "Ada Lovelace",
"timezone": "UTC"
}'user = client.create_user(user_id="ada", name="Ada Lovelace", timezone="UTC")
print(user.user_id, user.is_new) # "ada" True{
"user_id": "ada",
"name": "Ada Lovelace",
"timezone": "UTC",
"is_new": true
}Ingest dialog#
Append conversation messages to a user's memory. Omit session_id to start a new session; pass one to append turns.
| Field | Type | Required | Description |
|---|---|---|---|
user_id | string | Yes | |
messages | array | Yes | Each: {"role": "user"|"assistant", "content": "..."}. Optional: speaker, updated_at. |
session_id | string | No | Omit to start a new session; pass a previous value to append. |
tag | object | No | Key-value metadata. Applied only when creating a new session. |
curl -X POST "$PARCLE_BASE_URL/v1/memories/ingest_dialog" \
-H "Authorization: Bearer $PARCLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"user_id": "ada",
"messages": [
{"role": "user", "content": "I'\''m allergic to peanuts."},
{"role": "assistant", "content": "Got it -- I'\''ll avoid peanuts."}
],
"tag": {"app": "diet-tracker"}
}'dialog = client.ingest_dialog(
user_id="ada",
messages=[
{"role": "user", "content": "I'm allergic to peanuts."},
{"role": "assistant", "content": "Got it -- I'll avoid peanuts."},
],
tag={"app": "diet-tracker"},
)
print(dialog.session_id) # "ses_abc123"{"session_id": "ses_abc123", "event_id": "evt_xyz789"}Append to an existing session by passing session_id:
curl -X POST "$PARCLE_BASE_URL/v1/memories/ingest_dialog" \
-H "Authorization: Bearer $PARCLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"user_id": "ada",
"session_id": "ses_abc123",
"messages": [
{"role": "user", "content": "Also, I don'\''t eat shellfish."}
]
}'client.ingest_dialog(
user_id="ada",
session_id=dialog.session_id,
messages=[{"role": "user", "content": "Also, I don't eat shellfish."}],
)Ingest file#
Upload a file (PDF, Markdown, text, DOCX, PPTX, XLSX, …) into a user's memory. Uses multipart/form-data, not JSON. Max 10 MB.
| Field | Type | Required | Description |
|---|---|---|---|
user_id | form field | Yes | |
file | file | Yes | The file to upload. Max 10 MB. |
updated_at | form field | No | ISO 8601 timestamp for the file's content date. |
tag | form field | No | JSON-encoded key-value metadata. |
curl -X POST "$PARCLE_BASE_URL/v1/memories/ingest_files" \
-H "Authorization: Bearer $PARCLE_API_KEY" \
-F "user_id=ada" \
-F "file=@diet-notes.pdf" \
-F 'tag={"app":"diet-tracker","source":"file"}'file_result = client.ingest_file(
user_id="ada",
file="diet-notes.pdf",
tag={"app": "diet-tracker", "source": "file"},
)
print(file_result.file_id) # "file_abc123"{"file_id": "file_abc123", "event_id": "evt_xyz789"}Check ingestion status#
Poll after ingestion to know when content is searchable. status is one of: "pending", "processing", "ready", "failed". Poll until terminal ("ready" or "failed"). A 2-second interval is recommended.
The Python SDK waits automatically by default (wait=True). Pass wait=False to return immediately, then call client.wait_until_ready(user_id, event_id) yourself.
curl -X POST "$PARCLE_BASE_URL/v1/memories/events" \
-H "Authorization: Bearer $PARCLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"user_id": "ada", "event_id": "evt_xyz789"}'event = client.get_event(user_id="ada", event_id="evt_xyz789")
print(event.status) # "ready"
print(event.is_ready) # True{"event_id": "evt_xyz789", "status": "ready", "error": null}Search memory#
Ask a natural-language question over a user's memory. Returns a synthesized answer with a confidence score (0–1) and citations, not raw chunks.
The response is delivered as a Server-Sent Events (SSE) stream. The server emits keepalive comments (: keepalive) until the answer is ready, then a final event with the result. The Python SDK handles this transparently and returns a single SearchResult.
| Field | Type | Required | Description |
|---|---|---|---|
user_id | string | Yes | |
query | string | Yes | Natural-language question. |
tag_filter | object | No | Only search sources matching these tags. |
timezone | string | No | Override the user's default timezone for this query. |
curl -N -X POST "$PARCLE_BASE_URL/v1/memories/search" \
-H "Authorization: Bearer $PARCLE_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{
"user_id": "ada",
"query": "What food should I avoid?",
"tag_filter": {"app": "diet-tracker"}
}'result = client.search(
user_id="ada",
query="What food should I avoid?",
tag_filter={"app": "diet-tracker"},
)
print(result.answer) # "You're allergic to peanuts and don't eat shellfish."
print(result.confidence) # 0.92
print(result.citations) # [Citation(type='session', id='ses_...')]: keepalive
: keepalive
event: final
data: {"answer": "You're allergic to peanuts.", "confidence": 0.92, "citations": [{"type": "session", "id": "ses_abc123"}]}answer: a natural-language answer grounded in the user's memory.confidence: how strongly the answer is supported (0–1).citations:[{"type": "session"|"file", "id": "..."}]— the exact sources the answer drew on.
List sources#
List a user's ingested dialog sessions and files, with pagination.
| Field | Type | Required | Description |
|---|---|---|---|
user_id | string | Yes | |
type | string | No | "session" or "file". Omit for both. |
tag_filter | object | No | Filter by source tags. |
page | integer | No | Page number (default 1). |
limit | integer | No | Results per page (default 50). |
order | string | No | "desc" (default) or "asc". |
curl -X POST "$PARCLE_BASE_URL/v1/memories/sources" \
-H "Authorization: Bearer $PARCLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"user_id": "ada",
"type": "session",
"tag_filter": {"app": "diet-tracker"}
}'page = client.list_sources(user_id="ada", type="session")
for source in page.sources:
print(source.id, source.type, source.tag)
# Or iterate across all pages automatically:
for source in client.iter_sources(user_id="ada"):
print(source.id){
"sources": [
{
"id": "ses_abc123",
"type": "session",
"updated_at": "2025-06-20T12:00:00Z",
"tag": {"app": "diet-tracker"}
}
],
"page": 1,
"total_pages": 1,
"total": 1
}Get session#
Retrieve the original messages from a dialog session.
curl -X POST "$PARCLE_BASE_URL/v1/memories/sessions" \
-H "Authorization: Bearer $PARCLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"user_id": "ada", "session_id": "ses_abc123"}'session = client.get_session(user_id="ada", session_id="ses_abc123")
for msg in session.messages:
print(msg.role, msg.content){
"session_id": "ses_abc123",
"messages": [
{"role": "user", "content": "I'm allergic to peanuts."},
{"role": "assistant", "content": "Got it -- I'll avoid peanuts."}
],
"tag": {"app": "diet-tracker"}
}Delete memory#
Permanently delete memory by session, file, or tag. Removal is irreversible; to re-add content, ingest it again.
Delete by session
curl -X DELETE "$PARCLE_BASE_URL/v1/memories/by_session" \
-H "Authorization: Bearer $PARCLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"user_id": "ada", "session_id": "ses_abc123"}'client.delete_by_session(user_id="ada", session_id="ses_abc123")Delete by file
curl -X DELETE "$PARCLE_BASE_URL/v1/memories/by_file" \
-H "Authorization: Bearer $PARCLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"user_id": "ada", "file_id": "file_abc123"}'client.delete_by_file(user_id="ada", file_id="file_abc123")Delete by tag
curl -X DELETE "$PARCLE_BASE_URL/v1/memories/by_tag" \
-H "Authorization: Bearer $PARCLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"user_id": "ada", "tag_filter": {"app": "diet-tracker"}}'client.delete_by_tag(user_id="ada", tag_filter={"app": "diet-tracker"}){"deleted": true, "deleted_count": 1}API behavior#
| Concept | Details |
|---|---|
| Authentication | Every request must include Authorization: Bearer <PARCLE_API_KEY>. Keys start with pmem_. |
| User isolation | user_id is the memory isolation boundary. Different user_id values do not share memory. |
| Sessions | Omit session_id to create a new session; pass one to append turns. tag is applied only when creating a new session. |
| File uploads | Max 10 MB. Uses multipart form data. Supports PDF, Markdown, plain text, DOCX, PPTX, XLSX, and more. |
| Ingestion | Content is processed in the background. The Python SDK waits until content is searchable by default (wait=True). With curl, poll POST /v1/memories/events every 2 seconds until status is "ready". |
| Search (SSE) | Search is delivered as a Server-Sent Events stream. The server emits : keepalive comments until the answer is ready, then a final event. Set Accept: text/event-stream and use curl -N to disable buffering. |
| Tag filtering | Use tag on ingestion to label data, and tag_filter on search, list, and delete to scope operations within a user's memory. |
| Retries | The Python SDK automatically retries safe read requests on 429, 500, and 503 with exponential backoff. Writes, deletes, and search do not retry. |
Errors#
All errors return a JSON body:
{
"error": {
"code": "invalid_request",
"message": "A human-readable error message.",
"request_id": "req_..."
}
}| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | Missing or invalid fields. |
| 401 | unauthorized | Invalid or missing API key. |
| 413 | file_too_large | File exceeds 10 MB limit. |
| 415 | unsupported_file_type | File type not supported (e.g. .db, .sqlite). |
| 422 | validation_failed | Request body failed validation. |
| 429 | Rate limited. Retry after Retry-After header. | |
| 500 | Server error. Safe to retry with backoff. | |
| 503 | unavailable | Service unavailable. Safe to retry with backoff. |
Endpoint summary#
| Method | Endpoint | Description |
|---|---|---|
POST | /v1/users | Create or update a user |
POST | /v1/memories/ingest_dialog | Ingest conversation messages |
POST | /v1/memories/ingest_files | Upload a file (multipart) |
POST | /v1/memories/events | Check ingestion status |
POST | /v1/memories/search | Search memory (SSE stream) |
POST | /v1/memories/sources | List ingested sources |
POST | /v1/memories/sessions | Get session messages |
DELETE | /v1/memories/by_session | Delete by session |
DELETE | /v1/memories/by_file | Delete by file |
DELETE | /v1/memories/by_tag | Delete by tag filter |
Connect every agent and device you use to this one memory.
