Security at Wixzel Voice
Wixzel Voice encrypts carrier passwords at rest, stores API keys only as peppered hashes, signs every webhook, and keeps the API and its data in Helsinki, Finland. This page lists what the code enforces today, what is not in place yet, and how to report a vulnerability.
Last updated
Published
Wixzel Voice is a voice AI API for building AI agents that place and answer real phone calls over your own SIP trunk, or talk to people in your web and mobile apps. One API key and one prepaid balance cover every voice engine, billed per second of actual usage. That means it holds three things worth protecting: your carrier credentials, the keys that spend your balance, and the audio, transcripts and contacts of the calls your agents make.
What does Wixzel Voice encrypt, hash and sign?
| Fact | Value | Source |
|---|---|---|
| Carrier (SIP trunk) passwords | New and updated values encrypted at rest with AES-256-GCM under a per-deployment key; older values re-encrypted when next saved; never returned by the API again | API: credential encryption |
| API keys | Shown once; stored only as an HMAC-SHA256 with a server-side pepper; scoped, with no admin scope | API: keys |
| MCP connector sign-in | OAuth 2.1 authorization code flow, PKCE with S256 only; the token is a scoped, revocable API key | docs: MCP server |
| Webhooks | Signed: X-Wixzel-Signature is an HMAC-SHA256 of the raw body; URLs into private networks refused | docs: Webhooks |
| Money paths | Idempotency-Key required on POST /v1/calls, POST /v1/agents/{id}/test-call and POST /v1/billing/topups; kept 24 hours | API: idempotency |
| Rate limits | 10 requests per second per key, burst 20; login and signup throttled per IP and per email | docs: Rate limits |
| Transport | TLS on HTTPS and WebSocket connections to the API, console and MCP server, with HSTS for one year, including subdomains. SIP to your carrier uses the trunk's transport (UDP by default, TLS if configured), and call audio (RTP) is not encrypted | API: HTTP hardening, SIP config |
| Account passwords | bcrypt, cost 12 | API: accounts |
| Where data lives | Hetzner, Helsinki, Finland (EU); the console and website on Vercel | Privacy Policy |
| Card numbers | Never reach Wixzel Voice; Dodo Payments is merchant of record | Facts |
| Report a vulnerability | aqeel@wixzel.com, as listed in /.well-known/security.txt | security.txt |
How does Wixzel Voice protect carrier passwords?
Wixzel Voice needs your SIP trunk password to place calls through your carrier, so it cannot hash it; it encrypts it. New and updated trunk passwords, and every other third-party credential the platform holds on a customer's behalf, are encrypted at rest with AES-256-GCM. The master key is stretched with scrypt from a secret that exists only in the deployment's environment, each value gets its own message key through HKDF, and each ciphertext is stamped with the id of the key that wrote it, so a future key rotation can re-wrap values instead of losing them. Values written under the older scheme still read, and are re-encrypted this way the next time they are saved.
GCM is authenticated: a ciphertext altered in the database fails to decrypt instead of turning into a different password. A credential that will not decrypt is refused loudly rather than treated as empty, so a trunk can never silently become unauthenticated. The API never returns a trunk password again, including to you.
The telephony configuration generated from your trunk settings is hardened against injection, and the generator is fuzz-tested against that property.
How does Wixzel Voice store and scope API keys?
A wv_live_ key is shown once. Wixzel Voice stores an HMAC-SHA256 of it keyed with a server-side pepper, so a copy of the database alone cannot be used to recover or brute-force a key. The prefix and last four characters are kept only so you can recognise a key in a list.
- Scopes. Every key carries only the scopes you grant, such as
calls:readorsip_trunks:write. There is no admin scope, so no key can do everything. - Spend limits. A key can carry
spend_limit_micros; once a key has spent its limit, calls placed with it are refused before dialling. - Expiry and rotation. A key can carry
expires_at.POST /v1/api-keys/{id}/rotateissues a replacement and keeps the old key working for 24 hours, so a deploy never has a moment with no valid key. - Revocation. Revoking a key drops it from the authentication cache at once, so it stops working on the next request.
- Isolation. Every resource belongs to one account, and every query is scoped to the account that asked.
How does the Wixzel Voice MCP connector sign in?
AI clients such as claude.ai, Claude Desktop and Claude Code connect to the Wixzel Voice MCP server through an OAuth 2.1 authorization server built into the API. Clients register themselves (RFC 7591), send you to the console to consent, and exchange the code for a token. PKCE is required, S256 only; the plain method is refused.
The token the client receives is an ordinary scoped API key carrying only the scopes you approved, and revoking the grant revokes the key. The OAuth endpoints never accept an API key as a credential: a client proves itself with PKCE, and with a client secret when it registered one.
How does Wixzel Voice sign webhooks?
Every webhook POST carries X-Wixzel-Signature, an HMAC-SHA256 of the raw body keyed with your webhook secret. Verify it over the raw bytes, before parsing, and compare in constant time. The secret is returned once, when you rotate it, and never by GET /v1/webhook, so a leaked read-scoped key cannot be turned into forged events.
A webhook URL that resolves to loopback, link-local or private address space is refused with 400 unsafe_webhook_url when you set it and again at delivery time, and redirects are not followed. The platform can reach addresses you cannot, so it will not be aimed at them.
What stops a retry from charging twice on Wixzel Voice?
POST /v1/calls, POST /v1/agents/{id}/test-call and POST /v1/billing/topups require an Idempotency-Key. A replay with the same key and body returns the original response instead of dialling or charging again; the same key with a different body is refused with 422. Keys are kept for 24 hours. Both official SDKs send one automatically on those paths.
Credit is reserved before a call is placed, a call that cannot fund its first 15 seconds is refused with 402 before anything is allocated, and prices are frozen per call at admission. Card numbers never reach Wixzel Voice: payments go through Dodo Payments as merchant of record.
What protects Wixzel Voice from abuse?
- API rate limits: 10 requests per second per key, burst of 20. A
429carriesRetry-Afterand is never charged. - Sign-in and sign-up are throttled per IP address and per email address; passwords are hashed with bcrypt at cost 12, and an email sign-up is confirmed with a code sent to that address.
- Console sessions live in an httpOnly cookie, never in a URL; the session token's signing algorithm is pinned; and you can sign out every session at once.
- SIP scanners that probe for numbers are banned temporarily, and every inbound call attempt is rate-limited per source address before any credit is reserved, so refused traffic is never billed to anyone.
- Concurrent calls are capped per account, 5 by default, so one account cannot take every line on the platform.
- HTTP hardening: TLS redirect, HSTS for a year including subdomains, security headers, and stated request-body limits.
- SIP to your carrier uses the transport set on the trunk: UDP by default, or TCP or TLS if you set
transport. Call audio (RTP) is not encrypted.
Where does Wixzel Voice store data, and who processes it?
The Wixzel Voice API, its databases and call media run on Hetzner in Helsinki, Finland, in the European Union. The console and this website are served by Vercel. During a call, the speech and language providers for the agent's engine (Deepgram, ElevenLabs, OpenRouter, Google, or Sarvam) process the audio and text; they are in the United States, except Sarvam, which processes speech in India.
Transcripts, and recordings where a call is recorded, are kept with the call log until you delete them; DELETE /v1/calls/{id} removes the log, transcript and recording together, and closing an account from the console deletes everything in it at once. Request logs are dropped after three months. The full list of subprocessors, with each one's role and location, is in the Privacy Policy, and the Data Processing Addendum applies to every customer, with breach notice within 72 hours.
How is Wixzel Voice data backed up?
Both production databases are dumped every hour. A weekly drill restores the newest dump into a scratch database and checks that every account balance equals the sum of its ledger entries, so a backup is known to be usable, not just present. An encrypted copy is pushed off the server every hour with restic, which encrypts before anything leaves the host, and is itself test-restored every week.
One honest limit: the off-site copy is in the same Hetzner datacentre in Helsinki. It covers a dead disk, a lost volume, a destroyed server and a compromised host; it does not cover the loss of the whole site.
What is not in place yet at Wixzel Voice?
- Wixzel Voice is in alpha and operated by one person; access to production is limited to that operator.
- There is no SOC 2 report or ISO 27001 certification.
- Call media to carriers is not encrypted: call audio (RTP) travels without SRTP, and SIP signalling uses the trunk's transport, which is UDP unless you set TLS.
- Off-site backups are not geographically separate from the primary servers.
- Restricting an API key to particular source IP addresses is not offered through the API or the console yet.
How do I report a vulnerability in Wixzel Voice?
Email aqeel@wixzel.com. The address is also published in /.well-known/security.txt. Include what you found, the steps to reproduce it, and the request_id from any response involved. Messages are read by the operator, and most are answered within a working day.
Please test only against your own account and data, do not degrade the service for other customers, and give a reasonable time to fix an issue before you publish it.
Frequently asked questions
- How does Wixzel Voice store my SIP trunk password?
- Encrypted at rest with AES-256-GCM. A new or updated password gets its own message key, derived with HKDF from a master key that is itself stretched with scrypt from a secret held only by the deployment, and each ciphertext carries the id of the key it was written under. Values stored under the older scheme still read, and are re-encrypted this way the next time they are saved. GCM makes tampering a decryption failure rather than a silently different password. The API never returns the password again, including to you.
- Can Wixzel Voice see my API key?
- No. A key is shown once, when it is created, and only an HMAC-SHA256 of it, keyed with a server-side pepper, is stored. The
wv_live_prefix and the last four characters are kept so you can recognise a key in a list; nothing stored can reproduce it. A lost key can only be replaced. - How do I verify a webhook came from Wixzel Voice?
- Compute an HMAC-SHA256 over the raw request body with your webhook secret and compare it, in constant time, with the
X-Wixzel-Signatureheader. The secret is shown once, when you rotate it withPOST /v1/webhook/rotate-secret, andGET /v1/webhooknever returns it. - Where is Wixzel Voice data stored?
- The API, the databases and call media run on Hetzner in Helsinki, Finland, inside the European Union. Speech and language providers in the United States and India process audio and text for the duration of a call, and the sarvam engine processes speech in India. The full subprocessor list is in the Privacy Policy.
- Does Wixzel Voice have a security certification?
- Not today. Wixzel Voice is in alpha and run by one operator; there is no SOC 2 or ISO 27001 report. This page lists the measures the code enforces, and the Data Processing Addendum sets out the commitments, including breach notice within 72 hours.
- How do I report a security issue in Wixzel Voice?
- Email aqeel@wixzel.com with what you found, how to reproduce it, and the
request_idfrom any response involved. The same address is published in /.well-known/security.txt.
