LLOQU
ProductPricingBlogDocsLogin

On this page

What does NotAllowedError mean in getUserMedia?How do I tell which cause I have?Is the page in a secure context?Why an iframe needs allow="microphone"The user clicked Block, and the browser will not ask againOS and enterprise blocks: when the browser itself has no microphoneTwilio errors 31208 and 31401 are the same failure with a codeNotFoundError: no device, or constraints nothing can satisfyFAQEspecially if your dialer is a widget
All articles
WebRTCgetUserMediaErrors

getUserMedia NotAllowedError: Causes and How to Fix It

getUserMedia NotAllowedError has six causes: denied prompt, HTTP, iframe policy, OS and enterprise blocks. How to tell them apart and fix each.

Alloqui TeamAug 13, 202610 min read
On this page
What does NotAllowedError mean in getUserMedia?How do I tell which cause I have?Is the page in a secure context?Why an iframe needs allow="microphone"The user clicked Block, and the browser will not ask againOS and enterprise blocks: when the browser itself has no microphoneTwilio errors 31208 and 31401 are the same failure with a codeNotFoundError: no device, or constraints nothing can satisfyFAQEspecially if your dialer is a widget
Back to all articles
LLOQUAlloqui

Product

  • Features
  • Pricing
  • Blog

Developers

  • Documentation
  • @alloqui/dialer on npm

Company

  • Contact

Legal

  • Privacy
  • Terms

© 2026 Alloqui

NotAllowedError is the DOMException that navigator.mediaDevices.getUserMedia() rejects with when the browser refuses to hand over the microphone. The name points at a user clicking Block, and that is one of six causes that produce a byte-identical error object: an insecure page served over HTTP, an <iframe> missing allow="microphone", a Permissions-Policy header on the parent page, an operating-system block on the browser itself, and enterprise policy. This guide separates them with a 60-second diagnostic, then fixes each one — including the iframe case, which is where embedded dialers and CRM widgets fail and which most answers skip entirely.

What does NotAllowedError mean in getUserMedia?

MDN defines it as thrown "if one or more of the requested source devices cannot be used at this time," and then lists conditions that have nothing to do with a prompt: the browsing context being insecure, the user having denied access for the session or globally, and — on browsers that support it — "Permissions Policy is not configured to allow access to the input source(s)." One error name, five spec-level conditions, plus whatever the operating system decides.

The error message text is not a reliable discriminator either, because it varies by browser and by exactly how permission was refused. Twilio's own video guide enumerates three separate strings that all arrive as NotAllowedError: "Permission denied," "Permission dismissed," and "The request is not allowed by the user agent or the platform in the current context." A dismissal (clicking the X on the prompt) and an explicit Block are different user actions with different consequences — Chrome may prompt again after a dismissal, while a Block persists for the origin — and they land in your catch block wearing the same name.

Branch on err.name, never on err.message:

try {
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
} catch (err) {
  switch (err.name) {
    case 'NotAllowedError':    /* permission or policy — this guide */ break;
    case 'NotFoundError':      /* no device matches the constraints */ break;
    case 'NotReadableError':   /* granted, but the OS won't release it */ break;
    case 'OverconstrainedError': /* constraints impossible; err.constraint names it */ break;
  }
}

How do I tell which cause I have?

Run this in the console of the page that's failing, in the frame that calls getUserMedia(). It collects the four facts that split the six causes apart.

async function diagnoseMic() {
  const report = {
    secureContext: window.isSecureContext,
    mediaDevicesPresent: !!navigator.mediaDevices,
    inIframe: window.self !== window.top,
    permission: 'unknown',
  };

  try {
    const status = await navigator.permissions.query({ name: 'microphone' });
    report.permission = status.state; // 'granted' | 'prompt' | 'denied'
  } catch {
    report.permission = 'unsupported'; // Firefox throws TypeError on 'microphone'
  }

  const started = Date.now();
  try {
    const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
    stream.getTracks().forEach((t) => t.stop());
    report.result = 'granted';
  } catch (err) {
    report.result = err.name;
  }
  report.elapsedMs = Date.now() - started;
  return report;
}

Read the report top to bottom:

  • secureContext: false — stop here, it's the HTTP problem below. Nothing else matters until the origin is secure.
  • mediaDevicesPresent: false — you are getting a TypeError on undefined, not a NotAllowedError. Same root cause, different exception.
  • inIframe: true with permission: 'prompt' but an immediate rejection — the classic policy block. The browser never asked anyone anything.
  • permission: 'denied' — a stored decision, either the user's or an administrator's.
  • elapsedMs under ~300 with no prompt on screen — no human was involved. A prompt that a person has to see and click takes seconds; a policy or stored-setting rejection returns almost instantly. That timing trick comes from the most-viewed answer on the subject and is the only signal that works everywhere.

Two caveats on navigator.permissions.query({ name: 'microphone' }), which is the snippet everyone copies without them. Firefox does not accept 'microphone' as a PermissionName and rejects the promise with a TypeError (Mozilla bugs 1449783, 1609427 and 1712500 track it, still open as of August 2026), so the call must be wrapped in try/catch or it takes your diagnostic down with it. And a 'granted' state does not guarantee a stream — the operating system, another application, or a policy layer can still refuse below the browser.

Is the page in a secure context?

getUserMedia() is gated on secure contexts: HTTPS, or localhost. In an insecure context, current Chrome and Firefox make navigator.mediaDevices itself undefined, which is why the HTTP case usually surfaces as TypeError: Cannot read properties of undefined (reading 'getUserMedia') rather than the error in this post's title. Twilio documents exactly this for twilio-video.js — a TypeError when "your app is being served from a non-localhost non-secure context."

The version that catches teams is the LAN address. http://localhost:3000 is a secure origin; http://192.168.1.20:3000 is not, even though it is the same server on your own network. So the app works on the laptop and dies the moment you open it on a phone to test the microphone, with an error that says nothing about HTTP.

Three ways out, in order of how permanent they are:

  1. A tunnel — ngrok http 3000 or cloudflared tunnel --url http://localhost:3000 gives you a real HTTPS origin that phones and iframes both accept.
  2. A local certificate — mkcert installs a locally-trusted CA and issues a cert for your LAN IP or a .local hostname. Better for a team that tests on devices daily.
  3. A Chrome flag, for debugging only — --unsafely-treat-insecure-origin-as-secure="http://192.168.1.20:3000" combined with --user-data-dir=/tmp/insecure-profile. The flag silently does nothing without a separate profile directory, which is why half the Stack Overflow reports say it "doesn't work."

Why an iframe needs allow="microphone"

This is the cause that generic answers skip, and it is the one that matters most if your dialer is a widget: embedded in a CRM, in a helpdesk console, in a Salesforce Lightning component, or in any customer's page. The failure is silent and total — no prompt appears, getUserMedia() rejects immediately, and the user reports "it doesn't even ask me."

The mechanism is the Permissions Policy default. The default allowlist for the microphone directive is self, meaning the feature is available to the top-level document and same-origin frames only. A cross-origin <iframe> gets nothing by default, and MDN is explicit about the result: when policy blocks the feature, getUserMedia() "calls will return a Promise that rejects with a NotAllowedError DOMException."

So the embedding page has to delegate it:

<iframe src="https://dialer.example.com/widget" allow="microphone"></iframe>

A bare allow="microphone" is shorthand for allow="microphone 'src'" — permission is granted only while the frame's document matches its src origin. Four details decide whether that one attribute is enough:

  • The parent's header and the attribute intersect; they don't add up. If the top-level page sends Permissions-Policy: microphone=(), the iframe's allow attribute cannot override it. MDN's guidance is to grant the widest acceptable set in the header and narrow it per-frame: Permissions-Policy: microphone=(self "https://dialer.example.com") on the parent, plus the allow attribute on the frame.
  • Every level of nesting needs it. A frame three deep inherits nothing automatically; each parent must delegate to its child.
  • Cross-origin navigation drops the policy. If the frame navigates to a different origin than its src, the policy stops applying unless that origin is named in the allow attribute.
  • Legacy Feature-Policy headers still ship. Grep your backend and your Helmet/middleware config for it. A JHipster-generated backend that emitted microphone 'none' in its Feature-Policy header produced the perfect symptom for this class of bug: Twilio video calls failed in Chrome and worked in Firefox and Safari, because those browsers were not enforcing the directive at the time. Anything that breaks in exactly one browser and never prompts should send you straight to the response headers.

To verify without guessing: open Chrome DevTools → Application → Frames, select the frame, and read its Permissions Policy allowed/disallowed list. Then check the Network tab for a Permissions-Policy or Feature-Policy response header on the top-level document. The allow attribute is in the Elements panel on the <iframe> tag itself, where an ad-hoc postMessage-based integration frequently forgot to put it.

The user clicked Block, and the browser will not ask again

Once an origin is blocked, there is no API that re-prompts. Not a flag, not a retry, not a fresh getUserMedia() call. The Stack Overflow question asking for exactly this capability has 151,000 views and 121 upvotes and no accepted answer, thirteen years on, which is about as strong as negative evidence gets.

That makes recovery a UX problem with three parts:

Detect the state before you need the microphone. Run the permission query when the dialer mounts, not when the user hits Call. A blocked state discovered at mount can be surfaced calmly; discovered at call time it becomes a failed call.

Tell the user where the control is, precisely. "Please allow microphone access" is useless to someone who already denied it, because the prompt they need is gone. Name the site-settings icon at the left of the address bar, say that microphone has to be switched to Allow, and say the page needs a reload afterwards. Chrome does not apply the change to a live page.

Offer the reload yourself. Permission changes take effect on the next page load, so a "Retry" button that only re-calls getUserMedia() fails again identically. Reload the frame.

Two things not to do. Don't retry in a loop — with a persisted block, every call rejects instantly and you spin. And don't request the microphone on page load if you can avoid it: a prompt with no context is dismissed or blocked far more often than one that appears after someone clicks "Call," and in Chrome repeated dismissals escalate into an automatic block you cannot undo from JavaScript.

OS and enterprise blocks: when the browser itself has no microphone

Below the browser sit two more layers, both of which reject before any prompt renders.

Operating system. On macOS, each application holds its own microphone grant under System Settings → Privacy & Security → Microphone; if Chrome is off there, every site in Chrome fails while Safari works fine. That "works in one browser, not the other" pattern points at the OS as often as it points at Permissions Policy. On Windows the equivalent is Settings → Privacy & security → Microphone, including the separate "Let desktop apps access your microphone" toggle. Twilio's 31401 documentation lists the same check, calling out mobile devices in particular.

Enterprise policy. Chrome's AudioCaptureAllowed policy, when set to Disabled, turns off microphone prompts entirely; only origins listed in AudioCaptureAllowedUrls get access, and everything else receives an instant NotAllowedError. Edge ships the same pair. This is the explanation when one customer's whole company cannot use your dialer and no individual setting fixes it. Have the user open chrome://policy and search for AudioCapture — active policies are listed there with their values, which turns a week of back-and-forth into a screenshot.

Twilio errors 31208 and 31401 are the same failure with a code

If you reach NotAllowedError through the Twilio Voice JavaScript SDK, it arrives wrapped in a numbered error, and which number depends on your SDK major version:

CodeNameSDK
31208User denied access to microphoneVoice JS SDK 1.x
31401UserMedia Permission DeniedVoice JS SDK 2.x
31402UserMedia Acquisition FailedBoth — permission granted, acquisition failed

Twilio's own 31208 page states that "in Voice JavaScript SDK 2.x this condition is reported as error 31401 UserMedia Permission Denied," so if you are searching 31208 against a v2 codebase you are reading documentation for a code your SDK no longer emits. 31402 is a different animal: it means permission succeeded and capture still failed, which Twilio attributes to "an invalid device selection, overly restrictive getUserMedia() constraints, or a browser, operating system, or hardware issue."

The timing detail that causes most 31401 reports is buried in Twilio's own causes list: the SDK requests the microphone at device.connect() or call.accept(), not at construction. Permission failure therefore surfaces in the middle of placing a call, which is the worst possible moment for it. Twilio's recommended fix is to ask first and release immediately:

async function primeMicrophone() {
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
  stream.getTracks().forEach((track) => track.stop()); // drop the "recording" indicator
}

await primeMicrophone();       // fails here, with context, before any call exists
const device = new Device(token);

Stopping the tracks matters: without it the browser's recording indicator stays lit while the user sits idle, which generates its own support tickets. Twilio also notes that browsers "can return incomplete or unlabeled device information until getUserMedia() access is approved," so any device picker you build should be populated after this call, not before.

Two neighbouring failures worth naming. If the SDK reports a bare error 31000 instead of a permission code, the cause is elsewhere — the generic code hides token, webhook and TwiML App problems. And if permission succeeds, the call connects, and you still hear nothing, the block moved downstream to playback: see AudioContext was not allowed to start.

NotFoundError: no device, or constraints nothing can satisfy

NotFoundError is the sibling error and it means something narrower: MDN says it is thrown "if no media tracks of the type specified were found that satisfy the given constraints." Permission is not the issue. Common triggers are a Bluetooth headset that dropped off, a laptop with no input at all, or an input disabled in browser settings.

The distinction that saves debugging time is NotFoundError versus OverconstrainedError. If you remember a user's chosen device and replay it as an exact constraint:

// Unplugged device → OverconstrainedError, with err.constraint === 'deviceId'
getUserMedia({ audio: { deviceId: { exact: savedDeviceId } } });

// Preferred, with fallback → any working mic, no error
getUserMedia({ audio: { deviceId: { ideal: savedDeviceId } } });

exact fails hard when the device is gone; ideal falls back to the default. Since device IDs are scoped per origin and reset when a user clears site data, a stored deviceId goes stale on its own schedule — treat it as a preference, not an identifier.

One more branch to keep straight: if permission was granted and the device exists but the operating system or another application is holding it, you get NotReadableError instead, which has its own causes and fixes.

FAQ

How do I reset a blocked microphone permission in Chrome? Click the site-settings icon at the left of the address bar, set Microphone to Allow (or use "Reset permission"), then reload the page — changes don't apply to an already-loaded tab. The full list lives at chrome://settings/content/microphone. Firefox: the padlock, then clear the blocked permission. Safari: Settings → Websites → Microphone.

How do I test NotAllowedError without blocking my own microphone? Add a Block entry for your dev origin at chrome://settings/content/microphone, reproduce, then remove it. For the policy path, load your widget in a throwaway HTML page inside an <iframe> with no allow attribute — that reproduces the cross-origin block in about ten seconds, with no browser settings touched.

Does NotAllowedError happen in Electron and mobile WebViews? Yes, with host-specific fixes. Electron rejects media requests unless the app calls session.setPermissionRequestHandler and approves media. Android WebView needs WebChromeClient.onPermissionRequest. On iOS, WKWebView gained getUserMedia support only in iOS 14.3, so third-party iOS browsers built on it fail outright on older versions.

Why does the microphone work in Firefox but fail in Chrome? Two usual suspects. Chrome enforces Permissions-Policy and Feature-Policy microphone directives that Firefox and Safari have historically ignored, so a restrictive header breaks Chrome alone. And macOS grants microphone access per application, so Chrome can be blocked in System Settings while Safari is allowed.

Does getUserMedia require a user gesture or button click? Not by specification. The W3C working group considered enforcing a gesture requirement like getDisplayMedia's and rejected it as not web-compatible, since many pages call getUserMedia() on load. Calling it behind a click is still better practice: prompts with visible context get blocked far less often, and older iOS Safari versions failed without one.

Especially if your dialer is a widget

Re-read the iframe section if you embed anywhere — a CRM, a helpdesk console, a Lightning component. allow="microphone", the parent's Permissions-Policy intersecting rather than adding, every level of nesting needing its own delegation, a legacy Feature-Policy header breaking exactly one browser. That's the integration you'll debug over email with a customer's IT team. Alloqui ships <Dialer /> as a component you mount directly in the host app on your own Twilio or Plivo keys, so permission lives in the top-level document where the default allowlist already grants it. Same carrier account, same per-minute rates, one npm install.