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. |
Node
npm install @sphoro/voiceimport { 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_… queuedVerifying 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.
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 for the scheme and the traps.
Browser
npm install @sphoro/voice-webimport { 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 for the server half, and the realtime protocol 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.
/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:
- Send
Idempotency-Keyon everyPOST /v1/calls. Without it a retry places a second call to a real person. - Retry
429and5xxwith backoff; never retry a4xx. The response says how long to wait. - Verify webhook signatures against the raw body.
- Follow pagination rather than assuming one page. See using your API key.
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. |