Integrate the SDK › Implementation
Create the client
A client holds two things — the API key and the default account — and every session you open starts from a client. The form is not part of it: you name the form when you open a session, so one client serves every form in your app as long as they share the same API key and account. Only a part of the app that needs a different API key or account gets a client of its own. Where each applies depends on your framework: in React the nearest provider wins, so a nested provider gives its subtree another client; in Angular the client is provided, so a route or a component can carry its own; in plain JavaScript you keep one object per client.
Access to the Speechineer service is either unauthenticated or authenticated, and the client is created accordingly — each way has its own section below.
| Unauthenticated | Authenticated | ||
|---|---|---|---|
| Fit | public pages and development | anything behind a login | |
| Who can use Speechineer in your form | anyone who can open the form | only users your backend issues a token to | |
| API key | Mode | Unsigned | Signed |
| Location | your frontend — the client passes it in its apiKey option | your backend — the token carries it in its api_key claim | |
| Account | Location | your frontend — the client names it in its account option | your backend — the token carries it in its account_key claim |
| Signing key | not needed | needed — its private half signs every token | |
Unauthenticated
Unauthenticated access is the simplest way in: the frontend holds an Unsigned API key and tells Speechineer who the end user is by naming the account itself — nothing runs in your backend. It fits pages that anyone may open, an appointment request or a sign-up, and development, since what protects your usage is the quotas set on the API key rather than a login. The client takes the API key and the account once, where it is created, and every session inherits them.
Options
What the client is created with:
| Option | Type | What it does |
|---|---|---|
| apiKey | string | The Unsigned API key of your workspace. Readable by anyone who opens the page — use it where the page is public anyway, and in development. |
| account | { key: string; pseudonym?: string } | Who this end user is — a stable id per user, never a shared constant: usage is attributed and limited per account. Required here. The pseudonym is the name your workspace shows for it. |
Example
One client for the whole app — one call per framework:
// main.tsx — once, around your app.
import { SpeechineerProvider } from "@speechineer/react";
<SpeechineerProvider
apiKey={import.meta.env.VITE_SPEECHINEER_API_KEY} // the Unsigned API key
account={{ key: currentUser.id, pseudonym: currentUser.name }} // who this end user is
>
<App />
</SpeechineerProvider>// app.config.ts — once.
import { provideSpeechineer } from "@speechineer/angular";
export const appConfig: ApplicationConfig = {
providers: [
provideSpeechineer({
apiKey: environment.speechineerApiKey, // the Unsigned API key
account: { key: currentUser.id, pseudonym: currentUser.name }, // who this end user is
}),
],
};// speechineer-client.js — once, at app startup; import it wherever you open a session.
import { createClient } from "@speechineer/js";
export const speechineer = createClient({
apiKey: "spnr_live_…", // the Unsigned API key
account: { key: currentUser.id, pseudonym: currentUser.name }, // who this end user is
});Warning
AUTH_MODE_MISMATCH) and, worse, exposed. An Unsigned key belongs to a page whose visitors you would let use Speechineer anyway, capped by the quotas you set on that key.Authenticated
Authenticated access puts your own login in front of Speechineer: your backend signs a short-lived token for the user who is signed in, and the frontend hands the client a function that fetches it. The Signed API key and the account travel inside the token, so nothing secret ever sits in the browser and only users your backend vouches for can open a session. It fits anything behind a login — a members' area, a staff tool. Two pieces, then: an endpoint in your backend that signs the token, and a client in your frontend that calls it.
Options
What the client is created with:
| Option | Type | What it does |
|---|---|---|
| token | () => Promise<string> | A function that fetches a fresh token from your backend. The SDK calls it at every session start; the API key and the account travel inside the token. |
Note
token the client takes no apiKey and no account: both are inside the token, and an account given to the client or to a session is ignored.Generate a token
An endpoint of your backend, behind your own login, signs the token. It reads three values from your configuration — the Signed API key, the signing key's Key ID and its private half, all copied from the workspace — and never returns them; only the signed token leaves the server. The token carries:
| Part | Claim | Value |
|---|---|---|
| Header | alg | PS256 — the only algorithm a Signed API key accepts. |
| kid | The signing key's Key ID, as the workspace shows it under Signing keys. | |
| Payload | api_key | The Signed API key. This is how the token names your workspace. |
| account_key | The end user this session is for — your user id. Becomes the account in the workspace. | |
| account_pseudonym | Optional. The name the workspace shows for that account; applied on every session that carries it. | |
| aud | Always "speechineer". | |
| iat | When the token was issued, in Unix seconds. | |
| exp | When it expires — at most 24 hours after iat. Keep it short, minutes, since the SDK asks for a new token at every start. | |
| jti | A unique id per token. A token is accepted once; reuse fails with AUTH_ENVELOPE_REPLAYED. |
The endpoint itself, in Node.js or Python: it sits behind your own login, signs a token with the claims above for the user who is signed in, and returns it — nothing else leaves the server.
// server/speechineer-token.ts — sits behind your own login, like any other route.
import { Router } from "express";
import { SignJWT, importPKCS8 } from "jose";
import { randomUUID } from "node:crypto";
// From the workspace: the Signed API key, the signing key's Key ID and its private half.
const API_KEY = process.env.SPEECHINEER_API_KEY!;
const KEY_ID = process.env.SPEECHINEER_SIGNING_KEY_ID!;
const privateKey = await importPKCS8(process.env.SPEECHINEER_SIGNING_KEY_PEM!, "PS256");
export const speechineerToken = Router().get("/api/speechineer/token", async (req, res) => {
const user = req.user; // whoever your session middleware authenticated
if (!user) return res.status(401).end();
const token = await new SignJWT({
api_key: API_KEY, // which workspace, and that it is the Signed key
account_key: user.id, // the account this session is for — your user id
account_pseudonym: user.name, // optional: the name the workspace shows for it
})
.setProtectedHeader({ alg: "PS256", kid: KEY_ID })
.setAudience("speechineer")
.setIssuedAt()
.setExpirationTime("5m") // short — the SDK asks for a new one at every start
.setJti(randomUUID()) // unique per token — a token is accepted once
.sign(privateKey);
res.json({ token });
});# speechineer_token.py — sits behind your own login, like any other route.
import os, time, uuid
import jwt # PyJWT with the cryptography extra
from fastapi import APIRouter, Depends
from .auth import current_user # your own dependency
router = APIRouter()
# From the workspace: the Signed API key, the signing key's Key ID and its private half.
API_KEY = os.environ["SPEECHINEER_API_KEY"]
KEY_ID = os.environ["SPEECHINEER_SIGNING_KEY_ID"]
PRIVATE_KEY_PEM = os.environ["SPEECHINEER_SIGNING_KEY_PEM"]
@router.get("/api/speechineer/token")
def speechineer_token(user=Depends(current_user)):
now = int(time.time())
token = jwt.encode(
{
"api_key": API_KEY, # which workspace, and that it is the Signed key
"account_key": user.id, # the account this session is for — your user id
"account_pseudonym": user.name, # optional: the name the workspace shows for it
"aud": "speechineer",
"iat": now,
"exp": now + 300, # short — the SDK asks for a new one at every start
"jti": str(uuid.uuid4()), # unique per token — a token is accepted once
},
PRIVATE_KEY_PEM,
algorithm="PS256",
headers={"kid": KEY_ID},
)
return {"token": token}Danger
Additional information in
Example
The frontend never signs anything: it fetches a token from your endpoint and hands the client a function that does so. The SDK calls that function at every session start, so expiry and single-use tokens are never your problem. The provider function and the client together, once, at the root of the logged-in part of your app:
// main.tsx — once, around the part of your app that needs a login.
import { SpeechineerProvider } from "@speechineer/react";
// The token provider: the SDK calls it before every session start, so a fresh,
// short-lived token is always used. It runs in the browser with the user's own
// login cookie — nothing secret lives here.
async function fetchSpeechineerToken(): Promise<string> {
const res = await fetch("/api/speechineer/token", { credentials: "include" });
if (!res.ok) throw new Error(`Not signed in (${res.status})`);
const { token } = (await res.json()) as { token: string };
return token;
}
<SpeechineerProvider token={fetchSpeechineerToken}>
<App />
</SpeechineerProvider>// app.config.ts — once.
import { provideSpeechineer } from "@speechineer/angular";
// The token provider: the SDK calls it before every session start, so a fresh,
// short-lived token is always used. It runs in the browser with the user's own
// login cookie — nothing secret lives here.
async function fetchSpeechineerToken(): Promise<string> {
const res = await fetch("/api/speechineer/token", { credentials: "include" });
if (!res.ok) throw new Error(`Not signed in (${res.status})`);
const { token } = (await res.json()) as { token: string };
return token;
}
export const appConfig: ApplicationConfig = {
providers: [provideSpeechineer({ token: fetchSpeechineerToken })],
};// speechineer-client.js — once, at app startup; import it wherever you open a session.
import { createClient } from "@speechineer/js";
// The token provider: the SDK calls it before every session start, so a fresh,
// short-lived token is always used. It runs in the browser with the user's own
// login cookie — nothing secret lives here.
async function fetchSpeechineerToken() {
const res = await fetch("/api/speechineer/token", { credentials: "include" });
if (!res.ok) throw new Error(`Not signed in (${res.status})`);
const { token } = await res.json();
return token;
}
export const speechineer = createClient({ token: fetchSpeechineerToken });