# SDKs

The API is plain JSON over HTTPS and every language can call it directly. Where a client saves you real work, it is because of the parts that are not plain requests: the audio graph in a browser, signature verification, retries and idempotency.

## What you can install today

| Client | Install | For |
| --- | --- | --- |
| **Node** `@sphoro/voice` | `npm i @sphoro/voice` | Server-side: agents, calls, knowledge, webhooks, minting call tokens. |
| **Browser** `@sphoro/voice-web` | `npm i @sphoro/voice-web` | Talking to an agent from a page: microphone, playback, the realtime protocol, barge-in. |

> **Python, Go and Java clients exist and are not on a public registry.** They are handed out per account rather than published, so this page does not print an install line that would fail. [Ask us](https://voice.sphoro.com/docs/support) if you want one — and in the meantime, the API is nine endpoints for most integrations and a hand-written client is an afternoon.

## Node

**Shell**

```bash
npm install @sphoro/voice
```

**Node**

```js
import { Sphoro } from "@sphoro/voice";

const sphoro = new Sphoro({ apiKey: process.env.SPHORO_API_KEY, baseUrl: "https://voice.sphoro.com" });

const agent = await sphoro.agents.create({
  name: "Acme Clinic reception",
  languages: [{ code: "en-IN", greeting: "Thanks for calling Acme Clinic.",
                system_prompt: "You are the receptionist for Acme Clinic…" }],
});

const call = await sphoro.calls.create({
  agentId: agent.id,
  to: "+919876543210",
  callVariables: { first_name: "Priya" },
});

console.log(call.id, call.status);   // call_…  queued
```

### Verifying a webhook

The one piece of this that is genuinely easy to get subtly wrong by hand — the timestamp is part of what is signed, and the comparison has to be constant-time.

**Node**

```js
import { verifyWebhook } from "@sphoro/voice";

app.post("/hooks/sphoro", express.raw({ type: "application/json" }), (req, res) => {
  // The RAW body. Re-serialised JSON will not verify.
  const event = verifyWebhook(req.body, req.headers, process.env.SPHORO_WEBHOOK_SECRET);
  if (event.type === "call.analysed") {
    save(event.call_id, event.data.summary, event.data.extracted);
  }
  res.sendStatus(200);
});
```

See [webhooks](https://voice.sphoro.com/docs/webhooks) for the scheme and the traps.

## Browser

**Shell**

```bash
npm install @sphoro/voice-web
```

**Node**

```js
import { connect } from "@sphoro/voice-web";

// The url comes from YOUR server, which mints it with your API key.
// Never ship an API key to a page.
const { url } = await fetch("/api/voice-token", { method: "POST" }).then(r => r.json());

const call = await connect({ url });
call.on("state", s => setIndicator(s));            // listening | thinking | speaking
call.on("transcript", t => appendLine(t.role, t.text));
call.on("ended", e => showSummary(e.end_reason));

document.querySelector("#hangup").onclick = () => call.end();
```

It handles the microphone, playback, the wideband format negotiation, barge-in and the heartbeat — all of which are on the list of things a hand-written client gets wrong. See [talk from a browser](https://voice.sphoro.com/docs/browser) for the server half, and [the realtime protocol](https://voice.sphoro.com/docs/realtime) for what is underneath.

### Without a bundler

The browser client is also served by this deployment, so a page can load it with a plain script tag and no build step at all. The tag to paste, and the one server route behind it, are on [talk from a browser](https://voice.sphoro.com/docs/browser).

`GET /v1/embed/`

## Writing your own client

Perfectly reasonable, and for a server integration usually less work than evaluating a dependency. Four things to implement, in order of how much they matter:

1. **Send `Idempotency-Key` on every `POST /v1/calls`.** Without it a retry places a second call to a real person.
2. **Retry `429` and `5xx` with backoff; never retry a `4xx`.** The response says how long to wait.
3. **Verify webhook signatures against the raw body.**
4. **Follow pagination** rather than assuming one page. See [using your API key](https://voice.sphoro.com/docs/authentication).

> **Generate one instead.** This deployment serves its own OpenAPI description at [openapi.json](https://voice.sphoro.com/openapi.json), which most generators take directly. It is produced by the running server, so it describes the version you are actually talking to. See [API reference](https://voice.sphoro.com/docs/reference).

## What every client does the same way

|  |  |
| --- | --- |
| Base URL | `https://voice.sphoro.com`, overridable, so one build can point at more than one deployment. |
| Credential | An API key for anything server-side; a short-lived call token for anything a browser holds. |
| Errors | Raised with the status, the machine-readable code, and the field-level problems where there are any. |
| Identifiers | Opaque strings. Store them whole. |
