Parcle Personal Brain
API

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 a pmem_... API key.
  • parcle-sync: a separate repo for local sync workflows — do not install it through pip install parcle.

Authentication#

Every request requires a pmem_... API key passed as a Bearer token.

HTTP header
Authorization: Bearer pmem_...
Get an API key

Sign in at hackathon.parcle.ai, create a Developer Memory API key, and export it as PARCLE_API_KEY.

Quickstart (Python SDK)#

Install
pip install parcle
export PARCLE_API_KEY="pmem_..."
Python
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.

Setup
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.

FieldTypeRequiredDescription
user_idstringNoYour own user identifier. Auto-generated if omitted.
namestringNoDisplay name (for your own readability).
timezonestringNoIANA timezone (e.g. "America/New_York"). Used to interpret relative time in searches. Defaults to "UTC".
curl — POST /v1/users
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"
  }'
Python
user = client.create_user(user_id="ada", name="Ada Lovelace", timezone="UTC")
print(user.user_id, user.is_new)  # "ada" True
Response
{
  "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.

FieldTypeRequiredDescription
user_idstringYes
messagesarrayYesEach: {"role": "user"|"assistant", "content": "..."}. Optional: speaker, updated_at.
session_idstringNoOmit to start a new session; pass a previous value to append.
tagobjectNoKey-value metadata. Applied only when creating a new session.
curl — POST /v1/memories/ingest_dialog
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"}
  }'
Python
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"
Response
{"session_id": "ses_abc123", "event_id": "evt_xyz789"}

Append to an existing session by passing session_id:

curl — append
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."}
    ]
  }'
Python — append
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.

FieldTypeRequiredDescription
user_idform fieldYes
filefileYesThe file to upload. Max 10 MB.
updated_atform fieldNoISO 8601 timestamp for the file's content date.
tagform fieldNoJSON-encoded key-value metadata.
curl — POST /v1/memories/ingest_files
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"}'
Python
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"
Response
{"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.

Python SDK

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 — POST /v1/memories/events
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"}'
Python
event = client.get_event(user_id="ada", event_id="evt_xyz789")
print(event.status)     # "ready"
print(event.is_ready)   # True
Response
{"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.

FieldTypeRequiredDescription
user_idstringYes
querystringYesNatural-language question.
tag_filterobjectNoOnly search sources matching these tags.
timezonestringNoOverride the user's default timezone for this query.
curl — POST /v1/memories/search
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"}
  }'
Python
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_...')]
SSE response
: 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.

FieldTypeRequiredDescription
user_idstringYes
typestringNo"session" or "file". Omit for both.
tag_filterobjectNoFilter by source tags.
pageintegerNoPage number (default 1).
limitintegerNoResults per page (default 50).
orderstringNo"desc" (default) or "asc".
curl — POST /v1/memories/sources
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"}
  }'
Python
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)
Response
{
  "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 — POST /v1/memories/sessions
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"}'
Python
session = client.get_session(user_id="ada", session_id="ses_abc123")
for msg in session.messages:
    print(msg.role, msg.content)
Response
{
  "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 — DELETE /v1/memories/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"}'
Python
client.delete_by_session(user_id="ada", session_id="ses_abc123")

Delete by file

curl — DELETE /v1/memories/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"}'
Python
client.delete_by_file(user_id="ada", file_id="file_abc123")

Delete by tag

curl — DELETE /v1/memories/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"}}'
Python
client.delete_by_tag(user_id="ada", tag_filter={"app": "diet-tracker"})
Delete response
{"deleted": true, "deleted_count": 1}

API behavior#

ConceptDetails
AuthenticationEvery request must include Authorization: Bearer <PARCLE_API_KEY>. Keys start with pmem_.
User isolationuser_id is the memory isolation boundary. Different user_id values do not share memory.
SessionsOmit session_id to create a new session; pass one to append turns. tag is applied only when creating a new session.
File uploadsMax 10 MB. Uses multipart form data. Supports PDF, Markdown, plain text, DOCX, PPTX, XLSX, and more.
IngestionContent 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 filteringUse tag on ingestion to label data, and tag_filter on search, list, and delete to scope operations within a user's memory.
RetriesThe 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 response
{
  "error": {
    "code": "invalid_request",
    "message": "A human-readable error message.",
    "request_id": "req_..."
  }
}
StatusCodeMeaning
400invalid_requestMissing or invalid fields.
401unauthorizedInvalid or missing API key.
413file_too_largeFile exceeds 10 MB limit.
415unsupported_file_typeFile type not supported (e.g. .db, .sqlite).
422validation_failedRequest body failed validation.
429Rate limited. Retry after Retry-After header.
500Server error. Safe to retry with backoff.
503unavailableService unavailable. Safe to retry with backoff.

Endpoint summary#

MethodEndpointDescription
POST/v1/usersCreate or update a user
POST/v1/memories/ingest_dialogIngest conversation messages
POST/v1/memories/ingest_filesUpload a file (multipart)
POST/v1/memories/eventsCheck ingestion status
POST/v1/memories/searchSearch memory (SSE stream)
POST/v1/memories/sourcesList ingested sources
POST/v1/memories/sessionsGet session messages
DELETE/v1/memories/by_sessionDelete by session
DELETE/v1/memories/by_fileDelete by file
DELETE/v1/memories/by_tagDelete by tag filter
๐Ÿ”„Agent Sync

Connect every agent and device you use to this one memory.

Parcle Personal Brain ยท Documentation