Get started
Python SDK
Call Drex's decision model and run Perception's document tools from Python with nace-sdk — client options, calibrated questions, typed answers, sources, parse, split, classify, extract, ground, uploads, jobs, workspaces, workspace files, storage connections, search, saved schemas, errors, retries and logging.
nace-sdk is the official Python client for https://console.nace.ai. It covers Drex's decision model, system_one, decisions.create and models.list, and Perception's five document tools, uploads, jobs, job files, workspaces, workspace files, storage connections, search and saved schemas. It needs Python 3.10 or later and depends only on httpx, pydantic and typing-extensions, plus tomli on Python 3.10.
The client is built on the TypeSafe Python SDK, so system_one, Noul, Choice, Score, SystemOneResponse with .nouls, .choices and .scores, RetryPolicy and the error classes match typesafe-sdk. Code written for TypeSafeClient runs on NaceClient once you rename the imports. The differences are Drex's: the 402, 409 and 529 error classes, a 60-second default timeout, instructions and criteria typed as text (the API also renders JSON instructions, see Questions), and legend as a tuple (Drex answers the TypeSafe SDK's own client with an object, and X-Nace-Legend chooses the shape). See Migrate from TypeSafe.
pip install nace-sdk
export NACE_API_KEY="nace_sk_..."Create a key as shown in Authentication.
Configure the client
NaceClient(**options)
| Parameter | Type | Required | Description |
|---|---|---|---|
api_key | str | Yes, or NACE_API_KEY | Your nace_sk_ key. Surrounding whitespace is stripped; a missing key, or one with spaces or non-ASCII characters, raises NaceError. |
base_url | str | No | API root. Default: NACE_BASE_URL, else https://console.nace.ai. A trailing / is dropped. |
model | str | No | Default model for system_one. Default: NACE_DEFAULT_DECISION_MODEL, else drex-latest. |
timeout | float | httpx.Timeout | No | Timeout of each HTTP attempt, in seconds. Default: 60, or the timeout of the http_client you pass. Must be positive and finite. |
retry | RetryPolicy | No | How failed requests are retried. Default: RetryPolicy(); see Retries. |
headers | Mapping[str, str] | No | Extra headers sent on every request to Drex. Never sent to the document service's upload URLs or to signed file links. |
client_info | str | No | The application or agent using the SDK, as name/version, sent as X-Nace-Client so Nace can record it with each request, and appended to User-Agent after the SDK's own nace-sdk/<version>. Not sent to upload URLs or signed links. |
transport | httpx.BaseTransport | No | A custom httpx transport, closed with the client. Can't be combined with http_client (ValueError). |
http_client | httpx.Client | No | Your own httpx.Client for proxies, TLS or connection pooling. The SDK closes it when the client closes. Its own auth is never sent: requests to Drex carry the API key instead, and signed file links and the document service's upload URLs get neither. |
An argument you pass wins over its environment variable, and an empty or whitespace-only variable counts as unset:
| Variable | Read | What it sets |
|---|---|---|
NACE_API_KEY | By each NaceClient() | api_key |
NACE_BASE_URL | By each NaceClient() | base_url |
NACE_DEFAULT_DECISION_MODEL | By each NaceClient() | model |
NACE_LOG_LEVEL | Once, when nace_sdk is imported | The nace_sdk logger's level. See Logging. |
Each variable's name from before the packages were renamed from drex-* still works when the new one is unset or blank: DREX_API_KEY, DREX_BASE_URL, DREX_DEFAULT_MODEL and DREX_LOG_LEVEL. The new name wins when both are set.
NACE_API_KEY and NACE_BASE_URL configure the key and API root for cloud, custom and on-prem deployments. No constructor arguments or login profile are required. Explicit arguments override the environment, and the legacy DREX_* names remain supported. The configured API root is trusted configuration: select the key and root for the same deployment. A URL with a user@ part is refused.
For an on-prem deployment, set the environment pair and use the client normally:
export NACE_API_KEY="nace_sk_..."
export NACE_BASE_URL="https://nace.customer.example"NaceClient() reads this configuration. Explicit api_key and base_url arguments are supported too. When your HTTP client enables redirects, authenticated API and upload-grant requests follow only same-origin redirects; a change of scheme, host or effective port is rejected. Credential-free signed links may follow cross-origin redirects without inheriting API authentication.
NaceClient() never reads the CLI's ~/.nace/config.toml: pass api_key, or set NACE_API_KEY. The SDK sets Authorization, User-Agent, X-Nace-SDK, X-Nace-Runtime and, on a retry, X-Nace-Retry-Count itself, plus Idempotency-Key and Content-Type when a request carries them, so headers and extra_headers can't override those.
| Member | Description |
|---|---|
client.base_url | The resolved API root, as a str. |
client.models | A Models resource: List models. |
client.decisions | A Decisions resource: POST /v1/decisions, OpenAI's Decisions shape on the same models and billing. See OpenAI-compatible decisions. |
client.documents, client.jobs, client.extraction_schemas | Documents, Jobs and ExtractionSchemas: document jobs, uploads, job files and saved schemas. See Uploads, Jobs and Saved schemas. |
client.browser_runs | A BrowserRuns resource: a cloud browser agent doing a task on the sites you allow. See Browser runs. |
client.workspaces, client.search | Workspaces and Search: workspaces with their files (a WorkspaceFiles), and ingestion, and questions over a workspace. See Workspaces, Workspace files and Search. |
client.domains | A Domains resource: the taxonomies a workspace classifies its files against. See Domains. |
client.storage_connections | A StorageConnections resource: your own S3 buckets and Azure Blob containers a new workspace can keep its files in. See Storage connections. |
client.system_one(...) | Ask questions. |
client.close() | Closes the HTTP connections, including an http_client you passed. with NaceClient() as client: calls it for you. |
import httpx
from nace_sdk import NaceClient, RetryPolicy
client = NaceClient(
base_url="https://console.nace.ai",
timeout=httpx.Timeout(90, connect=5),
retry=RetryPolicy(max_retries=4, timeout=500), # allow 5 attempts; opt into a 500 s budget for starting retries
headers={"X-Team": "billing-bot"},
)
try:
print(client.base_url)
finally:
client.close()The SDK's X-Nace-SDK and X-Nace-Runtime remain the transport identity.
A valid X-Nace-Client is recorded separately as client_app; a recognized
application can determine the traffic channel without replacing SDK identity.
No client declaration is sent to external upload or signed download URLs.
Options for one call
Most methods of client.documents, client.jobs, client.extraction_schemas, client.browser_runs, client.workspaces, client.workspaces.files, client.domains, client.storage_connections and client.search also take retry, timeout and extra_headers for one call, and the five tools also take extra_body:
| Option | What it does |
|---|---|
retry | A RetryPolicy for this call, instead of the client's. |
timeout | A timeout for this call, instead of the client's. |
extra_headers | Headers to add to this call's request. |
extra_body | Fields to add to the request body. Only the five tools take it. |
job = client.documents.parse(
source,
retry=RetryPolicy(max_retries=0),
timeout=30,
extra_headers={"X-Team": "billing-bot"},
)These methods don't take them. They use the client's retry policy and timeout:
- Uploads:
documents.upload,documents.upload_part,documents.complete_upload_session,documents.get_upload_sessionanddocuments.abort_upload_session. - Jobs:
jobs.iter,jobs.wait,jobs.eventsandjobs.download. - Browser runs:
browser_runs.wait. - Storage connections:
storage_connections.wait_for_validation.
Two of those behave differently:
jobs.eventsis never retried.jobs.eventsandjobs.download: the timeout limits connecting and sending, not how long the stream keeps being read.jobs.wait,browser_runs.waitandstorage_connections.wait_for_validationhave atimeoutof their own: how long to keep polling.
Async
AsyncNaceClient takes the same arguments as NaceClient, with two exceptions: transport is an httpx.AsyncBaseTransport, and http_client is an httpx.AsyncClient.
Its resources have the same methods and parameters as the sync ones:
| Sync | Async |
|---|---|
models | AsyncModels |
documents | AsyncDocuments |
jobs | AsyncJobs |
extraction_schemas | AsyncExtractionSchemas |
browser_runs | AsyncBrowserRuns |
workspaces | AsyncWorkspaces |
workspaces.files | AsyncWorkspaceFiles |
domains | AsyncDomains |
storage_connections | AsyncStorageConnections |
search | AsyncSearch |
- Calls: await each call.
- Iterators: iterate
jobs.iter(),jobs.events(),extraction_schemas.iter(),extraction_schemas.iter_versions(),domains.iter(),storage_connections.iter(),workspaces.iter_all()andbrowser_runs.iter_all()withasync for. - Closing: close the client with
async withorawait client.aclose().
import asyncio
from nace_sdk import AsyncNaceClient, Noul
async def main() -> None:
async with AsyncNaceClient() as client:
result = await client.system_one(
state="I was charged twice for my March invoice.",
questions={"wants_refund": Noul(instructions="Is the customer asking for a refund?")},
)
print(result.nouls["wants_refund"].noul)
asyncio.run(main())The document methods are awaited the same way:
import asyncio
from nace_sdk import AsyncNaceClient
async def main() -> None:
async with AsyncNaceClient() as client:
job = await client.documents.parse("https://example.com/report.pdf")
async for event in client.jobs.events(job.job_id):
print(event.status, event.progress)
job = await client.jobs.wait(job.job_id)
print(job.result["document"]["markdown"])
asyncio.run(main())Document batches
Start with 8 concurrent documents, including each upload and wait in that worker's slot. wait_seconds=60 holds the create open for up to 60 seconds; if it returns a running job, jobs.wait polls with the SDK's delay. Job reads and upload setup each have their own RPM window, at 5x and 2x the create allowance respectively (including an account RPM override). Creates stay at 120/min free or 600/min paid; reads allow 600/3000 and upload setup 240/1200. They cannot spend the create budget, but can return their own 429; NDI can also throttle them. The current SDK honors Retry-After (seconds until retry) automatically; use Python nace-sdk 0.7.0 or later so a minute-long wait is not cut off by the old default retry budget. See Rate limits.
import asyncio
from pathlib import Path
from nace_sdk import AsyncNaceClient
async def main() -> None:
slots = asyncio.Semaphore(8)
async with AsyncNaceClient() as client:
async def process(path: Path):
async with slots:
file = await client.documents.upload(path)
job = await client.documents.extract(
file.as_source(),
schema={"type": "object", "properties": {"total": {"type": "number"}}},
wait_seconds=60,
)
return await client.jobs.wait(job.job_id)
# Keep failures visible while letting other documents finish.
paths = sorted(Path("invoices").glob("*.pdf"))
results = await asyncio.gather(*(process(path) for path in paths), return_exceptions=True)
for path, result in zip(paths, results):
print(path, result)
if any(isinstance(result, BaseException) for result in results):
raise RuntimeError("Some documents failed; inspect the per-file results")
asyncio.run(main())