Tex Python SDK
SuperMemory-compatible memory SDK for Python.
Installation
pip install tex-sdk
Optional HTTP/2 support:
pip install "tex-sdk[http2]"
Requires Python 3.9+.
Quick Start
API key
from tex import Tex
client = Tex(api_key="sk-...", base_url="http://localhost:8000")
client.add("The quick brown fox jumps over the lazy dog.")
results = client.search.documents("fox")
Environment variables
Set TEX_API_KEY and TEX_BASE_URL, then:
from tex import Tex
client = Tex()
Per-user clients (production)
To act as one end user of your app, pass that user's id alongside your API key.
The SDK makes a single POST /auth/token-exchange with
{api_key, user_id, session_id} and receives a JWT minted for that user:
import os
from tex import Tex
# base_url falls back to TEX_BASE_URL, api_key to TEX_API_KEY
alice = Tex(
api_key=os.environ["TEX_API_KEY"],
base_url=os.environ["TEX_BASE_URL"],
org_id="org1", # optional; if set, must match the key's org
user_id="alice",
session_id="chat-42", # optional
)
alice.recall(q="what did I order last week?", session_id="chat-42") # every call is scoped to alice
- The API key must carry an impersonation-capable scope, normally
impersonate_user(also accepted:tenant:write:any_user,agent,service,system,*). Without one the backend refuses theuser_idand the SDK raisesPermissionDeniedErrorsaying so. - The minted token carries an
actclaim naming the key: it is locked to that user and cannot impersonate anyone else. The SDK also checks the returned token is really for the requested user. - On expiry the SDK uses the refresh token, and otherwise re-exchanges for the
same
user_id. - The per-user token is sent on every call, including the
Supermemory-compatible ones (
memories.add,documents.add,search.memories,profile, ...), so they also act as that user with that user's team scopes.container_tagon such a client must be omitted or equal the user id; naming another user is refused with 403container_tag_not_permitted. (Requires a backend that accepts per-user tokens on/v3and/v4.) - Omit
user_idto act as the key's own identity; compat calls then send the raw API key andcontainer_tagselects the end user, Supermemory-style.
Org + user login (development servers only)
from tex import Tex
client = Tex(base_url="http://localhost:8000", org_id="org1", user_id="user1")
With no API key the SDK calls POST /auth/login, which exists only when the
backend runs with DEBUG=true. Production returns 403 and the SDK raises
PermissionDeniedError pointing you to the per-user API key pattern above.
Documents
Add, retrieve, update, delete, list, and batch-manage documents.
# Top-level shortcut
resp = client.add("Document content", container_tag="notes")
# Equivalent resource call
resp = client.documents.add("Document content", container_tag="notes")
# Get by ID
doc = client.documents.get("doc_abc123")
# Update
client.documents.update("doc_abc123", content="Updated content")
# Delete
client.documents.delete("doc_abc123")
# List with filters
docs = client.documents.list(container_tags=["notes"], limit=20)
# Batch add
client.documents.batch_add(
["First document", "Second document"],
container_tag="batch-import",
)
# Bulk delete
client.documents.delete_bulk(container_tags=["old-tag"])
Search
Document search
results = client.search.documents("quarterly revenue", limit=10)
# Alias
results = client.search.execute("quarterly revenue", limit=10)
Memory search
results = client.search.memories("user preferences", container_tag="user1")
Memories
# Add memories
client.memories.add(
[{"content": "User prefers dark mode"}],
container_tag="prefs",
)
# Forget a memory
client.memories.forget("prefs", id="mem_abc123")
# Update a memory
client.memories.update_memory("prefs", "User prefers light mode", id="mem_abc123")
Profile
profile = client.profile("user-container-tag")
print(profile.profile)
Tex-Unique Features
NLQ (Natural Language Query)
result = client.nlq.execute("What meetings did I have last week?")
print(result.text)
print(result.evidence)
Episodes
# Store a conversation
client.episodes.store(
[
{"role": "user", "content": "Book a table for two"},
{"role": "assistant", "content": "Which restaurant?"},
],
episode_id="ep_001",
)
# List episodes
episodes = client.episodes.list(limit=10)
Preferences
client.preferences.store([
{"key": "drink", "value": "espresso", "confidence": 0.9},
])
DB
result = client.db.query("MATCH (n) RETURN n LIMIT $limit", params={"limit": 5})
schema = client.db.schema()
Auth
info = client.auth.whoami()
Jobs
status = client.jobs.get("job_abc123")
Scope memberships
A user always sees their own user:<id> scope and their org:<org> scope — those are implicit. Every
shared team: or project: space is an explicit grant, and client.scopes lets your own org admin
manage those grants instead of asking us to.
Same key as deletion: a non-impersonated org-admin API key (role admin or *, on a client built
without user_id=). A per-user token inherits the key's roles and is refused, so it can never grant
itself a scope. The org is always the token's — cross-org grants are impossible here.
grant = client.scopes.grant("alice", "team:eng") # role="member" by default
print(grant.created) # False on an idempotent replay
print(grant.membership.role, grant.membership.created_at)
print(grant.note) # the propagation caveat, see below
listing = client.scopes.list(scope="team:eng") # both filters optional
print(listing.total)
for m in listing.memberships:
print(m.user_id, m.role)
client.scopes.revoke("alice", "team:eng") # NotFoundError if there is no such grant
- A change is not instant. A user's read-set is baked into their helix token when it is minted, so a grant or revocation reaches them on their next token refresh — up to 1 hour. Already-issued tokens keep their old read-set until they expire: a fresh grant is not immediately visible, and a revoked scope is not immediately unreadable. When the data itself must go, pair the revocation with a deletion.
- Only
team:andproject:scopes can be granted.user:is personal and implicit for its own user (granting someone else's would hand over their private memory) andorg:is already implicit for every member. Both raiseScopeNotDelegableErrorbefore any request; a malformed scope id raisesInvalidScopeError. - Granting to a user who has not logged in yet is fine. A user is just a string in a token, so the row sits inert until someone authenticates as that user in your org — onboarding before first login is a normal flow, not a workaround.
- Replays are idempotent. Re-granting returns the existing row with
created=False; passing a differentroleupdates it in place. - Implicit scopes are not rows, so
list()never shows them — an empty result does not mean the user sees nothing.
Platform operators have the same three calls across orgs at client.admin.scopes.* (explicit org_id=,
any scope kind); a normal customer key gets 403 there.
Deletion
client.deletions erases an org's data across every tier in one request and returns an auditable
receipt. It needs a non-impersonated org-admin API key — role admin (or *), on a client built
without user_id=. A per-user token inherits the key's roles but is still refused (403).
receipt = client.deletions.delete_user_scope("alice", reason="GDPR erasure request")
print(receipt.status) # "completed" | "partial" (also .completed / .partial)
print(receipt.purge_after) # when the retention window elapses
print(receipt.tiers.helixdb.deleted, receipt.tiers.helixdb.refused)
print(receipt.tiers.active_memory.fragments_deleted)
print(receipt.tiers.auth_db.usage_events_retained)
for gap in receipt.not_covered:
print("not covered:", gap.tier, "—", gap.reason)
# Re-read the receipt later
receipt = client.deletions.get(receipt.request_id)
Targets:
client.deletions.delete_user_scope("alice") # one user's personal scope
client.deletions.delete_scope("team:eng") # everything stamped with a scope
client.deletions.delete_user("alice") # full removal of a user in this org
client.deletions.delete_memory("node-7", owner_user_id="alice") # one memory node
client.deletions.create(kind="user_scope", value="alice") # general form
Every method takes reason= and idempotency_key= (replaying a key returns the first receipt instead of
re-running). A malformed scope id raises InvalidScopeError before any request.
What a deletion actually does
- Soft by default, and soft is the only mode an org owner may ask for. The target is tombstoned
across the graph engine, active memory and the auth DB: it disappears from every read path — recall,
search, profile, NLQ — immediately, and nothing can surface it again. It is not yet destroyed;
receipt.purge_aftersays when it becomes eligible for the hard purge. - Purge is platform-admin only.
mode="purge"onclient.deletions.*raisesPurgeNotPermittedErrorbefore any request. An org owner can make their data unreachable and start the clock; irreversible destruction is a platform operation, so a compromised org key cannot destroy anything on the spot. - Ingestion records and raw S3 files are not covered yet. The ingestion service's documents and jobs
and the raw uploaded objects in object storage are untouched; a follow-up phase owns them. Every receipt
lists them under
not_covered. - The graph engine tombstones rather than reclaiming space. HelixDB exposes only a soft delete, so even
a purge leaves tombstoned nodes for engine compaction (
helixdb_hard_deleteinnot_covered). Its delete is owner-only, so nodes it refuses are counted intiers.helixdb.refused(the receipt goespartial) rather than raising, and nodes with no scope stamp are left alone (skipped_unscoped) so a shared-scope deletion cannot erase someone's personal data. - Billing survives a soft deletion. Usage events are retained (
tiers.auth_db.usage_events_retained) and removed only by a purge.
Platform-admin surface
client.admin.deletions.* wraps /admin/deletions. These routes are for Tex operators — a normal
customer key gets 403. They take a required org_id= and may ask for mode="purge".
client.admin.deletions.delete_user("bob", org_id="org1", mode="purge")
client.admin.deletions.get("del_abc123") # any org's receipt
# Hard-delete everything past its retention window (point a cron at this)
result = client.admin.deletions.run_purges(limit=50)
print(result.purged, len(result.receipts))
Error Handling
The SDK provides a detailed error hierarchy:
TexError
APIError
APIConnectionError
APITimeoutError
APIStatusError
BadRequestError (400)
AuthenticationError (401)
PermissionDeniedError (403)
NotFoundError (404)
ConflictError (409)
UnprocessableEntityError (422)
RateLimitError (429)
InternalServerError (500+)
APIResponseValidationError
from tex import Tex, NotFoundError, AuthenticationError, APIStatusError
client = Tex(api_key="sk-...", base_url="http://localhost:8000")
try:
doc = client.documents.get("nonexistent")
except NotFoundError as e:
print(f"Not found: {e}")
except AuthenticationError as e:
print(f"Auth failed: {e}")
except APIStatusError as e:
print(f"HTTP {e.status_code}: {e.message}")
print(f"Request ID: {e.request_id}")
Active memory inactivity window
Conversation turns stored with conversations.remember land in short-term active memory first. An
active-memory episode (one per session_id) is closed and consolidated into long-term memory once it has been
idle for its inactivity window — 30 minutes by default. Tune it to the use case: a support chat can close
after a few minutes, a long-running agent task may need hours.
# Per conversation (the episode keeps this window for later writes)
client.conversations.remember(turns, session_id="ticket-981", inactivity_timeout_seconds=2 * 60 * 60)
# Client-wide default for every write that does not pass its own
client = Tex(api_key="sk-...", active_inactivity_timeout_seconds=10 * 60)
- Resolution: per-call value → client default → org default (set by an org admin with
PATCH /me/settings{"active_memory_inactivity_seconds": 3600}) → 30 minutes. - Bounds: 300–86400 seconds (5 minutes to 24 hours). Invalid values raise
InvalidInactivityTimeoutErrorbefore any request. memories.addandmemories.update_memoryaccept the sameinactivity_timeout_seconds.
Configuration
Use TexConfig and TexEndpoints for advanced configuration:
from tex import Tex, TexConfig, TexEndpoints
config = TexConfig(
base_url="https://api.example.com",
api_key="sk-...",
timeout_s=30.0,
max_retries=3,
http2=True,
active_inactivity_timeout_seconds=3600, # optional; see "Active memory inactivity window"
endpoints=TexEndpoints(
db_query="/custom/db/query",
db_schema="/custom/db/schema",
),
)
client = Tex(config=config)
Default timeout is 60 seconds. Default max retries is 2.
Migration from SuperMemory
| SuperMemory | Tex SDK |
|---|---|
client.add(content) |
client.add(content) or client.documents.add(content) |
client.documents.get(id) |
client.documents.get(id) |
client.documents.update(id) |
client.documents.update(id) |
client.documents.delete(id) |
client.documents.delete(id) |
client.documents.list() |
client.documents.list() |
client.documents.batch_add(docs) |
client.documents.batch_add(docs) |
client.search.execute(q) |
client.search.execute(q) or client.search.documents(q) |
client.search.memories(q) |
client.search.memories(q) |
client.memories.add(memories) |
client.memories.add(memories) |
client.memories.forget(tag) |
client.memories.forget(tag) |
client.profile(tag) |
client.profile(tag) |
Tex extends SuperMemory with additional resources: nlq, episodes, preferences, db, auth, jobs,
usage, scopes, and deletions (plus the platform-admin-only admin.scopes / admin.deletions).
HTTP/2
HTTP/2 is enabled by default. If the h2 package is not installed, the SDK falls back to HTTP/1.1 automatically. To install with HTTP/2 support:
pip install "tex-sdk[http2]"
To explicitly disable HTTP/2:
client = Tex(api_key="sk-...", base_url="http://localhost:8000", http2=False)
Metadata
Release files for tex-sdk 1.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| tex_sdk-1.3.0.tar.gz | 55.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tex_sdk-1.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 102.4 kB
Release files / tex_sdk-1.3.0.tar.gz
| Download URL | tex_sdk-1.3.0.tar.gz |
|---|---|
| Size | 55.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1144538328a73acf373f03007129ed9ad44bc1d6473dddd2a2e43f2ae5d0dee8
|
|
BLAKE2b-256 checksum How to use checksums |
0c2383629ce25585c8b0c11a0e8ef838c82cf6eec77b36a369c9d1def7b92579
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.5
|
Release files / tex_sdk-1.3.0-py3-none-any.whl
| Download URL | tex_sdk-1.3.0-py3-none-any.whl |
|---|---|
| Size | 46.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
532e6929360c895e3be4baeaa8d2bfdafff37d793874afe98955a7a270e3694b
|
|
BLAKE2b-256 checksum How to use checksums |
a3b5ec6d71a2879edf18e99c1c5baa64bd830a37366c281e9405355be5d47945
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.5
|