Master eToro API authentication — API key management, secure storage, token refresh patterns, and troubleshooting common auth errors.
Part of: Build a Trading Bot — Step 2 of 4
Every request to the eToro API must be authenticated. This guide covers the authentication model in depth — how keys work, how to store them safely, how to handle token expiration, and what to do when things go wrong.
If you haven't set up API access yet, start with the Getting Started guide first.
The eToro API uses a two-key system:
| Key | Header | Purpose |
|---|---|---|
| Public API Key | x-api-key |
Identifies your application |
| User Key | x-user-key |
Identifies the acting user |
Both headers are required on every request. Unlike OAuth token flows where tokens expire frequently, eToro API keys are long-lived credentials tied to your account.
const headers = {
"x-api-key": process.env.ETORO_API_KEY,
"x-user-key": process.env.ETORO_USER_KEY,
"Content-Type": "application/json",
};
const response = await fetch("https://public-api.etoro.com/api/v1/market-data/instruments", {
headers,
});
Warning: Treat your User Key like a password. Anyone with both keys can execute trades on your behalf.
Never hardcode API keys in your source code. Use environment variables or a secrets manager.
# .env file (add to .gitignore!)
ETORO_API_KEY=your_public_api_key_here
ETORO_USER_KEY=your_user_key_here
ETORO_ENVIRONMENT=demo
import "dotenv/config";
const config = {
apiKey: process.env.ETORO_API_KEY,
userKey: process.env.ETORO_USER_KEY,
baseUrl: "https://public-api.etoro.com/api/v1",
executionPrefix:
process.env.ETORO_ENVIRONMENT === "demo"
? "trading/execution/demo"
: "trading/execution",
};
if (!config.apiKey || !config.userKey) {
throw new Error("Missing API credentials. Check your .env file.");
}
For production deployments, use a secrets manager instead of .env files:
// AWS Secrets Manager example
import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager";
async function getCredentials() {
const client = new SecretsManagerClient({ region: "us-east-1" });
const response = await client.send(
new GetSecretValueCommand({ SecretId: "etoro-api-keys" })
);
return JSON.parse(response.SecretString);
}
Wrap authentication logic in a client class to avoid repeating headers:
class EtoroClient {
constructor({ apiKey, userKey, environment = "demo" }) {
this.baseUrl = "https://public-api.etoro.com/api/v1";
this.headers = {
"x-api-key": apiKey,
"x-user-key": userKey,
"Content-Type": "application/json",
};
}
async request(endpoint, options = {}) {
const url = `${this.baseUrl}${endpoint}`;
const response = await fetch(url, {
...options,
headers: { ...this.headers, ...options.headers },
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new ApiError(response.status, error.message || response.statusText);
}
return response.json();
}
async getInstruments(params = {}) {
const query = new URLSearchParams(params).toString();
return this.request(`/market-data/instruments${query ? `?${query}` : ""}`);
}
async getPortfolio() {
return this.request("/trading/info/real/portfolio");
}
}
class ApiError extends Error {
constructor(status, message) {
super(`API Error ${status}: ${message}`);
this.status = status;
}
}
Usage:
const client = new EtoroClient({
apiKey: process.env.ETORO_API_KEY,
userKey: process.env.ETORO_USER_KEY,
});
const instruments = await client.getInstruments({ type: "stock" });
console.log(`Found ${instruments.length} stocks`);
The eToro API enforces rate limits. When exceeded, the API returns 429 Too Many Requests with a Retry-After header. See the Rate Limits documentation for current limits and the Rate Limits & 429 Handling Playbook for implementation patterns.
Implement exponential backoff to handle rate limits gracefully:
async function requestWithRetry(fn, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (error.status === 429 && attempt < maxRetries) {
const delay = Math.pow(2, attempt) * 1000 + Math.random() * 500;
console.log(`Rate limited. Retrying in ${Math.round(delay)}ms...`);
await new Promise((r) => setTimeout(r, delay));
continue;
}
throw error;
}
}
}
// Usage
const data = await requestWithRetry(() => client.getInstruments());
| Status | Error | Cause | Fix |
|---|---|---|---|
401 |
Unauthorized | Missing or invalid API key | Verify x-api-key header is set correctly |
401 |
Invalid user key | Wrong or expired user key | Regenerate user key at api-portal.etoro.com |
403 |
Forbidden | Key lacks permission for this endpoint | Check your API key scopes |
403 |
KYC required | Real trading requires identity verification | Complete KYC on etoro.com |
429 |
Too Many Requests | Rate limit exceeded | Implement backoff (see above) |
async function debugAuth(apiKey, userKey) {
const response = await fetch(
"https://public-api.etoro.com/api/v1/market-data/instruments?limit=1",
{
headers: {
"x-api-key": apiKey,
"x-user-key": userKey,
},
}
);
console.log("Status:", response.status);
console.log("Headers:", Object.fromEntries(response.headers));
if (!response.ok) {
const body = await response.text();
console.log("Error body:", body);
} else {
console.log("Authentication successful!");
}
}
Was this helpful?
A step-by-step guide to building a simple trading bot using the eToro Demo Trading API.
A practical guide for transitioning your trading bot from eToro's demo sandbox to the real trading API — safety checks, key differences, and best practices.