This example demonstrates how to integrate the SpeedOf.Me speed test API with React.
A working example built on CDN builds of React and Babel is linked from this folder's page. Opening the source file directly will not run a test: the API validates the page URL against your registered domain, so serve your copy over http or https from that domain.
The index.html file shows how to use React with CDN builds. This is great for demos but not recommended for production.
npm create vite@latest my-speedtest -- --template react
cd my-speedtest
npm install
index.html:
<script src="https://speedof.me/api/api.js"></script>
src/hooks/useSpeedTest.js):
import { useState, useEffect, useCallback } from 'react';
export function useSpeedTest({ apiKey, domain, sustainTime = 6 }) {
const [status, setStatus] = useState('idle');
const [result, setResult] = useState(null);
const [progress, setProgress] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
window.SomApi.account = apiKey;
window.SomApi.domainName = domain;
window.SomApi.config.sustainTime = sustainTime;
window.SomApi.onTestCompleted = (testResult) => {
setResult(testResult);
setProgress(null);
setStatus('completed');
};
window.SomApi.onProgress = setProgress;
window.SomApi.onError = (err) => {
setError(err);
setStatus('error');
};
}, [apiKey, domain, sustainTime]);
const startTest = useCallback(() => {
setStatus('running');
setResult(null);
setError(null);
window.SomApi.startTest();
}, []);
return { status, result, progress, error, startTest };
}
import { useSpeedTest } from './hooks/useSpeedTest';
function SpeedTest() {
const { status, result, progress, error, startTest } = useSpeedTest({
apiKey: 'YOUR_API_KEY',
domain: 'your-domain.com'
});
return (
<div>
<button onClick={startTest} disabled={status === 'running'}>
{status === 'running' ? 'Testing...' : 'Start Test'}
</button>
{result && (
<div>
<p>Download: {result.download} Mbps</p>
<p>Upload: {result.upload} Mbps</p>
</div>
)}
</div>
);
}
// Required
window.SomApi.account = 'YOUR_API_KEY';
window.SomApi.domainName = 'your-domain.com';
// Optional
window.SomApi.config.sustainTime = 6; // 1-8 seconds (higher = more accurate)
window.SomApi.config.testServerEnabled = true;
window.SomApi.config.userInfoEnabled = true;
window.SomApi.config.latencyTestEnabled = true;
window.SomApi.config.uploadTestEnabled = true;
window.SomApi.config.progress.enabled = true; // fire onProgress during the test
window.SomApi.config.progress.verbose = true; // add currentSpeed and maxSpeed to it
The onTestCompleted callback receives a result object:
{
download: 150.5, // Download speed in Mbps
upload: 25.3, // Upload speed in Mbps ('' if uploadTestEnabled is false)
maxDownload: 162.0, // Peak download speed in Mbps
maxUpload: 27.1, // Peak upload speed in Mbps ('' if uploadTestEnabled is false)
latency: 12, // Latency in ms ('' if latencyTestEnabled is false)
jitter: 3, // Jitter in ms, whole number ('' if latencyTestEnabled is false)
testServer: 'Los Angeles 3', // Test server location ('' if testServerEnabled is false)
ip_address: '...', // Client IP ('0.0.0.0' if userInfoEnabled is false)
hostname: '...', // Client hostname ('' if userInfoEnabled is false)
userAgent: '...', // Browser user agent string
testDate: '...' // Test timestamp, ISO 8601
}
Every field is always present. Disabling a test blanks its fields rather than removing them.
The onProgress callback receives progress data during the test:
{
type: 'download', // 'latency', 'download' or 'upload'
percentDone: 75, // 0-100
pass: 3, // Current test pass ('' during the latency phase)
currentSpeed: 145.2, // Current speed in Mbps, verbose mode only: '' unless
// config.progress.verbose is true, and always ''
// on latency events
maxSpeed: 162.0, // Peak speed in Mbps, same rule as currentSpeed
latency: '', // Latency in ms, only on the final latency event
// (type 'latency' at percentDone 100), '' otherwise
jitter: '' // Jitter in ms, same rule as latency
}
Guard currentSpeed before formatting it, because the latency phase
carries '':
const speed = typeof progress.currentSpeed === 'number'
? `${progress.currentSpeed.toFixed(1)} Mbps`
: '';
The onError callback receives an error object:
{
code: 1001,
message: 'User data error: Invalid Account'
}
Error codes:
1001: Invalid Account, API key not found or inactive1002: Domain Mismatch, the page domain does not match your registered domain2001: Test Error2002: Invalid server response2003: Request timeout2004: Test timeout, the test did not finish within config.testTimeout2005: Speed test could not start (status N)2006: Speed test engine did not loadCode 2006, and 2005 with status 0, mean the request never left the browser,
which is almost always an ad blocker, a content blocker or a privacy extension on the
visitor's device. The one thing to check on your own side is a frame-src
Content-Security-Policy: it must allow https://speedof.me, or the engine
iframe can never load. A 2005 carrying any other status is a real HTTP response, so treat
it as a server-side failure.
For TypeScript projects, declare the SomApi global in src/types/speedofme.d.ts:
// Every field is always present. A disabled sub-test sends an empty string
// rather than omitting the key, which is what the unions below encode.
interface SpeedTestResult {
download: number;
upload: number | ''; // '' when uploadTestEnabled is false
maxDownload: number;
maxUpload: number | ''; // '' when uploadTestEnabled is false
latency: number | ''; // '' when latencyTestEnabled is false
jitter: number | ''; // '' when latencyTestEnabled is false
testServer: string; // '' when testServerEnabled is false
ip_address: string; // '0.0.0.0' when userInfoEnabled is false
hostname: string; // '' when userInfoEnabled is false
userAgent: string;
testDate: string; // ISO 8601
}
interface SpeedTestProgress {
type: 'download' | 'upload' | 'latency';
pass: number | ''; // '' during the latency phase
percentDone: number; // 0-100
currentSpeed: number | ''; // Mbps, verbose only, always '' on latency events
maxSpeed: number | ''; // Mbps, verbose only, '' until the max exceeds 0
latency: number | ''; // ms, on the final latency event only
jitter: number | ''; // ms, on the final latency event only
}
interface SpeedTestError {
code: number;
message: string;
}
interface SomApiConfig {
sustainTime: number;
maxTestPass: number;
testTimeout: number;
testServerEnabled: boolean;
userInfoEnabled: boolean;
latencyTestEnabled: boolean;
uploadTestEnabled: boolean;
progress: { enabled: boolean; verbose: boolean };
}
interface SomApi {
account: string;
domainName: string;
config: SomApiConfig;
startTest: () => void;
abortTest: () => void;
onTestCompleted: (result: SpeedTestResult) => void;
onProgress: (progress: SpeedTestProgress) => void;
onError: (error: SpeedTestError) => void;
}
declare global {
interface Window {
SomApi: SomApi;
}
}
// Required: `declare global` is only legal in a module, so a file with no
// other top-level import or export needs this line (TS2669).
export {};