temperDocs
Menu

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

bash
# 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-go

The 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:

VariableMeaning
TEMPER_API_KEYYour API key. Required.
TEMPER_BASE_URLThe API address. Optional: without it the SDKs use https://api.temper.im.
bash
export TEMPER_API_KEY=temper_sk_...
export TEMPER_BASE_URL=https://api.temper.im

Or 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 }). fetch lets you plug in your own fetch (for a proxy, retries or tests); headers are added to every request. Every method also takes a last opts argument with signal (an AbortSignal) and timeoutMs for that call. Without an API key the constructor throws a TemperError with code missing_api_key.
  • Python: Temper(api_key=None, base_url=None, *, timeout=30.0, http_client=None, transport=None). The client is built on httpx; pass your own httpx.Client as http_client (you then close it yourself) or a transport for tests. Use the client as a context manager or call close(). Without an API key the constructor raises ValueError.
  • Go: temper.New(opts ...Option) with WithAPIKey, WithBaseURL, WithHTTPClient, WithHeader and WithTimeout. Every method takes a context.Context; the client timeout applies only when the context has no deadline. Without an API key New returns an *temper.Error with code missing_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:

json
{ "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:

SDKError typeFieldsCheck a code
TypeScriptTemperErrorstatus, code, message, body, environmentIdisTemperError(err, "environment_exists")
Pythontemper.TemperErrorstatus, code, message, body, environment_ide.code == "environment_exists"
Go*temper.ErrorStatus, Code, Message, Err, EnvironmentIDtemper.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 as http_502, with the response text as the message.
  • Network failures: TypeScript and Go report them with status 0 and the code network_error, timeout or aborted (Go wraps the underlying error in Err). Python does not wrap them; you get the httpx exception.

Common error codes

StatusCodeMeaning
400bad_requestA parameter is invalid, for example an allow rule for a high-risk category.
401unauthorizedThe API key is missing, wrong or revoked.
403forbiddenThe caller is not allowed to do this.
403session_requiredOnly a console session can do this (managing members and API keys, setting the webhook URL).
403reauth_requiredThe console session must have signed in recently.
404not_foundNo such resource. Resources of other teams also look like this.
409environment_existsThe end user already has an environment that is not destroyed.
409invalid_state_transitionThe environment cannot do this in its current state.
409limit_exceededA monthly usage limit has been reached.
409already_decidedThe approval request was already approved, denied or expired.
422invalid_bodyThe request body does not match the schema (unknown fields are rejected).
429too_many_processes, wakeup_limit_reached, lease_limit_reachedA per-environment or per-team quota is full.
503environment_not_placed, environment_not_runningThe environment could not be placed on a host, or did not start in time.
503host_unavailable, guest_unavailableThe 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:

ResourceTypeScriptPythonGo
Environmentsenvironments.iterate()environments.iter()Environments.Iter()
Environment eventsenvironments.iterateEvents(id)environments.iter_events(id)Environments.IterEvents()
Approvalsapprovals.iterate()approvals.iter()Approvals.Iter()
Audit recordsaudit.iterate(envId)audit.iter(env_id)Audit.Iter()
Browser leasesbrowser.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 None
it := 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:

http
Temper-Signature: t=1760000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

v1 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:

SDKFunctionOn failure
TypeScriptawait verifyWebhook(secret, rawBody, header, { toleranceSecs, now })throws TemperError with code webhook_signature_missing, webhook_signature_malformed, webhook_signature_expired or webhook_signature_mismatch
Pythonverify_webhook(secret, raw_body, header, *, tolerance=300, now=None)raises WebhookVerificationError; reason is missing, malformed, expired or mismatch
Gotemper.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.