MetaMask is the default on-ramp for Ethereum for tens of millions of users. If you run a dApp, the MetaMask API is the key layer between your UI and user signing keys. The "MetaMask API" comprises two parts: the injected window.ethereum provider (as defined by EIP-1193), and the MetaMask SDK, which extends that same interface to mobile, React Native, and Node.js backends. Mastering the provider means you're 80% of the way to integrating with almost any wallet on the web.
This guide walks you through detecting the provider, requesting accounts, reading the current chain, signing messages (with both personal_sign and EIP-712), sending transactions, switching or adding chains, and using the MetaMask SDK outside browser extensions. We'll also cover how ethers.js v6 and viem fit in as wrappers, and how Apidog helps you test JSON-RPC calls without building throwaway frontend code.
If you're building anything wallet-related, bookmark this and also check our guide on the best crypto wallet API for a broader view.
TL;DR
- The MetaMask API is the EIP-1193 provider at
window.ethereumin browsers, plus the MetaMask SDK for mobile and Node. - Start with
eth_requestAccountsto prompt the user to connect, then listen onaccountsChangedandchainChanged. - Sign messages with
personal_sign, and structured data witheth_signTypedData_v4(EIP-712). - Switch networks with
wallet_switchEthereumChain(EIP-3326) and add new ones withwallet_addEthereumChain(EIP-3085). - Use higher-level libraries (ethers.js v6, viem, wagmi) to wrap the provider; Snaps extend MetaMask itself.
- Use Apidog to test JSON-RPC endpoints, mock responses, and debug signatures before deploying.
What is the MetaMask API?
The MetaMask API is the interface that MetaMask exposes to web pages and apps for Ethereum and EVM-compatible chains. In browsers, the extension injects a provider object at window.ethereum, conforming to EIP-1193. Any dApp using this standard works with MetaMask, Coinbase Wallet, Rabby, Frame, etc., with no code changes.
For non-browser environments, the MetaMask SDK provides the same provider interface in React Native, Node.js, Electron, and even server-side scripts. The SDK manages deep-linking/QR flows so that a mobile MetaMask wallet can sign requests from desktop or backend processes. Your app code barely changes—the SDK still speaks EIP-1193.
MetaMask also ships Snaps, a plugin system to extend the wallet with new chains, RPC methods, and account types. Snaps are out of scope for this post but are useful if you need non-EVM chains or custom signing inside MetaMask.
Authentication and Setup
There are no API keys for the provider. Authentication happens as users approve requests in their wallet UI. You need to:
- Detect the provider
- Listen for changes
To detect MetaMask, use the @metamask/detect-provider helper to handle edge cases (like multiple wallets), or check directly:
// Vanilla JS detection
import detectEthereumProvider from '@metamask/detect-provider';
const provider = await detectEthereumProvider({ mustBeMetaMask: true });
if (!provider) {
alert('Please install MetaMask');
} else {
console.log('MetaMask detected');
}
Set up event listeners before making requests. Missing the accountsChanged event is a common bug:
window.ethereum.on('accountsChanged', (accounts) => {
if (accounts.length === 0) {
console.log('User disconnected');
} else {
console.log('Active account:', accounts[0]);
}
});
window.ethereum.on('chainChanged', (chainId) => {
// Best practice: reload the page on chain change
window.location.reload();
});
For React apps, wagmi handles all this and auto-detects MetaMask via its injected connector.
Core Endpoints
All provider calls go through window.ethereum.request({ method, params }). The method is a JSON-RPC string, and params is an array/object per method. Here are the most common calls:
Request Accounts and Read Chain
// Prompt the user to connect
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts',
});
const account = accounts[0];
// Read the current chain
const chainId = await window.ethereum.request({
method: 'eth_chainId',
});
console.log(account, chainId); // e.g., '0x...' '0x1' (Ethereum mainnet)
To call the raw JSON-RPC endpoint (e.g., Infura), use curl:
curl https://mainnet.infura.io/v3/YOUR_KEY \
-X POST \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}'
For read-only calls, you don't need MetaMask—use node providers like Alchemy or Infura. See our Alchemy API guide for details.
Sign a Simple Message
personal_sign is the standard for human-readable signatures:
const message = 'Sign in to Apidog at ' + new Date().toISOString();
const signature = await window.ethereum.request({
method: 'personal_sign',
params: [message, account],
});
Sign Structured Data with EIP-712
For complex cases (e.g., permits, login challenges), use eth_signTypedData_v4:
const typedData = {
domain: { name: 'Apidog Demo', version: '1', chainId: 1 },
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
],
Login: [
{ name: 'wallet', type: 'address' },
{ name: 'nonce', type: 'uint256' },
],
},
primaryType: 'Login',
message: { wallet: account, nonce: 42 },
};
const sig = await window.ethereum.request({
method: 'eth_signTypedData_v4',
params: [account, JSON.stringify(typedData)],
});
Send a Transaction
Use eth_sendTransaction. MetaMask manages gas estimation and nonces:
const txHash = await window.ethereum.request({
method: 'eth_sendTransaction',
params: [{
from: account,
to: '0xRecipientAddressHere',
value: '0x38d7ea4c68000', // 0.001 ETH in wei (hex)
}],
});
Switch or Add a Chain
EIP-3326 and EIP-3085 handle switching/adding chains:
// Switch to Polygon (chainId 137 = 0x89)
try {
await window.ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: '0x89' }],
});
} catch (err) {
if (err.code === 4902) {
// Chain not added yet
await window.ethereum.request({
method: 'wallet_addEthereumChain',
params: [{
chainId: '0x89',
chainName: 'Polygon',
rpcUrls: ['https://polygon-rpc.com'],
nativeCurrency: { name: 'MATIC', symbol: 'MATIC', decimals: 18 },
}],
});
}
}
React with the MetaMask SDK
The SDK enables a unified integration path for extension, mobile deep-link, and in-app browser:
import { MetaMaskProvider, useSDK } from '@metamask/sdk-react';
function Connect() {
const { sdk, connected, account } = useSDK();
return (
<button onClick={() => sdk?.connect()}>
{connected ? account : 'Connect MetaMask'}
</button>
);
}
export default function App() {
return (
<MetaMaskProvider sdkOptions={{ dappMetadata: { name: 'My dApp' } }}>
<Connect />
</MetaMaskProvider>
);
}
For production, wrap the provider in ethers.js v6 or viem for typed contracts, ABI decoding, and better error handling. If you need email/social login as fallback, pair MetaMask with an embedded wallet—see our Privy API guide.
Common Errors and Rate Limits
MetaMask uses standard JSON-RPC error codes:
-
4001: User rejected the request (show a "connection cancelled" toast, don't auto-retry). -
4100: Unauthorized (account not connected—calleth_requestAccounts). -
4200: Unsupported method (ensure the wallet supports the method). -
4902: Chain not added (follow up withwallet_addEthereumChain). -
-32002: Request already pending (debounce your connect button).
The provider itself has no rate limits, but the underlying RPC endpoint may (e.g., Infura/Alchemy tiers). For fiat flows, pair with a fiat on-ramp/off-ramp API so users can top up in-dApp.
MetaMask API Pricing
The MetaMask extension and SDK are free—no per-connection/signature/transaction fees. MetaMask earns through swap fees and the MetaMask Card, not from dApp developers.
Your costs come from the RPC endpoint your dApp uses for reads. Free Alchemy or Infura tiers cover small apps; production dApps typically pay $49–$299/mo for dedicated throughput.
Testing the MetaMask API with Apidog
Browser-based signing flows are tough to debug since requests cross extension, page, and sometimes mobile. Apidog streamlines this: hit your dApp's JSON-RPC endpoints, confirm eth_chainId/eth_getBalance, and save the flows as test suites.
- Import the Ethereum JSON-RPC spec
- Set your node URL as an environment variable
- Get a reusable collection for every EVM chain
Apidog can mock responses—frontend developers can use a fake eth_sendTransaction while contracts are in audit. For CI, run collections from the command line and break the build on contract/API changes. If you've struggled with unsynced Postman collections, check our guide on API testing without Postman in 2026 for why Apidog works better for dApp teams.
Download Apidog to get started.
FAQ
Does the MetaMask API work on mobile?
Yes. Use the MetaMask SDK—it deep-links to the mobile app for signing. The provider interface is identical to the browser, so your code stays the same. For comparisons with other mobile wallet SDKs, see our best crypto wallet API roundup.
What is the difference between eth_sign, personal_sign, and eth_signTypedData_v4?
-
eth_signsigns raw bytes (dangerous—MetaMask warns users). -
personal_signprefixes a human-readable message. -
eth_signTypedData_v4signs structured EIP-712 data and displays fields in the MetaMask UI. Prefer the last two; avoideth_sign.
Do I need a separate API key from MetaMask?
No. The provider is free and keyless. For reads outside the wallet, use an RPC provider (e.g., Alchemy/Infura) with their own keys.
Can I use ethers.js or viem with MetaMask?
Yes. Both wrap the window.ethereum provider. Ethers v6 uses BrowserProvider(window.ethereum), viem uses createWalletClient({ transport: custom(window.ethereum) }). Most dApps use one of these.
What happens if a user has multiple wallets installed?
MetaMask implements EIP-6963, so dApps can detect all installed wallets (no more fighting over window.ethereum). Wagmi and RainbowKit handle this automatically.
Is MetaMask Snaps ready for production?
Yes. Snaps went GA in 2024. Most production uses are for non-EVM chain support, custom transaction insights, and hardware wallet integrations.
Top comments (0)