Rabel APIBeta
Rabel API documentation
Learn how to send Bell the words from your speech-to-text, and start your reply on Bell’s guess.
The Rabel API is in beta. Bell is $0.005 a minute, billed by the second while a conversation is open. Every account starts with 60 free minutes. The limits are below.
Quickstart
Base address: https://api.rabellabs.com
Get an API key
Sign up with your email:
curl -X POST https://api.rabellabs.com/v1/signup \
-H "Content-Type: application/json" \
-d '{"email": "you@example.com"}'
It answers status 202, and one email comes with a link. Open the link: your key is shown once; keep it. The link works once and expires in 24 hours. Signing up again with the same email gives a new key and turns the old one off; your use stays with your email.
Set RABEL_API_KEY as an environment variable, or
put your key in place of YOUR_API_KEY
in the snippets below.
Lost a key? Log in with your email and make a new one on the API keys page: it is shown once, and the old key stops working. Your free minutes, credit and use stay with your email.
Your API key is a secret. Do not share it, or put it in code that runs in a browser or an app.
Make your first request
Each snippet downloads the sample turn’s words
(/v1/sample.json), sends them to Bell at their
times as a speech-to-text sends them, and prints Bell’s guesses
and the end of the turn.
// Node 22: node snippet.ts
const API = "https://api.rabellabs.com";
const KEY = process.env.RABEL_API_KEY ?? "YOUR_API_KEY";
const sample = await (await fetch(`${API}/v1/sample.json`)).json();
const ids = new Set();
const ws = new WebSocket(`${API.replace("https", "wss")}/v1/conversation`);
ws.onopen = () => {
ws.send(JSON.stringify({ key: KEY }));
console.log("Connecting to Bell...");
};
ws.onmessage = async (e) => {
const m = JSON.parse(e.data);
if (m.type === "guess") { ids.add(m.id); console.log("guess:", m.text); }
if (m.type === "end_of_turn") {
console.log("end of turn:", m.text, ids.has(m.guess_id) ? `(matched guess ${m.guess_id})` : "(no guess matched)");
ws.close();
}
if (m.type === "error") console.log("error:", m.message);
if (m.type === "ready") {
let t = 0;
for (const w of sample.words) { // the turn's words so far, each time your speech-to-text has new words
await new Promise((r) => setTimeout(r, w.ms - t));
t = w.ms;
ws.send(JSON.stringify({ type: "heard", text: w.text }));
}
await new Promise((r) => setTimeout(r, 200));
ws.send(JSON.stringify({ type: "end_of_turn", text: sample.final })); // its final words
}
};
# pip install websockets; python snippet.py
import asyncio, json, os, urllib.request
import websockets
API = "https://api.rabellabs.com"
KEY = os.environ.get("RABEL_API_KEY", "YOUR_API_KEY")
async def main():
sample = json.load(urllib.request.urlopen(urllib.request.Request(API + "/v1/sample.json", headers={"User-Agent": "rabel-api-snippet"})))
async with websockets.connect(API.replace("https", "wss") + "/v1/conversation") as ws:
await ws.send(json.dumps({"key": KEY}))
print("Connecting to Bell...", flush=True)
ids = set()
async def send():
t = 0
for w in sample["words"]: # the turn's words so far, each time your speech-to-text has new words
await asyncio.sleep((w["ms"] - t) / 1000)
t = w["ms"]
await ws.send(json.dumps({"type": "heard", "text": w["text"]}))
await asyncio.sleep(0.2)
await ws.send(json.dumps({"type": "end_of_turn", "text": sample["final"]})) # its final words
async for msg in ws:
m = json.loads(msg)
if m["type"] == "ready":
sender = asyncio.create_task(send())
elif m["type"] == "guess":
ids.add(m["id"])
print("guess:", m["text"])
elif m["type"] == "end_of_turn":
print("end of turn:", m["text"], f"(matched guess {m['guess_id']})" if m["guess_id"] in ids else "(no guess matched)")
break
elif m["type"] == "error":
print("error:", m["message"])
asyncio.run(main())
More examples, with a speech-to-text of your own, starting a reply on Bell’s guess, handling errors and a Pipecat agent, are in the examples.
Your speech-to-text
Keep the speech-to-text you use, and send Bell its words. Bell needs them while the speaker is talking: the partial words, as they come.
heard is your speech-to-text’s interim (partial)
transcript, and end_of_turn is its final
transcript.
The end of the turn stays your speech-to-text’s: send
end_of_turn when it says the speaker has finished.
Bell has been run with Deepgram’s speech-to-text: its streaming speech-to-text, and Deepgram Flux in the Pipecat example (see the examples).
A conversation
Open a WebSocket to /v1/conversation. Send the key
first, either as the header
Authorization: Bearer <key> or as the first
text message {"key": "<key>"}
(within 10 seconds).
The API sends {"type": "ready"} when Bell is ready
for your words, usually within a second; at a busy time it can
take up to a minute. Send words only after ready.
Messages in
All are JSON text messages. The API takes text only.
-
{"type": "heard", "text": "<the turn's words so far>"}: send it each time your speech-to-text has new words.textis the whole turn so far, never only the new words. -
{"type": "end_of_turn", "text": "<the turn's final words>"}: send it when your speech-to-text ends the turn. The nextheardopens the next turn.
A conversation can hold several turns. To end it, close the
socket once the last end_of_turn has come back.
Messages out
All are JSON text messages.
-
{"type": "ready"}: Bell is ready; send the words. -
{"type": "guess", "turn", "id", "text", "heard"}: a guess of Bell’s at the turn’s words (text), and the words heard when it was made (heard). A guess already sent in the turn is not sent again. -
{"type": "end_of_turn", "turn", "text", "guess_id"}: the answer to yourend_of_turn: its final words, and theidof the earliest guess equal to them, ignoring case and punctuation (nullwhen none was). -
{"type": "error", "status", "error", "message", "retry_after_s"}: see Errors.
guess and end_of_turn carry their
turn’s number as turn, from 0.
The sample
GET /v1/sample.json is a sample turn’s words as a
speech-to-text sends them:
{"words": [{"ms", "text"}], "final"}. Each entry of
words is the turn’s words so far at
ms milliseconds from the start;
final is the turn’s final words.
Your account and your use
To see your account without your key, sign in by email:
POST /v1/login with
{"email": "you@example.com"} answers status 202,
and one email comes with a link that signs you in to your
account page. The link works
once and expires in 24 hours. It opens your account, your use,
codes and buying credit for an hour; it does not show or change
your key, and it cannot open a conversation. To replace a lost
key, sign up again with the same email.
-
GET /v1/accountwith the headerAuthorization: Bearer <key>answers{"email", "free_seconds_left", "credit_cents", "used_seconds_month", "limits"};limitsis{"seconds_a_conversation", "conversations_a_day", "conversations_at_once"}. -
GET /v1/usagewith the same header answers your use by day:{"unit": "seconds open", "days": [{"date", "seconds", "conversations"}]}. -
POST /v1/codes/redeemwith the same header and{"code": "..."}adds the code’s free seconds to your account and answers{"free_seconds_left"}. Each code works once for an account. A wrong, used-up or expired code answers 400bad_code. -
POST /v1/billing/checkoutwith the same header and{"amount_cents": 500}answers{"url"}, Stripe’s payment page for that much credit. The credit is used after the free seconds.
Use is counted in seconds open: the time a conversation is open,
from ready until the socket closes, by the second.
It is not the time of the speech in it.
Limits
- A conversation is open at most 2 minutes.
-
A conversation with no message for 2 minutes is closed
(
no_message). A quiet stretch shorter than that keeps it open. -
A
heardorend_of_turntext is at most 4,096 characters. - A key has at most 20 conversations a day, and at most 2 open at once.
-
A day is the UTC day; a conversation counts when
readyis sent.
Errors
On the WebSocket an error is the message
{"type": "error", "status", "error", "message",
"retry_after_s"}. The socket closes after it, except after a
bad_message for a text message, when the
conversation stays open. Over HTTP it is the status with
{"error", "message"}.
| Status | Error | When | Retry after |
|---|---|---|---|
401 |
bad_key |
The key is wrong. | 0 |
402 |
no_seconds |
The free seconds and the credit of this key are used up. | 0 |
429 |
key_day_limit |
This key has had its 20 conversations today. | Seconds to 00:00 UTC |
429 |
key_at_once |
This key already has 2 conversations open. | 1 s |
529 |
out_of_capacity |
Out of capacity, contact us for more usage. | 60 s |
503 |
waking |
Bell was not ready within 90 s. Try again in a minute. | 60 s |
413 |
audio_limit |
A conversation is open at most 120 s. Start a new conversation to go on. | 0 |
408 |
no_message |
No message for 120 s. The conversation is closed. | 0 |
400 |
bad_message |
A binary frame: the API takes text, the words of your speech-to-text, not audio. The socket closes. | 0 |
400 |
bad_message |
A text message other than heard or
end_of_turn with a text, or a
text over 4,096 characters. The
conversation stays open.
|
0 |
POST /v1/signup answers 400
bad_email when the body has no email, and 429
too_many_signups after too many signups from one
address in an hour. GET /v1/confirm answers 400
bad_link for a link that is wrong, used or expired.