Talk to the world's first real-time AI voice astrologer. Real charts.

The world's first real-time AI voice astrologer API. Audio streams in both directions over a clean WebSocket — sub-second turn-taking, barge-in support, multilingual, chart-grounded. One REST call to load your user's chart, one WebSocket for the live conversation. And the only AI voice astrologer API with native two-chart compatibility sessions.

<1s
Turn-taking
WSS
Real-time
2
Chart modes
Multi
lingual
World's first · Real-time voice

The world's first real-time AI voice astrologer — your users can interrupt mid-sentence.

Every other "voice astrologer" on the market is batch — record an audio file, wait several seconds, get a file back. Ours is genuinely real-time. One REST call loads the user's chart. One WebSocket carries a live, bidirectional PCM audio conversation. The astrologer speaks while thinking. Your user barges in and gets a fresh reply — exactly like a real consultation. Sub-second turn-taking. No third-party accounts, no SDK to bundle, fully white-label.

Sub-second turn-taking

Server-side voice activity detection. The astrologer starts replying as soon as you stop speaking. No round-trips, no batching, no queueing.

One chart or two

Single-chart voice consultations or two-chart kundli-matching voice calls. The astrologer speaks knowing both partners' charts — uncontested among voice astrology APIs.

Fully white-label

No third-party branding, no third-party account, no leaked provider identity. Customers connect to one WebSocket and see your astrologer, your voice, your product.

In the same call you can have
Sub-second response Mid-sentence barge-in Live transcript stream Language switching Wallet-status events Clean session end
What the astrologer covers

Real chart consultations, spoken in real time.

The AI voice astrologer carries the same astrological depth as a real consultation. Vedic Jyotish backbone with KP timing precision, Lal Kitab remedies where useful, and two-chart compatibility for matchmaking calls. Topics are as broad as any astrologer's day — relationships, career, finance, health, family, decisions.

𑖀Vedic chart grounding

Birth chart computed once at session start and held server-side for the whole call. Houses, lordships, divisional charts, current Mahadasha and Antardasha, active transits. The astrologer speaks to your user's current moment.

Timing & transits

Vimshottari dasha, sade sati, kantaka shani, retrograde windows. When a user asks "when will my career change," the astrologer can point to specific planetary periods, not vague generalities.

Two-chart voice calls

Native kundli matching by voice. Ashtakoot, Manglik, beeja-kshetra, house overlay — all available to the astrologer in a single conversation. Ask anything about the couple, get answers grounded in both charts.

KP & remedial systems

KP for sub-lord precision on timing-critical questions. Lal Kitab for plain, practical remedies. The astrologer applies them when the conversation calls for it — without lecturing.

Topics

  • Career, profession, business timing
  • Marriage, partner search, matchmaking
  • Love & relationship dynamics
  • Finance, wealth, investment timing
  • Health and wellbeing direction
  • Family, children, parenting
  • Travel, education, major decisions

🗣Conversation feel

  • Sub-second turn-taking
  • Barge-in mid-sentence
  • Multilingual switching mid-call
  • Live transcript while speaking
  • Off-topic requests politely declined
  • Tone that fits a consultation, not a chatbot
How to integrate

One REST call. One WebSocket. Done.

Phase 1 (REST): POST birth data to /start or /match-start, get a session_id. Phase 2 (WebSocket): open one connection to wss://starsapi.com/voice-relay-v2, authenticate, exchange audio frames. The relay handles VAD, turn-taking, barge-in, language detection, and billing events.

  1. 1

    Create the session (REST)

    POST /api/v2/partner/ai/voice/start (single chart) or /match-start (two charts) with the birth data. Server computes the chart, returns session_id. One HTTP call — done.

  2. 2

    Open the WebSocket

    Connect to wss://starsapi.com/voice-relay-v2. Send the first frame: { "type": "auth", "session_id", "api_key", "name", "lang" }. Wait for { "type": "ready", "astrologer": {...}, "language" }.

  3. 3

    Trigger the greeting

    Send { "type": "greet", "name", "lang" } (or with person1_name/person2_name for couple calls). The astrologer's greeting audio + transcript streams back. Hold mic capture until you receive assistant.done for the first turn.

  4. 4

    Stream mic audio

    Capture mic at 24 kHz mono PCM s16le, base64-encode each chunk, send as { "type": "audio.chunk", "data": "BASE64..." }. Server VAD signals when the user starts and stops talking via user.speech_started / user.speech_stopped events.

  5. 5

    Play assistant audio + render transcript

    Incoming assistant.audio frames carry base64 PCM (same 24 kHz mono format). Queue and play them in order. assistant.transcript events carry delta strings for live caption rendering.

  6. 6

    Handle barge-in

    When user.speech_started arrives while assistant audio is still playing, stop your playback and send { "type": "interrupt" }. The relay replies with assistant.cancelled and starts listening to the new turn.

  7. 7

    React to billing events

    billing.warning warns when wallet is getting low. billing.exhausted means the session is being closed — show your top-up flow. No webhook wiring needed.

WebSocket protocol

JSON frames in both directions.

All frames are one-line JSON objects. Audio is base64-encoded PCM (24 kHz mono, signed 16-bit little-endian). Same format inbound and outbound. Every frame has a type field — switch on it to drive your client.

↑ Client → server

  • auth{ session_id, api_key, name, lang }
    First frame. Establishes the session against the relay.
  • greet{ name, lang }
    Triggers the astrologer's opening response. For couples: { person1_name, person2_name, lang }.
  • audio.chunk{ data: BASE64_PCM }
    One mic chunk. 24 kHz mono PCM s16le. Send continuously while user speaks.
  • interrupt{}
    Cancel the current assistant response. Pair with stopping local audio playback.

↓ Server → client

  • ready{ astrologer: { name }, language }
    Auth accepted, session bound, astrologer loaded.
  • user.speech_started{}
    Server VAD detected the user is speaking.
  • user.speech_stopped{}
    VAD detected end of user turn.
  • user.transcript{ text }
    Live transcript of what the user just said.
  • assistant.speech_started{}
    Astrologer is about to speak. Show speaking state in UI.
  • assistant.audio{ data: BASE64_PCM }
    One chunk of astrologer audio. Queue and play in order.
  • assistant.transcript{ delta }
    Incremental caption string for the current astrologer turn.
  • assistant.done{}
    Astrologer finished speaking. Open mic for next user turn.
  • assistant.cancelled{}
    Acknowledgement that a previous interrupt was applied.
  • billing.warning{}
    Wallet running low — show a soft top-up nudge.
  • billing.exhausted{}
    Wallet empty — session ending. Show top-up flow.
  • error{ code, message }
    Recoverable error or fatal error with a code your client can switch on.
Code snippets

Drop-in code for any stack.

Single-chart flow shown below. Two-chart flow is identical except you POST to /match-start with person1 and person2 objects, and the auth/greet frames carry person1_name and person2_name instead of name. Mic capture and audio playback are platform-specific (24 kHz mono PCM s16le) — only the protocol is shown.

# Phase 1 — create the session (REST)
curl -X POST https://starsapi.com/api/v2/partner/ai/voice/start \
  -H "X-Api-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "astrologer":         "guruji",
    "year": 1990, "month": 6, "day": 15,
    "hour": 14,   "minute": 30,
    "timezone": "Asia/Kolkata",
    "latitude":           28.6139, "longitude": 77.2090,
    "name":          "Ravi",
    "gender":        "male",
    "preferred_language": "en"
  }\'

# Returns:  data.session_id

# Phase 2 — open the WebSocket (use a tool like websocat for testing)
websocat wss://starsapi.com/voice-relay-v2

# Send auth frame as your first message:
{"type":"auth","session_id":"abc123...","api_key":"YOUR_KEY","name":"Ravi","lang":"en"}

# Wait for: {"type":"ready", ...}
# Then send: {"type":"greet","name":"Ravi","lang":"en"}
# Then stream audio chunks: {"type":"audio.chunk","data":"BASE64_PCM_24KHZ_MONO"}
const BASE = "https://starsapi.com/api/v2/partner/ai/voice";
const RELAY = "wss://starsapi.com/voice-relay-v2";

// Phase 1 — REST: create the session
const r = await fetch(`${BASE}/start`, {
  method:  "POST",
  headers: { "X-Api-Key": API_KEY, "Content-Type": "application/json" },
  body: JSON.stringify({
    astrologer: "guruji",
    year: 1990, month: 6, day: 15, hour: 14, minute: 30, timezone: "Asia/Kolkata",
    latitude: 28.6139, longitude: 77.2090,
    name: "Ravi", gender: "male",
    preferred_language: "en",
  }),
}).then(r => r.json());
const sessionId = r.data.session_id;

// Phase 2 — WebSocket: real-time conversation
const ws = new WebSocket(RELAY);
ws.onopen = () => ws.send(JSON.stringify({
  type: "auth", session_id: sessionId, api_key: API_KEY,
  name: "Ravi", lang: "en",
}));

ws.onmessage = (e) => {
  const f = JSON.parse(e.data);
  switch (f.type) {
    case "ready":            ws.send(JSON.stringify({ type: "greet", name: "Ravi", lang: "en" })); break;
    case "user.speech_started":  if (assistantPlaying) ws.send(JSON.stringify({ type: "interrupt" })); break;
    case "user.transcript":      renderUserBubble(f.text); break;
    case "assistant.audio":      playPcmChunk(base64ToBytes(f.data)); break;
    case "assistant.transcript": appendAssistantCaption(f.delta); break;
    case "assistant.done":       enableMic(); break;
    case "billing.exhausted":    showTopUp(); break;
    case "error":                console.error(f.code, f.message); break;
  }
};

// Send mic chunks as they arrive (24kHz mono PCM s16le, base64-encoded)
function onMicChunk(int16Pcm) {
  ws.send(JSON.stringify({ type: "audio.chunk", data: bytesToBase64(int16Pcm) }));
}
import 'package:http/http.dart' as http;
import 'package:web_socket_channel/web_socket_channel.dart';
import 'dart:convert';

const BASE  = 'https://starsapi.com/api/v2/partner/ai/voice';
const RELAY = 'wss://starsapi.com/voice-relay-v2';

// Phase 1 — REST: create the session
final r = await http.post(
  Uri.parse('$BASE/start'),
  headers: { 'X-Api-Key': apiKey, 'Content-Type': 'application/json' },
  body: jsonEncode({
    'astrologer':         'guruji',
    'year': 1990, 'month': 6, 'day': 15,
    'hour': 14,   'minute': 30, 'timezone': 'Asia/Kolkata',
    'latitude':   28.6139, 'longitude': 77.2090,
    'name':          'Ravi',
    'gender':        'male',
    'preferred_language': 'en',
  }),
);
final sessionId = jsonDecode(r.body)['data']['session_id'];

// Phase 2 — WebSocket: real-time conversation
final ws = WebSocketChannel.connect(Uri.parse(RELAY));
ws.sink.add(jsonEncode({
  'type': 'auth', 'session_id': sessionId, 'api_key': apiKey,
  'name': 'Ravi', 'lang': 'en',
}));

ws.stream.listen((raw) {
  final f = jsonDecode(raw);
  switch (f['type']) {
    case 'ready':
      ws.sink.add(jsonEncode({ 'type': 'greet', 'name': 'Ravi', 'lang': 'en' })); break;
    case 'user.speech_started':
      if (assistantPlaying) ws.sink.add(jsonEncode({ 'type': 'interrupt' })); break;
    case 'user.transcript':       renderUserBubble(f['text']); break;
    case 'assistant.audio':       playPcmChunk(base64Decode(f['data'])); break;
    case 'assistant.transcript':  appendCaption(f['delta']); break;
    case 'assistant.done':        enableMic(); break;
    case 'billing.exhausted':     showTopUp(); break;
  }
});

// Stream mic — capture 24kHz mono s16le PCM and send base64 chunks
void onMicChunk(Uint8List pcm) {
  ws.sink.add(jsonEncode({ 'type': 'audio.chunk', 'data': base64Encode(pcm) }));
}
import requests, json, base64
import websocket  # pip install websocket-client

BASE  = "https://starsapi.com/api/v2/partner/ai/voice"
RELAY = "wss://starsapi.com/voice-relay-v2"

# Phase 1 — REST: create the session
r = requests.post(f"{BASE}/start",
    headers={ "X-Api-Key": API_KEY, "Content-Type": "application/json" },
    json={
        "astrologer":         "guruji",
        "year": 1990, "month": 6, "day": 15,
        "hour": 14,   "minute": 30, "timezone": "Asia/Kolkata",
        "latitude":   28.6139, "longitude": 77.2090,
        "name":          "Ravi",
        "gender":        "male",
        "preferred_language": "en",
    }).json()
session_id = r["data"]["session_id"]

# Phase 2 — WebSocket: real-time conversation
def on_message(ws, raw):
    f = json.loads(raw)
    t = f["type"]
    if t == "ready":
        ws.send(json.dumps({ "type": "greet", "name": "Ravi", "lang": "en" }))
    elif t == "user.speech_started" and assistant_playing:
        ws.send(json.dumps({ "type": "interrupt" }))
    elif t == "user.transcript":      render_user_bubble(f["text"])
    elif t == "assistant.audio":      play_pcm_chunk(base64.b64decode(f["data"]))
    elif t == "assistant.transcript": append_caption(f["delta"])
    elif t == "assistant.done":       enable_mic()
    elif t == "billing.exhausted":    show_top_up()

def on_open(ws):
    ws.send(json.dumps({
        "type": "auth", "session_id": session_id, "api_key": API_KEY,
        "name": "Ravi", "lang": "en",
    }))

ws = websocket.WebSocketApp(RELAY, on_open=on_open, on_message=on_message)

# Stream mic chunks (24kHz mono s16le PCM, base64-encoded)
def send_mic_chunk(pcm: bytes):
    ws.send(json.dumps({ "type": "audio.chunk", "data": base64.b64encode(pcm).decode() }))

ws.run_forever()
// Requires: java-websocket (org.java-websocket:Java-WebSocket) + Jackson.

import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import com.fasterxml.jackson.databind.*;
import java.net.URI;
import java.net.http.*;
import java.util.Base64;

static final String BASE  = "https://starsapi.com/api/v2/partner/ai/voice";
static final URI    RELAY = URI.create("wss://starsapi.com/voice-relay-v2");
static final ObjectMapper M = new ObjectMapper();

// Phase 1 — REST: create the session
String body = M.writeValueAsString(Map.of(
    "astrologer", "guruji",
    "year", 1990, "month", 6, "day", 15,
    "hour", 14,   "minute", 30, "timezone", 5.5,
    "latitude", 28.6139, "longitude", 77.2090,
    "name", "Ravi", "gender", "male",
    "preferred_language", "en"
));
HttpResponse<String> rest = HttpClient.newHttpClient().send(
    HttpRequest.newBuilder(URI.create(BASE + "/start"))
        .header("X-Api-Key",    API_KEY)
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(body)).build(),
    HttpResponse.BodyHandlers.ofString());
String sessionId = M.readTree(rest.body()).get("data").get("session_id").asText();

// Phase 2 — WebSocket
WebSocketClient ws = new WebSocketClient(RELAY) {
  @Override public void onOpen(ServerHandshake h) {
    send(M.writeValueAsString(Map.of(
      "type","auth","session_id",sessionId,"api_key",API_KEY,
      "name","Ravi","lang","en")));
  }
  @Override public void onMessage(String raw) {
    JsonNode f; try { f = M.readTree(raw); } catch(Exception e){ return; }
    switch (f.get("type").asText()) {
      case "ready":               send(M.writeValueAsString(Map.of(
                                    "type","greet","name","Ravi","lang","en")));      break;
      case "user.speech_started": if (assistantPlaying) send("{\"type\":\"interrupt\"}");   break;
      case "assistant.audio":     playPcmChunk(Base64.getDecoder().decode(f.get("data").asText())); break;
      case "assistant.transcript":appendCaption(f.get("delta").asText());                   break;
      case "assistant.done":      enableMic();                                              break;
      case "billing.exhausted":   showTopUp();                                              break;
    }
  }
  @Override public void onClose(int c, String r, boolean rem) {}
  @Override public void onError(Exception e) {}
};
ws.connect();

// Stream mic — base64 24kHz mono s16le chunks
void onMicChunk(byte[] pcm) throws Exception {
  ws.send(M.writeValueAsString(Map.of(
    "type", "audio.chunk", "data", Base64.getEncoder().encodeToString(pcm))));
}
package main

import (
    "bytes"
    "encoding/base64"
    "encoding/json"
    "net/http"
    "github.com/gorilla/websocket"
)

const (
    base  = "https://starsapi.com/api/v2/partner/ai/voice"
    relay = "wss://starsapi.com/voice-relay-v2"
)

func main() {
    // Phase 1 — REST: create the session
    payload, _ := json.Marshal(map[string]any{
        "astrologer":         "guruji",
        "year": 1990, "month": 6, "day": 15,
        "hour": 14, "minute": 30, "timezone": "Asia/Kolkata",
        "latitude": 28.6139, "longitude": 77.2090,
        "name": "Ravi", "gender": "male",
        "preferred_language": "en",
    })
    req, _ := http.NewRequest("POST", base+"/start", bytes.NewReader(payload))
    req.Header.Set("X-Api-Key",    apiKey)
    req.Header.Set("Content-Type", "application/json")
    resp, _ := http.DefaultClient.Do(req)
    var startResp struct{ Data struct{ SessionID string `json:"session_id"` } }
    json.NewDecoder(resp.Body).Decode(&startResp)
    sid := startResp.Data.SessionID

    // Phase 2 — WebSocket
    ws, _, _ := websocket.DefaultDialer.Dial(relay, nil)
    defer ws.Close()
    ws.WriteJSON(map[string]any{
        "type": "auth", "session_id": sid, "api_key": apiKey,
        "name": "Ravi", "lang": "en",
    })

    go func() {
        for {
            var f map[string]any
            if err := ws.ReadJSON(&f); err != nil { return }
            switch f["type"] {
            case "ready":
                ws.WriteJSON(map[string]any{ "type": "greet", "name": "Ravi", "lang": "en" })
            case "user.speech_started":
                if assistantPlaying { ws.WriteJSON(map[string]any{ "type": "interrupt" }) }
            case "assistant.audio":
                pcm, _ := base64.StdEncoding.DecodeString(f["data"].(string))
                playPcmChunk(pcm)
            case "assistant.transcript":
                appendCaption(f["delta"].(string))
            case "assistant.done":
                enableMic()
            case "billing.exhausted":
                showTopUp()
            }
        }
    }()

    // Stream mic — base64-encoded 24kHz mono s16le
    onMicChunk := func(pcm []byte) {
        ws.WriteJSON(map[string]any{ "type": "audio.chunk", "data": base64.StdEncoding.EncodeToString(pcm) })
    }
    _ = onMicChunk
    select {}
}
// Requires: cpprestsdk (HTTP) + Boost.Beast or libwebsockets (WS) + base64 helper.
// Sketch using cpprestsdk for HTTP and a generic WS client interface.

#include <cpprest/http_client.h>
#include <cpprest/json.h>
#include "WsClient.h"   // your WebSocket wrapper

using namespace web;
using namespace web::http;
using namespace web::http::client;

const auto BASE  = U("https://starsapi.com/api/v2/partner/ai/voice");
const auto RELAY = U("wss://starsapi.com/voice-relay-v2");

// Phase 1 — REST
http_client rest(BASE);
json::value body;
body[U("astrologer")]         = json::value::string(U("guruji"));
body[U("year")] = 1990; body[U("month")] = 6; body[U("day")] = 15;
body[U("hour")] = 14;   body[U("minute")] = 30; body[U("timezone")] = 5.5;
body[U("latitude")]  = 28.6139; body[U("longitude")] = 77.2090;
body[U("name")]          = json::value::string(U("Ravi"));
body[U("gender")]        = json::value::string(U("male"));
body[U("preferred_language")] = json::value::string(U("en"));
http_request req(methods::POST);
req.set_request_uri(U("/start"));
req.headers().add(U("X-Api-Key"),    U(API_KEY));
req.headers().add(U("Content-Type"), U("application/json"));
req.set_body(body);
auto resp = rest.request(req).get();
auto json = resp.extract_json().get();
auto sid  = json[U("data")][U("session_id")].as_string();

// Phase 2 — WebSocket
WsClient ws(RELAY);
ws.on_open([&] {
    json::value auth;
    auth[U("type")]       = json::value::string(U("auth"));
    auth[U("session_id")] = json::value::string(sid);
    auth[U("api_key")]    = json::value::string(U(API_KEY));
    auth[U("name")]  = json::value::string(U("Ravi"));
    auth[U("lang")]       = json::value::string(U("en"));
    ws.send(auth.serialize());
});
ws.on_message([&](const std::string& raw) {
    auto f = json::value::parse(raw);
    auto type = f[U("type")].as_string();
    if      (type == U("ready"))                ws.send(R"({"type":"greet","name":"Ravi","lang":"en"})");
    else if (type == U("user.speech_started"))  { if (assistantPlaying) ws.send(R"({"type":"interrupt"})"); }
    else if (type == U("assistant.audio"))      playPcmChunk(base64_decode(f[U("data")].as_string()));
    else if (type == U("assistant.transcript")) appendCaption(f[U("delta")].as_string());
    else if (type == U("assistant.done"))       enableMic();
    else if (type == U("billing.exhausted"))    showTopUp();
});
ws.connect();

// Stream mic — base64-encoded 24kHz mono s16le PCM
void on_mic_chunk(const std::vector<uint8_t>& pcm) {
    ws.send("{\"type\":\"audio.chunk\",\"data\":\"" + base64_encode(pcm) + "\"}");
}
Platform requirements

Libraries you need on each platform.

Voice integration needs five capabilities: HTTP for the REST call, WebSocket for the relay, mic capture at 24 kHz mono PCM s16le, audio playback at the same format, and base64 encoding. Below are the recommended libraries for each target — most of them ship in the platform's standard library.

🌐Web / Browser

All native — zero dependencies.

  • fetch — REST
  • WebSocket — relay
  • navigator.mediaDevices.getUserMedia({audio: {sampleRate:24000, channelCount:1, echoCancellation:true, noiseSuppression:true}})
  • AudioContext at sampleRate: 24000 + AudioWorkletNode (or ScriptProcessor) — mic capture
  • AudioBufferSourceNode — PCM playback
  • btoa/atob with Int16Array conversion — base64
  • HTTPS required for mic access in production

🎯Flutter / Dart

Four pub.dev packages.

  • http: ^1.0.0 — REST
  • web_socket_channel: ^3.0.0 — relay
  • flutter_sound: ^9.0.0 — mic capture and raw PCM playback (both at 24 kHz mono s16le)
  • Alternative: mic_stream + just_audio if you prefer separate packages
  • permission_handler: ^11.0.0 — runtime mic permission
  • dart:convert base64 (built-in)
  • iOS: add NSMicrophoneUsageDescription to Info.plist
  • Android: <uses-permission android:name="android.permission.RECORD_AUDIO"/> in manifest

📱iOS native (Swift)

Native Foundation + AVFoundation — no third-party deps.

  • URLSession — REST
  • URLSessionWebSocketTask — relay (iOS 13+)
  • AVAudioEngine + AVAudioInputNode with tap — mic capture
  • AVAudioPlayerNode + scheduled AVAudioPCMBuffer — PCM playback
  • AVAudioConverter — sample-rate conversion if hardware ≠ 24 kHz
  • AVAudioSession set to .playAndRecord with mode .voiceChat — echo cancellation
  • Data.base64EncodedString() — base64
  • Info.plist: NSMicrophoneUsageDescription
  • Minimum iOS 13 (for native WebSocket task)

🤖Android native (Kotlin)

OkHttp + native audio APIs.

  • com.squareup.okhttp3:okhttp:4.x — REST and WebSocket (same library)
  • AudioRecord with source MediaRecorder.AudioSource.VOICE_COMMUNICATION (built-in echo cancellation) at SAMPLE_RATE_HZ=24000, CHANNEL_IN_MONO, ENCODING_PCM_16BIT — mic capture
  • AudioTrack with MODE_STREAM at the same format — playback
  • android.util.Base64 or java.util.Base64 (API 26+)
  • com.squareup.moshi:moshi or gson — JSON parsing
  • Manifest: <uses-permission android:name="android.permission.RECORD_AUDIO"/>
  • Runtime: request RECORD_AUDIO permission before opening the session

React Native

Native fetch + WebSocket, package for PCM streaming.

  • fetch — REST (native)
  • WebSocket — relay (native)
  • react-native-live-audio-stream — mic at PCM 24 kHz mono
  • Or bridge native modules (AVAudioEngine / AudioRecord) for fine-grained control
  • PCM playback: typically requires a small native module per platform (or wrap chunks into WAV headers and use react-native-sound)
  • react-native-permissions — mic permission
  • iOS: NSMicrophoneUsageDescription in Info.plist
  • Android: RECORD_AUDIO in manifest

Server-side bots

Headless callers — transcript loggers, automated consultations, bridge bots.

  • Node: ws (npm) + native fetch
  • Python: websocket-client + requests + (optionally) pyaudio for I/O
  • Go: github.com/gorilla/websocket + net/http
  • Java: org.java-websocket:Java-WebSocket + HttpClient + Jackson
  • C++: Boost.Beast or libwebsockets + cpprestsdk or libcurl + nlohmann/json
  • Audio I/O usually not needed (transcript-only bots, archivers, IVR bridges) — you can just consume user.transcript and assistant.transcript events

Audio format — the one constant across every platform

Mic input and assistant output are both 24 kHz, mono, signed 16-bit linear PCM, little-endian, base64-encoded inside JSON frames. If your platform captures at 44.1 kHz or 48 kHz (most do natively), you'll need a one-line resampler — every audio library above supports this. Echo cancellation should be enabled on the input device wherever the platform offers it (VOICE_COMMUNICATION on Android, .voiceChat mode on iOS, echoCancellation: true in getUserMedia on web).

Phase 1 reference — REST

Session bootstrap payload & response.

One POST creates the session and returns the session_id you use on the WebSocket. The chart is computed and cached server-side for the entire call.

POST /api/v2/partner/ai/voice/start — single-chart voice session
// Request
{
  "astrologer":         "guruji",
  "year": 1990, "month": 6, "day": 15,
  "hour": 14,   "minute": 30,
  "timezone": "Asia/Kolkata",
  "latitude":           28.6139,
  "longitude":          77.2090,
  "name":          "Ravi",         // optional
  "gender":        "male",         // optional
  "preferred_language": "en",           // optional, ISO 639-1
  "device_language":    "en-US",        // optional
  "country_code":       "IN"            // optional
}

// Response
{
  "success": true,
  "timestamp": "2026-05-17T13:45:00+00:00",
  "response_time_ms": 412,
  "data": {
    "session_id": "abc123def456abc123def456abc12345",
    "turn":       0,
    "astrologer": {
      "key":              "guruji",
      "name":             "Guruji",
      "avatar_emoji":     "🙏",
      "astrology_system": "vedic"
    },
    "language": { "resolved": "en", "hint": "en" },
    "billing":  { /* current wallet state */ }
  }
}
POST /api/v2/partner/ai/voice/match-start — two-chart voice session
// Request
{
  "astrologer": "matchmaking",
  "preferred_language": "hi",
  "boy": {
    "name":      "Raj",          "gender":  "male",
    "year":      1985, "month":  6,  "day":    15,
    "hour":      10,   "minute": 30,
    "latitude":  28.6139, "longitude": 77.2090,
    "timezone":  "Asia/Kolkata"
  },
  "girl": {
    "name":      "Priya",        "gender":  "female",
    "year":      1990, "month":  8,  "day":    22,
    "hour":      14,   "minute": 45,
    "latitude":  19.0760, "longitude": 72.8777,
    "timezone":  "Asia/Kolkata"
  }
}

// Response — same envelope, includes both partner names
{
  "success": true,
  "data": {
    "session_id": "xyz789...",
    "turn":       0,
    "astrologer": { "key": "matchmaking", "name": "...", ... },
    "language":   { "resolved": "hi", "hint": "hi" },
    "boy":        { "name": "Raj" },
    "girl":       { "name": "Priya" },
    "billing":    { ... }
  }
}
Error envelope — REST or WebSocket
{
  "success": false,
  "timestamp": "...",
  "error": {
    "code":    "SESSION_NOT_FOUND",
    "message": "Session not found"
  }
}

// Codes:
//   INVALID_JSON          400 — body not parseable
//   METHOD_NOT_ALLOWED    405 — only POST on REST endpoints
//   AUTH_ERROR            401 — API key validation failed
//   VALIDATION_ERROR      400 — missing/invalid field
//   SESSION_NOT_FOUND     404 — session_id unknown
//   FORBIDDEN             403 — session belongs to a different API key
//   SESSION_CLOSED        409 — session marked inactive
//   CALCULATION_ERROR     500 — chart computation failed
//   AI_ERROR              502 — upstream voice service failed; safe to retry
//   CONFIG_ERROR          500 — astrologer config not found
//   INTERNAL_ERROR        500 — unhandled server error
Frequently asked

Questions developers ask before integrating.

How does the integration work end-to-end?
Two steps. Step 1: POST birth data to /api/v2/partner/ai/voice/start (or /match-start for couple sessions) with skip_ai_greeting: true. Server computes the chart, returns a session_id. Step 2: Open a WebSocket to wss://starsapi.com/voice-relay-v2, send an auth frame with the session ID, then exchange audio and event frames. That's the whole integration.
What does the WebSocket protocol look like?
JSON frames in both directions. Outbound from your client: auth, greet, audio.chunk (base64 PCM), and interrupt. Inbound from server: ready, user.speech_started/stopped, user.transcript, assistant.speech_started, assistant.audio, assistant.transcript, assistant.done, assistant.cancelled, plus billing and error events. Every frame is a one-line JSON object — easy to debug, easy to log.
What audio format does the relay use?
24 kHz mono PCM, signed 16-bit little-endian, base64-encoded inside JSON frames. Same format in both directions. Most platforms produce and consume this natively (web AudioWorklet, AVAudioEngine on iOS, AudioRecord on Android, MediaStream on Dart).
How fast is the turn-taking?
Sub-second. The relay carries audio continuously, the astrologer starts replying as soon as you finish speaking. Server-side voice activity detection signals user.speech_started and user.speech_stopped in real time, so your UI can react immediately.
Does it support barge-in (interrupt mid-sentence)?
Yes. When the user starts speaking while the astrologer is talking, send a {"type": "interrupt"} frame. The relay stops the current astrologer audio, fires an assistant.cancelled event back, and starts listening fresh. Stop playback on your client when you send the interrupt; the relay confirms the cancellation with the event.
What about two-chart kundli-matching voice calls?
Use /match-start instead of /start, passing boy and girl birth objects. The session loads both charts plus the compatibility analysis (Ashtakoot scores, Manglik analysis, beeja-kshetra, house overlay). On the WebSocket auth frame, send boy_name and girl_name instead of user_name. Same audio protocol, same event types. The astrologer speaks knowing both partners.
What astrological systems does the voice astrologer use?
Vedic Jyotish in the Parashari tradition for chart structure, planetary positions, divisional charts, and dasha periods. Active transits and current Mahadasha/Antardasha are loaded with the chart so the astrologer speaks to the user's present moment, not just their birth chart.
Is it multilingual?
Yes. Pass an initial lang on the auth frame (and preferred_language on the REST start call). The astrologer greets in that language and detects language switches mid-conversation from the user's speech. English, Hindi, and others on request.
Is it fully white-label?
Yes. Customers connect to wss://starsapi.com/voice-relay-v2 — there's no third-party branding, no third-party account on your side, no leaked provider identity. The protocol is provider-agnostic by design. Display the astrologer's name, voice, and identity as your own product.
How is billing handled?
Per session, metered server-side. The relay emits billing.warning when wallet is getting low and billing.exhausted when it runs out — your UI can react to both before the session is force-closed. No webhook wiring needed; the relay handles it.

Ship an AI voice astrologer this week.

One REST call, one WebSocket. Sub-second turn-taking. Two-chart compatibility voice. Fully white-label.

View pricing Talk to us