AIMERICAAPI · Developers

Browser phone

A dialer in your own web page registers one of your AIMERICA lines over a secure WebSocket and carries the audio itself. Your people call and answer without leaving your software; the line keeps every rule it has today.

What it is

To the platform, a browser phone is a phone. It signs in to a line the way a desk phone does and the line treats it the same way: it rings together with the person's other devices, the first to answer takes the call, the others stop; forwarding, voicemail, recording, hours, menus and keypad codes are the line's. Your web app handles none of that: it asks the API for a session for the person who is signed in, hands it to a standard SIP-over-WebSocket library in the page, and shows buttons. Nothing to install on the computer.

How the WSS works

A browser cannot open a plain SIP connection, so the dialer speaks SIP inside a WebSocket: wss://a1merica.ai/sip-ws, port 443, under TLS like any web page. On our side that WebSocket ends at the same registrar that serves desk phones (digest authentication; the username is the person's own ten-digit number, the realm a1merica.ai).

The audio never passes through your server. The page captures the microphone with WebRTC and sends it encrypted (DTLS-SRTP, Opus) to our media bridge, which converts it for the telephone network and hands it to the call like any other leg. When a network blocks direct audio, the browser falls back to our relay on turn2.a1merica.ai:3478 (UDP or TCP) with credentials the API gives you; a network that allows outgoing 443/TCP and 3478/TCP works.

How a web app plugs in

  1. Allow your web address. A browser phone may only register from the origins a company has listed (https://app.example.com, no path). The owner or an admin adds it in the app under Developer → Browser phone, or your software does it with PUT /v1/browser-phone/origins (scope phone:manage). A page served from anywhere else is refused, whatever credentials it holds.
  2. Get access to the API. An API key for your own company, or an OAuth 2.0 app that other companies authorize, with the scope phone:browser. The key or the token stays on your server; the browser only ever sees the session from step 3.
  3. Mint a session when the person opens your app. From your server, POST /v1/browser-phone/sessions with the person (their user id or email), your origin and, if you want one, a lifetime. The answer carries the username (their line), a one-time password, the realm, the WebSocket address and the ICE servers. Without ttl_hours the session lasts until you delete it: nobody updates a password, and we never revoke what you configured.
  4. Start the dialer. The page creates the SIP user agent with the session (the block below), registers and shows "Ready". From then on the person calls with ua.call(number) and answers with session.answer(); hold, mute and the keypad are one call each.
  5. End it when the person signs out. Stop the user agent in the page and DELETE /v1/browser-phone/sessions/{id} from your server.

A person who wants a browser phone outside any web app (a lone softphone page) does not need the API: in the app, under Developer → Browser phone, they create a device password for their line, shown once, and use it with the sample dialer or any SIP-over-WebSocket softphone.

The routes

RouteScopeWhat it does
POST /browser-phone/sessionsphone:browserMint a session for one person: a username and a one-time password that work until you revoke the session (or for the lifetime you set). Someone else's session needs an owner or admin behind the credential; the owner's line, the owner
DELETE /browser-phone/sessions/{id}phone:browserEnd a session early (the person signed out). Answers 204.
GET /browser-phone/icephone:browserThe ICE servers a page should use right now: a STUN entry always, relay entries with their own 12-hour credentials
GET /browser-phone/originsphone:manageThe company's allowed web addresses (an owner or admin, or the company's own app)
PUT /browser-phone/originsphone:manageReplace the list (an owner or admin, or the company's own app). An origin is scheme + host (+ port), lower case, no path

Mint a session

POST /v1/browser-phone/sessions
Authorization: Bearer aim_live_…        (your key, or an app token with phone:browser)
{
  "user": "ann@example.com",            // the person's AIMERICA user id or email; they must have a line
  "origin": "https://app.example.com",  // must be on the company's list
  "ttl_hours": 720                      // optional; leave it out and the session lasts until you revoke it
}
201 Created
{
  "id": "bps_8f1c…",
  "username": "8135550123",             // the person's ten-digit line
  "password": "k7Qp…",                  // shown ONCE; only its digest is kept
  "realm": "a1merica.ai",
  "wss_url": "wss://a1merica.ai/sip-ws",
  "expires_at": null,                   // null = until you revoke it
  "ice_servers": [
    { "urls": "stun:turn2.a1merica.ai:3478" },
    { "urls": ["turn:turn2.a1merica.ai:3478?transport=udp", "turn:turn2.a1merica.ai:3478?transport=tcp"],
      "username": "1759350000:8135550123", "credential": "…" }
  ]
}

Refusals: 403 origin_not_allowed (the origin is not on the list), 404 user_not_found, 409 no_line (the person has no number), and the usual error envelope.

Allowed web addresses

PUT /v1/browser-phone/origins           (scope phone:manage)
{ "origins": ["https://app.example.com", "https://staging.example.com:8443"] }

200 OK
{ "origins": ["https://app.example.com", "https://staging.example.com:8443"] }

Starting the dialer

// 1. your server minted a session for this person (POST /v1/browser-phone/sessions) and handed it to the page
const s = await fetch("/my-app/phone-session").then(r => r.json());

// 2. the user agent (JsSIP, https://jssip.net - any SIP-over-WebSocket library works the same way)
const socket = new JsSIP.WebSocketInterface(s.wss_url);            // wss://a1merica.ai/sip-ws
const ua = new JsSIP.UA({
  sockets: [socket],
  uri: `sip:${s.username}@${s.realm}`,                               // the person's line @ a1merica.ai
  authorization_user: s.username,
  password: s.password,
  realm: s.realm,
  display_name: "Ann",
  register: true,
  register_expires: 300,                                            // 60-3600 s
  session_timers: false,
  user_agent: "MyApp/1.0 (AIMERICA browser phone)"
});
const media = { mediaConstraints: { audio: true, video: false }, pcConfig: { iceServers: s.ice_servers } };
const speaker = document.getElementById("phone");                   // an audio element with autoplay
let current = null;

ua.on("registered", () => show("Ready"));
ua.on("registrationFailed", e => show("Not registered: " + e.cause));   // wrong password, origin not allowed, session revoked
ua.on("newRTCSession", ({ session, originator }) => {
  current = session;
  // attach the far side's audio HERE (for an outgoing call this event comes after "peerconnection")
  if (session.connection) session.connection.addEventListener("track", e => { speaker.srcObject = e.streams[0]; });
  session.on("peerconnection", ({ peerconnection }) =>
    peerconnection.addEventListener("track", e => { speaker.srcObject = e.streams[0]; }));
  session.on("confirmed", () => show("On a call"));
  session.on("ended",     () => show("Call ended"));
  session.on("failed",    e  => show("Call failed: " + e.cause));
  if (originator === "remote") show("Call from " + session.remote_identity.uri.user);   // offer Answer: current.answer(media)
});
ua.start();

// 3. during the day
ua.call("5551234567", media);                 // a number, a colleague's extension, or a keypad code the line knows
current.answer(media);                         // an incoming call
current.hold();   current.unhold();            // hold / resume
current.mute({ audio: true });  current.unmute({ audio: true });
current.sendDTMF("5");                         // keypad tones (RFC 2833) for a menu on the far side
current.terminate();                           // hang up

// 4. when the person signs out of your app
ua.stop();                                     // then your server: DELETE /v1/browser-phone/sessions/{id}

Incoming calls

An incoming call on the line reaches the dialer as a SIP INVITE over the same WebSocket the registration uses, at the same time as the person's other devices; nothing to poll, no webhook to wait for. In JsSIP that is the newRTCSession event with originator === "remote": session.answer() takes the call (the other devices stop ringing), session.terminate({ status_code: 486 }) declines it (the others keep ringing, then the line's rules run: forwarding, voicemail). The caller's identity is in From and P-Asserted-Identity (session.remote_identity.uri.user, E.164). The INVITE's X-AIMERICA-Call-Id header (session.request.getHeader("X-AIMERICA-Call-Id")) carries the platform's call id: the same id the webhooks calls.session.ringing, calls.session.answered, calls.session.ended and calls.log carry, that GET /v1/calls/{id} answers to, and that the recording (recordings.ready) and the hangup use. If nobody answers within the line's ring time the INVITE is cancelled (CANCEL, cause Canceled in JsSIP) and the call goes on to voicemail; a browser whose registration has expired or whose WebSocket is closed does not ring.

Twelve answers

QuestionAnswer
Does it ring with the desk phone and the app?Yes. Every device registered to the line rings; the first to answer takes the call and the others stop ringing.
What caller ID does the far side see?The line's own number, exactly as from the person's desk phone or the AIMERICA app.
Several tabs, or two computers?Each registration is a device on the line; all of them ring. Mint one session per browser, not one per company.
What if nobody answers?The line's own rules run: forwarding, a colleague, then voicemail — nothing configured on your side.
Hold, transfer, park, conference?Hold from the dialer (hold()). Transfer, park and the rest use the same keypad codes a desk phone on the line uses; dial them with ua.call() or sendDTMF().
Keypad tones to a phone menu?Yes. sendDTMF() sends them the standard way (RFC 2833) and menus on the far side hear them.
Which browsers?Current Chrome, Edge, Firefox and Safari on desktop and on phones. The page must be HTTPS and the person must allow the microphone.
Behind a strict firewall or a VPN?Signalling travels on TCP 443. Audio goes over UDP when it can; otherwise it is relayed through our relay on port 3478 (UDP or TCP) with the credentials in ice_servers.
Sound quality and codecs?Opus between the browser and the platform, wideband when the other side allows it; the platform converts for the phone network.
Is it encrypted?Yes: TLS on the WebSocket, DTLS-SRTP on the audio, between the page and the platform.
Can I ship a key or a long-lived password in the page?No. Sessions are minted on your server; the password is shown once in the answer and only its digest is kept. A person without a web app uses a device password they create themselves.
Who can register a browser phone?Only a page served from a web address on the company's list, with a session minted for that origin, or a device password of the line's owner. Ten wrong passwords in fifteen minutes lock that line for that address for fifteen minutes.

Limits and security rules

  • A session lasts until you revoke it, or for the lifetime you set (1 hour to a year), and is tied to the origin it was minted for.
  • Origins are exact: scheme, host and port, no path, no wildcards. Add a staging address as its own entry.
  • The password of a session or a device is shown once; the platform keeps a digest only and never writes the password to a log.
  • One session or device password registers one browser. For two computers, mint two sessions.
  • A browser phone may call what the person's desk phone may call: emergency numbers always; premium and most international destinations are blocked unless the company has opened them; the company's caps apply.
  • Emergency calls (911) go out with the line's registered emergency address, like a desk phone's. A browser is not a safe place for an emergency call when a telephone is at hand; tell your people.
  • Sessions and origin changes count as writes for the rate limits. Mint a session when a person opens the app, not on every page view.
  • Revoking: delete a session, or remove the origin from the list; the next registration fails and the current one lapses within its registration interval (five minutes in the example). We never revoke from our side.
  • The relay credentials in ice_servers expire after 12 hours: ask again (GET /v1/browser-phone/ice) for a page that lives longer.
The full guide with the sample dialer to copy: aimerica.si/browser-phone. Try it by hand first: a device password, the sample dialer, one call.