Examples
Voice agent integrations
Connect Vapi, Retell AI, or OpenAI voice calls to Currai with server-side credentials, readable conversation evidence, and nested model traces.
These guides show how to connect a browser voice agent in your own application to Currai. Choose your provider:
Runnable reference implementations are available in the public curraiapp/examples repository. Each provider lives in an independent folder with its own environment template, lockfile, commands, tests, and README.
| Guide | Use it when | Transcript strategy |
|---|---|---|
| Vapi voice agent | Your browser call uses @vapi-ai/web | Display partial updates, but capture finalized transcript messages once |
| Retell AI voice agent | Your application creates Retell web calls | Reconcile rolling updates, then prefer Retell's completed call transcript |
| OpenAI voice agent | Your browser uses OpenAI Realtime WebRTC or TTS | Reconcile Realtime item events; capture TTS as a nested rendering step |
git clone git@github.com:curraiapp/examples.git
cd examplesAll three integrations follow the same Currai contract. The voice provider runs the live conversation. Your server sends a separate, best-effort record to Currai without changing the call outcome.
Recommended: use the Currai Skill
The fastest path is to let your coding agent inspect the application and wire the real voice-call boundary. From the root of your application repository, install the published Currai Skill:
npx skills add https://github.com/curraiapp/skills --skill curraiConfigure the Currai credentials in the server environment first, then ask the coding agent:
Use $currai to connect this Vapi, Retell AI, or OpenAI voice application to Currai using
the CURRAI_PUBLIC_KEY, CURRAI_SECRET_KEY, and CURRAI_BASE_URL already configured
in the server environment. Find and instrument the live conversation path, not
only the assistant or agent creation route. Keep capture best-effort, restart or
deploy the application, complete one real call, and verify the session, readable
User Story, and nested provider conversation and model events in Currai.Tell the agent whether the application uses Vapi, Retell AI, or OpenAI and, in a monorepo, which package owns the browser call. The skill will:
- locate the provider's live call, transcript, error, and call-end handlers;
- add dependency-free native HTTP capture without installing a Currai SDK;
- keep Currai credentials, redaction, and retries in trusted server code;
- create one Currai session for each complete call and preserve parent-child event nesting;
- verify a real provider conversation instead of treating a standalone connectivity event as proof.
The provider guides below include ready-to-use prompts and the exact evidence that the skill should produce. Their manual sections are also the reference implementation when you want to review or customize the generated wiring.
Shared architecture
Browser voice call
├── provider client or audio transport
├── live transcript state
└── POST completed call to your server
│
Your server ├── validate and redact
├── create Currai session
├── emit call boundary
└── emit conversation turns
└── nested model eventsThe browser may receive a provider's public or temporary call credential. Currai ingestion credentials must always remain on your server.
Configure Currai
Create an ingestion key from Workspace Settings → API Keys, then add these variables to your server environment:
CURRAI_PUBLIC_KEY=<your-currai-public-key>
CURRAI_SECRET_KEY=<your-currai-secret-key>
CURRAI_BASE_URL=https://www.currai.appDo not use a browser-public prefix such as NEXT_PUBLIC_. Restart or redeploy the application after changing environment values.
Send native HTTP capture
You do not need a Currai SDK. Your server sends authenticated JSON to:
POST /api/v1/capture-session
POST /api/v1/capture-eventBuild HTTP Basic authentication only on the server:
function curraiAuthorization() {
const publicKey = process.env.CURRAI_PUBLIC_KEY;
const secretKey = process.env.CURRAI_SECRET_KEY;
if (!publicKey || !secretKey) return null;
return "Basic " + Buffer.from(`${publicKey}:${secretKey}`).toString("base64");
}Use a short timeout and return a boolean rather than throwing into the product request:
async function sendToCurrai(path: string, body: unknown) {
const authorization = curraiAuthorization();
const baseUrl = process.env.CURRAI_BASE_URL;
if (!authorization || !baseUrl) return false;
try {
const response = await fetch(`${baseUrl.replace(/\/$/, "")}${path}`, {
method: "POST",
headers: {
"content-type": "application/json",
authorization,
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(2_000),
});
return response.ok;
} catch {
return false;
}
}Before serialization, recursively redact object keys matching authorization headers, cookies, passwords, secrets, API keys, access tokens, refresh tokens, and bearer tokens.
Create the session first
Create one UUID session for one complete voice call. Await session capture before sending its first event:
const sessionAccepted = await sendToCurrai("/api/v1/capture-session", {
session_id: sessionId,
user_data: {
user_id: userId,
properties: { source: "browser-voice" },
},
metadata: {
route: "browser-voice-call",
environment: process.env.NODE_ENV ?? "development",
},
timestamp: startedAt,
client_config: "currai-native-http",
});
if (!sessionAccepted) {
return Response.json(
{ error: "Capture service unavailable." },
{ status: 503 },
);
}Do not emit events before this request succeeds. Otherwise, events may arrive without their session context.
Preserve readable conversation evidence
For every normalized turn, send a top-level agent event with recognized input and output fields:
{
event_id: agentEventId,
session_id: sessionId,
parent_id: null,
kind: "agent",
primitive_name: "provider.conversation.turn",
args: JSON.stringify({ input: userText }),
result: JSON.stringify({ output: assistantText }),
success: true,
}Currai recognizes user evidence in fields such as args.input, args.prompt, and args.message. It recognizes assistant evidence in result.output, result.text, result.response, and result.answer.
A custom transcript property is trace metadata, not a replacement for these recognized fields. A session and trace can exist while the User Story remains empty if the top-level agent events do not carry recognizable conversation evidence.
Nest provider execution
Emit the model event as a child of the conversation turn:
{
event_id: modelEventId,
session_id: sessionId,
parent_id: agentEventId,
kind: "model",
primitive_name: "provider.model",
args: JSON.stringify({ input: userText }),
result: JSON.stringify({ output: assistantText }),
success: true,
metadata: {
provider: knownModelProvider,
model: knownModelName,
},
}Only include provider and model values your application knows reliably. Do not infer them from a default when runtime routing can change them.
The expected event tree is:
event: provider.voice_call
agent: provider.conversation.turn
└── model: provider.modelStable retries
Generate sessionId, the call-boundary event ID, and child event IDs before the first upload. Build the completed payload once and reuse it on every retry.
This matters because Currai may accept the session and some events before the network fails. Fresh IDs would turn a retry into duplicate trace identities.
Retry temporary 429 and 5xx responses after short delays. Do not retry an unchanged invalid payload after a 400. Capture should always remain independent from the live call.
Definition of done
Complete one real provider call, then verify:
- The provider's call history contains the matching call.
- Currai Events contains one session and one call-boundary event.
- Each model event is nested beneath its conversation turn.
- Analyse → User Stories contains at least one visible user or assistant message.
- A failed provider call records
success: falseand a safe reason. - A Currai failure does not interrupt or change the voice call.
Choose Vapi, Retell AI, or OpenAI voice for the provider-specific workflow.
