Python
The Python SDK mirrors the TypeScript surface: one client, a profile module for memory, a tools module for connectors, and the same errors. Every example on this page runs as written against a test-mode sandbox.
bash
pip install configure-aiCredentials
Fastest path, no signup: provision a test-mode sandbox and export what it returns.
bash
curl -s -X POST https://api.configure.dev/v1/sandbox/provision
# -> secret_key (sk_test_...), agent, sandbox_user.external_id ("sandbox-user"),
# sandbox_user.test_phone (minted for your sandbox, not reusable from a doc)
export CONFIGURE_API_KEY=sk_test_...
export CONFIGURE_AGENT=sandbox-96d4069b
export CONFIGURE_TEST_PHONE=+1...For live users, use your own sk_ key and agent from Get your credentials. Nothing else on this page changes.
The client
python
import os
from configure_ai import ConfigureClient
client = ConfigureClient(
os.environ["CONFIGURE_API_KEY"],
agent=os.environ["CONFIGURE_AGENT"],
user_id="sandbox-user",
)user_id is your own identifier for the user, the default subject for profile calls. It is sent as X-User-Id only when a request carries no user token: a token always wins, because the backend rejects a request whose user_id disagrees with the token subject.
Both a sync ConfigureClient and an AsyncConfigureClient exist; the async one has the same methods with await and an async with context manager.
Read the profile
python
profile = client.profile.read_profile()
print(profile["identity"]["name"])
print(profile["preferences"][:3])
# Ready for a system prompt, without hand-formatting the JSON.
context = profile.format()read_profile() with no arguments returns the grounding overview: identity, preferences, a summary, connections, and an index of boxes and sources. Call it once before your agent's first message, not every turn.
Two narrower reads:
python
# Strict pages of the composed document, and nothing else.
imports = client.profile.read_profile(sections=["imports", "summary"])
# Open one shelf: a category ("work"), a source ("agents/atlas"), or "projects/<slug>".
work = client.profile.read_profile(box="work", detail="full")The box ids come from the overview's own index, so an agent never has to guess a name.
Search
python
hits = client.profile.search_profile(query="boat")
for memory in hits.results[:3]:
print(memory["text"], "-", memory.get("source"))search_profile() accepts box=, source=, from_=, to=, limit=, and detail=. It returns permitted attributed memories, so results can be filtered by the user's permissions rather than raising.
Write
Two ways in, and they are not interchangeable.
python
# One durable fact you already know is worth keeping.
client.profile.remember(fact="Prefers trains over planes under six hours", box="preferences")
# A whole turn. The backend extracts what is durable and drops the rest.
client.profile.commit(
messages=[
{"role": "user", "content": "Book me the 7am, I never take the aisle seat"},
{"role": "assistant", "content": "Booked the 7am, window."},
],
sync=True,
)Use remember() for a fact, commit() for evidence. commit() is bounded: at most 20 messages of 16k characters each, at most 20 explicit memories of 1k characters, 50k characters total. Passing sync=True blocks until extraction finishes, which is what you want in a test and not in a request handler.
Bulk context goes through import_profile(), which splits a structured export into facts and distills anything else into one filed note:
python
result = client.profile.import_profile(
text="Notes from kickoff: Nova wants metric units everywhere and reviews on Tuesdays.",
kind="context",
box="work",
)Forget
forget() deletes memories your agent wrote, and takes exactly one selector:
python
client.profile.forget(match="metric units") # preview, deletes nothing
client.profile.forget(match="metric units", confirm=True) # deletes the matches
client.profile.forget(scope="imports") # all of your own importsThe other two selectors are ids: forget(id="mem_...") deletes one memory, and forget(import_id=...) retracts a whole import (import_profile() returns that id as result.import_id when it split a structured export).
Without confirm=True, a match selector returns what it would delete. An unknown or malformed id is INVALID_INPUT, not NOT_FOUND, and retrying will not help.
Connectors need a user token
This is the part that trips people up. Profile read, search, and remember work with your secret key plus X-User-Id, which is what user_id= above sets. Connector queries do not: they require an agent-scoped token, which means the user has signed in and approved your agent.
The flow is: sign in, approve, agent token.
python
import httpx
resp = httpx.post(
"https://api.configure.dev/v1/auth/sign-in/recognize-phone",
headers={
"X-API-Key": os.environ["CONFIGURE_API_KEY"],
"X-Agent": os.environ["CONFIGURE_AGENT"],
},
# In test mode this is sandbox_user.test_phone from your own provision
# response: it is minted per sandbox, so no number can be hardcoded here.
json={"candidates": [os.environ["CONFIGURE_TEST_PHONE"]]},
).json()
token = resp["token"]In test mode the synthetic user has already approved the sandbox agent, so recognition returns the token immediately. For real users, that token comes from your sign-in flow: hosted SSO (Configure your agent) or the OAuth client (OAuth Client Reference). A phone OTP verification returns a user token, which is not the same thing and will be rejected on connector routes with token_wrong_type.
With the token in hand, enable the connectors you want this turn and call them through execute_tool():
python
client.profile.tools(connectors=["gmail", "calendar"])
mail = client.profile.execute_tool(token, None, {
"name": "configure_gmail_search",
"input": {"query": "harbor", "max_results": 2},
})
for email in mail.emails:
print(email.subject, "-", email.from_)
week = client.profile.execute_tool(token, None, {
"name": "configure_calendar_get",
"input": {"range": "week"},
})execute_tool() is the connector surface: it dispatches each tool name to the live endpoint behind it, and refuses any tool the last tools() call did not enable, so a model cannot reach a connector you did not offer. profile.search_emails(token, query=...) is the same Gmail call without the tool-name indirection.
Passing a token also switches the subject: the token identifies the user, and X-User-Id is dropped from the request entirely.
Connect and repair links
connect() mints a Configure-hosted link. Never build one yourself.
python
link = client.profile.connect(app="gmail", purpose="Read your inbox for scheduling")
print(link.status, link.connect_url)When a connector call fails because the connection died, the error carries the repair. See connector repair for the payload:
python
from configure_ai import ConfigureError
try:
client.profile.search_emails(token, query="in:inbox")
except ConfigureError as error:
repair = error.suggested_action
if isinstance(repair, dict) and repair.get("type") == "reconnect":
print("Ask the user to open:", repair["url"])
elif repair in ("reconnect", "connect_tool"):
print("Ask the user to open:", client.profile.connect(app="gmail").connect_url)
else:
raiseModel tool calls
tools() returns Anthropic-shaped tool definitions and enables exactly those tools for execute_tool():
python
definitions = client.profile.tools(connectors=["gmail"], actions=["email.send"])
names = [tool["name"] for tool in definitions]
result = client.profile.execute_tool(None, None, {
"name": "configure_profile_remember",
"input": {"fact": "Reviews hardware sketches on paper first"},
})execute_tool() refuses any tool the last tools() call did not enable, so a model cannot reach a connector you did not offer this turn. Use to_openai_functions() for the OpenAI shape.
Errors
python
from configure_ai import ConfigureError, ErrorCode
try:
client.profile.read_profile()
except ConfigureError as error:
print(error.code, error.status_code, error.retryable)The codes and their meanings match the TypeScript SDK: see Error Handling. Branch on error.code and the structured fields, never on message text.
Legacy methods
profile.get() and profile.get_memories() still call the old /v1/memory/* routes and emit a deprecation warning. They are not part of the current surface, and the routes they call are not mounted in production. Use read_profile() and search_profile().
Command line
bash
python -m configure_ai verify # real sign-in, token exchange, one live profile read
python -m configure_ai verify --offline # credentials and callback registration only
configure-ai add-callback --framework fastapi
configure-ai add-origin https://yourapp.com/auth/configure/callbackverify exits nonzero on failure, so CI can gate on a rotated secret or a callback that differs by a trailing slash.