SpeedOf.Me API - Windows Integration

This example demonstrates how to integrate the SpeedOf.Me speed test API into a Windows WPF application using WebView2.

How It Works

The SpeedOf.Me API is JavaScript-based, so Windows apps use WebView2 to:

  1. Load an HTML page containing the speed test
  2. Receive results via WebMessageReceived event

Files

Prerequisites

Quick Start

1. Create a New WPF Project

In Visual Studio: File > New > Project > WPF Application

2. Install WebView2 NuGet Package

Install-Package Microsoft.Web.WebView2

3. Add Files

Copy speedtest.html and the code from SpeedTestWindow.xaml.cs to your project.

4. Create the XAML

Create SpeedTestWindow.xaml with the XAML from the comments in the .cs file. The class is a partial class that calls InitializeComponent() and refers to SpeedTestWebView, StatusText, ResultsPanel, DownloadResult, UploadResult, LatencyResult and JitterResult. Those fields are generated from the XAML, so without this step the project does not compile.

5. Configure API Credentials

SomApi.account = "YOUR_API_KEY";
SomApi.domainName = "your-domain.com";

6. Host speedtest.html on Your Registered Domain

Upload speedtest.html to the domain registered on your account and navigate to that URL:

SpeedTestWebView.CoreWebView2.Navigate("https://your-domain.com/speedtest.html");

It has to be a remote http(s) URL. The engine validates the page that embeds api.js against the domain registered on your account, and it reads that from the page's own URL. A page with no host can never match, so every test fails with error 1002 (Domain Mismatch). That rules out a local file next to the exe (file://) and NavigateToString (which loads as about:blank): neither is a usable loading method for this API, so there is no need to copy the HTML to your output directory.

Architecture

WPF Application
   └── SpeedTestWindow (WPF Window)
           └── WebView2 Control
                   └── speedtest.html
                           └── api.js (defines the SomApi global)
                                   ▼
                   window.chrome.webview.postMessage(json)
                                   ▼
                   CoreWebView2.WebMessageReceived event

JavaScript-to-C# Communication

The HTML page posts the message object, not a JSON string:

window.chrome.webview.postMessage({
    type: 'completed',
    data: result
});

C# receives them:

SpeedTestWebView.CoreWebView2.WebMessageReceived += (sender, e) => {
    string json = e.WebMessageAsJson;
    // Parse and handle
};

Post the object itself. WebView2 then delivers real JSON and WebMessageAsJson deserializes it. If you wrap the payload in JSON.stringify() first, WebMessageAsJson returns a quoted JSON string literal instead of an object, JsonSerializer.Deserialize throws, and every message (ready, started, progress, completed, error) lands in your catch block. Use TryGetWebMessageAsString() if you would rather post a string.

Result Property Names

System.Text.Json with PropertyNameCaseInsensitive maps download to Download, but it does not bridge an underscore. ip_address needs an explicit attribute or it never binds:

using System.Text.Json.Serialization;

[JsonPropertyName("ip_address")]
public string? IpAddress { get; set; }

What the Engine Sends

speedtest.html normalizes the engine's payloads before posting them, so the C# side always deserializes numbers. This is what the raw callbacks give you inside the page.

onProgress(progress):

onTestCompleted(result) always carries every field. A disabled sub-test sends an empty string rather than omitting the key: download, upload, maxDownload, maxUpload, latency, jitter, testServer, ip_address, hostname, userAgent, testDate. upload and maxUpload are '' when uploadTestEnabled is false, latency and jitter are '' when latencyTestEnabled is false, testServer is '' when testServerEnabled is false, and ip_address is '0.0.0.0' with hostname '' when userInfoEnabled is false. Those empty strings are why the page normalizes before posting: a double property cannot deserialize "".

Configuration Options

SomApi.config.sustainTime = 6;           // 1-8 seconds
SomApi.config.testServerEnabled = true;
SomApi.config.userInfoEnabled = true;
SomApi.config.latencyTestEnabled = true;
SomApi.config.uploadTestEnabled = true;
SomApi.config.progress.enabled = true;
SomApi.config.progress.verbose = true;   // required for progress.currentSpeed

Error Codes

1002 almost always means the page is not being served from your registered domain. 2006, and 2005 with status 0, mean the request never left the machine, usually an ad blocker, a content blocker or a frame-src Content Security Policy that does not allow https://speedof.me. A 2005 carrying any other status is a real HTTP response: the request reached the network and came back refused.

WebView2 Runtime

Check availability:

try {
    string version = CoreWebView2Environment.GetAvailableBrowserVersionString();
    // Runtime is available
} catch {
    // Prompt user to install from:
    // https://developer.microsoft.com/microsoft-edge/webview2/
}

WinForms Alternative

using Microsoft.Web.WebView2.WinForms;

public partial class SpeedTestForm : Form {
    private WebView2 webView;

    public SpeedTestForm() {
        InitializeComponent();
        InitializeWebView();
    }

    private async void InitializeWebView() {
        webView = new WebView2();
        webView.Dock = DockStyle.Fill;
        Controls.Add(webView);

        await webView.EnsureCoreWebView2Async();
        webView.CoreWebView2.WebMessageReceived += OnWebMessageReceived;
        webView.CoreWebView2.Navigate("https://your-domain.com/speedtest.html");
    }

    private void OnWebMessageReceived(object sender, CoreWebView2WebMessageReceivedEventArgs e) {
        // Handle message
    }
}

.NET MAUI

MAUI's WebView exposes no host message bridge: it has no WebMessageReceived event and no window.chrome.webview.postMessage target, so the pattern above does not carry over. Reaching the underlying WebView2 takes a platform handler, which is out of scope for this example.

Production Considerations

  1. WebView2 Runtime: Include Evergreen Bootstrapper or Fixed Version in installer
  2. User Data Folder: Specify custom location for WebView2 data
  3. Error Handling: Handle initialization failures gracefully
  4. Updates: WebView2 Evergreen updates automatically with Edge

Troubleshooting

Links