Every request to the Trace gateway carries an API key in an Authorization
header. Keys are issued to a Trace account and belong to that account's credit
balance.
Key format#
trace_sk_<43 url-safe characters>
Trace stores the SHA-256 digest of the key and a display prefix
(trace_sk_ plus the first eight characters of the secret). The full key exists
in exactly one place: the response that created it. Nobody — including Trace
staff — can read it back afterwards. If you lose a key, revoke it and make
another.
A Clerk browser session is never accepted by the gateway. The two credentials are separate on purpose: a signed-in browser tab must not be replayable as API access.
Getting a key#
With trace login#
The normal path. Run:
trace login --name "Laptop"
- The SDK asks Trace to start a login and receives a
device_code(its own secret) and auser_code(the short string it prints). - Your browser opens
/account/link?code=USER-CODE. Approve there while signed in to Trace. - The SDK polls until the key comes back, then writes it to disk.
Login codes expire after 10 minutes and are single use. Approving a code does not create the key; the key is minted at the moment the SDK collects it, so no readable key ever sits in the database waiting.
From the account page#
Go to /account/developer, name a key, and create it. The key is shown once.
Use this when you cannot run a browser and a terminal on the same machine, or
when you want a separate key for a specific project.
How the SDK finds your key#
In this order, first match wins:
- The
api_keyargument:Trace(api_key="trace_sk_..."). - The
TRACE_API_KEYenvironment variable. ~/.trace/credentials.json, which is whattrace loginwrites.
If none of those produce a key, the first request raises
AuthenticationError telling you to run trace login.
from trace_sdk import Trace
client = Trace() # stored key or TRACE_API_KEY
client = Trace(api_key="trace_sk_...") # explicit
client = Trace(model="openai/gpt-5.6-luna") # and a default model, if you want one
How the SDK finds your Trace#
The base URL resolves the same way:
- The
base_urlargument. - The
TRACE_BASE_URLenvironment variable. - The
base_urlrecorded in~/.trace/credentials.json. - The hosted app.
Both the app origin and the full gateway path are accepted:
| You write | The SDK uses |
|---|---|
http://localhost:8000 | http://localhost:8000/api/sdk/v1 |
http://localhost:8000/api/sdk/v1 | http://localhost:8000/api/sdk/v1 |
The credentials file#
~/.trace/credentials.json, created mode 0600 inside a 0700 directory:
{
"api_key": "trace_sk_…",
"base_url": "https://trace.edu/api/sdk/v1",
"key_name": "Laptop"
}
Set TRACE_CONFIG_DIR to move the directory — useful in a shared lab image or
a container.
trace logout deletes this file. It does not revoke the key on your account;
revoke at /account/developer if the key may have been seen by someone else.
Keeping a key safe#
- Do not commit a key. Read it from the environment instead.
- Do not paste a key into a notebook you will share or screen-share.
trace whoamiprints only the prefix for this reason. - One key per machine or project makes revoking one harmless to the others.
- Revoking is immediate: the next request with that key gets a 401.
Child protection#
This is the reason the gateway exists. Trace does not hand a sixteen-year-old a raw provider key, because a raw provider key has no roster, no redaction, no ceiling, and no way for a school to answer for what happened. A Trace key has all four, and every SDK request runs the same student-safety pipeline as an agent-builder run — writing Python is not a way around anything the visual builder enforces.
The approved roster#
Only models on Trace's reviewed child-safe roster can be called, whatever id you send. The chat models available to the SDK are exactly the ones the agent builder offers. An unapproved id is a 404 rather than a request that leaves the building, and a model can be withdrawn deployment-wide without shipping new code to any student. See Models and pricing.
Personal information is redacted server-side#
Before any message text reaches a provider, Trace runs the same
sanitize_prompt an agent-builder run uses, over the same identifier list:
| Redacted | Replaced with |
|---|---|
| The signed-in student's own name, email, user id, and date of birth | [REDACTED_STUDENT_IDENTIFIER] |
| Any email address | [REDACTED_EMAIL] |
| Any phone number | [REDACTED_PHONE] |
| Anything shaped like a social security number | [REDACTED_SSN] |
| A date of birth introduced as one | [REDACTED_DOB] |
| A street address | [REDACTED_ADDRESS] |
| A student id or username introduced as one | [REDACTED_STUDENT_IDENTIFIER] |
It runs server-side, in the gateway, not in this package. A student cannot
turn it off, and neither can a modified client, because the redaction happens
after the request arrives and before it is forwarded. It covers plain string
content and the text parts of OpenAI content arrays.
Tool arguments and results are decoded, redacted recursively, and encoded again so personal information is removed without corrupting JSON.
Redaction is a safety net, not a licence. Do not send classmates' names, and do not send anything you would not put on a noticeboard.
Caps and ceilings#
| Control | Effect |
|---|---|
max_tokens capped at 4000 per request | No single request can run away. |
| Request bodies capped at 256 KB | No pasting a data set into a prompt. |
| 30 requests per minute per key | A runaway loop stops itself. |
| Credit balance | Must cover the reservation before a request starts. |
If provider cost exceeds an estimate, Trace records the difference and refuses future requests until the balance covers a new reservation. See Credits and limits.
Data handling#
Requests are forwarded with the provider's data collection denied and zero-data-retention requested. Trace stores the ledger row for each request — which model, how many tokens, what it cost — and does not store prompt or reply text for SDK traffic.
Using the official openai package#
The gateway is wire-compatible enough that the official client works for non-streaming chat completions:
from openai import OpenAI
client = OpenAI(api_key="trace_sk_...", base_url="https://trace.edu/api/sdk/v1")
reply = client.chat.completions.create(
model="deepseek/deepseek-v4-flash-0731:nitro",
messages=[{"role": "user", "content": "Say hello in three words."}],
)
The call is identical to the one you would write with trace_sdk, because
trace_sdk mirrors this library on purpose. What you lose by using the official
client here is the Trace-specific part: the optional model=, the .text and
.credits_spent shortcuts, Python functions as tools, client.credits, and
errors written for a student mid-tutorial rather than for a backend engineer.
The OpenAI client's typed response also drops credits_spent, which the
curriculum's checkpoints refer to.
Everything on this page still applies: the same key, the same roster, the same server-side redaction, the same caps. The protection is in the gateway, not in the client, so no client can opt out of it.
This is an advanced note, not the recommended path.