Documentation
Security
SIP traffic is authorized by a per-account source-IP allow-list with no SIP password anywhere in the design, and the portal and API use hashed passwords, short-lived tokens, rotating browser sessions and optional passkeys. This page is the operational detail behind that: what each credential can do, how accounts are isolated from each other, where call content is stored, and how it is deleted.
What this page is#
The security overview is the summary a buyer reads. This is the version for the reviewer who has to sign off: the same controls, with the behaviour you can verify from your own account and the answers to the questions a security questionnaire actually asks.
Two parties, not one
Some controls are yours — your IP list, your users, your webhook endpoint, your shared links. Some belong to whoever operates the deployment you are on — retention windows, transport at the edge, backups, where storage lives. Each item below says which.
How SIP traffic is authenticated#
By source IP, and only by source IP. There is no SIP registration, no digest credential and no SIP password anywhere in the design, so there is no SIP account to brute-force and no credential to leak from a switch configuration.
| Control | Behaviour |
|---|---|
| Source-IP allow-list | The packet's source address must fall inside an enabled entry on your list. The most specific matching entry wins. An unlisted source is answered 403 IP not whitelisted and no call record is created, so scanner traffic never enters the pipeline. |
| Ranges cannot be claimed twice | An entry that overlaps a range already registered to another account is refused with 409, and a range wider than /16 (IPv4) or /48 (IPv6) is refused with 422. No account can whitelist half the internet or another operator's addresses. |
| No registrar | REGISTER is answered 403 Registration Not Supported. |
| Destination guard | A destination that points at loopback, link-local, multicast or at the ingress itself is refused, both when you save it and again on the call. You cannot aim a destination at the host network or at a cloud metadata address. |
| Fail closed | If the authorization decision cannot be made, the INVITE is rejected with 503 rather than allowed. A failure costs you an analysis, never an unauthorized call. |
| Spend limits | Every call is capped in advance by the credit available and by your concurrent-call limit, so a compromised switch of yours cannot run up an unbounded bill. |
Captured SIP headers are labels, never authorization
Where a deployment captures custom headers from your INVITEs, those values are reporting metadata. They identify the sender to you; they never affect authorization, routing or billing, which are decided by the IP allow-list and by your own destinations and routing rules. Only header names the operator has configured are read, and each value is truncated before it is stored.
What is encrypted in transit#
- Portal and API
- HTTPS, with HSTS at the edge once a hostname is configured, under a strict Content Security Policy that permits same-origin script and style only. Authenticated responses are marked no-store, so JSON and recordings do not land in a browser cache.
- SIP signalling
- The ingress accepts UDP and TCP. The onward leg to your destination can use TLS where your destination is configured for it.
- Media
- Plain RTP, in both directions. SRTP towards the ingress is not supported — this is the trade-off that makes recording and therefore analysis possible, and it is stated in Getting started before you connect anything.
- Between you and us at the network level
- Nothing prevents you carrying the SIP and RTP legs over your own private link or tunnel to the ingress address. That is a deployment decision to make with your operator, not a product setting.
Portal accounts and what each one can do#
An account has one owner — the login the account was created with — and up to nine additional users, each with their own e-mail, password and login.
- Only the owner may invite, rename, deactivate, reactivate or re-invite users. The portal hides the controls from everyone else and the API refuses them (403 owner_required) — hiding a button is never the control.
- An invited user's temporary password is never returned to the browser. It is e-mailed, and the user is forced through a password change on first sign-in.
- Deactivating a user cuts their API access immediately and ends their browser session at its next renewal.
- Password rules: a minimum length is enforced, and a password longer than the hashing limit is rejected outright rather than silently truncated. Passwords are stored as bcrypt hashes.
- Sign-in failures are rate-limited per source address and per e-mail address, counted from the real client address rather than a header the client can set. Beyond the limit the API answers 429 too_many_attempts with the seconds to wait. Locking out someone else's account by hammering their e-mail does not work: only further wrong passwords are refused.
- A wrong e-mail and a wrong password produce the same response, in the same time. The sign-in form is not an account-existence oracle.
Browser sessions#
The portal never stores an access token in the browser. It keeps a short-lived token in memory and renews it from a refresh cookie that is HttpOnly, SameSite=Strict and scoped to the authentication path. The cookie is rotated on every renewal.
Presenting a refresh token that has already been rotated is treated as a stolen cookie: the whole session family is revoked at once and the browser is signed out. Renewal also requires a custom request header, which is what stops a cross-site request from using the cookie.
| Action | Effect |
|---|---|
| Sign out | Ends that browser's session. The access token it already holds stays valid for the few remaining minutes of its life. |
| Sign out everywhere | Ends every session of the account and invalidates every access token issued before that moment. |
| Change password | Same as above, except for the browser making the change. |
| Deactivate a user | Cuts that user's API access immediately and their browser session at its next renewal. |
Passkeys#
The account owner can register WebAuthn passkeys as a second factor. Registration challenges are single-use and bound to the account, a credential already registered on the platform is refused, and the authenticator's signature counter must move forward — a cloned authenticator is detected rather than accepted.
Registering the first passkey turns two-factor on for the account; removing the last one turns it off and the account falls back to password-only sign-in. Passkeys are managed by the owner, and an operator support session cannot touch them.
API keys#
API keys are the credential for anything unattended. They do not expire on a schedule, they are unaffected by password changes, and they are the right answer whenever a script would otherwise be storing a password.
- Shown once
- The full key is returned exactly once, at creation. Only a hash and a short prefix are stored, so it cannot be recovered from the platform or from a database dump — a lost key is replaced, never retrieved.
- Bound to the user who minted it
- A key carries the identity and the role of the portal user who created it. When that user is deactivated the key stops working with them. There is no orphan credential that outlives the person who made it, which is the single most common finding in an access review.
- Owner-only, and never from a support session
- Only the account owner can mint or revoke keys, and an operator acting inside a support session cannot mint one at all — nobody can leave behind a long-lived credential that outlasts the session it was created in.
- Revocation is immediate
- A revoked key is refused on the next request. Each key records its own last_used_at, so an unused key is easy to find and retire.
the key inventory, with last use
curl -s $API/api/auth/api-keys -H "X-API-Key: $OVS_KEY"
# [{"id":"...","name":"crm-integration","prefix":"ovs_a1b2","last_used_at":"...",
# "revoked_at":null,"created_at":"..."}]
Tenant isolation and the audit log#
Every customer-facing query is scoped to the calling account in the query itself, not filtered afterwards. Another account's call, IP entry, destination, key or share is indistinguishable from one that does not exist: the answer is 404, never 403, so the API cannot be used to confirm that a record belongs to someone else.
Responses are assembled from explicit allow-lists rather than by serializing a record and removing fields. A field is public because someone added it, not because someone forgot to take it out. That is why the shared-call view and the customer evidence bundle cannot leak internal fields.
Administrative and security-relevant actions are written to an audit log against the acting user's e-mail — a member's action is attributed to the member, not to the account. Settings changes, credit and plan changes, user management and support sessions are all recorded, and secret-shaped values are stripped from the recorded payload. Audit entries are not deleted by any retention job.
An operator can open your portal in a support session to reproduce a problem. That session is recorded in the audit log, and it cannot change your password, end your sessions, manage your passkeys or mint an API key.
Where call content lives#
| Artefact | Where | Contains |
|---|---|---|
| Recording | The operator's own disk, or an operator-controlled object store with server-side encryption on and public access blocked. | Both parties' audio, as a two-channel file. |
| Media capture | The operator's disk, briefly. | The raw media of the call, kept only long enough to produce the recording and to diagnose a failure. |
| Transcript and analysis | The database. | The full conversation text and everything derived from it. |
| SIP flow | The database. | The signalling of the call: numbers, addresses, timings. No audio. |
| Call record | The database. | Caller and dialled numbers, source address, destination, timestamps and billing. |
Nothing is stored in a bucket the operator does not own. Where call content is sent to a cloud speech-to-text or analysis provider, the portal marks each stage local or cloud per call, so you can tell from the call record whether that call's content left the machine. Providers are chosen by the operator, who can also run speech-to-text on their own hardware.
Retention and deletion#
Recordings, media captures and SIP logs each have their own retention window, set by the operator of your deployment. Expiry is a real deletion of the object, not a flag: the call keeps its metadata, and the missing artefact is reported as an omission — a 404 on the recording, an empty SIP flow — rather than as an error.
- Media captures go first. They are removed as soon as they have been converted, and orphans expire on a much shorter window than recordings.
- Recordings expire on their own window, locally and in the object store.
- Call records, transcripts, analyses, ledger entries and the audit log are kept indefinitely. There is no automatic deletion of these and no self-service delete button.
- Erasing an account or a specific call is a deliberate operator action, performed on request. Ask your operator what their turnaround is and put it in your contract; do not assume a self-service path exists.
- Database backups contain everything the database does, including webhook secrets, so the operator's backup store is secret material in its own right.
If you need a call to outlive its retention window, export it. The evidence bundle is a single self-contained file with the report, the analysis, the transcript, the recording and the SIP flow, and it opens with no account and no network. Exports are rate-limited per user and per address.
Shared call links#
A share is a password-protected public link to one call — the way you send a verdict to a carrier, a customer or an investigator without giving them an account.
| Control | Behaviour |
|---|---|
| The password is generated, not chosen | The server generates it and returns it exactly once. Only a bcrypt hash is stored, so nobody — including your operator — can read it back. The alphabet excludes look-alike characters and it is grouped for reading aloud. |
| The link alone is not enough | A long random token identifies the link; the password unlocks it. Unlocking returns a view token that is valid for 30 minutes and is bound to that one link — it does not open any other share. |
| Probing is useless | An unknown link, a wrong password and a dead link with a wrong password return the identical response, and the same work is spent on each, so the link space cannot be enumerated by timing or by error text. |
| Attempts are limited | Password attempts are capped both per link and per source address, over a rolling window. |
| Expiry and revocation | Set an expiry when you create the link (or none). Revoking kills it immediately. Rotating the password keeps the same link and issues a new password — existing viewers keep their 30-minute token, so revoke instead when the link itself must die. |
| Deliberately limited content | A share carries the call, the analysis, the transcript and the recording. It never carries your balance, plan, routing, the SIP flow, the media capture, or any identifier that could be used to find other calls. Before the password, a visitor sees only that the link is real and which company shared it. |
| Accountable | Each link records who created it, its view count and when it was last viewed, and creation and revocation are audited. |
Webhook signing and guards#
Outbound deliveries are signed so your receiver can prove they came from us, and the target URL is constrained so the platform cannot be turned into a probe of a private network.
- Signature
- X-OVS-Signature is sha256= followed by the hex HMAC-SHA256 of the raw request body under your webhook secret. Verify it before parsing, and compare in constant time.
- Replay and duplicate handling
- X-OVS-Delivery carries a delivery id and X-OVS-Event the event name. A retry repeats the delivery id, so a receiver keyed on it is idempotent.
- Your secret is write-only
- It can be set and replaced over the API, never read back. The stored copy is what computes the HMAC, which is why a database backup is treated as secret material.
- Outbound safety check
- The URL must be https and must resolve to a globally routable address. Loopback, private, link-local — including the cloud metadata address — multicast and reserved space are all refused, in IPv4-mapped and tunnelled forms too. The check runs when you save the URL and again at every delivery, on the address actually connected to, so a DNS answer that changes in between cannot redirect a delivery inward.
- No redirects, bounded time
- Redirects are not followed and the request times out after ten seconds. A transient failure is retried with a widening backoff, up to five attempts; a rejection by your endpoint is permanent.
Inbound payment notifications are verified against the provider's signature before anything changes and are idempotent on the session id, so a replayed or forged notification cannot credit an account. Card details never reach the platform.
Questions worth asking your operator#
These are deployment decisions, not product settings, and the answers should be in your contract rather than inferred from this page:
- The retention windows in force for recordings, media captures and SIP logs.
- Where recordings are stored, in which jurisdiction, and who can read that store.
- Which speech-to-text and analysis providers are configured, and whether speech-to-text runs on their own hardware.
- Their turnaround for an erasure request, and what it covers.
- Where backups live, how they are protected, and how a restore is tested.
- How to report a suspected vulnerability, and to whom.
Reporting a security issue
Report it privately to the operator of your deployment — never in a public tracker and never through a webhook. Include what you did, what happened and the call ids involved. Do not attach audio, transcripts or credentials: reference the call id instead.