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.

UnauthenticatedAuthenticated
Fitpublic pages and developmentanything behind a login
Who can use Speechineer in your formanyone who can open the formonly users your backend issues a token to
API keyModeUnsignedSigned
Locationyour frontend — the client passes it in its apiKey optionyour backend — the token carries it in its api_key claim
AccountLocationyour frontend — the client names it in its account optionyour backend — the token carries it in its account_key claim
Signing keynot neededneeded — 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:

OptionTypeWhat it does
apiKeystringThe 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:

tsx
// 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>

Warning

Never put a Signed API key here — it would be refused (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:

OptionTypeWhat 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

With 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:

PartClaimValue
HeaderalgPS256 — the only algorithm a Signed API key accepts.
kidThe signing key's Key ID, as the workspace shows it under Signing keys.
Payloadapi_keyThe Signed API key. This is how the token names your workspace.
account_keyThe end user this session is for — your user id. Becomes the account in the workspace.
account_pseudonymOptional. The name the workspace shows for that account; applied on every session that carries it.
audAlways "speechineer".
iatWhen the token was issued, in Unix seconds.
expWhen it expires — at most 24 hours after iat. Keep it short, minutes, since the SDK asks for a new token at every start.
jtiA 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.

ts
// 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 });
});

Danger

The private half of the signing key is the one secret in this setup: keep it in your backend's secret store, never in a repository, a bundle or a log. If it leaks, rotate the signing key in the workspace — the old one stops verifying at the deadline you pick.

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:

tsx
// 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>