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)

ParameterTypeRequiredDescription
api_keystrYes, or NACE_API_KEYYour nace_sk_ key. Surrounding whitespace is stripped; a missing key, or one with spaces or non-ASCII characters, raises NaceError.
base_urlstrNoAPI root. Default: NACE_BASE_URL, else https://console.nace.ai. A trailing / is dropped.
modelstrNoDefault model for system_one. Default: NACE_DEFAULT_DECISION_MODEL, else drex-latest.
timeoutfloat | httpx.TimeoutNoTimeout of each HTTP attempt, in seconds. Default: 60, or the timeout of the http_client you pass. Must be positive and finite.
retryRetryPolicyNoHow failed requests are retried. Default: RetryPolicy(); see Retries.
headersMapping[str, str]NoExtra headers sent on every request to Drex. Never sent to the document service's upload URLs or to signed file links.
client_infostrNoThe 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.
transporthttpx.BaseTransportNoA custom httpx transport, closed with the client. Can't be combined with http_client (ValueError).
http_clienthttpx.ClientNoYour 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:

VariableReadWhat it sets
NACE_API_KEYBy each NaceClient()api_key
NACE_BASE_URLBy each NaceClient()base_url
NACE_DEFAULT_DECISION_MODELBy each NaceClient()model
NACE_LOG_LEVELOnce, when nace_sdk is importedThe 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.

MemberDescription
client.base_urlThe resolved API root, as a str.
client.modelsA Models resource: List models.
client.decisionsA 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_schemasDocuments, Jobs and ExtractionSchemas: document jobs, uploads, job files and saved schemas. See Uploads, Jobs and Saved schemas.
client.browser_runsA BrowserRuns resource: a cloud browser agent doing a task on the sites you allow. See Browser runs.
client.workspaces, client.searchWorkspaces and Search: workspaces with their files (a WorkspaceFiles), and ingestion, and questions over a workspace. See Workspaces, Workspace files and Search.
client.domainsA Domains resource: the taxonomies a workspace classifies its files against. See Domains.
client.storage_connectionsA 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.
client.py
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:

OptionWhat it does
retryA RetryPolicy for this call, instead of the client's.
timeoutA timeout for this call, instead of the client's.
extra_headersHeaders to add to this call's request.
extra_bodyFields 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_session and documents.abort_upload_session.
  • Jobs: jobs.iter, jobs.wait, jobs.events and jobs.download.
  • Browser runs: browser_runs.wait.
  • Storage connections: storage_connections.wait_for_validation.

Two of those behave differently:

  • jobs.events is never retried.
  • jobs.events and jobs.download: the timeout limits connecting and sending, not how long the stream keeps being read.
  • jobs.wait, browser_runs.wait and storage_connections.wait_for_validation have a timeout of 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:

SyncAsync
modelsAsyncModels
documentsAsyncDocuments
jobsAsyncJobs
extraction_schemasAsyncExtractionSchemas
browser_runsAsyncBrowserRuns
workspacesAsyncWorkspaces
workspaces.filesAsyncWorkspaceFiles
domainsAsyncDomains
storage_connectionsAsyncStorageConnections
searchAsyncSearch
  • 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() and browser_runs.iter_all() with async for.
  • Closing: close the client with async with or await client.aclose().
decide_async.py
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:

parse_async.py
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.

batch.py
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())

On this page