Getting started
SDKs
Install and configure the TypeScript, Python and Go SDKs, and handle errors, pagination and webhook signatures.
Temper has official SDKs for TypeScript, Python and Go. All three are thin wrappers over the REST API: the same resources, the same parameters (in each language's naming style) and the same error codes. Anything you can do with the API key over HTTP, you can do with the SDKs.
Install
# TypeScript: Node 22 or later. No runtime dependencies; uses the global fetch.
npm install @temper-hq/sdk
# Python 3.10 or later
pip install temper-hq
# Go 1.21 or later
go get github.com/temper-hq/sdk-goThe Python import name is always temper. Attaching to a background process over WebSocket (processes.attach) needs an optional
extra: pip install 'temper-hq[attach]'.
The TypeScript package has a second entry point, @temper-hq/sdk/node, with Node-only helpers that stream files instead of reading them
into memory: uploadFile and downloadFile for files in an environment, and uploadPackageFile for agent packages.
Configure the client
Every client needs an API key (temper_sk_..., created in the console; see team and keys) and the API
address. If you don't pass them, the SDKs read two environment variables:
| Variable | Meaning |
|---|---|
TEMPER_API_KEY | Your API key. Required. |
TEMPER_BASE_URL | The API address. Optional: without it the SDKs use https://api.temper.im. |
export TEMPER_API_KEY=temper_sk_...
export TEMPER_BASE_URL=https://api.temper.imOr pass them explicitly:
import { Temper } from "@temper-hq/sdk";
const temper = new Temper({
apiKey: process.env.MY_TEMPER_KEY,
baseUrl: "https://api.temper.im",
timeoutMs: 30_000, // per-request timeout; 0 disables it
});from temper_hq import Temper
with Temper(api_key="temper_sk_...", base_url="https://api.temper.im", timeout=30.0) as temper:
env = temper.environments.get("env_...")import (
"time"
temper "github.com/temper-hq/sdk-go"
)
client, err := temper.New(
temper.WithAPIKey("temper_sk_..."),
temper.WithBaseURL("https://api.temper.im"),
temper.WithTimeout(30*time.Second),
)Options in each SDK:
- TypeScript:
new Temper({ apiKey, baseUrl, fetch, headers, timeoutMs }).fetchlets you plug in your own fetch (for a proxy, retries or tests);headersare added to every request. Every method also takes a lastoptsargument withsignal(anAbortSignal) andtimeoutMsfor that call. Without an API key the constructor throws aTemperErrorwith codemissing_api_key. - Python:
Temper(api_key=None, base_url=None, *, timeout=30.0, http_client=None, transport=None). The client is built onhttpx; pass your ownhttpx.Clientashttp_client(you then close it yourself) or atransportfor tests. Use the client as a context manager or callclose(). Without an API key the constructor raisesValueError. - Go:
temper.New(opts ...Option)withWithAPIKey,WithBaseURL,WithHTTPClient,WithHeaderandWithTimeout. Every method takes acontext.Context; the client timeout applies only when the context has no deadline. Without an API keyNewreturns an*temper.Errorwith codemissing_api_key.
The default timeout covers ordinary calls. Calls that run inside an environment (exec and the file operations) may have to wake it first, so their default timeouts are longer.
Errors
When the API returns a non-2xx response, the body is JSON with a machine-readable code and a message:
{ "error": "environment_exists", "message": "end user alice already has environment env_...", "environment_id": "env_..." }Each SDK turns this into an error that carries the HTTP status and the code:
| SDK | Error type | Fields | Check a code |
|---|---|---|---|
| TypeScript | TemperError | status, code, message, body, environmentId | isTemperError(err, "environment_exists") |
| Python | temper.TemperError | status, code, message, body, environment_id | e.code == "environment_exists" |
| Go | *temper.Error | Status, Code, Message, Err, EnvironmentID | temper.IsCode(err, temper.CodeEnvironmentExists) |
The environment id field is set on environment_exists and idempotency_key_reused: it is the environment that already exists.
import { Temper, isTemperError } from "@temper-hq/sdk";
const temper = new Temper();
try {
await temper.environments.create({ end_user_id: "alice" });
} catch (err) {
if (isTemperError(err, "environment_exists")) {
// reuse the existing environment
} else {
throw err;
}
}from temper_hq import Temper, TemperError
temper = Temper()
try:
temper.environments.create(end_user_id="alice")
except TemperError as e:
if e.code != "environment_exists":
raise
# reuse the existing environment_, err := client.Environments.Create(ctx, temper.CreateEnvironmentParams{EndUserID: "alice"})
if temper.IsCode(err, temper.CodeEnvironmentExists) {
// reuse the existing environment
} else if err != nil {
return err
}IsCode uses errors.As, so it also works on wrapped errors. The Go SDK exports constants for common codes, such as
CodeNotFound, CodeEnvironmentNotReady (environment_not_running) and CodeLimitExceeded.
Some details differ between the SDKs:
- A response that is not JSON (for example an error page from a proxy in front of the API) gets the code
http_<status>, such ashttp_502, with the response text as the message. - Network failures: TypeScript and Go report them with status
0and the codenetwork_error,timeoutoraborted(Go wraps the underlying error inErr). Python does not wrap them; you get thehttpxexception.
Common error codes
| Status | Code | Meaning |
|---|---|---|
| 400 | bad_request | A parameter is invalid, for example an allow rule for a high-risk category. |
| 401 | unauthorized | The API key is missing, wrong or revoked. |
| 403 | forbidden | The caller is not allowed to do this. |
| 403 | session_required | Only a console session can do this (managing members and API keys, setting the webhook URL). |
| 403 | reauth_required | The console session must have signed in recently. |
| 404 | not_found | No such resource. Resources of other teams also look like this. |
| 409 | environment_exists | The end user already has an environment that is not destroyed. |
| 409 | invalid_state_transition | The environment cannot do this in its current state. |
| 409 | limit_exceeded | A monthly usage limit has been reached. |
| 409 | already_decided | The approval request was already approved, denied or expired. |
| 422 | invalid_body | The request body does not match the schema (unknown fields are rejected). |
| 429 | too_many_processes, wakeup_limit_reached, lease_limit_reached | A per-environment or per-team quota is full. |
| 503 | environment_not_placed, environment_not_running | The environment could not be placed on a host, or did not start in time. |
| 503 | host_unavailable, guest_unavailable | The host or the VM could not be reached. |
The API reference lists the codes each operation can return.
Pagination
List operations return one page at a time: a data array and a next cursor (null / None / nil on the last page). Pass next
back as after to get the following page. limit sets the page size (50 by default, between 1 and 200).
Every paginated list also has an iterator that fetches pages for you:
| Resource | TypeScript | Python | Go |
|---|---|---|---|
| Environments | environments.iterate() | environments.iter() | Environments.Iter() |
| Environment events | environments.iterateEvents(id) | environments.iter_events(id) | Environments.IterEvents() |
| Approvals | approvals.iterate() | approvals.iter() | Approvals.Iter() |
| Audit records | audit.iterate(envId) | audit.iter(env_id) | Audit.Iter() |
| Browser leases | browser.iterateLeases() | (use browser.list_leases) | Browser.IterLeases() |
for await (const env of temper.environments.iterate({ state: "running" })) {
console.log(env.id, env.end_user_id);
}
// Or page by page:
const page = await temper.environments.list({ limit: 100 });
const nextPage = page.next ? await temper.environments.list({ limit: 100, after: page.next }) : null;for env in temper.environments.iter(state="running"):
print(env.id, env.end_user_id)
# Or page by page:
page = temper.environments.list(limit=100)
next_page = temper.environments.list(limit=100, after=page.next) if page.next else Noneit := client.Environments.Iter(temper.ListEnvironmentsParams{State: "running"})
for it.Next(ctx) {
env := it.Value()
fmt.Println(env.ID, env.EndUserID)
}
if err := it.Err(); err != nil {
return err
}In Go, Next returns false both at the end and on an error, so always check it.Err() after the loop.
Waiting for an environment state
State changes are asynchronous: resume returns once the desired state is set, and the environment reports running a little later.
Each SDK has a helper that waits for an actual state and returns the environment at that point. It fails with environment_failed
if the environment fails, environment_destroyed if it is destroyed while you wait (unless you are waiting for destroyed), and
wait_timeout after the timeout (default two minutes). These errors have status 0.
await temper.environments.resume(envId);
const env = await temper.environments.waitFor(envId, "running", { timeoutMs: 90_000 });temper.environments.resume(env_id)
env = temper.environments.wait_for(env_id, "running", timeout=90)if _, err := client.Environments.Resume(ctx, envID); err != nil {
log.Fatal(err)
}
env, err := client.Environments.WaitFor(ctx, envID, "running", &temper.WaitOptions{Timeout: 90 * time.Second})Verifying webhooks
Temper signs every webhook it sends to your backend (environment events, connection.*, approval.requested,
browser.takeover_requested) with your webhook signing secret (whsec_..., shown once in the console when you set the URL). The
Temper-Signature header looks like this:
Temper-Signature: t=1760000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdv1 is the hex HMAC-SHA256 of "<t>.<raw request body>" keyed with the secret. After you rotate the secret, the old one stays
valid for 24 hours and each delivery carries two v1 values, one per secret. Each event also has an id, repeated in the
Temper-Event-Id header, that stays the same across retries; use it to deduplicate.
Each SDK has a helper that checks the signature and the timestamp (5 minutes of tolerance by default), compares in constant time, and returns the parsed event:
| SDK | Function | On failure |
|---|---|---|
| TypeScript | await verifyWebhook(secret, rawBody, header, { toleranceSecs, now }) | throws TemperError with code webhook_signature_missing, webhook_signature_malformed, webhook_signature_expired or webhook_signature_mismatch |
| Python | verify_webhook(secret, raw_body, header, *, tolerance=300, now=None) | raises WebhookVerificationError; reason is missing, malformed, expired or mismatch |
| Go | temper.VerifyWebhook(secret, rawBody, header, tolerance, now) | returns an *temper.Error with code webhook_signature; pass 0 and time.Time{} for the defaults |
import { verifyWebhook } from "@temper-hq/sdk";
// rawBody: the request body as a string or Uint8Array, exactly as received
const event = await verifyWebhook(process.env.TEMPER_WEBHOOK_SECRET!, rawBody, signatureHeader);
console.log(event.id, event.type, event.environment_id, event.data);import os
from temper_hq import SIGNATURE_HEADER, WebhookVerificationError, verify_webhook
try:
event = verify_webhook(os.environ["TEMPER_WEBHOOK_SECRET"], raw_body, headers.get(SIGNATURE_HEADER))
except WebhookVerificationError as e:
... # respond 400; e.reason says why
print(event.id, event.type, event.environment_id, event.data)event, err := temper.VerifyWebhook(os.Getenv("TEMPER_WEBHOOK_SECRET"), rawBody, r.Header.Get(temper.SignatureHeader), 0, time.Time{})
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
if event.Type == "approval.requested" {
var a temper.ApprovalRequested
_ = json.Unmarshal(event.Data, &a)
fmt.Println(a.ApprovalID, a.Connector, a.Category, a.Summary)
}To test your own handler, the Python and Go SDKs can produce a valid header: sign_webhook(secret, payload, timestamp) and
temper.SignWebhook(secret, payload, t).
The approval webhooks guide walks through a complete approval handler, and the quickstart has a runnable server in all three languages.