Programmatic client (with audio)
The programmatic client mirrors the agent's AgentView and lets you send typed intents. Dial blocks
until the session is open, so you can Register and place or answer calls immediately — no readiness
handshake.
ctx := context.Background()
// PasswordGrant is a convenience for local tools/tests; in production, bring your own token.
token, err := bcclient.PasswordGrant(ctx, "https://login.example.com", user, pass)
if err != nil { log.Fatal(err) }
cli, err := bcclient.Dial(ctx, bcclient.Options{
Addr: "agent.example.com:7091", Token: token, AutoAnswer: true,
})
if err != nil { log.Fatal(err) } // Dial blocks until the session is open, so this catches connect failures
defer cli.Close()
cli.Subscribe(func(v *bcv1.AgentView) { render(v) }) // UI = f(state)
cli.Register("webrtc") // announce reachability (arms WebRTC)
cli.PlaceCall("+1990001000", "+1990002000", "+1990003000", false) // to, displayAsTo, displayAsFrom, record
:::tip Use PKCE for interactive apps
PasswordGrant is a convenience for local tools and tests. A program that logs a human in should use
Authorization Code + PKCE (GeneratePKCE → PkceAuthorizeURL → AuthorizationCodeGrant) so no
password is handled by the client. See Choosing a flow and the
Authorization Code + PKCE walkthrough.
:::
The two imports the example uses are the client package and the bcv1 type alias:
import (
"github.com/babelforce/babelconnect-sdk-go" // package bcclient
bcv1 "github.com/babelforce/babelconnect-proto/gen/go/babelconnect/v1"
)
What's happening
Dial()opens the gRPCAgent.Sessionand starts mirroringAgentViewinto an in-memoryStateCache. It blocks until the session is open, so the returnederrorcatches connect failures and you can send intents right away.Subscribe()fires on every state change — render straight from thevit hands you; never keep your own copy.Register("webrtc")announces the agent as reachable and arms the WebRTC media path.PlaceCall()dials out. WithAutoAnswer: truethe agent's own leg picks up; the audio is negotiated over WebRTC from the offer carried on the ringingCallState.
:::caution AutoAnswer is off by default
AutoAnswer is the zero value false, so set AutoAnswer: true (as above) for the agent's own leg to pick
up on PlaceCall. (The TypeScript client defaults autoAnswer to true instead, so this is one place the
two SDKs differ — see TypeScript vs Go.) It applies to outbound calls only:
an inbound call — or an outbound callback (a scheduled call the agent accepts) — always rings until you
Answer (accept) or Hangup (reject), even with AutoAnswer on.
:::
:::note Two address forms
PasswordGrant takes the server's HTTPS URL (it calls the /oauth/token endpoint), while Dial's
Addr is the gRPC host:port — a native gRPC dial target, not a URL.
:::
:::caution Pass "webrtc" explicitly
Go's Register(caps...) is variadic with no default, so calling Register() with no arguments leaves
the agent not WebRTC-reachable — pass Register("webrtc"). (TypeScript's register() defaults to
["webrtc"], so it differs here; see TypeScript vs Go.)
:::
Handling inbound calls
Inbound calls arrive as CallStates in the RINGING lifecycle and do not auto-answer. With
AutoAnswer: false you decide — accept with Answer, reject with Hangup:
cli.Subscribe(func(v *bcv1.AgentView) {
for _, call := range v.ActiveCalls {
ringing := call.State == bcv1.CallLifecycle_CALL_LIFECYCLE_RINGING
inbound := call.Direction == bcv1.CallDirection_CALL_DIRECTION_INBOUND
if ringing && inbound {
cli.Answer(call.Id) // accept …
// cli.Hangup(call.Id) // … or reject
}
}
})
:::note Enum members are prefixed
The Go enum members carry the full proto prefix —
bcv1.CallDirection_CALL_DIRECTION_INBOUND, bcv1.CallLifecycle_CALL_LIFECYCLE_RINGING — not the short
CallDirection.INBOUND the TypeScript SDK uses.
:::
You send the intent; the result comes back as a callUpsert patch (the call flips to in-progress) — you
never set call state yourself. That round-trip is the whole model; see
State & events and the full Intents reference.
:::tip Reading state directly
Besides Subscribe, cli.View() returns the current *bcv1.AgentView synchronously (a fresh clone, safe to
hold; an empty view before the first snapshot, never nil), and cli.ActiveCall() returns the first active
call (or nil) — handy for a single-call flow without holding your own copy.
:::
Concurrency
The client is built for concurrent use:
- Intent methods (
PlaceCall,Answer,Transfer, …) are safe to call from any goroutine — sends are serialised internally. - Your
Subscribecallback is invoked from a single internal goroutine, one update at a time (never concurrently), so you don't need a lock inside it. The*bcv1.AgentViewit receives is a private clone, safe to read directly. - That callback runs on the receive loop, so keep it quick — hand slow work to your own goroutine or channel, or it will delay later state updates.
Subscribereturns nothing — call it once andClose()the client to stop (there's no per-callback unsubscribe like the TypeScript client's).
Bring your own media
The default SyntheticMediaFactory is cgo-free — it answers in PCMA, streams A-law silence, and counts
inbound RTP (two-way media is verifiable, but nothing is heard). For real mic/speaker audio, supply your own
MediaFactory via Options.Media — no control/state code changes. See
Back-end automation → Adding real audio for the
Media interface and how the SDK drives it.
Next steps
- Intents reference — the full catalogue of what you can send (calls, conferencing, recording, SMS, presence, identity).
- Errors & reconnects — handle command rejections, sequence gaps, and disconnects to make a long-lived session production-ready.
- Back-end automation (no audio) — the same client without a real media leg, for services, dashboards, and CLIs.