`
API key for authentication (overrides config)
```bash theme={null}
60db --api-key your_key users --list
```
# Installation
Source: https://docs.60db.ai/cli-reference/installation
How to install 60db CLI globally or locally
# Installation
**Source code:** [github.com/60db-ai/cli](https://github.com/60db-ai/cli) · **npm:** [`60db-cli`](https://www.npmjs.com/package/60db-cli)
## Prerequisites
* Node.js 16 or higher
* npm or yarn package manager
## Global Installation
Install globally to use the `60db` command anywhere:
```bash theme={null}
npm install -g 60db-cli
```
Verify installation:
```bash theme={null}
60db --version
```
## Local Installation
Install as a dependency in your project:
```bash theme={null}
npm install 60db-cli
```
Use with npx:
```bash theme={null}
npx 60db users --list
```
## Install from Source
Clone the public GitHub repo and link the binary locally — useful when you want to build from a specific branch or commit:
```bash theme={null}
git clone https://github.com/60db-ai/cli.git
cd cli
npm install
npm link # exposes the `60db` binary on your PATH
60db --version
```
## Post-Installation
After installation, configure your API credentials:
```bash theme={null}
# Set API URL
60db config --set apiBaseUrl=https://api.60db.ai
# Set API key
60db config --set apiKey=your_api_key_here
# Or login with email/password
60db auth:login --email your@email.com --password yourpassword
```
## Troubleshooting
### Command not found
If you get `command not found` after global installation:
1. Check npm global bin directory:
```bash theme={null}
npm config get prefix
```
2. Add it to your PATH (if not already):
```bash theme={null}
export PATH=$(npm config get prefix)/bin:$PATH
```
3. Or reinstall with sudo (Linux/Mac):
```bash theme={null}
sudo npm install -g 60db-cli
```
# 60db CLI
Source: https://docs.60db.ai/cli-reference/introduction
Agent-native command-line interface for the full 60db platform — AI completions, TTS, STT, voice cloning, memory/RAG, Cerbos policy checks, billing, and workspaces with structured JSON output.
# 60db CLI
Agent-native command-line interface for the full 60db platform. Covers AI completions, TTS, STT, voice cloning, persistent memory with document upload (PDF, DOCX, XLSX, scanned images with OCR), Cerbos policy checks, billing, and workspace management — all with structured JSON output for scripts and AI agents.
**Source code:** [github.com/60db-ai/cli](https://github.com/60db-ai/cli) · **npm:** [`60db-cli`](https://www.npmjs.com/package/60db-cli) · **License:** MIT
## Features
* **Dual Mode**: Interactive REPL + subcommand interface
* **Agent-Native**: Structured JSON output with `--json` flag
* **AI Completions**: Chat, meeting notes analysis, and text completion
* **TTS (Text-to-Speech)**: Generate speech from text with multiple voices
* **STT (Speech-to-Text)**: Transcribe audio files with multiple languages
* **Memory & RAG**: Ingest memories, upload documents (91+ formats with OCR), hybrid semantic search, LLM-ready context assembly, per-month spend tracking
* **Authorization (Cerbos)**: Inspect effective permissions and probe `(resource, action)` checks before attempting gated operations
* **Authentication**: Secure login and token management
* **Complete Coverage**: Users, Credits, Billing, Workspaces, Categories, Memory, Authz
* **Configuration**: Persistent config with `60db config`
## Installation
### Global Installation
```bash theme={null}
npm install -g 60db-cli
```
### Local Installation
```bash theme={null}
npm install 60db-cli
```
### From Source
```bash theme={null}
git clone https://github.com/60db-ai/cli.git
cd cli
npm install
npm link # exposes the `60db` binary globally
```
## Quick Start
### Interactive Mode (REPL)
```bash theme={null}
60db
60db --version
```
Enter the interactive shell with tab completion and persistent session:
```
60db> users
60db> credits:add --user-id 123 --amount 50
60db> config --list
60db> exit
```
### Command Mode
```bash theme={null}
# List all users
60db users --list
# Add credits to user
60db credits:add --user-id 123 --amount 50 --currency USD
# Upload a document to memory with OCR
60db memory:upload --file ~/Documents/handbook.pdf --type knowledge
# Search with hybrid semantic + keyword recall
60db memory:search --query "refund policy" --limit 5
# Check if current user can ingest memories
60db authz:check --resource memory --action create
# Get JSON output for agent consumption
60db --json users --list
```
## Environment Variables
```bash theme={null}
# Set API URL
export SIXTYDB_API_URL=https://api.60db.ai
# Set API Key
export SIXTYDB_API_KEY=your_api_key_here
```
Legacy `X60DB_*` variable names are still honored for backward compatibility.
## JSON Output (for AI Agents)
All commands support `--json` flag for structured output:
```bash theme={null}
60db --json users --list
```
Response:
```json theme={null}
{
"success": true,
"users": [
{
"id": 123,
"email": "user@example.com",
"full_name": "John Doe",
"system_role": "user",
"is_active": true,
"is_verify_email": true,
"created_at": "2024-01-15T10:30:00Z"
}
],
"pagination": {
"total_users": 1,
"total_pages": 1,
"current_page": 1,
"limit": 10
}
}
```
## Error Handling
All commands return structured error responses:
```json theme={null}
{
"success": false,
"error": "User not found"
}
```
## Requirements
* Node.js >= 16.0.0
* npm or yarn
## Support
For issues or questions, please visit:
* GitHub: [https://github.com/60db-ai/cli](https://github.com/60db-ai/cli)
* Issues: [https://github.com/60db-ai/cli/issues](https://github.com/60db-ai/cli/issues)
## License
MIT
# Chat
Source: https://docs.60db.ai/features/llm-chat
Powerful AI chat with text correction, streaming, and function calling
## Overview
60db's LM (Large Language Model) API provides intelligent chat completions with advanced text correction capabilities, streaming responses, and function calling support. Our Small Language Model (SLM) is optimized for fast, efficient responses.
## Features
Drop-in compatible with OpenAI's chat completion format
Smart text correction with dictionary and style options
Server-Sent Events for instant response streaming
Built-in tool/function calling support
## Basic Usage
```javascript theme={null}
import { SixtyDBClient } from '60db';
const client = new SixtyDBClient('your-api-key');
const response = await client.chat.completions.create({
model: '60db-tiny',
messages: [
{ role: 'system', content: 'You are a helpful assistant.' },
{ role: 'user', content: 'How can I improve my English?' }
],
stream: true
});
for await (const chunk of response) {
console.log(chunk.choices[0]?.delta?.content);
}
```
```python theme={null}
from sixtydb import SixtyDBClient
client = SixtyDBClient('your-api-key')
response = client.chat.completion(
model='60db-tiny',
messages=[
{'role': 'system', 'content': 'You are a helpful assistant.'},
{'role': 'user', 'content': 'How can I improve my English?'}
],
stream=True
)
print(response['choices'][0]['message']['content'])
```
```bash theme={null}
curl --location 'https://api.60db.ai/v1/chat/completions' \
--header 'Content-Type: application/json' \
--data '{
"model": "60db-tiny",
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Hello! How are you?"}
],
"stream": true
}'
```
## Streaming Responses
Real-time streaming with Server-Sent Events:
```javascript theme={null}
const response = await fetch("https://api.60db.ai/v1/chat/completions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "60db-tiny",
messages: [{ role: "user", content: "Tell me a story" }],
stream: true,
}),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split("\n");
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = JSON.parse(line.slice(6));
if (data.choices?.[0]?.delta?.content) {
console.log(data.choices[0].delta.content);
}
}
}
}
```
## Function Calling (Tools)
Define tools the model can use:
```javascript theme={null}
const response = await client.chat.completions.create({
model: "60db-tiny",
messages: [
{ role: "user", content: "What is the weather in San Francisco?" },
],
tool: [
{
name: "get_weather",
description: "Get the current weather for a location",
parameters: {
type: "object",
properties: {
location: {
type: "string",
description: "The city and state, e.g. San Francisco, CA",
},
unit: {
type: "string",
enum: ["celsius", "fahrenheit"],
},
},
required: ["location"],
},
},
],
});
```
### Streaming Response (SSE)
```
data: {"chat_id": "550e8400-e29b-41d4-a716-446655440000", "type": "chat_id"}
data: {"id": "chatcmpl-123", "choices": [{"delta": {"content": "I'm"}}]}
data: {"id": "chatcmpl-123", "choices": [{"delta": {"content": " doing"}}]}
data: {"id": "chatcmpl-123", "choices": [{"delta": {"content": " well!"}}]}
data: {"type": "done", "response_time_ms": 1250}
data: [DONE]
```
## Application Contexts
Supported contexts for text correction:
| Context | Description |
| ---------- | -------------------------------- |
| `email` | Professional email communication |
| `chat` | Casual chat/messaging |
| `document` | Formal documents/reports |
| `message` | Short messages/notifications |
| `social` | Social media posts |
```javascript theme={null}
const response = await client.chat.completions.create({
text: "hey can u send me the report pls",
appContext: "email",
style: { tone: "professional", autoCapitalize: true },
});
// Output: "Hey, can you send me the report, please?"
```
## Best Practices
* Be specific in your system prompt
* Provide examples for few-shot learning
* Use appropriate tone for your use case
* Test different prompts for optimal results
* Use streaming for better UX - Handle connection errors gracefully - Buffer
chunks for smooth display - Implement timeout handling
* Cache common responses - Use shorter prompts when possible - Enable chat
history for context - Monitor token usage
* Use dictionary for domain-specific terms
* Set appropriate app context
* Test style options for your use case
* Combine multiple style options
## Use Cases
### Customer Support Chatbot
```javascript theme={null}
async function handleCustomerMessage(message) {
const response = await client.chat.completions.create({
model: "60db-tiny",
messages: [
{
role: "system",
content:
"You are a helpful customer support assistant. Be friendly and professional.",
},
{ role: "user", content: message },
],
stream: true,
save_chat: true,
chat_id: customerId,
});
return response;
}
```
### Text Correction Service
```javascript theme={null}
async function correctText(text, context = "email") {
const response = await client.chat.completions.create({
text: text,
appContext: context,
dictionary: commonTypos,
style: {
tone: "professional",
autoCapitalize: true,
autoPunctuate: true,
},
});
return response.choices[0].message.content;
}
```
### AI Assistant with Tools
```javascript theme={null}
async function aiAssistant(userQuery) {
const response = await client.chat.completions.create({
messages: [{ role: "user", content: userQuery }],
tool: [getWeatherTool, searchDatabaseTool, sendEmailTool],
});
// Handle tool calls and return results
return response;
}
```
## Pricing
Charges are based on token usage:
| Metric | Cost |
| --------- | ----------- |
| Per Token | \~\$0.00002 |
Token usage is tracked and billed to your workspace. Monitor usage via the Analytics API.
## API Reference
For detailed API documentation, see:
Complete API reference with all parameters and examples
# Memory & RAG
Source: https://docs.60db.ai/features/memory
Persistent memory, semantic recall, and retrieval-augmented generation (RAG) for AI chat
## Overview
60db's **Memory** system gives your AI applications a long-term, searchable memory. It stores user preferences, conversation history, knowledge base documents, and arbitrary facts, then retrieves the most relevant ones on demand using hybrid semantic + keyword search.
Built on a multi-layer retrieval architecture combining vector search with a knowledge graph, Memory enables:
* **Personalized AI chat** — the SLM remembers user preferences across sessions
* **Knowledge base Q\&A** — ingest docs and retrieve grounded answers
* **Multi-user collaboration** — shared "team" memory collections
* **Graph-aware recall** — find related concepts through knowledge-graph traversal
Semantic (vector) + keyword (BM25) scoring with configurable weights
One-shot endpoint returns an LLM-ready context string for any query
Extracted facts link together as a knowledge graph
Personal, team, knowledge, and hive (cross-collection) memory types
Upload PDFs, Office docs, and scanned images — text extraction + OCR built in
PDF, DOCX, XLSX, PPTX, EML, MSG, HTML, scanned images with built-in OCR
## Core concepts
### Memory collections
Memories live in **collections** scoped to your workspace. Each collection is one of:
| Kind | Visibility | Who can write |
| ------------- | --------------------------------- | ------------------------------------- |
| **personal** | Only the owning user | Owner + admin (automatic per-user) |
| **team** | All members of the workspace | Owner/admin create; all members write |
| **knowledge** | All members (read-only reference) | Owner/admin only |
| **hive** | Cross-collection shared facts | Owner/admin only |
Your personal collection is created automatically the first time you use Memory. Team/knowledge/hive collections are created by owners/admins.
### Memory types
When you store a memory, you specify its type:
* **`user`** — private, user-scoped facts. Auto-extracted from conversations or manually entered.
* **`knowledge`** — reference content (docs, policies, FAQs). Read by all members.
* **`hive`** — workspace-wide shared facts that appear in every user's search results.
### Two search modes
* **Fast** — single-query hybrid search. Returns in \~100ms. Default.
* **Thinking** — multi-query expansion with reranking. Better recall quality, slower (\~1-2s).
## Storing memories
```javascript theme={null}
import { SixtyDBClient } from '60db';
const client = new SixtyDBClient('your-api-key');
// Store a personal memory
await client.memory.ingest({
text: "I prefer vegetarian food and am lactose intolerant",
type: "user",
infer: true, // extract structured facts via LLM
});
// Store a knowledge base entry
await client.memory.ingest({
text: "Our refund policy allows returns within 30 days of purchase.",
title: "Refund Policy",
type: "knowledge",
collection: "company_handbook",
});
```
```python theme={null}
from sixtydb import SixtyDBClient
client = SixtyDBClient(api_key="your-api-key")
# Store a personal memory
client.memory.ingest(
text="I prefer vegetarian food and am lactose intolerant",
type="user",
infer=True,
)
# Store a knowledge base entry
client.memory.ingest(
text="Our refund policy allows returns within 30 days of purchase.",
title="Refund Policy",
type="knowledge",
collection="company_handbook",
)
```
```bash theme={null}
curl -X POST https://api.60db.com/memory/ingest \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "I prefer vegetarian food and am lactose intolerant",
"type": "user",
"infer": true
}'
```
### Response
```json theme={null}
{
"success": true,
"data": {
"collection_id": "user_abc123",
"collection_label": "Personal Memories",
"memory_type": "user",
"total_queued": 1,
"results": [
{ "id": "mem_01HV...", "status": "pending", "message": "Queued for processing" }
]
}
}
```
Memories are processed asynchronously. Poll `GET /memory/:id/status` to check if ingestion is complete.
## Uploading documents
For longer-form content — PDFs, Word docs, spreadsheets, scanned pages, emails — use `POST /memory/documents/extract`. The server handles **format detection, OCR, and chunking** for you, so you just upload the raw file and 60db does the rest.
Under the hood, 60db runs your file through its document extraction engine (91+ formats with built-in OCR for scanned documents), splits the extracted text into overlapping chunks, and ingests each chunk as a `knowledge`-type memory in one batch.
```javascript theme={null}
// Upload a PDF from a file input
const form = new FormData();
form.append('file', pdfFile); // File or Blob
form.append('collection', 'company_handbook');
form.append('type', 'knowledge');
form.append('title', 'Employee Handbook 2026');
const res = await fetch('https://api.60db.com/memory/documents/extract', {
method: 'POST',
headers: { 'Authorization': 'Bearer sk_your_api_key' },
body: form, // do NOT set Content-Type — browser adds multipart boundary
});
const { data } = await res.json();
console.log(`Uploaded ${data.filename} → ${data.chunks} chunks (${data.characters} chars)`);
```
```python theme={null}
import requests
with open('handbook.pdf', 'rb') as f:
res = requests.post(
'https://api.60db.com/memory/documents/extract',
headers={'Authorization': 'Bearer sk_your_api_key'},
files={'file': ('handbook.pdf', f, 'application/pdf')},
data={
'collection': 'company_handbook',
'type': 'knowledge',
'title': 'Employee Handbook 2026',
},
)
data = res.json()['data']
print(f"Uploaded {data['filename']} → {data['chunks']} chunks")
```
```bash theme={null}
curl -X POST https://api.60db.com/memory/documents/extract \
-H "Authorization: Bearer sk_your_api_key" \
-F "file=@handbook.pdf" \
-F "collection=company_handbook" \
-F "type=knowledge" \
-F "title=Employee Handbook 2026"
```
### What you can upload
| Category | Formats |
| --------------------- | --------------------------------------------- |
| **Documents** | PDF, DOCX, DOC, ODT, RTF, TXT, MD, HTML, EPUB |
| **Spreadsheets** | XLSX, XLS, CSV, ODS |
| **Presentations** | PPTX, PPT, ODP |
| **Email** | EML, MSG, PST, MBOX |
| **Images (OCR)** | PNG, JPG, JPEG, TIFF, BMP, GIF |
| **Code & structured** | JSON, XML, YAML, LaTeX |
| **Archives** | ZIP, TAR, GZIP, 7Z (extracted recursively) |
Max file size: **200 MB**. Max chunks per document: **100** (tune `chunk_size` for larger docs).
### Response
```json theme={null}
{
"success": true,
"data": {
"collection_id": "company_handbook",
"collection_label": "Company Handbook",
"filename": "handbook.pdf",
"chunks": 18,
"characters": 24680,
"memory_type": "knowledge",
"total_queued": 18,
"results": [ /* one {id, status, message} per chunk */ ],
"metadata": {
"source": "document_upload",
"mime_type": "application/pdf",
"page_count": 24,
"detected_languages": ["eng"],
"total_chunks": 18
}
}
}
```
The returned `metadata.page_count` and `detected_languages` come from the document extraction engine and are useful for displaying upload progress or filtering by source language. Scanned PDFs will list the OCR-detected language codes (`eng`, `fra`, `spa`, etc.).
### Chunking controls
Two optional form fields tune how text is split:
| Field | Default | Description |
| --------------- | ------- | ----------------------------------------------------- |
| `chunk_size` | 1500 | Max characters per chunk (200–8000) |
| `chunk_overlap` | 200 | Characters of overlap between chunks (\< chunk\_size) |
See the [API reference](/api-reference/memory/extract-document) for a tuning table by document type.
## Searching memories
```javascript theme={null}
const results = await client.memory.search({
query: "What are my dietary preferences?",
mode: "fast",
max_results: 10,
alpha: 0.8, // 0.8 semantic / 0.2 keyword
recency_bias: 0.1,
});
results.data.sources.forEach(source => {
console.log(source.title, source.text, source.score);
});
```
```python theme={null}
results = client.memory.search(
query="What are my dietary preferences?",
mode="fast",
max_results=10,
alpha=0.8,
recency_bias=0.1,
)
for source in results["data"]["sources"]:
print(source["title"], source["text"], source["score"])
```
```bash theme={null}
curl -X POST https://api.60db.com/memory/search \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "What are my dietary preferences?",
"mode": "fast",
"max_results": 10,
"alpha": 0.8
}'
```
### Tuning search
| Parameter | Default | Description |
| --------------- | ------- | -------------------------------------------------------------- |
| `mode` | `fast` | `fast` (single-query) or `thinking` (multi-query reranked) |
| `alpha` | 0.8 | Weight of semantic search: 0 = keyword only, 1 = semantic only |
| `recency_bias` | 0.0 | Weight given to newer memories (0-1) |
| `max_results` | 10 | Max results returned (capped at 50) |
| `graph_context` | false | Include knowledge-graph relationships in response |
## Context assembly (RAG for SLM chat)
The `/memory/context` endpoint is purpose-built for retrieval-augmented generation. Given a user query, it fetches the most relevant memories, recent events, and graph relationships, and returns a pre-formatted context string ready to prepend to your LLM prompt.
```javascript theme={null}
// Before calling the SLM, assemble context
const ctx = await client.memory.context({
query: userMessage,
session_id: chatSessionId,
top_k: 8,
max_context_length: 2000,
include_timeline: true,
include_graph: false,
});
// Prepend to system message
const systemMessage = `You are a helpful assistant.
## Relevant memories
${ctx.data.prompt_ready}`;
// Call SLM chat with enriched context
const response = await client.chat.completions.create({
model: '60db-tiny',
messages: [
{ role: 'system', content: systemMessage },
{ role: 'user', content: userMessage },
],
});
```
```python theme={null}
# Before calling the SLM, assemble context
ctx = client.memory.context(
query=user_message,
session_id=chat_session_id,
top_k=8,
max_context_length=2000,
include_timeline=True,
)
system_message = f"""You are a helpful assistant.
## Relevant memories
{ctx['data']['prompt_ready']}
"""
response = client.chat.completions.create(
model="60db-tiny",
messages=[
{"role": "system", "content": system_message},
{"role": "user", "content": user_message},
],
)
```
## Built-in SLM Chat integration
When using 60db's UI at `/app/slm-chat`, there's a **Memory** toggle next to Auto-clear. When enabled, every message you send is pre-processed:
1. Your message is sent to `/memory/context` with your session ID
2. Relevant memories and recent events are fetched (semantic + keyword + temporal)
3. The returned `prompt_ready` string is prepended to the system message
4. The enriched prompt goes to the SLM
This means your AI chat remembers context across sessions automatically. Toggle it off if you want a fresh, memoryless conversation.
## Collections management
```javascript theme={null}
// List your collections
const collections = await client.memory.collections.list();
// Create a team collection (admin/owner only)
await client.memory.collections.create({
collection_id: "customer_support",
label: "Customer Support KB",
kind: "team",
shared: true,
});
```
## Role-based access
Memory operations are gated by your workspace role:
| Action | Owner | Admin | Developer | Member | Viewer |
| ---------------------- | :---: | :---: | :-------: | :----: | :----: |
| Search memories | ✓ | ✓ | ✓ | ✓ | ✓ |
| Create personal memory | ✓ | ✓ | ✓ | ✓ | — |
| Delete own memory | ✓ | ✓ | ✓ | ✓ | — |
| Delete any memory | ✓ | ✓ | — | — | — |
| Create team collection | ✓ | ✓ | — | — | — |
| Create knowledge/hive | ✓ | ✓ | — | — | — |
| Export memories | ✓ | ✓ | ✓ | ✓ | — |
## API key access
To use Memory via an API key (for programmatic access), the key must have the `memory` scope. When creating an API key in Settings → Developers, check the "Memory & RAG" box.
```javascript theme={null}
// API key with memory scope
const response = await fetch('https://api.60db.com/memory/search', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_your_api_key',
'Content-Type': 'application/json',
},
body: JSON.stringify({ query: 'user preferences', max_results: 5 }),
});
```
## Pricing
Memory is **pay-as-you-go** — no subscription, no seat pricing, no minimum commitment. You pay only for the operations you run, deducted from a single workspace wallet you top up via Stripe, Razorpay, or Dodo Payments.
| Operation | Rate |
| ---------------------------- | ------------------------------------------- |
| Ingest a memory | **\$0.0001 per 1,000 characters** |
| Upload a document (extract) | **\$0.003 per MB** |
| Upload a document (ingest) | **\$0.0001 per 1,000 extracted characters** |
| Search (hybrid recall) | **\$0.0003 per query** |
| Context assembly (LLM-ready) | **\$0.0005 per query** |
**Real-world cost examples:**
* A knowledge base with 100 MB of docs + 10,000 searches/month → **\~\$23/month**
* A personal assistant with 1,000 user memories + 500 searches/day → **\~\$5/month**
* A support bot with 1 GB of docs + 100,000 searches/month → **\~\$53/month**
Compared to proprietary memory services at $249–$5,000/month flat, 60db Memory is 5–50x cheaper for most workloads — and you only pay for what you actually use.
Every billable request returns these response headers so you can track spend without polling:
```
x-credit-balance: 9.465200 ← wallet balance after this charge
x-credit-charged: 0.000300 ← amount charged for this request
x-billing-tx: 84ffd09e-... ← audit row UUID
```
**Automatic refunds** — if a request fails after being charged (upstream outage, corrupt file, etc.), the charge is reversed automatically and logged as a compensating row in `transaction_log`. No support tickets required.
**Never billed** — listing collections, creating collections, checking memory status, deleting memories, and `GET /memory/usage` are **always free** so you can still manage your data when the wallet is empty.
See the full [Pricing & Billing reference](/api-reference/memory/pricing) for rate details, refund policy, and the complete header/error reference.
## Handling insufficient credits
When the wallet runs out, billable endpoints return **HTTP 402**:
```json theme={null}
{
"success": false,
"message": "Insufficient credits",
"error_code": "INSUFFICIENT_CREDITS",
"details": {
"required": 0.0003,
"available": 0.00001,
"shortfall": 0.00029
}
}
```
Your client should catch `error_code === "INSUFFICIENT_CREDITS"` and prompt the user to top up. Here's a pattern for the search endpoint:
```javascript theme={null}
async function searchWithTopUpPrompt(query) {
const res = await fetch('https://api.60db.com/memory/search', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_your_api_key',
'Content-Type': 'application/json',
},
body: JSON.stringify({ query, max_results: 10 }),
});
if (res.status === 402) {
const err = await res.json();
showTopUpModal({
shortfall: err.details.shortfall,
available: err.details.available,
topUpUrl: 'https://app.60db.com/app/billing',
});
return null;
}
const data = await res.json();
console.log(`Balance: $${res.headers.get('x-credit-balance')}`);
return data;
}
```
## Tracking usage
Call [`GET /memory/usage`](/api-reference/memory/usage) to get a monthly spend breakdown by operation type. This is what powers the **Spend this month** card on the 60db Memory dashboard.
```javascript theme={null}
const res = await fetch('https://api.60db.com/memory/usage?period=current_month', {
headers: { 'Authorization': 'Bearer sk_your_api_key' },
});
const { data } = await res.json();
console.log(`Spent $${data.total.net_spend_usd.toFixed(4)} this month`);
console.log(`Wallet: $${data.billing_owner.current_balance_usd.toFixed(4)}`);
```
## Failure handling
The Memory service is designed to **degrade gracefully**:
* If the memory layer is temporarily unreachable, `POST /memory/ingest` queues your memory in a retry table, returns `202 Accepted`, and **automatically refunds** the charge so you aren't billed for work that didn't happen.
* `POST /memory/context` returns an empty prompt on outage — your SLM chat still works, just without memory context. No charge when context is empty.
* `POST /memory/search` returns `503` — the UI shows a "Memory temporarily unavailable" banner without blocking other features. Auto-refunded.
* `POST /memory/documents/extract` auto-refunds the extract fee if extraction fails (corrupt file, empty PDF, OCR error). If extraction succeeds but the wallet can't cover the post-extraction ingest fee, the extract fee is refunded and a `402` is returned.
## Limits
* **Ingest batch**: Up to 100 memories per request
* **Memory text**: Max 100,000 characters per entry
* **Query length**: Max 2,000 characters
* **Results**: Max 50 per search (refine query for more precise results)
* **Context length**: Max 16,000 tokens assembled per request
* **Document upload**: Max 200 MB per file, max 100 chunks per document
* **Rate limit**: 30 ingests/min per workspace
## Further reading
* [API Reference: Pricing & Billing](/api-reference/memory/pricing)
* [API Reference: Get Memory Usage](/api-reference/memory/usage)
* [API Reference: Ingest memory](/api-reference/memory/ingest)
* [API Reference: Upload document](/api-reference/memory/extract-document)
* [API Reference: Search memories](/api-reference/memory/search)
* [API Reference: Context assembly](/api-reference/memory/context)
* [API Reference: Collections](/api-reference/memory/collections)
# Speech-to-Text
Source: https://docs.60db.ai/features/speech-to-text
Transcribe audio to text with high accuracy
## Overview
60db's Speech-to-Text (STT) API converts spoken audio into written text with high accuracy across 39 languages, including code-switched Indic+English. Powered by 60db STT v01 (a non-hallucinating, multi-backend speech recognition stack) — non-hallucinating models that don't invent text on silent or noisy input.
## Features
39 languages with auto-detection and Indic+English code-switching
Opt-in pyannote speaker diarization via `diarize: true`
Word-level timestamps included automatically
Non-hallucinating backend that emits blank tokens on silence — no phantom text
## Basic Usage
```javascript theme={null}
import { SixtyDBClient } from '60db';
const client = new SixtyDBClient('your-api-key');
const file = document.querySelector('input[type="file"]').files[0];
const result = await client.speechToText(file, {
language: 'en'
});
console.log('Transcription:', result.text);
console.log('Confidence:', result.confidence);
```
```python theme={null}
from sixtydb import SixtyDBClient
client = SixtyDBClient('your-api-key')
with open('recording.mp3', 'rb') as audio_file:
result = client.speech_to_text(audio_file, language='en')
print(f"Transcription: {result['text']}")
print(f"Confidence: {result['confidence']}")
```
## Supported Formats
| Format | Max Size | Max Duration | Quality |
| ------ | -------- | ------------ | --------- |
| MP3 | 25MB | 10 min | Good |
| WAV | 25MB | 10 min | Excellent |
| FLAC | 25MB | 10 min | Lossless |
| OGG | 25MB | 10 min | Good |
| M4A | 25MB | 10 min | Good |
## Language Support
### Auto-Detection
Let the API automatically identify the language. **Omit the `language` field** (or pass the string `"auto"`) and the server runs language identification across all 39 supported languages:
```javascript theme={null}
// Simplest form — omit language entirely
const result = await client.speechToText(file);
// Equivalent — the REST endpoint treats "auto" as omission.
// Note: on the WebSocket streaming endpoint you must use `languages: null`,
// not the literal string "auto".
const result2 = await client.speechToText(file, { language: 'auto' });
console.log('Detected language:', result.language);
```
### Specify Language
For lowest latency, pass a single ISO 639-1 code and the server skips language identification entirely:
```javascript theme={null}
const result = await client.speechToText(file, {
language: 'hi' // ISO 639-1 code
});
```
**Unsupported codes** (`ur`, `ja`, `ko`, `zh`, `th`, `vi`, `id`, `tl`, `sw`, `tr`, `fa`, `he`) and Arabic dialect tags (`ar-eg`, `ar-lv`, …) return an `unsupported_language` error. For non-MSA Arabic audio, pass `ar` for best-effort MSA transcription.
### Get Supported Languages
```javascript theme={null}
const languages = await client.getLanguages();
languages.forEach(lang => {
console.log(`${lang.name} (${lang.code})`);
});
```
## Advanced Features
### Word-Level Timestamps
Word timings are always included in the response — no flag needed:
```javascript theme={null}
const result = await client.speechToText(file);
// Word-level timing lives inside each segment
result.segments.forEach(segment => {
segment.words.forEach(word => {
console.log(`${word.word}: ${word.start}s - ${word.end}s (${word.confidence})`);
});
});
```
### Speaker Diarization
Identify different speakers with `diarize: true`:
```javascript theme={null}
const result = await client.speechToText(file, {
diarize: true
});
// Each segment carries a `speakers` array when diarization is enabled.
// Raw labels are SPEAKER_00, SPEAKER_01, …
result.segments.forEach(segment => {
const speaker = segment.speakers?.[0]?.speaker ?? 'unknown';
console.log(`[${speaker}] ${segment.text}`);
});
```
Client applications typically re-label the raw `SPEAKER_NN` IDs as "Speaker 1", "Speaker 2" in order of first appearance for readability.
## Best Practices
* Use high-quality recordings (16kHz+ sample rate)
* Minimize background noise
* Ensure clear speech
* Avoid audio compression when possible
* Specify the language when known
* Use appropriate model for your use case
* Provide clean audio without music
* Split very long recordings
* Keep files under 25MB
* Use appropriate format (WAV for quality, MP3 for size)
* Process in batches for multiple files — but stay under the 8-concurrent STT cap per user
## Use Cases
### Meeting Transcription
```python theme={null}
with open('meeting.mp3', 'rb') as audio:
result = client.speech_to_text(
audio,
diarize=True,
)
# Generate meeting notes
for segment in result['segments']:
speaker = (segment.get('speakers') or [{}])[0].get('speaker', 'unknown')
print(f"[{segment['start']:.1f}s] {speaker}: {segment['text']}")
```
### Voice Commands
```javascript theme={null}
async function processVoiceCommand(audioBlob) {
const result = await client.speechToText(audioBlob, {
language: 'en',
});
const command = parseCommand(result.text);
executeCommand(command);
}
```
### Subtitle Generation
```javascript theme={null}
const result = await client.speechToText(videoAudio);
const subtitles = generateSRT(result.words);
saveFile('subtitles.srt', subtitles);
```
## API Reference
View complete API documentation
# Streaming
Source: https://docs.60db.ai/features/streaming
Real-time audio streaming for low-latency applications
## Overview
Streaming allows you to receive audio chunks in real-time as they're generated, enabling immediate playback without waiting for the entire audio file to be created. This is essential for interactive applications like voice assistants and chatbots.
## Benefits
Start playing audio within milliseconds
Process chunks instead of loading entire file
Progressive audio playback feels more responsive
Handle unlimited text length efficiently
## How It Works
1. **Send Request**: Submit text to the streaming endpoint
2. **Receive Chunks**: Get audio chunks as they're generated
3. **Play Immediately**: Start playing the first chunk
4. **Continue Streaming**: Receive and play subsequent chunks
5. **Complete**: Receive completion notification
## Implementation
```javascript theme={null}
import { SixtyDBClient } from '60db';
const client = new SixtyDBClient('your-api-key');
// Audio player setup
const audioContext = new AudioContext();
const audioQueue = [];
await client.textToSpeechStream(
{
text: 'This is a longer text that will be streamed in real-time for immediate playback.',
voice_id: 'default-voice'
},
{
onChunk: async (chunk) => {
// Convert chunk to audio buffer
const audioBuffer = await audioContext.decodeAudioData(chunk.buffer);
// Add to queue and play
audioQueue.push(audioBuffer);
if (audioQueue.length === 1) {
playNextChunk();
}
},
onComplete: () => {
console.log('Streaming complete');
},
onError: (error) => {
console.error('Streaming error:', error);
}
}
);
function playNextChunk() {
if (audioQueue.length === 0) return;
const buffer = audioQueue[0];
const source = audioContext.createBufferSource();
source.buffer = buffer;
source.connect(audioContext.destination);
source.onended = () => {
audioQueue.shift();
playNextChunk();
};
source.start();
}
```
```python theme={null}
from sixtydb import SixtyDBClient
import pyaudio
client = SixtyDBClient('your-api-key')
# Audio player setup
p = pyaudio.PyAudio()
stream = p.open(
format=pyaudio.paInt16,
channels=1,
rate=24000,
output=True
)
def handle_chunk(chunk):
# Play audio chunk immediately
stream.write(chunk)
def handle_complete():
print("Streaming complete")
stream.stop_stream()
stream.close()
p.terminate()
def handle_error(error):
print(f"Error: {error}")
# Stream text to speech
client.text_to_speech_stream(
text='This is a longer text that will be streamed in real-time.',
on_chunk=handle_chunk,
on_complete=handle_complete,
on_error=handle_error,
voice_id='default-voice'
)
```
## Advanced Usage
### React Component
```jsx theme={null}
import { useState } from 'react';
import { SixtyDBClient } from '60db';
function StreamingTTS() {
const [isStreaming, setIsStreaming] = useState(false);
const [text, setText] = useState('');
const client = new SixtyDBClient(process.env.REACT_APP_API_KEY);
const streamAudio = async () => {
setIsStreaming(true);
await client.textToSpeechStream(
{ text, voice_id: 'default-voice' },
{
onChunk: (chunk) => {
// Play chunk
playAudioChunk(chunk);
},
onComplete: () => {
setIsStreaming(false);
},
onError: (error) => {
console.error(error);
setIsStreaming(false);
}
}
);
};
return (
);
}
```
### Voice Assistant
```javascript theme={null}
class VoiceAssistant {
constructor(apiKey) {
this.client = new SixtyDBClient(apiKey);
this.audioContext = new AudioContext();
}
async speak(text) {
return new Promise((resolve, reject) => {
this.client.textToSpeechStream(
{ text, voice_id: 'assistant-voice' },
{
onChunk: (chunk) => this.playChunk(chunk),
onComplete: () => resolve(),
onError: (error) => reject(error)
}
);
});
}
async playChunk(chunk) {
const audioBuffer = await this.audioContext.decodeAudioData(chunk.buffer);
const source = this.audioContext.createBufferSource();
source.buffer = audioBuffer;
source.connect(this.audioContext.destination);
source.start();
}
}
// Usage
const assistant = new VoiceAssistant('your-api-key');
await assistant.speak('Hello! How can I help you today?');
```
## Performance Optimization
### Buffering Strategy
```javascript theme={null}
class AudioStreamer {
constructor() {
this.chunks = [];
this.isPlaying = false;
this.minBufferSize = 3; // Wait for 3 chunks before playing
}
addChunk(chunk) {
this.chunks.push(chunk);
if (!this.isPlaying && this.chunks.length >= this.minBufferSize) {
this.startPlayback();
}
}
async startPlayback() {
this.isPlaying = true;
while (this.chunks.length > 0) {
const chunk = this.chunks.shift();
await this.playChunk(chunk);
}
this.isPlaying = false;
}
async playChunk(chunk) {
// Play audio chunk
return new Promise((resolve) => {
// Audio playback logic
setTimeout(resolve, chunkDuration);
});
}
}
```
## Best Practices
* Maintain a small buffer (2-3 chunks) for smooth playback
* Handle network interruptions gracefully
* Implement retry logic for failed chunks
* Always implement onError callback
* Provide user feedback during streaming
* Have fallback for streaming failures
* Reuse AudioContext instances
* Clean up resources after playback
* Monitor memory usage for long streams
* Show loading indicator before first chunk
* Allow users to stop streaming
* Provide playback controls
## Use Cases
### Real-time Chat
```javascript theme={null}
async function sendMessage(message) {
// Display user message
displayMessage('user', message);
// Get AI response
const response = await getAIResponse(message);
displayMessage('assistant', response);
// Stream audio response
await client.textToSpeechStream(
{ text: response },
{
onChunk: (chunk) => playAudioChunk(chunk),
onComplete: () => enableInput()
}
);
}
```
### Audiobook Player
```javascript theme={null}
async function playChapter(chapterText) {
let currentPosition = 0;
await client.textToSpeechStream(
{ text: chapterText },
{
onChunk: (chunk) => {
playChunk(chunk);
currentPosition += chunk.duration;
updateProgress(currentPosition);
},
onComplete: () => {
moveToNextChapter();
}
}
);
}
```
## API Reference
View complete streaming API documentation
# Text-to-Speech
Source: https://docs.60db.ai/features/text-to-speech
Convert text to natural-sounding speech
## Overview
60db's Text-to-Speech (TTS) API converts written text into natural-sounding speech using advanced AI models. Our TTS engine supports multiple voices, languages, and customization options.
## Features
Choose from 50+ pre-built voices or create custom voices
Adjust speed, stability, and similarity
Crystal-clear audio with natural intonation
Support for MP3, WAV, OGG, and FLAC output formats
## Basic Usage
```javascript theme={null}
import { SixtyDBClient } from '60db';
const client = new SixtyDBClient('your-api-key');
const audio = await client.textToSpeech({
text: 'Hello, world!',
voice_id: 'default-voice',
enhance: true,
speed: 1.0
});
```
```python theme={null}
from sixtydb import SixtyDBClient
client = SixtyDBClient('your-api-key')
audio = client.text_to_speech(
text='Hello, world!',
voice_id='default-voice',
enhance=True,
speed=1.0
)
with open('output.mp3', 'wb') as f:
f.write(audio)
```
## Voice Parameters
### Speed
Control the speaking rate of the generated audio:
```javascript theme={null}
const audio = await client.textToSpeech({
text: 'This will be spoken faster',
speed: 1.5 // Range: 0.5 to 2.0
});
```
* `0.5`: Half speed (slow)
* `1.0`: Normal speed (default)
* `2.0`: Double speed (fast)
### Stability
Control how expressive versus consistent the generated voice sounds:
```javascript theme={null}
const audio = await client.textToSpeech({
text: 'More consistent delivery',
stability: 50 // Range: 0 to 100 (default 50)
});
```
* Lower values produce more expressive, varied output
* Higher values produce more consistent, stable output
### Similarity
Control how closely the output matches the source voice:
```javascript theme={null}
const audio = await client.textToSpeech({
text: 'Close clone match',
similarity: 75 // Range: 0 to 100 (default 75)
});
```
### Enhancement
Enable audio enhancement for better quality:
```javascript theme={null}
const audio = await client.textToSpeech({
text: 'Enhanced audio quality',
enhance: true // Default: true
});
```
## Output Formats
Supported audio formats:
| Format | Quality | File Size | Use Case |
| ------ | --------- | --------- | --------------------- |
| MP3 | Good | Small | Web, mobile apps |
| WAV | Excellent | Large | Professional audio |
| OGG | Good | Small | Web streaming |
| FLAC | Lossless | Medium | High-quality archival |
```javascript theme={null}
const audio = await client.textToSpeech({
text: 'Hello, world!',
output_format: 'wav' // mp3, wav, ogg, flac
});
```
## Best Practices
* Use proper punctuation for natural pauses
* Break long texts into paragraphs
* Use SSML tags for advanced control (coming soon)
* Test multiple voices for your use case
* Consider accent and gender for your audience
* Use custom voices for brand consistency
* Cache frequently used audio
* Batch requests when possible
* Use appropriate audio format for your use case
* Enable enhancement for production use
* Use WAV format for highest quality
* Test with different speed settings
## Use Cases
### Voice Assistants
```javascript theme={null}
// Voice assistant
async function speakResponse(text) {
const audio = await client.textToSpeech({
text: text,
voice_id: 'assistant-voice',
enhance: true
});
playAudio(audio);
}
```
### Content Narration
```javascript theme={null}
// Generate audiobook chapter
const audio = await client.textToSpeech({
text: chapterText,
voice_id: 'narrator-voice',
speed: 0.95,
output_format: 'mp3'
});
saveToFile(`chapter-${chapterNum}.mp3`, audio);
```
### Accessibility
```javascript theme={null}
// Make web content accessible
async function readAloud(element) {
const text = element.textContent;
const audio = await client.textToSpeech({
text,
voice_id: 'clear-voice',
enhance: true
});
playAudio(audio);
}
```
## API Reference
For detailed API documentation, see:
Standard TTS endpoint
# Custom Voices
Source: https://docs.60db.ai/features/voices
Create and manage custom voice profiles
## Overview
Create custom voice profiles that match your brand identity or clone specific voices for personalized experiences. Our voice cloning technology requires just a few minutes of audio to create a high-quality custom voice.
## Creating a Custom Voice
### Requirements
* **Minimum**: 3 audio files
* **Maximum**: 10 audio files
* **Duration**: 10-60 seconds per file
* **Total**: At least 2 minutes combined
* **Format**: MP3, WAV, or FLAC
* **Quality**: 44.1kHz+ sample rate recommended
### Step-by-Step Guide
Record or collect 3-10 high-quality audio samples of the voice you want to clone
Use the API or dashboard to upload your audio files
Voice cloning typically takes 10-15 minutes
Generate test audio to verify quality
Start using your custom voice in your applications
## Code Examples
```javascript theme={null}
import { SixtyDBClient } from '60db';
const client = new SixtyDBClient('your-api-key');
// Create custom voice
const files = [
document.querySelector('#file1').files[0],
document.querySelector('#file2').files[0],
document.querySelector('#file3').files[0]
];
const voice = await client.createVoice({
name: 'My Brand Voice',
description: 'Professional voice for customer service',
language: 'en',
gender: 'female',
files: files
});
console.log('Voice ID:', voice.id);
console.log('Status:', voice.status);
// Use the custom voice
const audio = await client.textToSpeech({
text: 'Hello from my custom voice!',
voice_id: voice.id
});
```
```python theme={null}
from sixtydb import SixtyDBClient
client = SixtyDBClient('your-api-key')
# Create custom voice
files = [
open('sample1.mp3', 'rb'),
open('sample2.mp3', 'rb'),
open('sample3.mp3', 'rb')
]
voice = client.create_voice(
name='My Brand Voice',
files=files,
description='Professional voice for customer service',
language='en',
gender='female'
)
print(f"Voice ID: {voice['id']}")
print(f"Status: {voice['status']}")
# Close files
for f in files:
f.close()
# Use the custom voice
audio = client.text_to_speech(
text='Hello from my custom voice!',
voice_id=voice['id']
)
with open('output.mp3', 'wb') as f:
f.write(audio)
```
## Audio Sample Guidelines
### Content Recommendations
* Include different sentence types (questions, statements, exclamations)
* Cover various emotions and tones
* Use different speaking speeds
* Include both short and long sentences
* Record in a quiet environment
* Use a good quality microphone
* Maintain consistent volume
* Avoid background music or noise
* No echo or reverb
* Sample rate: 44.1kHz or higher
* Bit depth: 16-bit or higher
* Format: WAV (lossless) preferred
* Mono or stereo both acceptable
* Natural, conversational speech
* Clear pronunciation
* Consistent accent
* Avoid reading in monotone
* Include natural pauses
## Managing Custom Voices
### List Your Voices
```javascript theme={null}
const voices = await client.getVoices();
// Filter custom voices
const customVoices = voices.filter(v => v.is_custom);
customVoices.forEach(voice => {
console.log(`${voice.name} (${voice.id})`);
});
```
### Update Voice Metadata
```javascript theme={null}
await client.updateVoice('voice-id', {
name: 'Updated Voice Name',
description: 'Updated description'
});
```
### Delete a Voice
```javascript theme={null}
await client.deleteVoice('voice-id');
```
## Voice Quality Tips
Record in a quiet room with minimal echo and background noise
Use a quality microphone for best results
Speak naturally and expressively
Provide at least 2 minutes of total audio
## Use Cases
### Brand Voice
Create a consistent voice for all your brand communications:
```javascript theme={null}
const brandVoice = await client.createVoice({
name: 'Acme Brand Voice',
description: 'Official voice for Acme Corporation',
files: brandAudioSamples
});
// Use in all customer touchpoints
const greeting = await client.textToSpeech({
text: 'Welcome to Acme Corporation. How can we help you today?',
voice_id: brandVoice.id
});
```
### Personal Assistant
Clone your own voice for a personalized assistant:
```javascript theme={null}
const myVoice = await client.createVoice({
name: 'My Personal Voice',
description: 'My voice for personal assistant',
files: myRecordings
});
// Personal reminders in your own voice
const reminder = await client.textToSpeech({
text: 'Remember to call mom at 3 PM',
voice_id: myVoice.id
});
```
### Character Voices
Create unique voices for game characters or audiobooks:
```javascript theme={null}
const characterVoice = await client.createVoice({
name: 'Wizard Character',
description: 'Mystical wizard voice for fantasy game',
files: characterSamples
});
```
## Pricing
Custom voice creation is available on Pro and Enterprise plans:
| Plan | Custom Voices | Processing Time |
| ---------- | ------------- | ------------------- |
| Free | 0 | - |
| Starter | 0 | - |
| Pro | 5 | 10-15 min |
| Enterprise | Unlimited | Priority (5-10 min) |
## API Reference
Create a new custom voice
List, update, and delete voices
# LiveKit Integration
Source: https://docs.60db.ai/integrations/livekit
Use 60db STT, TTS, and LLM inside a LiveKit Agents voice pipeline.
# LiveKit Integration
Build real-time voice agents with [LiveKit Agents](https://github.com/livekit/agents) powered by 60db's speech and language services — STT, TTS, and LLM all in one plugin.
Real-time streaming transcription via WebSocket with interim results
Low-latency streaming synthesis with chunked audio delivery
OpenAI-compatible chat completions with tool-call support
***
## Installation
Requires Python **3.10+**.
```bash theme={null}
pip install livekit-plugins-60db
```
Choose one of the following methods:
**Option A — Environment variable (recommended):**
```bash theme={null}
export SIXTY_DB_API_KEY="your-api-key"
```
**Option B — `.env.local` file:**
```
SIXTY_DB_API_KEY=your-api-key
```
**Option C — Pass it directly in code:**
```python theme={null}
from livekit.plugins._60db import _60dbClient
client = _60dbClient("your-api-key") # sets global default
```
***
## Quick Start
Wire all three services together in a `VoicePipelineAgent`:
```python theme={null}
import asyncio
from livekit.agents import VoicePipelineAgent, cli, WorkerType
from livekit.plugins import silero
from livekit.plugins._60db import _60dbClient, STT, TTS, LLM
client = _60dbClient("your-api-key") # sets global default
async def entrypoint(ctx):
await ctx.connect()
agent = VoicePipelineAgent(
vad=silero.VAD.load(), # voice activity detection
stt=STT(), # 60db speech-to-text
llm=LLM(), # 60db language model
tts=TTS(), # 60db text-to-speech
)
agent.start(ctx.room)
if __name__ == "__main__":
cli.run_app(worker_type=WorkerType.ROOM, entrypoint_fnc=entrypoint)
```
***
## Configuration
### Environment Variables
All services read from the same environment variables by default. You only need to override them if you use a custom deployment.
| Variable | Default | Description |
| ------------------ | ----------------------------------------- | ---------------------------- |
| `SIXTY_DB_API_KEY` | — | Your 60db API key (required) |
| `SIXTY_DB_STT_URL` | `wss://api.60db.ai/ws/stt` | STT WebSocket endpoint |
| `SIXTY_DB_TTS_URL` | `wss://api.60db.ai/ws/tts` | TTS WebSocket endpoint |
| `SIXTY_DB_LLM_URL` | `https://api.60db.ai/v1/chat/completions` | LLM HTTP endpoint |
Each service also accepts a direct `ws_url` (or `api_url`) constructor argument which takes precedence over environment variables.
***
## Services
### Speech-to-Text
The STT service streams audio to 60db over WebSocket and returns transcriptions in real time — including interim (partial) results as the speaker is still talking.
#### Parameters
| Name | Type | Default | Description |
| ----------------- | ------------------- | ----------------------- | ------------------------------------------------ |
| `api_key` | `str \| None` | global client / env var | Your 60db API key |
| `ws_url` | `str \| None` | env var | WebSocket endpoint URL |
| `languages` | `list[str] \| None` | `["en"]` | Language codes for recognition |
| `encoding` | `str` | `"mulaw"` | Audio encoding format |
| `sample_rate` | `int` | `8000` | Audio sample rate in Hz |
| `continuous_mode` | `bool` | `True` | Keep the session open for continuous recognition |
#### Example
```python theme={null}
from livekit.plugins._60db import STT
stt = STT() # picks up api_key from global client or env var
async with stt.stream() as stream:
stream.push_frame(audio_frame) # push raw audio frames
async for event in stream:
if event.type == stt.SpeechEventType.FINAL_TRANSCRIPT:
print(event.alternatives[0].text)
```
#### Audio Formats
The plugin automatically handles audio conversion for you — no manual preprocessing needed.
| Step | What happens |
| ------------- | --------------------------------------------------------------------- |
| Stereo → Mono | Downmix via `audioop.tomono` |
| Resampling | Any input rate → target rate via `audioop.ratecv` |
| Encoding | LINEAR16 PCM → mulaw via `audioop.lin2ulaw` (when `encoding="mulaw"`) |
| Parameter | Default | Supported |
| ----------- | --------- | ------------------------- |
| Encoding | `mulaw` | `mulaw`, `LINEAR16` |
| Sample rate | `8000 Hz` | Any rate (auto-resampled) |
### Text-to-Speech
The TTS service converts text to speech over WebSocket. It supports both **one-shot** synthesis and **streaming** mode for low-latency incremental output.
#### Parameters
| Name | Type | Default | Description |
| ------------- | ------------- | ---------------------------------------- | ------------------------ |
| `api_key` | `str \| None` | global client / env var | Your 60db API key |
| `ws_url` | `str \| None` | env var | WebSocket endpoint URL |
| `voice_id` | `str` | `"fbb75ed2-975a-40c7-9e06-38e30524a9a1"` | Voice identifier |
| `encoding` | `str` | `"LINEAR16"` | Output audio encoding |
| `sample_rate` | `int` | `16000` | Output sample rate in Hz |
#### Examples
**One-shot synthesis** — best for short, complete responses:
```python theme={null}
from livekit.plugins._60db import TTS
tts = TTS()
async for chunk in tts.synthesize("Hello, how can I help you?"):
# chunk.data contains raw PCM audio bytes
print(f"Received {len(chunk.data)} bytes")
```
**Streaming synthesis** — best for incremental LLM output:
```python theme={null}
async with tts.stream() as stream:
stream.push_text("Hello, ")
stream.push_text("how can I help you?")
stream.end_input()
async for chunk in stream:
print(f"Received {len(chunk.data)} bytes")
```
#### Audio Format
| Parameter | Default | Notes |
| ----------- | ---------- | ------------------------------ |
| Encoding | `LINEAR16` | Raw PCM |
| Sample rate | `16000 Hz` | Configurable via `sample_rate` |
Audio is returned as base64-encoded PCM chunks by the server, decoded automatically by the plugin.
### Language Model
The LLM service provides OpenAI-compatible chat completions with SSE streaming and tool-call support.
#### Parameters
| Name | Type | Default | Description |
| ---------------------- | --------------- | ----------------------- | ----------------------------------- |
| `api_key` | `str \| None` | global client / env var | Your 60db API key |
| `api_url` | `str \| None` | env var | Chat completions endpoint URL |
| `model` | `str` | `"qcall/slm-3b-int4"` | Model identifier |
| `temperature` | `float \| None` | `None` | Sampling temperature (0–2) |
| `top_p` | `float \| None` | `None` | Nucleus sampling probability |
| `top_k` | `int \| None` | `None` | Top-K sampling parameter |
| `max_tokens` | `int \| None` | `None` | Maximum tokens in the response |
| `chat_template_kwargs` | `dict \| None` | `None` | Extra template kwargs for the model |
#### Example
```python theme={null}
from livekit.plugins._60db import LLM
from livekit.agents import llm
model = LLM(temperature=0.7)
chat_ctx = llm.ChatContext()
chat_ctx.append(role="system", text="You are a helpful voice assistant.")
chat_ctx.append(role="user", text="What is the capital of France?")
async for chunk in model.chat(chat_ctx=chat_ctx):
print(chunk.choices[0].delta.content, end="")
```
#### Tool Calls
The LLM supports OpenAI-style function tool calls. When the model calls a tool, the plugin accumulates the streamed argument fragments and emits a complete `FunctionToolCall` once the stream finishes.
***
## Timeouts
Control timeouts per-request by passing `APIConnectOptions`:
```python theme={null}
from livekit.agents import APIConnectOptions
opts = APIConnectOptions(timeout=30.0)
# STT
async with stt.stream(conn_options=opts) as s:
...
# TTS
async for chunk in tts.synthesize("Hello", conn_options=opts):
...
# LLM
async for chunk in model.chat(chat_ctx=ctx, conn_options=opts):
...
```
***
## Error Handling
All three services raise standard LiveKit Agents exceptions:
| Exception | When it's raised |
| -------------------- | ----------------------------------------------------------------------- |
| `APIConnectionError` | WebSocket handshake failure, HTTP error, or unexpected protocol message |
| `APITimeoutError` | Request exceeds the configured timeout |
| `ValueError` | Missing API key or service URL at construction time |
### Common HTTP Error Codes (LLM)
| Status | Meaning |
| ------ | -------------------------- |
| `401` | Invalid or missing API key |
| `429` | Rate limit exceeded |
| `500` | Server error |
| `503` | Service unavailable |
### Retry Tips
* **STT**: If you receive an `error` message during the handshake, reconnect with a short delay. A `connecting` status before `connection_established` is normal.
* **TTS**: Retry connection failures with exponential backoff.
* **LLM**: `httpx.TimeoutException` → `APITimeoutError`; `httpx.HTTPStatusError` → `APIConnectionError`.
***
## WebSocket Protocol Reference
**Connect:**
```
wss://api.60db.ai/ws/stt?apiKey={API_KEY}
```
**Handshake:**
Server → `{"connection_established": true}`
Client → start command:
```json theme={null}
{
"type": "start",
"languages": ["en"],
"config": {
"encoding": "mulaw",
"sample_rate": 8000,
"continuous_mode": true
}
}
```
Server → `{"type": "connected"}`
**Audio:** Send raw audio as **binary WebSocket frames**.
**Transcription response:**
```json theme={null}
{
"type": "transcription",
"text": "Hello, world",
"is_final": false,
"language": "en"
}
```
**Stop:**
Client → `{"type": "stop"}`
Server →
```json theme={null}
{
"type": "session_stopped",
"billing_summary": { "total_cost": "..." }
}
```
**Connect:**
```
wss://api.60db.ai/ws/tts?apiKey={API_KEY}
```
**Create context:**
```json theme={null}
{
"create_context": {
"context_id": "unique-id",
"voice_id": "fbb75ed2-975a-40c7-9e06-38e30524a9a1",
"audio_config": {
"audio_encoding": "LINEAR16",
"sample_rate_hertz": 16000
}
}
}
```
**Send text:**
```json theme={null}
{ "send_text": { "context_id": "unique-id", "text": "Hello!" } }
```
**Flush** (trigger audio generation):
```json theme={null}
{ "flush_context": { "context_id": "unique-id" } }
```
**Audio response:**
```json theme={null}
{ "audio_chunk": { "audioContent": "" } }
```
Followed by `{"flush_completed": true}` when all audio has been delivered.
**Close context:**
```json theme={null}
{ "close_context": { "context_id": "unique-id" } }
```
# Introduction
Source: https://docs.60db.ai/introduction
Welcome to 60db - The Modern Voice AI Platform
## What is 60db?
60db is a modern AI platform that combines state-of-the-art voice capabilities (TTS, STT, custom voices) with a hosted **LLM chat** and a **Memory / RAG layer** for building assistants that remember context and retrieve knowledge on demand. Developers can integrate natural-sounding voice, accurate speech recognition, and persistent memory into their applications with just a few lines of code.
## Key Features
Convert text to natural-sounding speech with multiple voice options
Transcribe audio to text with high accuracy across multiple languages
Create and manage custom voice profiles for your brand
Stream audio in real-time for low-latency applications
OpenAI-compatible chat completions with 60db's hosted SLM models
Persistent memory, document upload (PDF/DOCX/OCR), and semantic recall for AI chat
## Why Choose 60db?
Our TTS engine produces natural-sounding speech with proper intonation,
emotion, and clarity that rivals human speech.
Support for 50+ languages and dialects, making your application globally
accessible.
Simple SDKs for JavaScript/TypeScript and Python make integration
straightforward and quick.
Built on robust infrastructure that scales automatically to handle your
growing needs.
Low-latency streaming capabilities for real-time voice applications.
## Use Cases
* **Voice Assistants**: Build intelligent voice-enabled applications
* **Content Creation**: Generate voiceovers for videos and podcasts
* **Accessibility**: Make your content accessible to visually impaired users
* **Customer Service**: Create automated voice response systems
* **E-learning**: Develop interactive educational content with voice
* **Gaming**: Add realistic character voices to your games
## Getting Started
Ready to get started? Check out our [Quickstart Guide](/quickstart) to integrate 60db into your application in minutes.
Get up and running in under 5 minutes
Explore our comprehensive API documentation
# Claude desktop
Source: https://docs.60db.ai/mcp-server/claude-desktop
# Claude Desktop Integration
Integrate the 60db MCP Server with Claude Desktop.
## Configuration
### Step 1: Build the Server
```bash theme={null}
cd /path/to/mcp-server
npm install
npm run build
```
### Step 2: Locate Claude Desktop Config
**macOS**:
```
~/Library/Application Support/Claude/claude_desktop_config.json
```
**Windows**:
```
%APPDATA%/Claude/claude_desktop_config.json
```
### Step 3: Add Configuration
```json theme={null}
{
"mcpServers": {
"60db": {
"command": "node",
"args": ["/absolute/path/to/mcp-server/dist/index.js"],
"env": {
"SIXTYDB_API_KEY": "sk_your_api_key_here",
"SIXTYDB_API_BASE_URL": "https://api.60db.ai"
}
}
}
}
```
Use absolute paths. Relative paths may not work.
### Step 4: Restart Claude Desktop
Quit and restart Claude Desktop.
## Usage Examples
### Generate Speech
```
You: Convert "Hello, world!" to speech
```
### Transcribe Audio
```
You: Transcribe this audio: https://example.com/meeting.mp3
```
### List Voices
```
You: What English voices are available?
```
### Create Voice
```
You: Create a voice named "Brand Voice" from this sample
```
## Troubleshooting
### Server Not Appearing
1. Check the config file path
2. Verify the server path is absolute
3. Check the API key is set
4. Restart Claude Desktop
### Authentication Error
```bash theme={null}
export SIXTYDB_API_KEY=sk_your_api_key_here
```
## Next Steps
* [MCP Inspector](/mcp-server/mcp-inspector) - Test the server
# Configuration
Source: https://docs.60db.ai/mcp-server/configuration
# Configuration
Configure the 60db MCP Server for your environment.
## Environment Variables
### Required
```bash theme={null}
# 60db API Key (required)
export SIXTYDB_API_KEY=sk_your_api_key_here
```
### Optional
```bash theme={null}
# API Base URL (default: https://api.60db.ai)
export SIXTYDB_API_BASE_URL=https://api.60db.ai
# Debug mode (default: false)
export SIXTYDB_DEBUG=true
```
## Claude Desktop Configuration
Add to your Claude Desktop config:
**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
**Windows**: `%APPDATA%/Claude/claude_desktop_config.json`
```json theme={null}
{
"mcpServers": {
"60db": {
"command": "node",
"args": ["/absolute/path/to/mcp-server/dist/index.js"],
"env": {
"SIXTYDB_API_KEY": "sk_your_api_key_here",
"SIXTYDB_API_BASE_URL": "https://api.60db.ai"
}
}
}
}
```
Use absolute paths for the args array.
## Custom Application Integration
```typescript theme={null}
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
const transport = new StdioClientTransport({
command: "node",
args: ["/path/to/mcp-server/dist/index.js"],
env: {
SIXTYDB_API_KEY: "sk_your_api_key_here",
SIXTYDB_API_BASE_URL: "https://api.60db.ai"
}
});
const client = new Client({
name: "my-app",
version: "1.0.0"
}, {
capabilities: {}
});
await client.connect(transport);
```
## Testing
Test with MCP Inspector:
```bash theme={null}
npm install -g @modelcontextprotocol/inspector
mcp-inspector node dist/index.js
```
## Next Steps
* [Quick Start](/mcp-server/quickstart) - Test your setup
# Installation
Source: https://docs.60db.ai/mcp-server/installation
# Installation
Install and set up the 60db MCP Server.
**Source code:** [github.com/60db-ai/mcp](https://github.com/60db-ai/mcp) · **npm:** [`60db-mcp-server`](https://www.npmjs.com/package/60db-mcp-server)
## Prerequisites
* **Node.js 18+** - [Download](https://nodejs.org/)
* **60db API Key** - Get from [https://app.60db.ai](https://app.60db.ai)
## Install from npm
```bash theme={null}
npm install -g 60db-mcp-server
```
Verify the binary is on your PATH:
```bash theme={null}
60db-mcp-server --help
```
## Install from Source
```bash theme={null}
# Clone the public repository
git clone https://github.com/60db-ai/mcp.git
cd mcp
# Install dependencies
npm install
# Build the server (TypeScript → dist/)
npm run build
# Run it
npm start
```
## Verify Installation
```bash theme={null}
# Check if server is installed
60db-mcp-server --version
# Or if installed from source
npm start
```
## Environment Setup
Set your API key:
```bash theme={null}
# Set 60db API key
export SIXTYDB_API_KEY=sk_your_api_key_here
# Set API base URL (optional, defaults to https://api.60db.ai)
export SIXTYDB_API_BASE_URL=https://api.60db.ai
```
Alternatively, authenticate with a JWT:
```bash theme={null}
export SIXTYDB_JWT_TOKEN=your_jwt_token_here
```
Legacy `QLABS_API_KEY`, `QLABS_JWT_TOKEN`, and `QLABS_API_BASE_URL` env vars are still honored for backward compatibility — existing configs will continue to work after the rename, but new deployments should use the `SIXTYDB_*` names.
### Making it Permanent
Add to your `~/.bashrc`, `~/.zshrc`, or `~/.profile`:
```bash theme={null}
echo 'export SIXTYDB_API_KEY=sk_your_api_key_here' >> ~/.bashrc
source ~/.bashrc
```
## Development Setup
For development with auto-reload:
```bash theme={null}
npm run dev
```
## Production Build
```bash theme={null}
npm run build
npm start
```
## Next Steps
* [Configuration](/mcp-server/configuration) - Complete the setup
* [Claude Desktop](/mcp-server/claude-desktop) - Integrate with Claude
# Introduction
Source: https://docs.60db.ai/mcp-server/introduction
# MCP Server
The 60db MCP (Model Context Protocol) Server enables AI assistants like Claude to directly interact with the full 60db platform — voice AI, persistent memory, and access-control checks.
**Source code:** [github.com/60db-ai/mcp](https://github.com/60db-ai/mcp) · **npm:** [`60db-mcp-server`](https://www.npmjs.com/package/60db-mcp-server) · **License:** MIT
## What is the MCP Server?
The MCP Server provides a standardized interface for AI assistants to:
* **Generate speech** from text using any voice
* **Transcribe audio** files with speaker diarization
* **Create custom voices** from audio samples
* **Manage workspaces** and team collaboration
* **Store and recall memories** — persistent memory and RAG across sessions
* **Upload documents** — PDF, DOCX, XLSX, PPTX, EML, scanned images with built-in OCR
* **Assemble LLM-ready context** — purpose-built for retrieval-augmented generation
* **Check permissions** — query Cerbos policy before attempting gated actions
* **Track usage** and analytics
* **View monthly spend** across every billable operation
## Quick Start
### 1. Install the Server
```bash theme={null}
# From npm
npm install -g 60db-mcp-server
# Or clone from source
git clone https://github.com/60db-ai/mcp.git
cd mcp
npm install
npm run build
```
### 2. Configure Claude Desktop
Add to your Claude Desktop config:
**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
**Windows**: `%APPDATA%/Claude/claude_desktop_config.json`
```json theme={null}
{
"mcpServers": {
"60db": {
"command": "node",
"args": ["/path/to/mcp-server/dist/index.js"],
"env": {
"SIXTYDB_API_KEY": "sk_your_api_key_here",
"SIXTYDB_API_BASE_URL": "https://api.60db.ai"
}
}
}
}
```
### 3. Use with Claude
Restart Claude Desktop and start using 60db voice AI:
```
You: Can you convert "Hello, world!" to speech using a female English voice?
Claude: I'll use the 60db TTS tool to generate speech for you.
```
## Available Tools
| Category | Tools |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Voices** | List voices, get voice details, create custom voices |
| **TTS** | Synthesize speech, view generation history |
| **STT** | Transcribe audio, view transcription history |
| **Workspaces** | Manage team workspaces and members |
| **Memory & RAG** | Ingest memories, upload documents (91+ formats with OCR), hybrid semantic search, context assembly, manage collections, track spend (10 tools total) |
| **Authz** | Fetch effective permissions map, probe single `(resource, action)` checks — useful before attempting gated actions |
| **Analytics** | Track usage statistics |
| **Billing** | View plans, subscriptions, and invoices |
## Features
### Text-to-Speech
Generate natural speech in 40+ languages:
```typescript theme={null}
// Synthesize speech
{
"text": "Hello, world!",
"voice_id": "voice_abc123",
"speed": 1.0,
"output_format": "mp3"
}
```
### Speech-to-Text
Transcribe audio with speaker identification:
```typescript theme={null}
// Transcribe audio
{
"audio_url": "https://example.com/meeting.mp3",
"language": "en-US",
"diarization": true,
"timestamps": true
}
```
### Voice Cloning
Create custom voices from audio samples:
```typescript theme={null}
// Create custom voice
{
"name": "Brand Voice",
"sample_url": "https://example.com/sample.mp3",
"language": "en-US"
}
```
### Memory & RAG
Give your AI assistant a persistent, searchable long-term memory:
```typescript theme={null}
// Store a user preference
{
"tool": "sixtydb_memory_ingest",
"arguments": {
"text": "User prefers dark mode and metric units",
"type": "user"
}
}
// Upload a document (PDF, DOCX, scanned image, etc.)
{
"tool": "sixtydb_memory_upload_document",
"arguments": {
"file_path": "/Users/me/Documents/handbook.pdf",
"collection": "company_handbook",
"type": "knowledge"
}
}
// Search with hybrid semantic + keyword recall
{
"tool": "sixtydb_memory_search",
"arguments": {
"query": "What is the refund policy?",
"alpha": 0.8,
"mode": "fast"
}
}
// Assemble LLM-ready context for RAG
{
"tool": "sixtydb_memory_context",
"arguments": {
"query": "Tell me about my customer's recent orders"
}
}
```
All memory operations are [pay-as-you-go](/api-reference/memory/pricing) — the tool response surfaces `charged`, `balance`, and `tx` details in the billing footer on every call.
### Authorization checks
Before attempting a gated action, query the Cerbos permission map so the agent can fail fast on disallowed operations:
```typescript theme={null}
// Fetch the full permission map (once per session)
{
"tool": "sixtydb_get_permissions",
"arguments": {}
}
// Probe a specific action
{
"tool": "sixtydb_check_permission",
"arguments": {
"resource": "memory",
"action": "create"
}
}
```
This lets the agent hide or disable features that the user's role can't use, instead of watching for `403 POLICY_DENY` responses mid-flow.
## Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Claude (AI Assistant) │
└─────────────────────────┬───────────────────────────────────┘
│
│ MCP Protocol (stdio)
│
┌─────────────────────────▼───────────────────────────────────┐
│ 60db MCP Server │
├─────────────────────────────────────────────────────────────┤
│ Voice: sixtydb_tts_synthesize, sixtydb_stt_transcribe │
│ sixtydb_list_voices, sixtydb_create_voice │
│ Memory: sixtydb_memory_ingest, sixtydb_memory_search │
│ sixtydb_memory_upload_document │
│ sixtydb_memory_context, sixtydb_memory_get_usage │
│ Authz: sixtydb_get_permissions, sixtydb_check_permission│
│ Admin: sixtydb_list_workspaces, sixtydb_get_usage_stats │
│ sixtydb_list_invoices │
│ ... 30+ tools total │
└─────────────────────────┬───────────────────────────────────┘
│
│ HTTPS (JWT or API key)
│
┌─────────────────────────▼───────────────────────────────────┐
│ 60db API │
│ (api.60db.ai) │
│ │
│ Every request passes through: │
│ Cerbos policy check → Memory billing → Controller │
└─────────────────────────────────────────────────────────────┘
```
## Next Steps
* [Installation](/mcp-server/installation) - Install and configure the server
* [Configuration](/mcp-server/configuration) - Set up authentication
* [Quick Start](/mcp-server/quickstart) - Run your first request
* [Claude Desktop](/mcp-server/claude-desktop) - Integration guide
## Requirements
* **Node.js**: 18 or higher
* **API Key**: 60db API key from dashboard
* **MCP Client**: Claude Desktop or custom MCP client
## Support
* **Documentation**: [https://docs.60db.ai](https://docs.60db.ai)
* **GitHub**: [https://github.com/60db-ai/mcp](https://github.com/60db-ai/mcp)
* **Issues**: [https://github.com/60db-ai/mcp/issues](https://github.com/60db-ai/mcp/issues)
# Mcp inspector
Source: https://docs.60db.ai/mcp-server/mcp-inspector
# MCP Inspector
Test and debug the 60db MCP Server with the MCP Inspector.
## Installation
```bash theme={null}
npm install -g @modelcontextprotocol/inspector
```
## Usage
### Start the Inspector
```bash theme={null}
# From the server directory
mcp-inspector node dist/index.js
# With environment variables
SIXTYDB_API_KEY=sk_your_key mcp-inspector node dist/index.js
```
The Inspector opens in your browser at `http://localhost:5173`.
## Features
* **View all tools**: Browse 20+ available tools
* **Execute tools**: Test tools with parameters
* **View responses**: See formatted responses
* **Debug logging**: View server logs in real-time
## Example Workflows
### List Voices
1. Click `60db_list_voices`
2. Enter: `{"language": "en-US", "limit": 5}`
3. Click "Execute"
4. View available voices
### Synthesize Speech
1. Click `60db_tts_synthesize`
2. Enter: `{"text": "Hello!", "voice_id": "voice_abc123"}`
3. Click "Execute"
4. Receive audio URL
### Transcribe Audio
1. Click `60db_stt_transcribe`
2. Enter: `{"audio_url": "https://example.com/audio.mp3"}`
3. Click "Execute"
4. View transcription
## Debugging
### Enable Debug Logging
```bash theme={null}
SIXTYDB_DEBUG=true mcp-inspector node dist/index.js
```
### View Server Logs
The Inspector displays real-time logs:
```
60db MCP Server starting...
API URL: https://api.60db.ai
Auth: API Key
60db MCP Server running via stdio
```
## Best Practices
1. **Test before integrating**: Always test in Inspector first
2. **Use sample data**: Start with simple examples
3. **Monitor response times**: Check tool execution times
4. **Validate responses**: Verify response formats
## Next Steps
* [Claude Desktop](/mcp-server/claude-desktop) - Integrate with Claude
# Memory
Source: https://docs.60db.ai/mcp-server/memory
# Memory & RAG Tools
The 60db MCP Server exposes the full Memory/RAG layer to MCP clients like Claude Desktop. These tools let an AI assistant persist user memories, ingest knowledge base documents (91+ formats with built-in OCR), run hybrid semantic recall, and assemble LLM-ready context for retrieval-augmented generation.
All memory tools are **pay-as-you-go** from the workspace wallet. Every billable response surfaces the new balance, the charge amount, and the transaction ID in the formatted output so the agent can reason about cost. See the [pricing reference](/api-reference/memory/pricing) for rates and refund policy.
## Tool summary
| Tool | Billed | Purpose |
| ---------------------------------- | ------ | ----------------------------------------------------------- |
| `sixtydb_memory_ingest` | ✓ | Store a single memory |
| `sixtydb_memory_ingest_batch` | ✓ | Batch-store up to 100 memories |
| `sixtydb_memory_upload_document` | ✓✓ | Extract + ingest a document (PDF, DOCX, XLSX, images, etc.) |
| `sixtydb_memory_search` | ✓ | Hybrid semantic + keyword recall |
| `sixtydb_memory_context` | ✓ | Assemble LLM-ready context for RAG |
| `sixtydb_memory_list_collections` | — | List collections in the workspace |
| `sixtydb_memory_create_collection` | — | Create a team/knowledge/hive collection |
| `sixtydb_memory_get_usage` | — | Monthly spend breakdown + wallet balance |
| `sixtydb_memory_get_status` | — | Poll a memory's ingestion state |
| `sixtydb_memory_delete` | — | Soft-delete a memory (24h undo) |
## Ingest a memory
Store a single fact, preference, or conversation snippet:
```json theme={null}
{
"tool": "sixtydb_memory_ingest",
"arguments": {
"text": "User prefers vegetarian food and is lactose intolerant.",
"title": "Dietary preferences",
"type": "user",
"infer": true
}
}
```
**Parameters:**
* `text` (required) — content to store, max 100,000 characters
* `title` (optional) — display title
* `collection` (optional) — target collection ID (defaults to personal)
* `type` — `user`, `knowledge`, or `hive` (default: `user`)
* `infer` — if true, the memory service extracts structured facts via LLM
**Cost:** \$0.0001 per 1,000 characters.
## Upload a document
The most powerful memory tool — give the agent a file path and 60db handles format detection, OCR, chunking, and ingestion:
```json theme={null}
{
"tool": "sixtydb_memory_upload_document",
"arguments": {
"file_path": "/Users/me/Documents/quarterly-report.pdf",
"collection": "company_reports",
"type": "knowledge",
"title": "Q4 2026 Quarterly Report"
}
}
```
**Parameters:**
* `file_path` (required) — absolute path on the agent's local filesystem
* `collection` (optional) — target collection
* `type` — `user` | `knowledge` | `hive` (default: `knowledge` — right choice for docs)
* `title` (optional) — display title, defaults to filename
* `chunk_size` (optional) — characters per chunk (200–8000, default 1500)
* `chunk_overlap` (optional) — character overlap between chunks (default 200)
**Supported formats:** PDF, DOCX, DOC, ODT, RTF, TXT, MD, HTML, EPUB, XLSX, XLS, CSV, ODS, PPTX, PPT, ODP, EML, MSG, PNG, JPG, TIFF, BMP, and 70+ more. OCR is applied automatically to scanned PDFs and images.
**Max file size:** 200 MB. **Max chunks per document:** 100.
**Cost (two-stage):**
* Extract fee: \$0.003 per MB (pre-charged)
* Ingest fee: \$0.0001 per 1,000 extracted characters (post-charged)
Both fees are automatically refunded on any failure.
## Search memories
Hybrid semantic + keyword recall with optional cross-encoder reranking, across user memories AND knowledge documents in one call:
```json theme={null}
{
"tool": "sixtydb_memory_search",
"arguments": {
"query": "What are my dietary preferences?",
"mode": "thinking",
"max_results": 10,
"alpha": 0.8
}
}
```
**Parameters:**
* `query` (required) — search text, max 2,000 chars
* `collection` (optional) — collection to search
* `mode` — `fast` (dense retrieval, \~100-200ms) or `thinking` (wider pool + cross-encoder rerank, \~200-400ms)
* `max_results` — 1–50, default 10
* `alpha` — 0 (keyword only) to 1 (semantic only), default 0.8
* `recency_bias` — weight for newer memories (0–1), default 0
* `graph_context` — include knowledge-graph relationships
**Advanced reranker knobs** (optional, override server defaults):
* `rerank_top_k` — max candidates the cross-encoder reranks (1-500)
* `rerank_timeout_ms` — hard timeout for rerank call (50-5000ms)
* `min_rerank_score` — drop results below this score (0-1)
* `fetch_multiplier` — in thinking mode, fetch N x max\_results candidates (1-10)
When the reranker is active, each result includes a `rerank_score` (cross-encoder confidence, 0-1) alongside the regular `score` (dense similarity). The rerank score is the more reliable ranking signal.
**Tuning by query type:**
| Query type | Recommended |
| --------------------- | ------------------------------- |
| Exact phrase match | `alpha: 0.2, mode: fast` |
| Conceptual question | `alpha: 0.9, mode: fast` |
| Complex multi-faceted | `alpha: 0.7, mode: thinking` |
| Latest-events focus | `alpha: 0.6, recency_bias: 0.3` |
**Cost:** flat \$0.0003 per query regardless of parameters.
## Assemble context (RAG)
Purpose-built for retrieval-augmented generation. Returns a `prompt_ready` string you can prepend directly to an LLM system message:
```json theme={null}
{
"tool": "sixtydb_memory_context",
"arguments": {
"query": "What should I tell the customer about their order?",
"session_id": "chat_abc123",
"top_k": 8,
"max_context_length": 2000
}
}
```
**Graceful degradation** — if the memory layer is unreachable, the tool returns an empty `prompt_ready` string and the charge is automatically refunded so the chat flow keeps working.
**Cost:** flat \$0.0005 per query.
## Collections
List everything the caller can see:
```json theme={null}
{
"tool": "sixtydb_memory_list_collections",
"arguments": {}
}
```
Create a team/knowledge/hive collection (admin/owner only):
```json theme={null}
{
"tool": "sixtydb_memory_create_collection",
"arguments": {
"collection_id": "customer_support",
"label": "Customer Support KB",
"kind": "knowledge",
"shared": true
}
}
```
Personal collections are auto-created per user on first use and cannot be created via this tool.
**Both are unbilled.**
## Monitor spend
Track usage and wallet balance without affecting the wallet:
```json theme={null}
{
"tool": "sixtydb_memory_get_usage",
"arguments": {
"period": "current_month"
}
}
```
Returns net spend, operation count, refund count, and a per-service-type breakdown, plus the billing owner's current wallet balance. Always free, works even when the wallet is empty.
**Periods:** `current_month` (default), `last_30_days`, `all_time`.
## Handling insufficient credits
When the wallet runs out, billable tools return an error with `INSUFFICIENT_CREDITS` structure. Agents should:
1. Catch the 402 case
2. Call `sixtydb_memory_get_usage` to show the shortfall to the user
3. Prompt the user to top up via the 60db dashboard (link: `/app/billing`)
4. Retry the original operation once the wallet is funded
## Billing transparency in every response
Every billable tool response includes a **Billing** footer in the formatted output:
```
**Memory queued** in collection `user_abc123`
Type: user
Total queued: 1
- `mem_01HV...` — pending: Memory queued for processing
_Billing: charged **$0.000013** · balance **$9.464587** · tx `84ffd09e`_
```
The JSON response format also embeds a `billing: { balance, charged, txId }` object for programmatic consumers.
## Related
* [Memory pricing & billing](/api-reference/memory/pricing)
* [Get memory usage endpoint](/api-reference/memory/usage)
* [Memory feature overview](/features/memory)
* [Authz MCP tools](/mcp-server/authz)
# Quickstart
Source: https://docs.60db.ai/mcp-server/quickstart
# Quick Start
Get started with the 60db MCP Server in minutes.
## Step 1: Install the Server
```bash theme={null}
npm install @60db
```
## Step 2: Set Your API Key
```bash theme={null}
export SIXTYDB_API_KEY=sk_your_api_key_here
```
## Step 2: Start the Server
```bash theme={null}
npm start
```
You should see:
```
60db MCP Server starting...
API URL: https://api.60db.ai
Auth: API Key
60db MCP Server running via stdio
```
## Step 3: Test with Claude Desktop
Configure Claude Desktop and restart. Then ask:
```
Can you list available English female voices?
```
Claude will use the MCP server to fetch voices.
## Example Usage
### Text-to-Speech
```
You: Convert "Hello, world!" to speech using voice "Sarah"
```
### Speech-to-Text
```
You: Transcribe this meeting audio: https://example.com/meeting.mp3
```
### Create Custom Voice
```
You: Create a custom voice named "Brand Voice" from this sample
```
## Next Steps
* [Voices](/mcp-server/voices) - Learn about voice tools
* [TTS](/mcp-server/tts) - Text-to-speech guide
* [STT](/mcp-server/stt) - Speech-to-text guide
# Stt
Source: https://docs.60db.ai/mcp-server/stt
# Speech-to-Text (STT)
Transcribe audio files to text with optional speaker diarization.
## Available Tools
| Tool | Description |
| ------------------------ | ---------------------------------- |
| `sixtydb_stt_transcribe` | Transcribe audio to text |
| `sixtydb_stt_logs` | View transcription history |
| `sixtydb_stt_get` | Get specific transcription details |
## Transcribe Audio
Convert audio to text via the `sixtydb_stt_transcribe` tool:
```json theme={null}
{
"audio_url": "https://example.com/meeting.mp3",
"language": "auto",
"diarize": true
}
```
### Auto-detect vs explicit language
* **Omit `language`** (or pass `"auto"`) to enable auto-detection across all 39 supported languages. The MCP shim strips `"auto"` before forwarding so the server's language identification runs.
* **Pass a single ISO 639-1 code** (e.g. `"hi"`, `"en"`, `"ar"`) to skip language identification and run the fast path for that language.
* Do **not** pass unsupported codes (`ur`, `ja`, `ko`, `zh`, `th`, `vi`, `id`, `tl`, `sw`, `tr`, `fa`, `he`) or Arabic dialect tags (`ar-eg`, `ar-lv`, …) — they return an `unsupported_language` error. For non-MSA Arabic audio pass `"ar"` for best-effort MSA transcription.
## Parameters
| Parameter | Type | Default | Description |
| ----------------- | ------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `audio_url` | string | Required | URL to audio file (downloaded and forwarded as multipart to `POST /stt`; max 25 MB) |
| `language` | string | *auto* | ISO 639-1 code, or `"auto"` / omit for auto-detect |
| `diarize` | boolean | `false` | Enable pyannote speaker diarization — adds a `speakers` array to each segment with `SPEAKER_00`, `SPEAKER_01`, … labels |
| `context` | string | — | Free-form paragraph describing the session (domain, speakers, jargon). When supplied, the server runs a background LLM refinement pass; response text is polished for proper nouns, filler removal, and punctuation. Omit to skip refinement. |
| `response_format` | string | `"markdown"` | `"markdown"` or `"json"` |
### Context string example
```
Cricket coaching session. Players: Arjun Mehta, Ishaan Verma, Aryan Khan, Rohan. Discussing batting technique, stamina, running between wickets, off-side balls.
```
`context` is a **plain string** on the REST `POST /stt` tool (this page). The WebSocket `/ws/stt` endpoint takes a structured `{general, text, terms}` **object** instead — see the [WebSocket STT reference](/api-reference/websocket/stt).
## Response shape (JSON)
```json theme={null}
{
"request_id": "req_...",
"text": "Hello, thanks for joining...",
"language": "en",
"language_name": "English",
"duration_sec": 5.2,
"segments": [
{
"start": 0.0,
"end": 3.1,
"text": "Hello, thanks for joining the call today.",
"confidence": 0.92,
"words": [
{ "word": "Hello", "start": 0.0, "end": 0.32, "confidence": 0.94 }
],
"speakers": [
{ "speaker": "SPEAKER_00", "start": 0.0, "end": 3.1 }
]
}
],
"warning_codes": []
}
```
**Empty-speech signal**: A successful response with `text: ""` and `warning_codes: ["no_speech_detected"]` means the audio contained no speech. This is not an error — do not retry.
## Usage in Claude
```
You: Transcribe this meeting audio
You: Transcribe this with speaker identification
You: Transcribe only in Hindi (use language "hi")
```
## Supported Audio Formats
* WAV, MP3, M4A, OGG, FLAC, WebM, MP4 audio track
* Max file size: 25 MB
* Max duration: 1 hour
* Recommended: 16 kHz+ sample rate
## Supported Languages
39 languages total — fetch the live catalog from the `sixtydb_stt_languages` tool (or the REST `GET /stt/languages` endpoint). Includes 25 European languages, 13 Indic languages with English code-switching, and Arabic MSA. See the [`Get STT Languages`](/api-reference/stt/get-languages) reference for the full list.
## Related
* [STT Models](/api-reference/models/get-stt-models) — exposed STT model catalog (currently `60db-stt-v01`)
* [WebSocket STT](/api-reference/websocket/stt) — real-time streaming variant (note: the WS form uses `languages: null` for auto-detect, not `"auto"`)
* [TTS](/mcp-server/tts) — Text-to-speech
# Tts
Source: https://docs.60db.ai/mcp-server/tts
# Text-to-Speech (TTS)
Generate natural speech from text using the MCP server.
## Available Tools
| Tool | Description |
| --------------------- | ------------------------------- |
| `60db_tts_synthesize` | Convert text to speech |
| `60db_tts_logs` | View TTS generation history |
| `60db_tts_get` | Get specific generation details |
## Synthesize Speech
Convert text to audio:
```typescript theme={null}
{
"text": "Hello, world!",
"voice_id": "voice_abc123",
"speed": 1.0,
"output_format": "mp3"
}
```
**Response**:
```markdown theme={null}
# TTS Generation Complete
**Audio URL**: https://cdn.60db.ai/audio/tts_123456.mp3
**Duration**: 2.3 seconds
**Voice**: Sarah (en-US)
```
## Parameters
| Parameter | Type | Default | Description |
| --------------- | ------ | -------- | ----------------------------- |
| `text` | string | Required | Text to synthesize |
| `voice_id` | string | Required | Voice to use |
| `speed` | number | 1 | Speech speed (0.25-2.0) |
| `stability` | number | 50 | Voice stability (0-100) |
| `similarity` | number | 75 | Voice similarity (0-100) |
| `output_format` | string | mp3 | Output format (mp3, wav, ogg) |
## Usage in Claude
```
You: Convert "Welcome to our podcast" to speech
You: Generate speech with faster speed
You: Use a Spanish voice for this text
```
## Supported Languages
* English (US, UK, Australian)
* Spanish, French, German, Italian
* Portuguese, Japanese, Korean, Chinese
* And 30+ more languages
## Related
* [Voices](/mcp-server/voices) - Browse available voices
* [STT](/mcp-server/stt) - Speech-to-text
# Voices
Source: https://docs.60db.ai/mcp-server/voices
# Voices
Manage and use voices for text-to-speech synthesis through the MCP server.
## Available Tools
| Tool | Description |
| ------------------- | --------------------------------------- |
| `60db_list_voices` | List available voices with filtering |
| `60db_get_voice` | Get detailed voice information |
| `60db_create_voice` | Create a cloned voice from audio sample |
## List Voices
Browse available voices by language, gender, and type:
```typescript theme={null}
// List English female voices
{
"language": "en-US",
"gender": "Female",
"limit": 10
}
```
**Response**:
```markdown theme={null}
# Available Voices
| Voice ID | Name | Language | Gender |
|----------|------|----------|--------|
| voice_abc123 | Sarah | en-US | Female |
| voice_def456 | Emma | en-US | Female |
```
## Get Voice Details
Retrieve detailed information about a voice:
```typescript theme={null}
{
"voice_id": "voice_abc123"
}
```
## Create Custom Voice
Create a cloned voice from an audio sample:
```typescript theme={null}
{
"name": "Brand Voice",
"sample_url": "https://example.com/sample.mp3",
"language": "en-US",
"gender": "Female"
}
```
Voice cloning requires a 30+ second audio sample with clear speech.
## Usage in Claude
```
You: List all available English voices
You: Get details for voice "Sarah"
You: Create a custom voice from this sample
```
## Related
* [TTS](/mcp-server/tts) - Use voices for speech synthesis
# Workspaces
Source: https://docs.60db.ai/mcp-server/workspaces
# Workspaces
Manage team workspaces and collaboration through the MCP server.
## Available Tools
| Tool | Description |
| ---------------------------- | ---------------------- |
| `60db_list_workspaces` | List all workspaces |
| `60db_get_workspace` | Get workspace details |
| `60db_create_workspace` | Create a new workspace |
| `60db_get_workspace_members` | List workspace members |
## List Workspaces
View all workspaces:
```typescript theme={null}
{
"limit": 20
}
```
## Create Workspace
Create a new team workspace:
```typescript theme={null}
{
"name": "Engineering Team",
"description": "Main engineering workspace"
}
```
## Get Workspace Members
List members of a workspace:
```typescript theme={null}
{
"workspace_id": "workspace_abc123"
}
```
## Usage in Claude
```
You: List all my workspaces
You: Create a new workspace for the design team
You: Who are the members of the Engineering workspace?
```
## Related
* [Analytics](/mcp-server/analytics) - Track usage
# Quickstart
Source: https://docs.60db.ai/quickstart
Start using 60db in under 5 minutes
## Get Your API Key
First, you'll need an API key to authenticate your requests:
Create an account at [app.60db.ai](https://app.60db.ai)
Go to Settings → API Keys in your dashboard
Click "Create API Key" and give it a descriptive name
Copy your API key and store it securely
Keep your API key secret! Never commit it to version control or expose it in client-side code.
## Choose Your SDK
### Installation
```bash theme={null}
npm install 60db
```
### Basic Usage
```typescript theme={null}
import { SixtyDBClient } from '60db';
// Initialize the client
const client = new SixtyDBClient('your-api-key');
// Text to Speech
const audio = await client.textToSpeech({
text: 'Hello, world!',
voice_id: 'default-voice'
});
// Get all voices
const voices = await client.getVoices();
console.log(voices);
```
### Installation
```bash theme={null}
pip install 60db
```
### Basic Usage
```python theme={null}
from sixtydb import SixtyDBClient
# Initialize the client
client = SixtyDBClient('your-api-key')
# Text to Speech
audio = client.text_to_speech('Hello, world!', voice_id='default-voice')
# Save audio to file
with open('output.mp3', 'wb') as f:
f.write(audio)
# Get all voices
voices = client.get_voices()
print(voices)
```
## Next Steps
Learn about all available features
Dive into the complete API documentation
Create your own custom voice profiles
Give your AI persistent memory and upload documents (PDF/DOCX/OCR)
OpenAI-compatible chat completions with 60db models
Set up webhooks for event notifications
# JavaScript/TypeScript SDK
Source: https://docs.60db.ai/sdks/javascript
Complete guide to the 60db JavaScript/TypeScript SDK
## Installation
```bash theme={null}
npm install 60db
```
## Initialization
```typescript theme={null}
import { SixtyDBClient } from "60db";
// Simple initialization
const client = new SixtyDBClient("your-api-key");
```
## Text-to-Speech
### Basic TTS
```typescript theme={null}
const audio = await client.textToSpeech({
text: 'Hello, world!',
voice_id: 'default-voice',
enhance: true,
speed: 1.0,
language:"en-us"
});
### Get all voices
const voices = await client.getVoices();
### Get all lanuages
const lanuages = await client.getLanguages();
### audio is an ArrayBuffer containing the audio data
```
## Speech-to-Text
### Transcribe Audio
```typescript theme={null}
const file = document.querySelector('input[type="file"]').files[0];
const result = await client.speechToText(file, {
language: "en",
});
console.log(result.text);
```
### With Context Refinement
Supply a `context` string to enable server-side LLM polishing — proper nouns, filler removal, and punctuation are cleaned on the response text:
```typescript theme={null}
const result = await client.speechToText(file, {
language: "hi",
diarize: true,
context:
"Cricket coaching session. Players: Arjun Mehta, Ishaan Verma. Discussing batting technique.",
});
```
Omit `context` entirely to skip refinement. Note: this is the REST shape — the WebSocket streaming endpoint takes a `{general, text, terms}` object instead.
### Get Supported Languages
```typescript theme={null}
const languages = await client.getLanguages();
console.log(languages);
```
## Voice Management
### List All Voices
```typescript theme={null}
const voices = await client.getVoices();
voices.forEach((voice) => {
console.log(`${voice.name} (${voice.id})`);
});
```
### Get Specific Voice
```typescript theme={null}
const voice = await client.getVoice("voice-id");
console.log(voice);
```
### Create Custom Voice
```typescript theme={null}
const files = [file1, file2, file3]; // File objects
const newVoice = await client.createVoice({
name: "My Custom Voice",
files: files,
description: "A custom voice for my brand",
});
console.log("Created voice:", newVoice.id);
```
### Update Voice
```typescript theme={null}
await client.updateVoice("voice-id", {
name: "Updated Voice Name",
description: "Updated description",
});
```
### Delete Voice
```typescript theme={null}
await client.deleteVoice("voice-id");
```
## Authentication
### Sign Up
```typescript theme={null}
const user = await client.signUp({
email: "user@example.com",
password: "secure-password",
name: "John Doe",
});
```
### Sign In
```typescript theme={null}
const session = await client.signIn({
email: "user@example.com",
password: "secure-password",
});
console.log("Token:", session.token);
```
### Get Profile
```typescript theme={null}
const profile = await client.getProfile();
console.log(profile);
```
### Update Profile
```typescript theme={null}
await client.updateProfile({
name: "Jane Doe",
company: "Acme Inc",
});
```
## Workspace Management
### List Workspaces
```typescript theme={null}
const workspaces = await client.getWorkspaces();
```
### Create Workspace
```typescript theme={null}
const workspace = await client.createWorkspace({
name: "My Workspace",
description: "Team workspace",
});
```
## Billing
### Get Available Plans
```typescript theme={null}
const plans = await client.getPlans();
plans.forEach((plan) => {
console.log(`${plan.name}: $${plan.price}/month`);
});
```
### Get Current Plan
```typescript theme={null}
const currentPlan = await client.getCurrentPlan();
console.log("Current plan:", currentPlan.name);
```
### Subscribe to Plan
```typescript theme={null}
await client.subscribe("plan-id");
```
## Analytics
### Get Usage Statistics
```typescript theme={null}
const usage = await client.getUsage();
console.log("Characters used:", usage.characters);
console.log("API calls:", usage.api_calls);
```
## API Key Management
### List API Keys
```typescript theme={null}
const apiKeys = await client.getApiKeys();
```
### Create API Key
```typescript theme={null}
const newKey = await client.createApiKey("Production Key");
console.log("New API key:", newKey.key);
```
### Delete API Key
```typescript theme={null}
await client.deleteApiKey("key-id");
```
## Webhooks
### List Webhooks
```typescript theme={null}
const webhooks = await client.getWebhooks();
```
### Create Webhook
```typescript theme={null}
const webhook = await client.createWebhook({
url: "https://example.com/webhook",
events: ["tts.completed", "stt.completed"],
});
```
### Delete Webhook
```typescript theme={null}
await client.deleteWebhook("webhook-id");
```
## Error Handling
```typescript theme={null}
try {
const audio = await client.textToSpeech({
text: "Hello, world!",
voice_id: "invalid-voice",
});
} catch (error) {
if (error.response) {
// API error
console.error("API Error:", error.response.status);
console.error("Message:", error.response.data.message);
} else {
// Network or other error
console.error("Error:", error.message);
}
}
```
## TypeScript Types
The SDK includes full TypeScript type definitions:
```typescript theme={null}
import { SixtyDBClient, SixtyDBConfig, TTSResponse } from "60db";
```
# Python SDK
Source: https://docs.60db.ai/sdks/python
Complete guide to the 60db Python SDK
## Installation
```bash theme={null}
pip install 60db
```
## Initialization
```python theme={null}
from sixtydb import SixtyDBClient
# Simple initialization
client = SixtyDBClient('your-api-key')
# With custom configuration
### Get all voices
voices = await client.getVoices();
### Get all lanuages
lanuages = await client.getLanguages();
### audio is an ArrayBuffer containing the audio data
```
## Text-to-Speech
### Basic TTS
```python theme={null}
audio = client.text_to_speech(
text='Hello, world!',
voice_id='default-voice',
enhance=True,
speed=1.0,
language='en-us'
)
# Save to file
with open('output.mp3', 'wb') as f:
f.write(audio)
```
## Speech-to-Text
### Transcribe Audio
```python theme={null}
with open('audio.mp3', 'rb') as audio_file:
result = client.speech_to_text(audio_file, language='en')
print(result['text'])
```
### With Context Refinement
Supply a `context` string to enable server-side LLM polishing — proper nouns, filler removal, and punctuation are cleaned on the response text:
```python theme={null}
with open('audio.mp3', 'rb') as audio_file:
result = client.speech_to_text(
audio_file,
language='hi',
diarize=True,
context='Cricket coaching session. Players: Arjun Mehta, Ishaan Verma. Discussing batting technique.',
)
print(result['text'])
```
Omit `context` entirely to skip refinement. Note: this is the REST shape — the WebSocket streaming endpoint takes a `{general, text, terms}` object instead.
### Get Supported Languages
```python theme={null}
languages = client.get_languages()
for lang in languages:
print(f"{lang['name']} ({lang['code']})")
```
## Voice Management
### List All Voices
```python theme={null}
voices = client.get_voices()
for voice in voices:
print(f"{voice['name']} ({voice['id']})")
```
### Get Specific Voice
```python theme={null}
voice = client.get_voice('voice-id')
print(voice)
```
### Create Custom Voice
```python theme={null}
files = [
open('sample1.mp3', 'rb'),
open('sample2.mp3', 'rb'),
open('sample3.mp3', 'rb')
]
new_voice = client.create_voice(
name='My Custom Voice',
files=files,
description='A custom voice for my brand'
)
print(f"Created voice: {new_voice['id']}")
# Close files
for f in files:
f.close()
```
### Update Voice
```python theme={null}
client.update_voice(
voice_id='voice-id',
name='Updated Voice Name',
description='Updated description'
)
```
### Delete Voice
```python theme={null}
client.delete_voice('voice-id')
```
## Authentication
### Sign Up
```python theme={null}
user = client.sign_up(
email='user@example.com',
password='secure-password',
name='John Doe'
)
```
### Sign In
```python theme={null}
session = client.sign_in(
email='user@example.com',
password='secure-password'
)
print(f"Token: {session['token']}")
```
### Get Profile
```python theme={null}
profile = client.get_profile()
print(profile)
```
### Update Profile
```python theme={null}
client.update_profile(
name='Jane Doe',
company='Acme Inc'
)
```
## Workspace Management
### List Workspaces
```python theme={null}
workspaces = client.get_workspaces()
```
### Create Workspace
```python theme={null}
workspace = client.create_workspace(
name='My Workspace',
description='Team workspace'
)
```
## Billing
### Get Available Plans
```python theme={null}
plans = client.get_plans()
for plan in plans:
print(f"{plan['name']}: ${plan['price']}/month")
```
## Analytics
### Get Usage Statistics
```python theme={null}
usage = client.get_usage()
print(f"Characters used: {usage['characters']}")
print(f"API calls: {usage['api_calls']}")
```
## API Key Management
### List API Keys
```python theme={null}
api_keys = client.get_api_keys()
```
### Create API Key
```python theme={null}
new_key = client.create_api_key('Production Key')
print(f"New API key: {new_key['key']}")
```
### Delete API Key
```python theme={null}
client.delete_api_key('key-id')
```
## Webhooks
### List Webhooks
```python theme={null}
webhooks = client.get_webhooks()
```
### Create Webhook
```python theme={null}
webhook = client.create_webhook(
url='https://example.com/webhook',
events=['tts.completed', 'stt.completed']
)
```
### Delete Webhook
```python theme={null}
client.delete_webhook('webhook-id')
```
## Error Handling
```python theme={null}
from requests.exceptions import HTTPError
try:
audio = client.text_to_speech(
text='Hello, world!',
voice_id='invalid-voice'
)
except HTTPError as e:
print(f"HTTP Error: {e.response.status_code}")
print(f"Message: {e.response.json()['message']}")
except Exception as e:
print(f"Error: {str(e)}")
```
## Type Hints
The SDK includes type hints for better IDE support:
```python theme={null}
from sixtydb import SixtyDBClient
from typing import Dict, List, Any
client: SixtyDBClient = SixtyDBClient('your-api-key')
voices: List[Dict[str, Any]] = client.get_voices()
```
## Async Support
For async applications, you can use the SDK with asyncio:
```python theme={null}
import asyncio
from sixtydb import SixtyDBClient
async def main():
client = SixtyDBClient('your-api-key')
# Run in executor for async compatibility
loop = asyncio.get_event_loop()
audio = await loop.run_in_executor(
None,
client.text_to_speech,
'Hello, world!',
'default-voice'
)
with open('output.mp3', 'wb') as f:
f.write(audio)
asyncio.run(main())
```
# Codex Skills
Source: https://docs.60db.ai/skills/introduction
Drop 60db voice into Claude Code, OpenAI Codex, OpenCode, or any agent with a skills directory — triggered by /60db.
# 60db Agent Skills
Bring 60db's full voice stack — **text-to-speech, speech-to-text, voice cloning, voice management, and the LLM core** — directly into your coding agent. Install it once and call it with `/60db` from [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [OpenAI Codex](https://developers.openai.com/codex), [OpenCode](https://opencode.ai), or any agent that reads a skills directory.
It's a single **zero-dependency** Python CLI (stdlib only, Python 3.8+) — no SDK, no `pip install`, no version drift.
Native skill — `/60db` and `/setup-60db`
Same skill folders, copied into `~/.codex/skills`
Plain markdown + stdlib Python runs anywhere
Source repository: [github.com/60db-ai/60db.ai-skills](https://github.com/60db-ai/60db.ai-skills) — Apache-2.0.
***
## Why a Skill, Not Just the SDK
The skill **encodes the behavior that's actually true** against the live API, so you never relearn the gotchas. It's stdlib-only, so it keeps working regardless of SDK or doc drift. The five mistakes it pins for you:
Concatenate each line's `result.audioContent` (base64 LINEAR16 PCM). The CLI does this for you.
`44100` is rejected with a `400`.
The API always returns raw PCM — wrap it as WAV yourself (the CLI does).
The model is band-limited at \~8 kHz, so 48 kHz is upsampled headroom. `enhance` and `60db-quality` do not raise the ceiling.
Language and gender live under `labels` — confirmed by live inspection.
***
## How It Works
One CLI — `skills/60db/scripts/sixtydb.py` — fronts every 60db use case against `https://api.60db.ai`.
```
/60db
1. Setup once → user runs `init`: hidden key prompt → config (mode 600)
2. Ask if unclear → prompt for: use case · voice · model · kHz · format
3. Route → tts / stt / voices / clone / chat / langs / doctor
4. Verify → `doctor` reports key presence + resolved defaults (never the key)
```
Every setting resolves the same way, so you can override per-call or store defaults once:
```
CLI flag > env SIXTYDB_* > config file > built-in default
```
**Setup is the user's job.** The API key is never typed by the agent, never passed on the command line, never printed, never committed. The user runs `init` (a hidden prompt) or sets `SIXTYDB_API_KEY`. In a chat session where a hidden prompt isn't reachable, the agent scaffolds defaults with `init --no-key` and hands the key step back to the user.
***
## Use Cases
What you can build with `/60db` without leaving your coding agent:
Turn a script, blog post, or `.txt` file into studio-quality audio for videos, podcasts, or demos — `tts script.txt --voice --out vo.wav`.
Convert recordings, calls, or meetings into text with speaker labels and word timings, then build SRT/VTT — `stt rec.mp4 --diarize --timestamps --json`.
Wire `stt` (ears) + `chat` (brain) + `tts` (mouth) into a conversational loop for support, reception, or IVR-style flows.
Clone a voice from a few samples and reuse it across all your audio — `clone --name "Brand VO" --sample a.wav --sample b.wav`.
Add read-aloud audio to docs, articles, and apps for visually impaired or on-the-go users.
Generate speech and transcribe audio across 30+ languages — check `langs` / `langs --stt` for coverage.
### Who it's for
Add TTS/STT to an app without learning the SDK or fighting the docs — the skill drives one stdlib-only CLI that already pins the API's real behavior.
Produce voiceovers and dubbed audio straight from a script file, with a consistent brand voice via cloning.
Prototype phone/voice bots from the `stt + chat + tts` parts, and transcribe call recordings for QA or summaries.
If you live in Claude Code, Codex, or OpenCode, `/60db` lets you ship audio without context-switching to a dashboard or another tool.
### A typical workflow
Run `/setup-60db` to store your API key and defaults (voice, model, sample rate).
Run `voices` to list available voice IDs (add `--mine` for your cloned voices).
Call `tts` to make audio or `stt` to make text — the skill asks for anything it needs.
Adjust `--speed` / `--stability`, or chain `stt → chat → tts` for a full voice-agent turn.
***
## The Two Skills
| Skill | Trigger | What it does |
| -------------- | ------------- | -------------------------------------------------------------------------- |
| **60db** | `/60db` | The worker — routes every voice use case to the CLI |
| **setup-60db** | `/setup-60db` | First-run onboarding: collects the key privately, picks defaults, verifies |
The worker drives one CLI with **9 subcommands**:
| Subcommand | What it does | Status |
| -------------- | ---------------------------------------------------------------------- | ------------------------ |
| `init` | Hidden key prompt → config (mode 600); `--no-key` stores defaults only | ✅ |
| `doctor` | Diagnose setup — key presence (never revealed), resolved defaults | ✅ |
| `tts` | Text or `.txt` → WAV (REST NDJSON; `--ws` for legacy WebSocket) | ✅ verified |
| `stt` | Audio → transcript (`--diarize`, `--timestamps`, `--json`) | ✅ verified |
| `voices` | List voice ids (built-in + your cloned), `--mine`, `--json` | ✅ verified |
| `clone` | Train a new voice from samples (`--sample` / `--sample-url`) | ⚠️ doc-only — test first |
| `delete-voice` | Hard-delete one of your custom voices | ⚠️ doc-only |
| `langs` | Supported languages (`--stt` for STT's 39) | ✅ |
| `chat` | LLM core (`60db-tiny`) you pair with stt+tts for a voice agent | documented |
***
## Install
**Claude Code (recommended):**
```bash theme={null}
npx skills add 60db-ai/60db.ai-skills
```
**Codex / OpenCode / other agents** — copy the two skill folders into that agent's skills directory:
```bash theme={null}
git clone https://github.com/60db-ai/60db.ai-skills.git
cp -r 60db.ai-skills/skills/60db 60db.ai-skills/skills/setup-60db ~/.codex/skills/
```
Skills are only picked up on session start — this is an agent-platform limitation, not a bug.
Run `/setup-60db`, or directly:
```bash theme={null}
E=skills/60db/scripts/sixtydb.py
python3 $E init # hidden prompt for your API key → config (mode 600)
python3 $E doctor # verify (reports key presence without revealing it)
```
Get a key at **app.60db.ai → Settings → Developer → API Keys**.
The API key is **yours to enter** — never paste it into the agent chat (chat history is retained by the platform). It's stored locally at `~/.config/60db/config.json` (mode 600) or read from `SIXTYDB_API_KEY`, and `.gitignore` keeps it out of git.
***
## Run It
```bash theme={null}
E=skills/60db/scripts/sixtydb.py
python3 $E tts "Hello there." --out out/hello.wav # text → WAV (48 kHz, 60db-quality)
python3 $E tts script.txt --voice --out out/vo.wav
python3 $E stt recording.mp3 --diarize --timestamps # audio → transcript
python3 $E voices # list voice ids
python3 $E langs --stt # 39 STT languages
python3 $E chat "One-line summary of attachment theory." # LLM core for agents
python3 $E doctor # diagnose setup
```
Store defaults once so you stop repeating flags:
```bash theme={null}
python3 $E init --no-key --voice --model 60db-quality --sample-rate 48000
```
***
***
## Quick Decision Guide
| I want to... | Use |
| ---------------------------------------- | ---------------------------------------------------------------------- |
| Narration / voiceover from text | `tts "..." --out out/vo.wav` |
| Read a script file aloud | `tts script.txt --voice --out out/vo.wav` |
| Transcribe audio with speaker labels | `stt rec.mp3 --diarize --timestamps` |
| See which voices I can use | `voices` (then `voices --mine`) |
| Use a specific language | `langs` / `langs --stt` |
| Clone my own brand voice | `clone --name "Brand VO" --sample a.wav --sample b.wav` *(test first)* |
| Build a phone/voice bot | `chat` as the brain + `stt` ears + `tts` mouth |
| Figure out why audio sounds "compressed" | `doctor` → troubleshooting |
| Set up for the first time | `/setup-60db` |
***
## Examples
```bash Voiceover from text theme={null}
python3 $E tts "Welcome to the show." --voice --out out/intro.wav
# wrote out/intro.wav (412160 B PCM, ~4.3s @ 48000Hz, REST, model=60db-quality)
```
```bash Narrate a script file theme={null}
python3 $E tts episode.txt --voice --out out/ep.wav --speed 0.95 --stability 65
```
```bash Transcribe with speakers + timings theme={null}
python3 $E stt interview.m4a --diarize --timestamps --out interview.txt
# [English, 42.7s] So the first thing we noticed was the latency...
```
```bash Structured JSON for subtitles theme={null}
python3 $E stt talk.mp4 --timestamps --confidence --json > talk.json
# talk.json → words[] each with {word, start, end} → build SRT/VTT
```
```bash One voice-agent turn: ears → brain → mouth theme={null}
USER=$(python3 $E stt caller.wav --json | python3 -c 'import json,sys;print(json.load(sys.stdin)["text"])')
REPLY=$(python3 $E chat "$USER" --system "Concise support agent." --chat-id call-42 | head -1)
python3 $E tts "$REPLY" --voice --out out/reply.wav
```
***
## Gotchas That Bite Everyone
| Symptom | Cause | Fix |
| --------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------ |
| Audio sounds a touch "compressed" | Model is \~16 kHz-native, band-limited to \~8 kHz | Expected; `enhance` / `60db-quality` don't lift it |
| Garbled / truncated audio | REST returns **NDJSON**, not one JSON blob | Concatenate every line's `result.audioContent` (the CLI does this) |
| `400` on synthesize | Sample rate `44100` (or anything off-menu) | Use `8000 / 16000 / 24000 / 48000` only |
| Format flag seems ignored | `output_format` **is** ignored — always raw PCM | Wrap the PCM as WAV yourself (the CLI does) |
| Long script cut off | TTS caps at **5000 chars** per request | Split, synthesize, concatenate WAVs |
| `voices` prints `None` for the id | Field is `voice_id`; language/gender under `labels` | The CLI already handles this |
| `no API key found` | Key not in config or env | Run `init`, or export `SIXTYDB_API_KEY` |
***
## Coverage
| Use case | Command | Status |
| ---------------------------------- | -------------- | ---------------------------------- |
| Text-to-speech (REST + WS) | `tts` | ✅ verified |
| Speech-to-text (batch + streaming) | `stt` | ✅ endpoint verified |
| List voices | `voices` | ✅ verified (`voice_id` / `labels`) |
| Voice cloning | `clone` | ⚠️ doc-only — test first |
| Update / delete voice | `delete-voice` | ⚠️ doc-only |
| Languages | `langs` | ✅ |
| LLM chat (voice-agent core) | `chat` | documented; no native calling API |
***
## How It Helps
Stdlib-only Python — no SDK, no `requests`, no version drift. Only `python3` 3.8+ required.
Every endpoint is annotated verified vs. doc-only; live-API findings win over the published docs.
No native telephony API — build a voice agent from `stt` (ears) + `chat` (brain) + `tts` (mouth).
The key is never typed by the agent, never printed, never committed.
***
## FAQ
`npx skills add 60db-ai/60db.ai-skills`, then restart your agent and run `/setup-60db`.
Into a local config at `~/.config/60db/config.json` (mode 600), or the `SIXTYDB_API_KEY` env var. It's never passed on the command line, never printed, and `.gitignore` keeps it out of git. The agent never types it for you.
No. The model is \~16 kHz-native and band-limited to \~8 kHz, so 48 kHz is upsampled headroom. `enhance` and `60db-quality` don't raise the ceiling.
`8000`, `16000`, `24000`, `48000` only. `44100` is rejected.
There is no native calling/telephony API. You build a voice agent from the parts: `stt` (ears) + `chat` (brain, `60db-tiny`) + `tts` (mouth).
Yes — it's markdown + stdlib Python. Copy the two skill folders into Codex's or OpenCode's skills directory. Only the onboarding prompt is Claude-specific and degrades gracefully.
No. Only `python3` 3.8+. The single optional dependency is `websockets`, used solely by the legacy `--ws` TTS path.
Full command reference, recipes, and the complete troubleshooting list live in the repo's [GUIDE.md](https://github.com/60db-ai/60db.ai-skills/blob/main/GUIDE.md) and [references/](https://github.com/60db-ai/60db.ai-skills/tree/main/skills/60db/references).
# WebSocket API
Source: https://docs.60db.ai/websocket-api
Real-time Speech-to-Text (STT) and Text-to-Speech (TTS) streaming API
# WebSocket API
Real-time bidirectional streaming API for Speech-to-Text (STT) and Text-to-Speech (TTS) services.
## Overview
The 60db WebSocket API provides real-time streaming capabilities for:
* **Speech-to-Text (STT)**: Convert audio to text with 99+ language support
* **Text-to-Speech (TTS)**: Synthesize natural-sounding speech with multiple voices
## Base URLs
| Environment | STT URL | TTS URL |
| ------------------- | -------------------------- | -------------------------- |
| Production | `ws://api.60db.ai/ws/stt` | `ws://api.60db.ai/ws/tts` |
| Production (Secure) | `wss://api.60db.ai/ws/stt` | `wss://api.60db.ai/ws/tts` |
## Authentication
WebSocket connections require authentication using API key via query parameter:
```
ws://api.60db.ai/ws/stt?apiKey=sk_live_your_api_key_here
```
**Getting Your API Key:**
1. Go to [app.60db.ai](https://app.60db.ai)
2. Navigate to **Settings → Developer → API Keys**
3. Click **Create API Key**
4. Copy and store your API key securely
***
## 🎤 STT WebSocket (Speech-to-Text)
**What it does:** You send audio → You get text back
**Use for:** Transcribing speech, voice commands, call center analytics, meeting transcription
### Quick Start
```javascript theme={null}
const WebSocket = require('ws');
const API_KEY = 'sk_live_your_key';
const ws = new WebSocket(`wss://api.60db.ai/ws/stt?apiKey=${API_KEY}`);
ws.onopen = () => console.log('✅ Connected');
ws.onmessage = (data) => {
const msg = JSON.parse(data);
// STT tags this frame with `type` and keeps the fields top-level
// (TTS nests them under `connection_established` — the shapes differ).
if (msg.type === 'connection_established') {
console.log('✅ Authenticated!');
// Start session
ws.send(JSON.stringify({
type: 'start',
languages: ['en'],
config: {
encoding: 'mulaw',
sample_rate: 8000,
continuous_mode: true
}
}));
}
// Wait for `session_started`, not `connected` — `connected` fires before
// the session exists and arrives twice (proxy frame, then upstream frame).
if (msg.type === 'session_started') {
console.log('✅ Ready! Send audio now');
// Send audio chunks
const interval = setInterval(() => {
ws.send(audioBuffer); // Your audio data
}, 60);
// Stop after 5 seconds
setTimeout(() => {
clearInterval(interval);
ws.send(JSON.stringify({ type: 'stop' }));
}, 5000);
}
if (msg.type === 'transcription') {
console.log('📝 Text:', msg.text);
}
if (msg.type === 'session_stopped') {
console.log('✅ Done! Cost:', msg.billing_summary.total_cost);
ws.close();
}
};
```
### How STT Works
```
1. Connect → 2. Authenticate → 3. Start session → 4. Send audio → 5. Get text → 6. Stop
```
***
## 🔊 TTS WebSocket (Text-to-Speech)
**What it does:** You send text → You get audio back
**Use for:** Voice assistants, audiobooks, accessibility, chatbots
### Quick Start
```javascript theme={null}
const WebSocket = require('ws');
const fs = require('fs');
const API_KEY = 'sk_live_your_key';
const ws = new WebSocket(`wss://api.60db.ai/ws/tts?apiKey=${API_KEY}`);
const contextId = 'my-session-' + Date.now();
const audioChunks = [];
ws.onopen = () => console.log('✅ Connected');
ws.onmessage = (data) => {
const msg = JSON.parse(data);
if (msg.connection_established) {
console.log('✅ Authenticated!');
// Create context
ws.send(JSON.stringify({
create_context: {
context_id: contextId,
voice_id: 'fbb75ed2-975a-40c7-9e06-38e30524a9a1',
audio_config: {
audio_encoding: 'LINEAR16',
sample_rate_hertz: 16000
}
}
}));
}
if (msg.context_created) {
console.log('✅ Context created!');
// Send text
ws.send(JSON.stringify({
send_text: {
context_id: contextId,
text: 'Hello, this is a test of the text to speech service.'
}
}));
// Flush
ws.send(JSON.stringify({
flush_context: { context_id: contextId }
}));
}
if (msg.audio_chunk) {
const audioData = Buffer.from(msg.audio_chunk.audioContent, 'base64');
audioChunks.push(audioData);
console.log('🔊 Audio chunk received');
}
if (msg.flush_completed) {
console.log('✅ All audio received!');
// Close context
ws.send(JSON.stringify({
close_context: { context_id: contextId }
}));
}
if (msg.context_closed) {
console.log('✅ Done!');
// Save audio
const audio = Buffer.concat(audioChunks);
fs.writeFileSync('output.pcm', audio);
console.log('💾 Saved output.pcm');
ws.close();
}
};
```
### How TTS Works
```
1. Connect → 2. Authenticate → 3. Create context → 4. Send text → 5. Flush → 6. Get audio → 7. Close
```
***
## 📋 STT vs TTS Comparison
| Feature | STT (Speech-to-Text) | TTS (Text-to-Speech) |
| ----------------- | ------------------------ | --------------------------- |
| **Input** | Audio (binary) | Text (string) |
| **Output** | Text (string) | Audio (binary) |
| **Use Case** | Transcribe speech | Generate speech |
| **Direction** | Audio → Text | Text → Audio |
| **First Message** | `{ type: "start", ... }` | `{ create_context: {...} }` |
| **Session End** | `{ type: "stop" }` | `{ close_context: {...} }` |
| **Pricing** | \$0.00000833/second | \$0.00002/character |
***
## 📚 Full Documentation
* 🎤 [STT WebSocket](/websocket-api/stt) - Complete STT guide with all parameters
* 🔊 [TTS WebSocket](/websocket-api/tts) - Complete TTS guide with all parameters
***
## 💡 Key Concepts
### STT Messages
```javascript theme={null}
// Start session
{ type: "start", languages: ["en"], config: {...} }
// Stop session
{ type: "stop" }
```
### TTS Messages
```javascript theme={null}
// Create context
{ create_context: { context_id, voice_id, audio_config } }
// Send text
{ send_text: { context_id, text: "..." } }
// Get audio
{ flush_context: { context_id } }
// Close session
{ close_context: { context_id } }
```
***
## 📊 Pricing
| Service | Rate | Minimum |
| ------- | ------------------- | ------- |
| STT | \$0.00000833/second | \$0.01 |
| TTS | \$0.00002/character | \$0.01 |
***
## 🆘 Support
* **Email**: [support@60db.ai](mailto:support@60db.ai)
* **Documentation**: [https://docs.60db.ai](https://docs.60db.ai)
* **Status**: [https://status.60db.ai](https://status.60db.ai)
# STT WebSocket
Source: https://docs.60db.ai/websocket-api/stt
WebSocket /ws/stt
Real-time Speech-to-Text WebSocket API for streaming audio transcription
# STT WebSocket API
Real-time Speech-to-Text transcription via WebSocket streaming with support for 39 languages (including code-switched Indic+English) and telephony integration. Powered by 60db STT v01 (a non-hallucinating, multi-backend speech recognition stack).
## 🚀 Quick Start (Copy & Paste)
```javascript theme={null}
const WebSocket = require('ws');
// 1. Your API key
const API_KEY = 'sk_live_your_api_key';
// 2. Connect
const ws = new WebSocket(`wss://api.60db.ai/ws/stt?apiKey=${API_KEY}`);
// 3. Handle messages
ws.on('message', (data) => {
const msg = JSON.parse(data);
// Authenticated? Start session!
// NOTE: on STT the fields are top-level and the frame is tagged by `type`.
if (msg.type === 'connection_established') {
console.log('✅ Authenticated');
ws.send(JSON.stringify({
type: 'start',
languages: ['en'],
config: { encoding: 'mulaw', sample_rate: 8000, continuous_mode: true }
}));
}
// Session ready? Send audio!
// Gate on `session_started`, not `connected` — `connected` fires before the
// session exists and arrives twice (proxy frame, then upstream frame).
if (msg.type === 'session_started') {
console.log('✅ Ready! Send audio now');
// Send dummy audio (480 bytes every 60ms)
let count = 0;
const interval = setInterval(() => {
ws.send(Buffer.alloc(480, 0xff));
if (++count >= 83) { // 5 seconds
clearInterval(interval);
ws.send(JSON.stringify({ type: 'stop' }));
}
}, 60);
}
// Got text!
if (msg.type === 'transcription' && msg.is_final) {
console.log('📝', msg.text);
}
// Done!
if (msg.type === 'session_stopped') {
console.log('✅ Complete! Cost:', msg.billing_summary.total_cost);
ws.close();
}
});
```
**That's it!** You'll see:
* ✅ Authenticated
* ✅ Ready! Send audio now
* 📝 Hello world (transcribed text)
* ✅ Complete! Cost: \$0.000043
***
## 📖 How It Works (5 Simple Steps)
1. **Connect** with your API key
2. **Send** `{ type: "start", ... }` to begin session
3. **Stream** audio data (binary chunks)
4. **Receive** text transcriptions in real-time
5. **Stop** with `{ type: "stop" }` when done
***
## Endpoint
`ws://api.60db.ai/ws/stt` or `wss://api.60db.ai/ws/stt`
## Authentication
Query parameter authentication:
Your API key for authentication
Example:
```
ws://api.60db.ai/ws/stt?apiKey=sk_live_your_api_key
```
## Connection Details
| Property | Value |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Protocol | WebSocket (RFC 6455) |
| Frame types | Binary (telephony) or Text/JSON (browser) |
| Ping/keepalive | The server answers client-initiated WebSocket pings with a pong; it does not ping first. Send a ping every 20–30 s so intermediaries do not idle the socket out. |
## Session Lifecycle
```
Client Server
│ │
│──── TCP/TLS connect ─────────►│
│◄─── {"type":"connecting"} ────│ authenticating...
│◄─── {"type":"connected"} ─────│ proxy wired to upstream
│◄─── connection_established ──│ authenticated — safe to send `start`
│ │
│──── {"type":"start", ...} ───►│
│◄─── {"type":"session_started"}│ safe to send audio
│ │
│──── audio frames / messages ─►│
│◄─── {"type":"speech_started"} │ VAD detected voice
│◄─── {"type":"transcription"} │ is_final=true, speech_final=false (first emit, context only)
│◄─── {"type":"transcription"} │ is_final=true, speech_final=true (canonical answer)
│ │
│──── {"type":"stop"} ─────────►│
│◄─── {"type":"session_stopped"}│
```
**Two-phase finals (context-gated LLM refinement).** When you supply a `context` object on `start`, every utterance produces **two** `transcription` events sharing a `sentence_id`:
1. **First emit** — `is_final: true, speech_final: false` — fast dict-corrected text. Use for low-latency UI paint and barge-in.
2. **Canonical** — `is_final: true, speech_final: true` — definitive LLM-refined answer. Always arrives.
When `context` is **omitted**, every utterance produces a single `transcription` event with `is_final: true, speech_final: true` (no first emit). Simple consumers can gate exclusively on `speech_final: true` and ignore the rest — that gives them exactly one canonical event per utterance regardless of whether refinement is on.
## Client → Server Messages
### `start` — Begin session
Sent once after connection is established. Must be sent before any audio.
```json Request theme={null}
{
"type": "start",
"languages": ["en", "hi"],
"context": {
"general": [
{ "key": "domain", "value": "Healthcare" },
{ "key": "doctor", "value": "Dr. Martha Smith" }
],
"text": "Routine diabetes follow-up consultation.",
"terms": ["Celebrex", "Zyrtec", "Metformin", "HbA1c"]
},
"config": {
"encoding": "mulaw",
"sample_rate": 8000,
"utterance_end_ms": 1000,
"continuous_mode": true,
"interim_results_frequency": 300,
"audio_enhancement": "adaptive",
"diarize": false,
"remove_fillers": false
}
}
```
**Parameters:**
Array of ISO 639-1 codes from the supported set (see `GET /stt/languages`), e.g. `["en", "hi"]`. Max 5 per session. Omit or send `null` to auto-detect across all 39 v1 languages. Arabic dialect tags (`ar-eg`, …) are rejected. Unsupported: `ur`, `ja`, `ko`, `zh`, `th`, `vi`, `id`, `tl`, `sw`, `tr`, `fa`, `he`.
**Important:** never send `languages: "auto"` or `["auto"]`. The auto entry in `GET /stt/languages` is a convenience for the REST `/stt` form-upload flow; on WebSocket you must use `null` instead. Sending the literal string `"auto"` returns `language 'auto' is not in the v1 supported`. The 60db `/ws/stt` proxy strips it automatically as a safety net, but your client should send `null` directly.
Optional hint object `{general, text, terms}` that opens the server-side LLM refinement gate. When supplied, each utterance emits **two** `transcription` events sharing a `sentence_id` — a fast first emit (`speech_final: false`) with dict-corrected text, followed \~300–700 ms later by a canonical emit (`speech_final: true`) with LLM-refined text (proper nouns corrected, fillers removed, punctuation added, script consistency enforced). See [Canonical-answer semantics](#canonical-answer-semantics-speech-final) below.
* `general` — array of `{key, value}` pairs. Free-form metadata (`domain`, `topic`, speaker names) surfaced to the LLM verbatim as hint lines.
* `text` — background paragraph describing the session. Useful for narrative context.
* `terms` — array of proper nouns, acronyms, and domain-specific jargon to preserve in the transcript.
All three fields are optional; at least one should be populated. Omit `context` entirely to disable refinement for the session — each utterance then arrives as a single `speech_final: true` event (no first emit).
Audio encoding format. Use `"mulaw"` for telephony/Twilio, `"linear"` (Int16 PCM) for browser capture. Options: `"mulaw"`, `"linear"`. The first raw binary frame auto-selects `mulaw`.
Actual sample rate of the audio being sent — 8000 for telephony, 48000 for typical browser capture. Server resamples to 16 kHz internally. Options: `8000`, `16000`, `24000`, `44100`, `48000`. Must match the real capture rate; a mismatch produces garbled audio with no error.
Silence duration (ms) after last speech chunk before finalizing the utterance. **Minimum 1000 ms** — the upstream server rejects anything lower, so the 60db proxy clamps sub-1000 values up to 1000 ms (on `start` and on mid-session `config`) rather than letting the session die. Sending `500` is accepted but behaves as `1000`. Recommended 1000–1500 ms for voicebots; for barge-in, react to `speech_started` and interim results instead of lowering this. Range: `≥ 1000`
Keep session alive between utterances instead of stopping after the first transcription. Required for voicebot / phone call use cases. Send `false` for single-shot transcription.
How often (ms) to emit interim (partial) transcription results during speech. Use 300ms for barge-in, 500ms otherwise. Disabled by default. Range: `≥ 300`
Run pyannote speaker diarization on each finalized utterance and attach a `speakers` array. Requires `HF_TOKEN` on the server. Adds \~50–150 ms latency per final.
Lower bound on diarization speaker count. Only read when `diarize=true`.
Upper bound on diarization speaker count. Only read when `diarize=true`.
Real-time audio enhancement to improve transcription quality on noisy input. There are **3 options** available:
| Value | Description |
| ------------ | -------------------------------------------------------------------------------------------------- |
| `"off"` | No audio processing (default) |
| `"light"` | Noise reduction only — best for mildly noisy environments |
| `"adaptive"` | Automatic noise reduction + gain control based on input levels — best for variable/telephony audio |
Use `"adaptive"` for telephony or noisy environments, `"light"` when you only need mild cleanup, and `"off"` when the input is already clean.
Ask the LLM refinement pass to strip filler words (`um`, `uh`, `like`, `you know`, …) from the canonical transcript. Only takes effect when `context` is set — refinement is gated on context, and the raw first-emit still contains the fillers. Non-boolean values are coerced to `false` by the 60db proxy.
Reserved for legacy client compatibility. **Ignored by the 60db STT backend** — the non-hallucinating backends don't emit a `no_speech_prob`.
### `audio` — JSON audio chunk (browser mode)
```json Request theme={null}
{
"type": "audio",
"audio": "",
"encoding": "linear",
"sample_rate": 48000,
"timestamp": 1700000000000
}
```
**Fields:**
Must be `"audio"`
Base64-encoded audio bytes (Int16 PCM or μ-law)
`"linear"` or `"mulaw"`
Actual sample rate of the audio
Unix ms timestamp — useful for latency measurement
### Binary frame — raw μ-law audio (telephony mode)
Send a raw WebSocket binary frame with μ-law bytes, no JSON wrapper.
The server auto-detects this as telephony mode on the first binary frame.
```
Recommended chunk size: 480 bytes = 60ms at 8kHz
Twilio default: 160 bytes = 20ms — batch 3 chunks into 60ms before sending
```
### `config` — Change language mid-session
```json Request theme={null}
{
"type": "config",
"languages": ["hi"],
"continuous_mode": true
}
```
Both `languages` and `continuous_mode` are optional; include only fields you want to change.
Send `"languages": null` to revert to auto-detect.
### `stop` — End session
```json Request theme={null}
{
"type": "stop"
}
```
Server processes any remaining audio buffer, sends `session_stopped`, then closes.
### `test` — Ping / latency check
```json Request theme={null}
{
"type": "test",
"message": "ping",
"timestamp": 1700000000000
}
```
Server echoes `test_response` with the same `timestamp` for round-trip measurement.
## Server → Client Messages
### `connecting` — Authentication in progress
```json Response theme={null}
{
"type": "connecting",
"message": "Authenticating...",
"timestamp": 1775465918269
}
```
### `connection_established` — Authentication successful
```json Response theme={null}
{
"type": "connection_established",
"service": "stt",
"user_id": 43,
"credit_balance": 9.97,
"workspace": "default"
}
```
On STT these fields are **top-level** and the frame is tagged by `type`. Check `msg.type === "connection_established"` — `msg.connection_established` is the TTS frame's shape, and a client written against it will never start its STT session.
**Fields:**
Service name: `"stt"`
Your user ID
Available credits
Workspace name
### `connected` — After `start` message is processed
```json Response theme={null}
{
"type": "connected",
"server_info": {
"server_type": "60db STT",
"device": "cuda",
"model": "60db-stt-v01",
"processing_mode": "sentence_based_modular",
"supported_languages": { "en": "English", "hi": "Hindi" },
"total_languages": 40,
"features": {
"sentence_based_processing": true,
"real_time_streaming": true,
"telephony_support": true,
"unicode_support": true,
"mixed_language_support": true
}
}
}
```
Expect **two** `connected` frames: the proxy's own handshake (`session_id` plus a small `server_info`), then the upstream capability frame shown above. Neither means "audio is accepted" — wait for `session_started`. Keep any `connected` handler idempotent so audio capture does not start twice.
### `session_started` — `start` accepted, audio is now allowed
```json Response theme={null}
{
"type": "session_started",
"session_id": "sess_8c3d1a9f4b7e2c51",
"language": "Multi-language: EN, HI",
"languages": ["en", "hi"],
"model": "60db-stt-v01",
"processing_mode": "sentence_based_continuous",
"continuous_mode": true,
"interim_frequency": 300,
"diarize": false,
"llm_refinement": true
}
```
Send audio only after this event — earlier frames are answered with `unknown message type: audio`. `llm_refinement: true` confirms your `context` opened the refinement gate, so every utterance will arrive as a two-phase pair; `false` means one canonical event per utterance. `languages` is the resolved candidate list after normalization.
### `speech_started` — VAD detected voice activity
```json Response theme={null}
{
"type": "speech_started",
"timestamp": 1700000000.123
}
```
Use this for barge-in: interrupt TTS playback when this arrives.
Fired after 2 consecutive VAD-positive chunks (\~64ms of confirmed speech).
### `transcription` — Transcription result
All results (interim and final) share the same `transcription` type — differentiate with flags.
**Final result** (`is_final=true`, `speech_final=true`):
```json Response theme={null}
{
"type": "transcription",
"text": "Hello, how are you?",
"confidence": 0.87,
"language": "en",
"language_name": "EN",
"is_final": true,
"speech_final": true,
"is_partial": false,
"sentence_id": 3,
"processing_mode": "sentence_complete",
"duration": 1.82,
"latency": 0.43,
"timestamp": 1700000000.456,
"words": [
{ "word": "Hello", "start": 0.0, "end": 0.32, "confidence": 0.94 },
{ "word": "how", "start": 0.35, "end": 0.52, "confidence": 0.92 }
],
"utterance_end_ms": 1820
}
```
**Empty speech\_final signal** (`text=""`, `is_final=true`, `speech_final=true`):
Sent when audio was detected but transcription was rejected (silence, hallucination, low confidence, wrong language).
Client should reset its state on this message and not treat it as an error.
```json Response theme={null}
{
"type": "transcription",
"text": "",
"confidence": 0.0,
"is_final": true,
"speech_final": true,
"processing_mode": "speech_end_no_result",
"timestamp": 1700000000.789
}
```
**Interim result** (`is_final=false`, `speech_final=false`) — only sent when `interim_results_frequency` is set:
```json Response theme={null}
{
"type": "transcription",
"text": "Hello how",
"confidence": 0.72,
"language": "en",
"is_final": false,
"speech_final": false,
"is_partial": true
}
```
Use interims only for barge-in word-count checks. Never send interim text to the LLM — a final with `is_final=true, speech_final=true` will follow.
**Response Fields:**
Transcribed text. Empty string = speech-end-no-result signal.
0.0–1.0. Telephony typically 0.35–0.75; browser 0.55–0.95.
Detected language code e.g. `"en"`.
Uppercase language code e.g. `"EN"`.
`true` = end of speech reached. May still be followed by a canonical upgrade if LLM refinement is active.
`true` = canonical answer, will not be revised. When LLM refinement is on, one `is_final: true, speech_final: false` event is followed by one `is_final: true, speech_final: true`. When refinement is off, every final is `speech_final: true`. See [Canonical-answer semantics](#canonical-answer-semantics-speech-final).
`true` for interim results only.
Monotonically increasing counter per session.
Duration (seconds) of the audio segment transcribed.
Seconds from processing start to result ready (excludes queue time).
Word-level timestamps `[{word, start, end, confidence}]`. Note: the field is **`confidence`**, not `probability`. Present on finals; empty on interims.
Timestamp (ms) of last word in the utterance.
Marker for utterances the consumer should skip. Omitted on ordinary finals. **None of these are billed.**
| Value | Meaning |
| ------------------------ | ------------------------------------------------------------------------- |
| `speech_end_no_result` | Speech ended but the recognizer produced no text. |
| `speech_end_too_short` | Utterance below the minimum duration to recognize. |
| `hallucination_rejected` | Word-rate guard rejected the output as a likely hallucination. |
| `low_snr_dropped` | Audio dropped before language ID / ASR — signal-to-noise below the floor. |
On canonical emits where refinement ran: `true` = `text` is the LLM-refined version, `false` = the LLM was skipped or failed and the fast first-emit text was promoted unchanged. `llm_reason` carries the why, `llm_latency_ms` the round-trip time. The canonical always arrives either way.
Added by the 60db proxy when the upstream rejected the utterance as a suspected hallucination but interim text was available — the proxy sends that text instead of an empty final, with `tentative_reason: "hallucination_rejected"`. Safe to route on; flag it for review.
### Canonical-answer semantics: `speech_final`
`is_final` and `speech_final` are **NOT identical** when LLM refinement is active — they split into two distinct meanings:
| `is_final` | `speech_final` | Meaning |
| ---------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `false` | `false` | Interim partial — text may still change as more audio arrives. |
| `true` | `false` | End-of-speech reached, dict-corrected text. **The LLM is still processing.** A follow-up event with `speech_final: true` will arrive shortly with the canonical text. Only emitted when refinement is active for this utterance. |
| `true` | `true` | **Canonical answer.** Definitive, will not be revised. LLM-refined text when context was supplied, otherwise the original ASR text. |
The same `sentence_id` is echoed across both phases so clients can reconcile.
**Canonical event example (after LLM refinement):**
```json Response theme={null}
{
"type": "transcription",
"sentence_id": 3,
"text": "डॉक्टर साहब, मेरा sugar level बहुत high है। Metformin की dose बढ़ाओ।",
"confidence": 0.87,
"language": "hi",
"language_name": "HI",
"is_final": true,
"speech_final": true,
"is_partial": false,
"duration": 1.82,
"words": [
{ "word": "डॉक्टर", "start": 0.0, "end": 0.32, "confidence": 0.94 }
],
"speakers": null,
"timestamp": 1700000000.789
}
```
**Guarantees:**
* **Exactly one canonical event per utterance.** When refinement is on, you get two `transcription` events per utterance (first emit + canonical). When refinement is off, you get one (`speech_final: true`). Never zero, never three.
* **Same `sentence_id`** across both phases. Reconcile on that key.
* **The canonical always arrives.** Consumers waiting on `speech_final: true` never hang.
* **`sentence_id` ordering is preserved per session**, but canonicals are NOT guaranteed to arrive in `sentence_id` order when LLM is on — two utterances finalizing close in time may complete refinement out of order. Key on `sentence_id`, not arrival order.
* **`words[]` corresponds to the original ASR output** on both phases — the LLM does not realign tokens. Use `words[]` for word-level timing, `text` for display.
**Recommended client patterns:**
*Simplest — don't care about the first-emit optimization:*
```js theme={null}
function onMessage(msg) {
if (msg.type === 'transcription' && msg.speech_final) {
// Canonical — render and forget
render(msg);
}
// Ignore is_final && !speech_final (intermediate, will be replaced)
// Ignore is_partial (interim, handle separately if needed)
}
```
*With fast first-emit (UX-aware):*
```js theme={null}
const livePainted = new Map(); // sentence_id → line slot
function onMessage(msg) {
if (msg.type !== 'transcription') return;
const sid = msg.sentence_id;
if (msg.is_partial) { renderPartial(msg); return; }
let entry = livePainted.get(sid);
if (!entry) {
entry = createLine();
livePainted.set(sid, entry);
}
entry.text = msg.text;
entry.pending = msg.is_final && !msg.speech_final; // dim while LLM runs
render(entry);
if (msg.speech_final) {
finalize(entry);
livePainted.delete(sid);
}
}
```
**For voicebot NLU routing**: feed the first-emit text (`speech_final: false`) to NLU immediately for fast intent dispatch — don't wait for canonical. If your NLU benefits from proper-noun accuracy (name-spelling slots, drug-name lookup), run a second-pass call on the canonical (`speech_final: true`) text and reconcile on `sentence_id`.
**Legacy `refined` event.** Earlier builds emitted a separate `refined` event \~400 ms after the final instead of a second `transcription`. The 60db `/ws/stt` proxy transparently handles both shapes — if you're still seeing `refined` events in the wire trace, upstream workers haven't been restarted onto the two-phase build yet. New client code should target the two-phase flow only; `refined` is accepted but deprecated.
### `language_changed` — After `config` message changes language
```json Response theme={null}
{
"type": "language_changed",
"language": "Multi-language: HI",
"language_code": ["hi"]
}
```
### `mode_changed` — After `config` message changes `continuous_mode`
```json Response theme={null}
{
"type": "mode_changed",
"continuous_mode": true,
"mode_name": "continuous",
"silence_threshold": 0.5
}
```
### `session_stopped` — After `stop` is processed
```json Response theme={null}
{
"type": "session_stopped",
"billing_summary": {
"total_duration_seconds": 5.2,
"total_cost": 0.000043,
"characters_transcribed": 42,
"client_estimated_seconds": 5.24
}
}
```
Only canonical finals (`is_final: true` **and** `speech_final: true`) with a `duration` are charged — first emits are previews, so turning on LLM refinement does not double your bill, and the four skip modes above cost nothing. `client_estimated_seconds` is a diagnostic estimate of what the client sent; never show it as a billed figure.
### `error` — Processing error
```json Response theme={null}
{
"type": "error",
"error": "Audio processing error: ...",
"timestamp": 1700000000.0
}
```
### `test_response` — Reply to `test` ping
```json Response theme={null}
{
"type": "test_response",
"message": "pong - qlabs-stt-proxy ready",
"timestamp": 1700000000000,
"processing_mode": "proxy"
}
```
Answered by the 60db proxy itself — it never reaches the STT server, so a reply confirms the proxy hop only, and the echoed `timestamp` measures round-trip to the proxy.
## Complete Example
```javascript theme={null}
const WebSocket = require('ws');
const API_KEY = 'sk_live_your_key';
const ws = new WebSocket(`ws://api.60db.ai/ws/stt?apiKey=${API_KEY}`);
ws.on('open', () => {
console.log('✓ Connected');
});
ws.on('message', (data) => {
const msg = JSON.parse(data);
console.log('←', msg.type || Object.keys(msg)[0]);
if (msg.type === 'connection_established') {
console.log(' User ID:', msg.user_id);
console.log(' Credits:', msg.credit_balance);
// Start session
ws.send(JSON.stringify({
type: 'start',
languages: ['en', 'hi'],
config: {
encoding: 'mulaw',
sample_rate: 8000,
continuous_mode: true,
utterance_end_ms: 1000,
interim_results_frequency: 300
}
}));
} else if (msg.type === 'session_started') {
console.log('✓ Session started! Send audio now...');
// Send audio chunks (480 bytes = ~60ms at 8kHz)
const audioInterval = setInterval(() => {
const audioChunk = getAudioChunk();
ws.send(audioChunk);
}, 60);
// Stop after 5 seconds
setTimeout(() => {
clearInterval(audioInterval);
ws.send(JSON.stringify({ type: 'stop' }));
}, 5000);
} else if (msg.type === 'speech_started') {
console.log('🎤 Speech detected - barge-in opportunity');
} else if (msg.type === 'transcription') {
if (msg.is_final) {
console.log('✓ Final:', msg.text, `(confidence: ${msg.confidence})`);
} else {
console.log(' Partial:', msg.text);
}
} else if (msg.type === 'session_stopped') {
console.log('✓ Session stopped');
console.log(' Duration:', msg.billing_summary.total_duration_seconds, 's');
console.log(' Cost: $', msg.billing_summary.total_cost);
ws.close();
}
});
ws.on('error', (error) => {
console.error('Error:', error);
});
ws.on('close', () => {
console.log('Connection closed');
});
```
```python theme={null}
import asyncio
import json
import websockets
async def stt_websocket():
API_KEY = "sk_live_your_key"
url = f"ws://api.60db.ai/ws/stt?apiKey={API_KEY}"
async with websockets.connect(url) as ws:
# Wait for authentication: `connecting`, then `connected`, then this
while True:
msg = json.loads(await ws.recv())
if msg.get("type") == "connection_established":
print(f"✓ Connected (User: {msg['user_id']})")
print(f" Credits: ${msg['credit_balance']}")
break
# Start session
await ws.send(json.dumps({
"type": "start",
"languages": ["en", "hi"],
"config": {
"encoding": "mulaw",
"sample_rate": 8000,
"continuous_mode": True,
"utterance_end_ms": 1000,
"interim_results_frequency": 300
}
}))
# Audio is accepted only once the session is started
while True:
msg = json.loads(await ws.recv())
if msg.get("type") == "session_started":
break
print("✓ Session started!")
# Send audio for 5 seconds
for _ in range(83): # ~5000ms / 60ms
audio_chunk = get_audio_chunk()
await ws.send(audio_chunk) # Send as binary
await asyncio.sleep(0.06)
# Stop session
await ws.send(json.dumps({"type": "stop"}))
# Process remaining messages
while True:
msg = json.loads(await ws.recv())
msg_type = msg.get("type")
if msg_type == "speech_started":
print("🎤 Speech detected")
elif msg_type == "transcription":
if msg.get("is_final"):
print(f"✓ {msg['text']} (confidence: {msg['confidence']})")
else:
print(f" {msg['text']}...")
elif msg_type == "session_stopped":
print(f"✓ Session stopped")
print(f" Duration: {msg['billing_summary']['total_duration_seconds']}s")
print(f" Cost: ${msg['billing_summary']['total_cost']}")
break
asyncio.run(stt_websocket())
```
```javascript theme={null}
const ws = new WebSocket('ws://api.60db.ai/ws/stt?apiKey=sk_live_your_key');
let mediaRecorder;
let audioContext;
ws.onopen = () => {
console.log('✓ Connected');
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'connection_established') {
console.log('✓ Authenticated!');
// Start session
ws.send(JSON.stringify({
type: 'start',
languages: ['en'],
config: {
encoding: 'linear',
sample_rate: 48000,
continuous_mode: true,
interim_results_frequency: 300
}
}));
} else if (msg.type === 'session_started') {
console.log('✓ Session started! Start speaking...');
startAudioCapture();
} else if (msg.type === 'speech_started') {
console.log('🎤 Speech detected');
} else if (msg.type === 'transcription') {
if (msg.is_final) {
console.log('✓', msg.text);
updateTranscriptDisplay(msg.text);
} else {
console.log('...', msg.text);
updateInterimDisplay(msg.text);
}
} else if (msg.type === 'session_stopped') {
console.log('✓ Session stopped');
stopAudioCapture();
ws.close();
}
};
async function startAudioCapture() {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
audioContext = new AudioContext({ sampleRate: 48000 });
const source = audioContext.createMediaStreamSource(stream);
const processor = audioContext.createScriptProcessor(4096, 1, 1);
processor.onaudioprocess = (e) => {
const inputData = e.inputBuffer.getChannelData(0);
const pcm16 = new Int16Array(inputData.length);
for (let i = 0; i < inputData.length; i++) {
pcm16[i] = Math.max(-32768, Math.min(32767, inputData[i] * 32768));
}
ws.send(JSON.stringify({
type: 'audio',
audio: btoa(String.fromCharCode(...new Uint8Array(pcm16.buffer))),
encoding: 'linear',
sample_rate: 48000
}));
};
source.connect(processor);
processor.connect(audioContext.destination);
mediaRecorder = { stream, processor, source };
}
function stopAudioCapture() {
if (mediaRecorder) {
mediaRecorder.stream.getTracks().forEach(track => track.stop());
mediaRecorder.processor.disconnect();
audioContext.close();
}
}
```
## Audio Requirements
| Property | Telephony (μ-law) | Browser (PCM) |
| ----------- | ----------------- | ----------------------------- |
| Encoding | `mulaw` (8-bit) | `linear` (16-bit) |
| Sample Rate | 8000 Hz | 16000, 24000, 44100, 48000 Hz |
| Chunk Size | 480 bytes (60ms) | 960-1920 bytes (60-120ms) |
| Channels | Mono (1 channel) | Mono (1 channel) |
## Supported Languages
39 languages total, backed by Parakeet-TDT (25 European), Vaani-FastConformer (13 Indic + Hinglish), and FC-Arabic (MSA). Fetch the full catalog from `GET /stt/languages`.
**Not supported** (explicit rejection, no silent aliasing): `ur`, `ja`, `ko`, `zh`, `th`, `vi`, `id`, `tl`, `sw`, `tr`, `fa`, `he`. Arabic dialect tags (`ar-eg`, `ar-lv`, …) return `dialect_not_supported` — pass `ar` for best-effort MSA.
Common languages:
| Code | Language | Code | Language |
| ---- | --------- | ---- | -------- |
| `en` | English | `hi` | Hindi |
| `bn` | Bengali | `es` | Spanish |
| `fr` | French | `de` | German |
| `gu` | Gujarati | `ta` | Tamil |
| `te` | Telugu | `kn` | Kannada |
| `ml` | Malayalam | `mr` | Marathi |
| `pa` | Punjabi | `ar` | Arabic |
## Pricing
* **Rate**: \$0.00000833 per second
* **Minimum**: \$0.01 per session
* **Billing**: Per second of audio processed
## Related
* [TTS WebSocket](/websocket-api/tts) - Text-to-Speech endpoint
* [WebSocket Quick Start](/websocket-api/quickstart) - Get started guide
* [WebSocket Playground](/websocket-playground) - Test in browser
# TTS WebSocket
Source: https://docs.60db.ai/websocket-api/tts
WebSocket /ws/tts
Real-time Text-to-Speech WebSocket API for streaming audio synthesis
# TTS WebSocket API
Real-time Text-to-Speech synthesis via WebSocket streaming with full-duplex bidirectional communication.
## 🚀 Quick Start (Copy & Paste)
```javascript theme={null}
const WebSocket = require('ws');
const fs = require('fs');
// 1. Your API key
const API_KEY = 'sk_live_your_api_key';
// 2. Connect
const ws = new WebSocket(`wss://api.60db.ai/ws/tts?apiKey=${API_KEY}`);
// 3. Unique context ID
const contextId = 'my-session-' + Date.now();
const audioChunks = [];
// 4. Handle messages
ws.on('message', (data) => {
const msg = JSON.parse(data);
// Authenticated? Create context!
if (msg.connection_established) {
console.log('✅ Authenticated');
ws.send(JSON.stringify({
create_context: {
context_id: contextId,
voice_id: 'fbb75ed2-975a-40c7-9e06-38e30524a9a1',
audio_config: { audio_encoding: 'LINEAR16', sample_rate_hertz: 16000 }
}
}));
}
// Context ready? Send text!
if (msg.context_created) {
console.log('✅ Context created');
// Send your text
ws.send(JSON.stringify({
send_text: { context_id: contextId, text: 'Hello, world!' }
}));
// Flush to get audio
ws.send(JSON.stringify({
flush_context: { context_id: contextId }
}));
}
// Got audio? Save it!
if (msg.audio_chunk) {
const audioData = Buffer.from(msg.audio_chunk.audioContent, 'base64');
audioChunks.push(audioData);
console.log('🔊 Audio chunk:', audioData.length, 'bytes');
}
// All audio received?
if (msg.flush_completed) {
console.log('✅ All audio received!');
console.log(' Total:', audioChunks.length, 'chunks');
// Close context
ws.send(JSON.stringify({
close_context: { context_id: contextId }
}));
}
// Done! Save audio
if (msg.context_closed) {
console.log('✅ Complete!');
// Save to file
const completeAudio = Buffer.concat(audioChunks);
fs.writeFileSync('output.pcm', completeAudio);
console.log('💾 Saved: output.pcm');
console.log(' Size:', completeAudio.length, 'bytes');
ws.close();
}
});
```
**That's it!** You'll see:
* ✅ Authenticated
* ✅ Context created
* 🔊 Audio chunk: 1024 bytes (multiple times)
* ✅ All audio received!
* ✅ Complete!
* 💾 Saved: output.pcm
***
## 📖 How It Works (5 Simple Steps)
1. **Connect** with your API key
2. **Create** a context with voice settings
3. **Send** your text message
4. **Flush** to trigger synthesis
5. **Close** when done (receive audio file)
***
## Endpoint
`ws://api.60db.ai/ws/tts`
## Authentication
Query parameter authentication:
Your API key for authentication
Example:
```
ws://api.60db.ai/ws/tts?apiKey=sk_live_your_api_key
```
## Protocol Overview
```
Client Server
| |
|─── create_context ──────────────────▶ |
|◀── context_created ───────────────── |
| |
|─── send_text ───────────────────────▶ |
|─── flush_context ───────────────────▶ |
|◀── audio_chunk #1 ────────────────── |
|◀── audio_chunk #N ────────────────── |
|◀── flush_completed ───────────────── |
| |
|─── close_context ───────────────────▶ |
|◀── context_closed ────────────────── |
```
## Connection Sequence
### 1. Connect
```javascript theme={null}
const ws = new WebSocket('ws://api.60db.ai/ws/tts?apiKey=sk_live_your_key');
```
### 2. Receive Authentication Message
```json Response theme={null}
{
"connecting": true,
"message": "Authenticating...",
"timestamp": 1775465918269
}
```
### 3. Receive Connection Established
```json Response theme={null}
{
"connection_established": {
"service": "tts",
"user_id": 43,
"credit_balance": 9.97,
"workspace": "default"
}
}
```
**Fields:**
Service name: `"tts"`
Your user ID
Available credits
Workspace name
## Client → Server Messages
### 1. create\_context
**Must be the first message.** Initializes the TTS session with voice and audio settings.
```json Request theme={null}
{
"create_context": {
"context_id": "my-session-123",
"voice_id": "7911a3e8",
"audio_config": {
"audio_encoding": "LINEAR16",
"sample_rate_hertz": 16000
},
"speed": 1,
"stability": 50,
"similarity": 75
}
}
```
**Parameters:**
Unique session identifier. Default: auto-generated UUID
Voice ID to use for synthesis
Audio encoding. Options: `LINEAR16`, `PCM`, `MULAW`, `ULAW`, `OGG_OPUS`
Sample rate in Hz. Options: `8000`, `16000`, `24000`, `48000`
Speech speed multiplier (0.5 – 2.0).
Voice stability (0-100). Lower = more expressive, higher = more consistent.
Voice similarity (0-100). How closely the output matches the source voice.
**Supported encoding + sample rate combinations:**
| `audio_encoding` | Supported `sample_rate_hertz` | Output format |
| ---------------- | ------------------------------------------- | ------------------------------------------ |
| `LINEAR16` | `8000`, `16000` (default), `24000`, `48000` | Raw PCM, 16-bit signed little-endian, mono |
| `PCM` | `8000`, `16000` (default), `24000`, `48000` | Same as LINEAR16 |
| `MULAW` | `8000` | G.711 μ-law encoded, mono |
| `ULAW` | `8000` | Same as MULAW |
| `OGG_OPUS` | `24000` | Ogg Opus compressed audio |
> **Note:** `MULAW`/`ULAW` only works at `8000` Hz. `OGG_OPUS` only works at `24000` Hz.
**Limits:**
| Parameter | Min | Max | Default |
| ------------------------- | ------ | ------------ | ------- |
| `speed` | 0.5 | 2.0 | 1 |
| `stability` | 0 | 100 | 50 |
| `similarity` | 0 | 100 | 75 |
| `text` (per send\_text) | 1 char | — | — |
| text buffer (accumulated) | — | 50,000 chars | — |
### 2. send\_text
Append text to the internal buffer. Text is accumulated until a `flush_context` or `close_context` is received.
```json Request theme={null}
{
"send_text": {
"context_id": "my-session-123",
"text": "Hello, how are you doing today?"
}
}
```
**Fields:**
Session identifier
Text to append to buffer (max cumulative 50,000 characters)
You can send multiple `send_text` messages to build up text incrementally (e.g., from an LLM token stream):
```json theme={null}
{"send_text": {"context_id": "ctx-1", "text": "Hello, "}}
{"send_text": {"context_id": "ctx-1", "text": "how are you "}}
{"send_text": {"context_id": "ctx-1", "text": "doing today?"}}
```
### 3. flush\_context
Triggers synthesis of all accumulated text. The server responds with `audio_chunk` messages followed by `flush_completed`.
```json Request theme={null}
{
"flush_context": {
"context_id": "my-session-123"
}
}
```
### 4. close\_context
Flushes any remaining text, sends final audio, and closes the WebSocket connection.
```json Request theme={null}
{
"close_context": {
"context_id": "my-session-123"
}
}
```
## Server → Client Messages
### context\_created
Confirms the session was initialized successfully.
```json Response theme={null}
{
"context_created": {
"context_id": "my-session-123"
}
}
```
### audio\_chunk
Contains a chunk of synthesized audio. Multiple chunks are sent per flush.
```json Response theme={null}
{
"audio_chunk": {
"context_id": "my-session-123",
"audioContent": "SGVsbG8gd29ybGQ..."
}
}
```
**Fields:**
Session identifier
Base64-encoded audio bytes
The audio encoding and chunk format depend on `audio_config`:
| Encoding | Chunk format | Notes |
| ------------------ | ------------------------------- | ---------------------------------------------------------------------------------- |
| `LINEAR16` / `PCM` | Raw PCM, 16-bit signed LE, mono | Chunks can be concatenated directly |
| `MULAW` / `ULAW` | G.711 μ-law, 8-bit, mono | Chunks can be concatenated directly |
| `OGG_OPUS` | Independent Ogg Opus files | Each chunk is a self-contained OGG file. **Chunks cannot be naively concatenated** |
### flush\_completed
Signals that all audio for the flushed text has been sent.
```json Response theme={null}
{
"flush_completed": {
"context_id": "my-session-123"
}
}
```
### context\_closed
Confirms the session is closed. The WebSocket connection closes after this message.
```json Response theme={null}
{
"context_closed": {
"context_id": "my-session-123"
}
}
```
### error
Sent if synthesis fails or a protocol violation occurs.
```json Response theme={null}
{
"error": {
"context_id": "my-session-123",
"message": "voice_id required"
}
}
```
**Common errors:**
| Message | Cause |
| -------------------------------------------- | ------------------------------------------ |
| `voice_id required` | `create_context` sent without `voice_id` |
| `text_buffer exceeded 50000 character limit` | Too much text accumulated without flushing |
| `Unsupported audio_encoding: X` | Invalid encoding value |
| `Unsupported sample_rate_hertz: X` | Invalid sample rate |
## Complete Example
```javascript theme={null}
const WebSocket = require('ws');
const API_KEY = 'sk_live_your_key';
const ws = new WebSocket(`ws://api.60db.ai/ws/tts?apiKey=${API_KEY}`);
const contextId = 'test-' + Date.now();
const audioChunks = [];
ws.on('open', () => {
console.log('✓ Connected');
});
ws.on('message', (data) => {
const msg = JSON.parse(data);
const msgType = Object.keys(msg)[0];
console.log('←', msgType);
if (msg.connection_established) {
console.log(' Credits:', msg.connection_established.credit_balance);
// Create context
ws.send(JSON.stringify({
create_context: {
context_id: contextId,
voice_id: 'fbb75ed2-975a-40c7-9e06-38e30524a9a1',
audio_config: {
audio_encoding: 'LINEAR16',
sample_rate_hertz: 16000
},
speed: 1,
stability: 50,
similarity: 75
}
}));
} else if (msg.context_created) {
console.log('✓ Context created!');
// Send text
ws.send(JSON.stringify({
send_text: {
context_id: contextId,
text: 'Hello, how are you doing today?'
}
}));
// Flush
ws.send(JSON.stringify({
flush_context: { context_id: contextId }
}));
} else if (msg.audio_chunk) {
const audioData = Buffer.from(msg.audio_chunk.audioContent, 'base64');
audioChunks.push(audioData);
console.log(' Received audio chunk:', audioData.length, 'bytes');
} else if (msg.flush_completed) {
console.log('✓ Flush completed!');
console.log(' Total audio size:', audioChunks.reduce((sum, chunk) => sum + chunk.length, 0), 'bytes');
// Close context
ws.send(JSON.stringify({
close_context: { context_id: contextId }
}));
} else if (msg.context_closed) {
console.log('✓ Context closed');
// Save audio
const audio = Buffer.concat(audioChunks);
require('fs').writeFileSync('output.pcm', audio);
console.log('Saved output.pcm');
ws.close();
}
if (msg.error) {
console.error('TTS Error:', msg.error.message);
}
});
ws.on('error', (error) => {
console.error('Error:', error);
});
ws.on('close', () => {
console.log('Connection closed');
});
```
```python theme={null}
import asyncio
import json
import base64
import websockets
async def tts_websocket():
API_KEY = "sk_live_your_key"
url = f"ws://api.60db.ai/ws/tts?apiKey={API_KEY}"
context_id = f"session-{int(asyncio.get_event_loop().time())}"
async with websockets.connect(url) as ws:
# Wait for connection
resp = json.loads(await ws.recv())
if resp.get('connection_established'):
print(f"✓ Connected (Credits: ${resp['connection_established']['credit_balance']})")
# Create context
await ws.send(json.dumps({
"create_context": {
"context_id": context_id,
"voice_id": "fbb75ed2-975a-40c7-9e06-38e30524a9a1",
"audio_config": {
"audio_encoding": "LINEAR16",
"sample_rate_hertz": 16000
},
"speed": 1,
"stability": 50,
"similarity": 75
}
}))
# Wait for context_created
resp = json.loads(await ws.recv())
assert "context_created" in resp
print("✓ Context created!")
# Send text + flush
await ws.send(json.dumps({
"send_text": {
"context_id": context_id,
"text": "Hello, how are you doing today?"
}
}))
await ws.send(json.dumps({
"flush_context": {"context_id": context_id}
}))
# Receive audio chunks until flush_completed
audio_data = b""
chunk_count = 0
while True:
msg = json.loads(await ws.recv())
if "audio_chunk" in msg:
audio_data += base64.b64decode(msg["audio_chunk"]["audioContent"])
chunk_count += 1
print(f" Received chunk #{chunk_count}")
elif "flush_completed" in msg:
print("✓ Flush completed!")
break
elif "error" in msg:
print(f"Error: {msg['error']['message']}")
break
# Close context
await ws.send(json.dumps({
"close_context": {"context_id": context_id}
}))
resp = json.loads(await ws.recv())
assert "context_closed" in resp
# Save audio
with open("output.pcm", "wb") as f:
f.write(audio_data)
print(f"✓ Saved {len(audio_data)} bytes to output.pcm")
asyncio.run(tts_websocket())
```
```javascript theme={null}
const ws = new WebSocket('ws://api.60db.ai/ws/tts?apiKey=sk_live_your_key');
const contextId = 'test-' + Date.now();
const audioChunks = [];
let audioCtx;
ws.onopen = () => {
console.log('✓ Connected');
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
const msgType = Object.keys(data)[0];
console.log('←', msgType);
if (data.connection_established) {
console.log(' Credits:', data.connection_established.credit_balance);
// Create context
ws.send(JSON.stringify({
create_context: {
context_id: contextId,
voice_id: 'fbb75ed2-975a-40c7-9e06-38e30524a9a1',
audio_config: { audio_encoding: 'LINEAR16', sample_rate_hertz: 16000 },
speed: 1,
stability: 50,
similarity: 75
}
}));
} else if (data.context_created) {
console.log('✓ Context created!');
// Send text + flush
ws.send(JSON.stringify({
send_text: { context_id: contextId, text: 'Hello, how are you?' }
}));
ws.send(JSON.stringify({
flush_context: { context_id: contextId }
}));
} else if (data.audio_chunk) {
// Collect base64 audio
const binary = atob(data.audio_chunk.audioContent);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
audioChunks.push(bytes);
console.log(' Received audio chunk');
} else if (data.flush_completed) {
console.log('✓ Flush completed!');
// Play collected audio
playPCM16(audioChunks, 16000);
// Close context
ws.send(JSON.stringify({
close_context: { context_id: contextId }
}));
}
if (data.error) {
console.error('TTS Error:', data.error.message);
}
if (data.context_closed) {
console.log('✓ Context closed');
}
};
function playPCM16(chunks, sampleRate) {
const totalBytes = chunks.reduce((s, c) => s + c.length, 0);
const merged = new Uint8Array(totalBytes);
let offset = 0;
for (const chunk of chunks) {
merged.set(chunk, offset);
offset += chunk.length;
}
const int16 = new Int16Array(merged.buffer);
const float32 = new Float32Array(int16.length);
for (let i = 0; i < int16.length; i++) float32[i] = int16[i] / 32768;
audioCtx = audioCtx || new AudioContext({ sampleRate });
const buf = audioCtx.createBuffer(1, float32.length, sampleRate);
buf.getChannelData(0).set(float32);
const src = audioCtx.createBufferSource();
src.buffer = buf;
src.connect(audioCtx.destination);
src.start();
}
```
## Audio Configuration
| Encoding | Sample Rates | Description |
| ---------- | ------------------------- | ----------------------- |
| `LINEAR16` | 8000, 16000, 24000, 48000 | PCM 16-bit signed |
| `MULAW` | 8000 | G.711 μ-law (telephony) |
| `OGG_OPUS` | 24000 | Compressed audio |
For telephony integration (Twilio, etc.), use `MULAW` at `8000` Hz.
## Default Voice
The default voice ID is:
```
fbb75ed2-975a-40c7-9e06-38e30524a9a1
```
To get more voices, use the [Voices API](/api-reference/voices/get-voices).
## Context Management
### Reuse Context
Keep a context open for multiple syntheses:
```javascript theme={null}
// Create once
ws.send(JSON.stringify({
create_context: { context_id, voice_id, audio_config }
}));
// Send multiple texts
ws.send(JSON.stringify({ send_text: { context_id, text: "Hello" } }));
ws.send(JSON.stringify({ flush_context: { context_id } }));
ws.send(JSON.stringify({ send_text: { context_id, text: "World" } }));
ws.send(JSON.stringify({ flush_context: { context_id } }));
// Close when done
ws.send(JSON.stringify({ close_context: { context_id } }));
```
### Multiple Contexts
You can create multiple contexts in one connection:
```javascript theme={null}
const context1 = 'ctx-1';
const context2 = 'ctx-2';
// Create both contexts
ws.send(JSON.stringify({
create_context: { context_id: context1, voice_id: voice1, audio_config }
}));
ws.send(JSON.stringify({
create_context: { context_id: context2, voice_id: voice2, audio_config }
}));
```
## Supported Languages
The TTS model supports synthesis in multiple Indic languages and English. The language is auto-detected from the input text.
| Language | ID |
| --------- | -- |
| English | en |
| Hindi | hi |
| Bengali | bn |
| Gujarati | gu |
| Kannada | kn |
| Malayalam | ml |
| Marathi | mr |
| Punjabi | pa |
| Tamil | ta |
| Telugu | te |
## Pricing
* **Rate**: \$0.00002 per character
* **Minimum**: \$0.01 per context
* **Billing**: Per character synthesized
## Related
* [STT WebSocket](/websocket-api/stt) - Speech-to-Text endpoint
* [WebSocket Quick Start](/websocket-api/quickstart) - Get started guide
* [WebSocket Playground](/websocket-playground) - Test in browser
* [Voices API](/api-reference/voices/get-voices) - Get available voices