A phone inside your web app
A dialer in your own 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; hold, transfer, park, the keypad codes, voicemail and the company's hours and menus all apply unchanged. What is different is where the audio goes: straight into the browser, encrypted between the page and the platform, with the Opus codec and the browser's own echo cancellation.
Your web app does not handle any of that. It asks our API for a short-lived session for the person who is signed in, hands it to a standard SIP-over-WebSocket library in the page (the examples use JsSIP), and shows buttons. There is no plug-in, no extension and nothing to install on the computer.
One screen
Calls arrive in the app they already have open, with the customer record next to them. Click to call from any number on the page.
The same line
The person's own number, the company's rules, the call records and recordings, the reports — nothing is split between two systems.
Three calls and a library
Allow your web address, mint a session, start the dialer. The rest is the library's events and your buttons.
How a web app plugs in
- Allow your web address. A browser phone may only register from the web addresses (origins) a company has listed —
https://app.example.com, not a path. The company's owner or admin adds yours in the app under Settings, or your software does it withPUT /v1/browser-phone/origins(scopephone:manage). A page served from anywhere else is refused, whatever credentials it holds. - Get access to the API. Your own company uses an API key; an app that other companies authorize uses OAuth 2.0 and asks for the scope
phone:browser. Never put the key or the token in the page — the browser only ever sees the session from step 3. - Mint a session when the person opens your app. From your server,
POST /v1/browser-phone/sessionswith the person (their AIMERICA user id or email), your origin and how long the session should live (1 to 12 hours). The answer carries the username (the person's line), a one-time password, the WebSocket address, the realm and the ICE servers. Hand that to the page; keep nothing else. - 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 incoming calls withsession.answer(); hold, mute and the keypad are one call each. - 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 session also dies by itself at its expiry; your page then asks your server for a new one and registers again.
The API
Base address https://api.a1merica.ai/v1, JSON in and out, the usual bearer token. Errors come in the one error shape of the API; writes are rate-limited like every other write (rate limits).
| Route | Scope | What it does |
|---|---|---|
POST /browser-phone/sessions | phone:browser | Mint a session for one person: a username and a one-time password good for 1-12 h, valid only from the given origin. |
DELETE /browser-phone/sessions/{id} | phone:browser | End a session early (the person signed out). Answers 204. |
GET /browser-phone/ice | phone:browser | The ICE servers a page should use right now: a STUN entry always, relay entries with their own 12-hour credentials when the relay is on. |
GET /browser-phone/origins | phone:manage | The company's allowed web addresses. |
PUT /browser-phone/origins | phone:manage | Replace the list. An origin is scheme + host (+ port), lower case, no path. |
Mint a session
POST /v1/browser-phone/sessions
{
"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": 8 // 1-12, default 8
}
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": "2026-10-01T21:00:00Z",
"ice_servers": [
{ "urls": "stun:…" },
{ "urls": ["turn:…:3478?transport=udp", "turn:…:3478?transport=tcp"],
"username": "1759350000:8135550123", "credential": "…" }
]
}
Refused with 403 and a sentence when the origin is not on the company's list, with 404 when the person is not in the company, with 422 when the TTL is out of range.
Allowed web addresses
PUT /v1/browser-phone/origins
{ "origins": ["https://app.example.com", "https://staging.example.com:8443"] }
200 OK
{ "origins": ["https://app.example.com", "https://staging.example.com:8443"] }
ICE servers
GET /v1/browser-phone/ice
200 OK
{ "ice_servers": [ { "urls": "stun:…" }, { "urls": ["turn:…"], "username": "…", "credential": "…" } ] }
The session answer already carries the same list; ask for it again when a page lives longer than 12 hours.
Starting the dialer
Any SIP-over-WebSocket library works (RFC 7118). With JsSIP 3.x from a CDN, the whole dialer is this:
// 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
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,
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.querySelector("audio#phone"); // <audio id="phone" autoplay></audio>
let current = null;
ua.on("registered", () => show("Ready"));
ua.on("registrationFailed", e => show("Not registered: " + e.cause)); // wrong password, origin not allowed, session expired
ua.on("newRTCSession", ({ session, originator }) => {
current = session;
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 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}
Two things the page must do for the browser: be served over HTTPS (a microphone is only granted to secure pages), and create the user agent after a click — browsers only let a page play sound once the person has interacted with it.
Twelve answers
| Question | Answer |
|---|---|
| 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 — the same path as today, with 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() as the code requires. |
| Keypad tones to a phone menu? | Yes. sendDTMF() sends them the standard way 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; when the network blocks that, it is relayed through our relay on port 3478 (UDP or TCP) using the credentials in ice_servers. A network that allows outgoing 443/TCP and 3478/TCP works. |
| What about 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. Inside the platform calls are handled the same way as every other call. |
| Can I ship a key or a long-lived password in the page? | No. Sessions are 1-12 hours and minted on your server; the password is shown once in the answer and only its digest is kept anywhere. A person without a web app uses a device password they create themselves in the app. |
| 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 lives 1 to 12 hours (default 8) and is tied to the origin it was minted for; a page served from another address cannot use it.
- 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, in the answer that creates it; the platform keeps a digest only and never writes the password to a log.
- One session or device password registers one browser. To let a person use 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 daily and burst caps apply.
- Emergency calls (911) placed from a browser 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.
- Recording, consent notices, do-not-call rules, hours and menus are the line's and the company's; the browser phone adds none and removes none.
- Rate limits: sessions and origin changes count as writes. 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 above).
- A relay is only used when the browser cannot reach us directly; its credentials in
ice_serversexpire after 12 hours — ask again for a page that lives longer.
Try it
The sample dialer is the complete minimal page: fields for the WebSocket address, the line, the password and the realm, buttons to register, call, answer, hold and hang up, a keypad and a status line. Sign in to the app, create a device password under Settings → Browser phone, paste it in, and call your own cell phone.
The sample's source is plain HTML and JavaScript, meant to be copied. Questions, or a web address to allow for a company you are building for: write to us.
