Skip to main content
A person calls your agent from the Relay app, and your agent answers by joining the call’s room. Relay sends call.created on the webhook or WebSocket your agent already uses for messages, and carries the voice and camera over WebRTC. Your backend runs the agent; it registers nothing for calls.

Answer a call

When call.created arrives with status: "ringing", join the room for call.id within 32 seconds. Joining is the answer: the call becomes in-progress. A call nobody joins ends no-answer. Each package below joins the room and runs the WebRTC connection for you, so your code never handles SDP, ICE or TURN credentials. Choose the one that matches your agent’s runtime.
Install the SDK with the WebRTC packages it lists as optional peer dependencies. Add node-webcodecs only if your agent sends or receives video.
The person’s audio reaches your agent once both sides are connected. Your agent’s first words are held until the person can hear them, so nothing your agent says early is cut off. Send and receive audio covers formats, playout and interruption; video calls covers the camera.

Show video or Rive in a call

Your agent can show the person something while it talks. It sends live video, or it names a Rive file that the person’s phone draws and your agent drives. When video from your agent is arriving, the phone shows the video; otherwise it shows the Rive file.

Video

Publish a camera track and capture frames into it with transport.publishTrack(LocalVideoTrack) and VideoSource.captureFrame. Video calls covers frame formats, rates and bitrates. Show a talking avatar turns your agent’s speech into a lip-synced face with Simli.

Rive

Set a .riv file on your agent’s Contact Card, then open the call’s rive channel with transport.rive() to set View Model values, fire triggers and switch files, timed to your agent’s speech. Show Rive in a call walks through it.

Decline a call

To decline a ringing call, end it without joining. A call your agent ends while it rings becomes no-answer.

End a call

End the call for both sides with end() on the transport (transport.end() in Pipecat), with relay.calls.end(callId), or with POST /v1/calls/{callId}/end. An answered call becomes completed. close() in TypeScript and aclose() in Python only clean up locally; the call goes on until someone ends it.

The call room

The room is a WebSocket at GET /v1/calls/{callId}/room, opened with your Agent Token. It carries only signaling: JSON frames with WebRTC session descriptions. Audio and video travel over WebRTC, never through this socket.
  1. Send {"type":"join"} when the socket opens. Relay answers with iceServers and then roomState.
  2. Send an offer that publishes one track named audio. Relay answers with answer. The first time your camera turns on, send another offer that adds a track named video.
  3. Send connected when your peer connection is connected.
  4. Relay sends an offer for each of the person’s tracks. Reply with answer.
The room reference lists every frame. Use one of the packages above unless you run your own WebRTC stack.

The call in the chat

Relay writes one system message into the chat with is_system_message: true and system_event.type: "call". The marker exists from the moment the call is placed; call.status carries its state. The event’s actor is the caller, its subject is the callee, and its call object carries its current state. parts[0].value is a plain sentence such as “Atlas called you” or “Atlas called you with no answer”. The Relay app shows it as a call bubble on the caller’s side. Agents receive it like any other system message and should not reply to it.

The Call object states

status uses Twilio’s call status words. ringing and in-progress are live. The other five are terminal and set ended_at.

See also