# SDKs

Official clients for TypeScript, Python and Go: install, set up, and how they handle idempotency, retries and errors. All three behave the same way.

> **Note.** The SDKs are built but not published to npm, PyPI or a public Go module proxy yet. Until they are, install them from the toolkit repository, or use curl or any HTTP client.

## Install

| Language | Package | Needs |
|---|---|---|
| TypeScript / JavaScript | `@redswitches/api` | Node 20+ or any runtime with `fetch`. No other dependencies. |
| Python | `redswitches` | Python 3.9+. One dependency: `httpx`. Sync `Client` and async `AsyncClient`. |
| Go | `github.com/redswitches/redswitches-go` | Go 1.23+. Standard library only. |

Until the packages are published, install from a checkout of the toolkit repository:

```bash
npm install ./sdk/typescript          # TypeScript: @redswitches/api
pip install ./sdk/python              # Python: redswitches
# Go: point a replace directive in your go.mod at the sdk/go folder
```

## Set up a client

Each client reads the key from `REDSWITCHES_API_KEY` when you do not pass one. The key is sent only in the `Authorization` header and never appears in logs, errors or printed objects.

```bash
export REDSWITCHES_API_KEY='rsw_live_XXXX…'
```

```ts
import { RedSwitches } from "@redswitches/api";

const rs = new RedSwitches(); // or new RedSwitches({ apiKey, timeoutMs: 30000, maxRetries: 3 })
const key = await rs.auth.verify();
```

```python
from redswitches import Client

with Client() as client:  # or Client(api_key="rsw_live_XXXX…", max_retries=3)
    key = client.auth.verify()
```

```go
client := redswitches.NewClient("") // "" reads REDSWITCHES_API_KEY
key, err := client.Auth.Verify(ctx)
```

Lists can be read page by page (`list` with `limit` and `cursor`) or walked to the end (`iterate` / `all` in TypeScript and Python, `All` in Go), which follows `next_cursor` for you.

## Idempotency

- Every POST and DELETE carries an `Idempotency-Key`. Pass your own (`idempotencyKey`, `idempotency_key=`, `RequestOptions{IdempotencyKey}`), or the SDK makes a fresh one for the call.
- Every retry of that call reuses the same key, so a retried order or reboot happens once.
- An order dry run (`dryRun: true`, `dry_run=True`, `OrderOptions{DryRun: true}`) sends no `Idempotency-Key`: it changes nothing.
- A real order needs `expected_total` from a dry run; the SDKs refuse to send one without it.
- If a write fails with a network error, call it again with the same key to get the original outcome. Python errors carry the key that was sent (`err.idempotency_key`).

See [Idempotency and retries](idempotency.md) for what the API returns on a repeat.

## Retries

The SDKs retry only when it is safe and the API asks for it:

| Answer | Retried? |
|---|---|
| Network error or timeout | Yes, with the same Idempotency-Key. Never for `GET /actions/{id}/credentials`. |
| `429 rate_limited` | Yes, after `Retry-After`. |
| `503 unavailable` | Yes, after `Retry-After`, else 2, 4, 8 seconds. Never for `GET /actions/{id}/credentials`. |
| `409 in_progress` | Yes, after `Retry-After` / `details.retry_after`, with the same key. |
| `429 action_limit` | No. A ceiling of the account; it will not pass in seconds. |
| `403 cooling_off` | No. Ordering opens later; `details.retry_after` says when. |
| Anything else (`500`, `409 server_busy`, `409 price_changed`, `409 order_under_review`, …) | No. |

At most 3 retries by default. A requested wait longer than about a minute is returned as the error instead of slept.

`GET /actions/{id}/credentials` is never retried after a network error or a 5xx answer: the lost answer may already have used up the one read. It is retried only after `429 rate_limited`, when nothing was read. See [Rescue and one-time credentials](rescue.md#read-the-credentials-once).

## Errors

Every failure is one error type with the API's `code`, `status`, `message`, `details` and request id. Write your code against `code`; quote the request id to support.

```ts
import { RedSwitchesError } from "@redswitches/api";

try {
  await rs.orders.create({ ...body, expected_total: quote.total });
} catch (e) {
  if (e instanceof RedSwitchesError && e.code === "price_changed") console.log("now", e.details.total);
  else throw e;
}
```

```python
from redswitches import RedSwitchesError

try:
    client.orders.create(**body, expected_total=quote.total)
except RedSwitchesError as err:
    if err.code != "price_changed":
        raise
    print("now", err.details)
```

```go
_, err := client.Orders.Create(ctx, req, nil)
if redswitches.ErrorCode(err) == "price_changed" {
	var apiErr *redswitches.Error
	if errors.As(err, &apiErr) {
		fmt.Println("now", apiErr.Details.Total)
	}
}
```

Client-side failures have their own codes in TypeScript (`network_error`, `timeout`, `invalid_argument`, …), and their own exception classes in Python (`APIConnectionError`, …). Go returns the transport error itself.

All error codes are on [Rate limits and errors](errors.md).

## Also built on the SDKs

- [CLI: rxctl](cli.md), on the TypeScript SDK.
- [MCP server](mcp.md), on the TypeScript SDK.
- [Terraform provider](terraform.md), on the Go SDK.
