The WebRTC signaling backbone. The conference page upgrades the HTTP connection here to a WebSocket and stays there for the duration of the call. A handful of plain HTTP query flags expose anonymous-safe metadata used by the status page, the public virtual numbers directory, and the camera registry.
Anonymous-safe. The endpoints listed below intentionally never reveal a caller number, an internal IP, or a tenant secret. Anything that does — ?stats, ?diallog, ?cdrlog, ?registrar, ?aliases, ?whitelist, the ?virtualnumbers CRUD — lives behind admin HMAC or an OIDC Bearer and is excluded here.
Native clients: the full frame catalogue and JSON Schemas
for every message (client → server and server → client) live at
/asyncapi.json
(AsyncAPI 3.0). Import it into Studio, Postman, or Insomnia and generate a
typed client. The remote-control extension is documented at
/remote_control_api.html.
Bidirectional WebRTC signaling channel. The browser conference client connects here on page load and remains for the lifetime of the call. Every signaling message after the upgrade is JSON-encoded.
Request
Standard WebSocket upgrade. No query parameters required for join. The server reads room + display-name + optional dial=<vnum> from the URL the page was loaded with and conveys them to the server via the first join frame.
Response
Server → client frame types include welcome (peer-id + minted ICE servers including TURN credentials when configured), peer-joined, peer-left, signal (carries SDP + ICE candidates from another peer), ring, ring-cleared, kicked, error. Client → server frame types include join, signal (to relay SDP/ICE to a specific peer), dial, leave, ping.
Errors
The endpoint also responds to a plain GET (no Upgrade header) with a one-line build-stamp text/plain response, useful for liveness probes.
Designed for the bundled browser client. Keepalive pings every ~25 s. Anonymous join is allowed; OIDC sign-in upgrades the connection's verifiedIdentity field which is broadcast to other peers as a green badge.
Public health snapshot consumed by status.html. Lists live rooms, current peer count, trunk status (with hostnames redacted), and CDR aggregates. Safe to expose — no caller numbers, no IPs.
The shape is intentionally minimal — just the timestamp the snapshot was taken and the up/down state of the configured SIP trunks. Per-room headcount and CDR aggregates live behind the admin UI; if you need them, ask the operator for an OIDC client with the right scope.
List of public virtual numbers a visitor can dial right now. Each row carries the dial-in URL, the human name, and the mode (AI receptionist vs. TTS).
Request
No body. Optionally accepts an OIDC Bearer token; signed-in users see additional internal rows. Anonymous callers see only rows marked visibility=anonymous.
The top-level role field reflects the caller’s identity at request time. Anonymous callers receive only visibility=anonymous rows; an OIDC Bearer with role=admin additionally sees internal vnums.
Used by status.html and the public virtual-numbers-overview.html directory page. Only rows marked visibility=anonymous appear publicly; operators who set a vnum name to personal data should mark it non-anonymous to keep it off this endpoint.
Internal SIP-bridge webhook the bridge POSTs to when an inbound PSTN call lands. Not intended for third-party integration — documented here for transparency and security review. HMAC-validated against the per-tenant bridge secret; only the local bridge can reach it.
Request
POST JSON body with the inbound call descriptor plus an X-Sig: HMAC-SHA256 header signing office-ring|<host>|<body> with the bridge shared secret.
Response
Returns { "delivered": N } where N is the number of CodeB Webphone instances that accepted the ring.
Errors
401 on signature mismatch.
Only the SIP bridge running on the same machine knows the secret. Documented here because it appears on the wire and any reviewer should know what it is.
Internal SIP-bridge webhook the bridge POSTs when an inbound call is cancelled, hung up, or picked up elsewhere. Not intended for third-party integration — documented here for transparency and security review. Same HMAC envelope as ?office-ring.
Request
POST JSON with the same HMAC-SHA256 envelope as ?office-ring.
Returns only the calling user's own passkeys — cross‑user enumeration is impossible from this endpoint. The COSE public key is intentionally not exposed; only metadata.
Begin a passkey registration ceremony. Returns a fully-formed PublicKeyCredentialCreationOptions JSON document that navigator.credentials.create({ publicKey }) consumes directly.
Request
OIDC Bearer required. Optional JSON body: {"credentialName":"Stefan's laptop"} — a human label so the user can later identify which device a credential belongs to.
Phase 0 policy: attestation=none (no enterprise device‑binding gates), userVerification=required, RP ID equals the tenant hostname. See loginpasskey.html for the matching sign‑in ceremony.
Complete a passkey registration ceremony. Verifies the attestation, parses the COSE public key, and stores it under the user's passkey collection.
Request
OIDC Bearer required. JSON body with the WebAuthn attestation response (as produced by navigator.credentials.create()) plus the session from register-start.
Revoke one of the calling user's passkeys. Lost device, retired token, end‑of‑employment — any reason. The credential is removed from the per‑user store and any future assertion presented for it is rejected.
Deletion is scoped to the calling user; admins cannot delete another user's passkey via this endpoint. For administrative revocation, the registered user's profile is rewritten directly.
Cheap read of whether a room is currently active plus its shape. Anonymous, no PII, safe to poll from a mobile client. Used by the native tray app to short-circuit UI flow before opening a WebSocket.
Room does not exist means either "nobody joined yet" or "everybody left"; there is no create-then-linger state today. Rooms disappear from the server when the last peer's WebSocket closes.
Mint a fresh room code (or accept a caller-supplied one) plus, optionally, a short-lived invite JWS the peer will pass as ?tok= on the WS URL. Auth: OIDC Bearer or ak_ API key.
Request
POST /signal.ashx?room-create
Authorization: Bearer <token>
Content-Type: application/json
{
"code": "hostinghelp", // optional; server picks a 6-char base32 code when absent
"mintInvite": true, // optional; false by default
"inviteExpMin": 60 // optional; default 60, max 1440 (24 h)
}
Response
{
"code": "hostinghelp",
"invite": "eyJhbGciOi...", // null when mintInvite was false
"join": "wss://phone.aloaha.com/signal.ashx"
}
Codes are not reserved — anybody who knows the code can join, subject to lock/knock. Use mintInvite=true to bind an invite to your identity in the audit trail.
The JWS is HS256, signed with WebPhone:InviteSecret (falls back to WebPhone:AdminSharedSecret). Present it on the WS URL as ?tok=<jws> and the join is verified before the first frame.
Mint fresh TURN credentials outside of a WebSocket session, e.g. so a native client can pre-configure its RTCPeerConnection before joining. Same shape as the welcome.iceServers array.
TTL is the tenant's WebPhone:TurnTtlSec (default 3600 s; clamped 60 s to 86400 s). The client should refresh before expiresIn lapses and call RTCPeerConnection.setConfiguration().
Verify an identity attestation minted by the identity-attest WS frame. Public; the JWS itself is short-lived (5 min) so exposing verification does not open a replay window.
Full AsyncAPI 3.0 document for the /signal.ashx WebSocket. Every frame (both directions) is a named message with a complete JSON Schema payload. Import into your AsyncAPI tool of choice.
Example
curl https://phone.aloaha.com/asyncapi.json
The convenience alias /signal.ashx?asyncapi=1 302-redirects here.
// After welcome mode="sfu" (either the join triggered an auto-promote, or a
// mode-changed frame arrived mid-call). Bob publishes his own audio track:
→ { "type":"sfu-offer", "sdp":"v=0\r\no=- ..." }
← { "type":"sfu-answer", "sdp":"v=0\r\no=- ...", "phase":"1b.0" }
// Bob would like to receive Alice's audio. Today (2026-09) this comes back
// as sfu-subscribe-failed because /sfu/subscribe is still pending on the
// bridge. Native clients should stay in mesh for receive until that lands.
→ { "type":"sfu-subscribe-offer", "publisherPeerId":"aa11bb22cc33",
"sdp":"v=0\r\no=- ..." }
← { "type":"sfu-subscribe-failed", "publisherPeerId":"aa11bb22cc33",
"code":"pending", "error":"bridge /sfu/subscribe returns 501" }
Recommendation for the native tray app: stay in mesh (≤ 6 humans). Mesh works fully today for both audio and video. The SFU subscribe path lands in the next bridge phase.
Consent-gated screen control on top of a CodeB meeting. A helper asks; the target's application shows the consent dialog; on Allow, both peers open an RTCDataChannel labelled codeb-control-v1 on their existing PeerConnection and exchange pointer / keyboard events.
Full protocol spec:/remote_control_api.html. Frame schemas: /asyncapi.json. Webhook audit events: control.requested, control.granted, control.revoked (payloads in remote_control_api.html#webhooks).
The remote-control channel is opt-in per-tenant on the target side (target user clicks Allow every session; the panic hotkey Ctrl+Alt+F12 kills the session immediately). Moderators can shut new sessions off room-wide with the control-lock frame.
Need an admin endpoint? Admin-only and OIDC Bearer-gated routes
are documented inside the admin UI itself (visible only to signed-in admins
on this host). The public API set on this page is the surface you can
integrate against without provisioning a CodeB user.