---
title: SDKs
description: Native SDKs are on the roadmap. Today, call the REST API directly or generate a client from the OpenAPI spec.
---

# SDKs

> **Status: not yet published.** There is no `pip install arcanum` or
> `npm install @arcnm/sdk` package today — native SDKs are on the
> roadmap. In the meantime the REST API is small and stable, and you can
> generate a typed client from the public OpenAPI spec. This page will
> document the official SDKs when they ship.

The full machine-readable spec lives at:

```
https://api.arcnm.io/api/v1/openapi-public.json
```

---

## Generate a client from OpenAPI

Use [openapi-generator](https://openapi-generator.tech/) or
[Speakeasy](https://www.speakeasyapi.dev/) to scaffold a client in your
language:

```bash
curl https://api.arcnm.io/api/v1/openapi-public.json > arcanum.json
openapi-generator-cli generate -i arcanum.json -g python -o ./arcanum-py
# or -g typescript-fetch / go / java / ruby / rust / csharp …
```

Remember to send the API key in the `X-API-Key` header (see
[Authentication](./authentication.md)).

---

## Python — thin wrapper with `requests`

```python
import os, time, requests

API = "https://api.arcnm.io/api/v1"
HEADERS = {"X-API-Key": os.environ["ARCNM_API_KEY"]}

def upload_and_quote(env_id, part_number, cad_path, **fields):
    with open(cad_path, "rb") as cad:
        r = requests.post(
            f"{API}/parts/calculations/upload-and-quote",
            headers=HEADERS,
            data={"costing_environment_id": env_id, "part_number": part_number, **fields},
            files={"cad_file": cad},
        )
    r.raise_for_status()
    return r.json()

def wait(calc_id, timeout=120):
    deadline = time.time() + timeout
    while time.time() < deadline:
        calc = requests.get(f"{API}/parts/calculations/{calc_id}", headers=HEADERS).json()
        if calc["status"] in ("succeeded", "failed", "cancelled", "timed_out"):
            return calc
        time.sleep(2)
    raise TimeoutError(calc_id)

calc = upload_and_quote(env_id, "BRACKET-001", "bracket.step", lot_size=50, material_ref="1.4301")
result = wait(calc["id"])
print(result["status"], result["unit_cost"], result["currency"])
```

---

## TypeScript — thin wrapper with `fetch`

```typescript
const API = "https://api.arcnm.io/api/v1"
const H = { "X-API-Key": process.env.ARCNM_API_KEY! }

async function uploadAndQuote(envId: string, partNumber: string, cad: Blob, fields: Record<string, string> = {}) {
  const form = new FormData()
  form.set("costing_environment_id", envId)
  form.set("part_number", partNumber)
  for (const [k, v] of Object.entries(fields)) form.set(k, v)
  form.set("cad_file", cad, "part.step")
  const res = await fetch(`${API}/parts/calculations/upload-and-quote`, { method: "POST", headers: H, body: form })
  if (!res.ok) throw new Error(`${res.status} ${await res.text()}`)
  return res.json()
}

async function wait(calcId: string, timeoutMs = 120_000) {
  const deadline = Date.now() + timeoutMs
  while (Date.now() < deadline) {
    const calc = await (await fetch(`${API}/parts/calculations/${calcId}`, { headers: H })).json()
    if (["succeeded", "failed", "cancelled", "timed_out"].includes(calc.status)) return calc
    await new Promise((r) => setTimeout(r, 2000))
  }
  throw new Error(`timeout: ${calcId}`)
}
```

> In the browser, **never** ship a live `ak_live_…` key. Proxy requests
> through your backend.

---

## Errors & retries

Switch on the error `code` in the [standard envelope](./errors.md), and
back off on `429` / `5xx` (see [Rate limits](./rate-limits.md)). Pair
retries with an [`Idempotency-Key`](./idempotency.md).

## Webhooks

Verify webhook signatures with the Standard-Webhooks scheme — a complete
Python and TypeScript verifier is in [Webhooks → verifying
signatures](./webhooks.md#verifying-signatures).

---

## Stay in the loop

Want the native SDK when it lands? Email `hello@arcnm.io` and we'll
notify you.
