diff --git a/src/citra/citra.cpp b/src/citra/citra.cpp
index 14574e56c11ca66e8fc9c40b3fd2d73e4cf7a548..e524c55356189f510f2fc553c42fb042387abebd 100644
--- a/src/citra/citra.cpp
+++ b/src/citra/citra.cpp
@@ -165,6 +165,8 @@ int main(int argc, char** argv) {
         break; // Expected case
     }
 
+    Core::Telemetry().AddField(Telemetry::FieldType::App, "Frontend", "SDL");
+
     while (emu_window->IsOpen()) {
         system.RunLoop();
     }
diff --git a/src/citra/config.cpp b/src/citra/config.cpp
index 73846ed915846ecdb01fa4c29cd7b8fa1f97fb9a..3869b6b5d5b5c4bebd453ca45be6dedfc468ca8d 100644
--- a/src/citra/config.cpp
+++ b/src/citra/config.cpp
@@ -156,8 +156,12 @@ void Config::ReadValues() {
         static_cast<u16>(sdl2_config->GetInteger("Debugging", "gdbstub_port", 24689));
 
     // Web Service
+    Settings::values.enable_telemetry =
+        sdl2_config->GetBoolean("WebService", "enable_telemetry", true);
     Settings::values.telemetry_endpoint_url = sdl2_config->Get(
         "WebService", "telemetry_endpoint_url", "https://services.citra-emu.org/api/telemetry");
+    Settings::values.citra_username = sdl2_config->Get("WebService", "citra_username", "");
+    Settings::values.citra_token = sdl2_config->Get("WebService", "citra_token", "");
 }
 
 void Config::Reload() {
diff --git a/src/citra/default_ini.h b/src/citra/default_ini.h
index 9ea779dd8470eea5f70b14f71ad6529f3f34cc03..ea02a788d7ef1734768703a7ef51070e55304aaa 100644
--- a/src/citra/default_ini.h
+++ b/src/citra/default_ini.h
@@ -176,7 +176,14 @@ use_gdbstub=false
 gdbstub_port=24689
 
 [WebService]
+# Whether or not to enable telemetry
+# 0: No, 1 (default): Yes
+enable_telemetry =
 # Endpoint URL for submitting telemetry data
-telemetry_endpoint_url =
+telemetry_endpoint_url = https://services.citra-emu.org/api/telemetry
+# Username and token for Citra Web Service
+# See https://services.citra-emu.org/ for more info
+citra_username =
+citra_token =
 )";
 }
diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt
index f364b22843eca11b1702cb3d449784b7790cfa49..e0a19fd9e7198990e4d9d47abc0e4b2c2e97c419 100644
--- a/src/citra_qt/CMakeLists.txt
+++ b/src/citra_qt/CMakeLists.txt
@@ -12,6 +12,7 @@ set(SRCS
             configuration/configure_graphics.cpp
             configuration/configure_input.cpp
             configuration/configure_system.cpp
+            configuration/configure_web.cpp
             debugger/graphics/graphics.cpp
             debugger/graphics/graphics_breakpoint_observer.cpp
             debugger/graphics/graphics_breakpoints.cpp
@@ -42,6 +43,7 @@ set(HEADERS
             configuration/configure_graphics.h
             configuration/configure_input.h
             configuration/configure_system.h
+            configuration/configure_web.h
             debugger/graphics/graphics.h
             debugger/graphics/graphics_breakpoint_observer.h
             debugger/graphics/graphics_breakpoints.h
@@ -71,6 +73,7 @@ set(UIS
             configuration/configure_graphics.ui
             configuration/configure_input.ui
             configuration/configure_system.ui
+            configuration/configure_web.ui
             debugger/registers.ui
             hotkeys.ui
             main.ui
diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp
index 6e42db00725689306a7546b5b00c4d067f0669c2..e2dceaa4c62a31a7ece753db9d6fca2eaab919a2 100644
--- a/src/citra_qt/configuration/config.cpp
+++ b/src/citra_qt/configuration/config.cpp
@@ -139,10 +139,13 @@ void Config::ReadValues() {
     qt_config->endGroup();
 
     qt_config->beginGroup("WebService");
+    Settings::values.enable_telemetry = qt_config->value("enable_telemetry", true).toBool();
     Settings::values.telemetry_endpoint_url =
         qt_config->value("telemetry_endpoint_url", "https://services.citra-emu.org/api/telemetry")
             .toString()
             .toStdString();
+    Settings::values.citra_username = qt_config->value("citra_username").toString().toStdString();
+    Settings::values.citra_token = qt_config->value("citra_token").toString().toStdString();
     qt_config->endGroup();
 
     qt_config->beginGroup("UI");
@@ -194,6 +197,7 @@ void Config::ReadValues() {
     UISettings::values.show_status_bar = qt_config->value("showStatusBar", true).toBool();
     UISettings::values.confirm_before_closing = qt_config->value("confirmClose", true).toBool();
     UISettings::values.first_start = qt_config->value("firstStart", true).toBool();
+    UISettings::values.callout_flags = qt_config->value("calloutFlags", 0).toUInt();
 
     qt_config->endGroup();
 }
@@ -283,8 +287,11 @@ void Config::SaveValues() {
     qt_config->endGroup();
 
     qt_config->beginGroup("WebService");
+    qt_config->setValue("enable_telemetry", Settings::values.enable_telemetry);
     qt_config->setValue("telemetry_endpoint_url",
                         QString::fromStdString(Settings::values.telemetry_endpoint_url));
+    qt_config->setValue("citra_username", QString::fromStdString(Settings::values.citra_username));
+    qt_config->setValue("citra_token", QString::fromStdString(Settings::values.citra_token));
     qt_config->endGroup();
 
     qt_config->beginGroup("UI");
@@ -320,6 +327,7 @@ void Config::SaveValues() {
     qt_config->setValue("showStatusBar", UISettings::values.show_status_bar);
     qt_config->setValue("confirmClose", UISettings::values.confirm_before_closing);
     qt_config->setValue("firstStart", UISettings::values.first_start);
+    qt_config->setValue("calloutFlags", UISettings::values.callout_flags);
 
     qt_config->endGroup();
 }
diff --git a/src/citra_qt/configuration/configure.ui b/src/citra_qt/configuration/configure.ui
index 85e206e42798555064eebabb85f5a888dc9a3215..6abd1917eaca064aed1dc4248970d4ade560bdd4 100644
--- a/src/citra_qt/configuration/configure.ui
+++ b/src/citra_qt/configuration/configure.ui
@@ -6,8 +6,8 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>441</width>
-    <height>501</height>
+    <width>740</width>
+    <height>500</height>
    </rect>
   </property>
   <property name="windowTitle">
@@ -49,6 +49,11 @@
        <string>Debug</string>
       </attribute>
      </widget>
+     <widget class="ConfigureWeb" name="webTab">
+      <attribute name="title">
+       <string>Web</string>
+      </attribute>
+     </widget>
     </widget>
    </item>
    <item>
@@ -97,6 +102,12 @@
    <header>configuration/configure_graphics.h</header>
    <container>1</container>
   </customwidget>
+  <customwidget>
+   <class>ConfigureWeb</class>
+   <extends>QWidget</extends>
+   <header>configuration/configure_web.h</header>
+   <container>1</container>
+  </customwidget>
  </customwidgets>
  <resources/>
  <connections>
diff --git a/src/citra_qt/configuration/configure_dialog.cpp b/src/citra_qt/configuration/configure_dialog.cpp
index dfc8c03a7ac54f9edaa72e68bab739737140c7ea..b87dc0e6c020cc4cbd28f618b02a13fa4ddf6e6e 100644
--- a/src/citra_qt/configuration/configure_dialog.cpp
+++ b/src/citra_qt/configuration/configure_dialog.cpp
@@ -23,5 +23,6 @@ void ConfigureDialog::applyConfiguration() {
     ui->graphicsTab->applyConfiguration();
     ui->audioTab->applyConfiguration();
     ui->debugTab->applyConfiguration();
+    ui->webTab->applyConfiguration();
     Settings::Apply();
 }
diff --git a/src/citra_qt/configuration/configure_web.cpp b/src/citra_qt/configuration/configure_web.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..8715fb0181eb16814aacffcdbf032babf05462c8
--- /dev/null
+++ b/src/citra_qt/configuration/configure_web.cpp
@@ -0,0 +1,52 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include "citra_qt/configuration/configure_web.h"
+#include "core/settings.h"
+#include "core/telemetry_session.h"
+#include "ui_configure_web.h"
+
+ConfigureWeb::ConfigureWeb(QWidget* parent)
+    : QWidget(parent), ui(std::make_unique<Ui::ConfigureWeb>()) {
+    ui->setupUi(this);
+    connect(ui->button_regenerate_telemetry_id, &QPushButton::clicked, this,
+            &ConfigureWeb::refreshTelemetryID);
+
+    this->setConfiguration();
+}
+
+ConfigureWeb::~ConfigureWeb() {}
+
+void ConfigureWeb::setConfiguration() {
+    ui->web_credentials_disclaimer->setWordWrap(true);
+    ui->telemetry_learn_more->setOpenExternalLinks(true);
+    ui->telemetry_learn_more->setText("<a "
+                                      "href='https://citra-emu.org/entry/"
+                                      "telemetry-and-why-thats-a-good-thing/'>Learn more</a>");
+
+    ui->web_signup_link->setOpenExternalLinks(true);
+    ui->web_signup_link->setText("<a href='https://services.citra-emu.org/'>Sign up</a>");
+    ui->web_token_info_link->setOpenExternalLinks(true);
+    ui->web_token_info_link->setText(
+        "<a href='https://citra-emu.org/wiki/citra-web-service/'>What is my token?</a>");
+
+    ui->toggle_telemetry->setChecked(Settings::values.enable_telemetry);
+    ui->edit_username->setText(QString::fromStdString(Settings::values.citra_username));
+    ui->edit_token->setText(QString::fromStdString(Settings::values.citra_token));
+    ui->label_telemetry_id->setText("Telemetry ID: 0x" +
+                                    QString::number(Core::GetTelemetryId(), 16).toUpper());
+}
+
+void ConfigureWeb::applyConfiguration() {
+    Settings::values.enable_telemetry = ui->toggle_telemetry->isChecked();
+    Settings::values.citra_username = ui->edit_username->text().toStdString();
+    Settings::values.citra_token = ui->edit_token->text().toStdString();
+    Settings::Apply();
+}
+
+void ConfigureWeb::refreshTelemetryID() {
+    const u64 new_telemetry_id{Core::RegenerateTelemetryId()};
+    ui->label_telemetry_id->setText("Telemetry ID: 0x" +
+                                    QString::number(new_telemetry_id, 16).toUpper());
+}
diff --git a/src/citra_qt/configuration/configure_web.h b/src/citra_qt/configuration/configure_web.h
new file mode 100644
index 0000000000000000000000000000000000000000..20bc254b909e809d82ec856a9bbe9582052b5aea
--- /dev/null
+++ b/src/citra_qt/configuration/configure_web.h
@@ -0,0 +1,30 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <memory>
+#include <QWidget>
+
+namespace Ui {
+class ConfigureWeb;
+}
+
+class ConfigureWeb : public QWidget {
+    Q_OBJECT
+
+public:
+    explicit ConfigureWeb(QWidget* parent = nullptr);
+    ~ConfigureWeb();
+
+    void applyConfiguration();
+
+public slots:
+    void refreshTelemetryID();
+
+private:
+    void setConfiguration();
+
+    std::unique_ptr<Ui::ConfigureWeb> ui;
+};
diff --git a/src/citra_qt/configuration/configure_web.ui b/src/citra_qt/configuration/configure_web.ui
new file mode 100644
index 0000000000000000000000000000000000000000..d8d283fad4b1ef51f9de225b7d1b4e4877870337
--- /dev/null
+++ b/src/citra_qt/configuration/configure_web.ui
@@ -0,0 +1,153 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ConfigureWeb</class>
+ <widget class="QWidget" name="ConfigureWeb">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>400</width>
+    <height>300</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Form</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout">
+   <item>
+    <layout class="QVBoxLayout" name="verticalLayout_3">
+     <item>
+      <widget class="QGroupBox" name="groupBoxWebConfig">
+       <property name="title">
+        <string>Citra Web Service</string>
+       </property>
+       <layout class="QVBoxLayout" name="verticalLayoutCitraWebService">
+        <item>
+         <widget class="QLabel" name="web_credentials_disclaimer">
+          <property name="text">
+           <string>By providing your username and token, you agree to allow Citra to collect additional usage data, which may include user identifying information.</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <layout class="QGridLayout" name="gridLayoutCitraUsername">
+          <item row="0" column="0">
+           <widget class="QLabel" name="label_username">
+            <property name="text">
+             <string>Username: </string>
+            </property>
+           </widget>
+          </item>
+          <item row="0" column="1">
+           <widget class="QLineEdit" name="edit_username">
+            <property name="maxLength">
+             <number>36</number>
+            </property>
+           </widget>
+          </item>
+          <item row="1" column="0">
+           <widget class="QLabel" name="label_token">
+            <property name="text">
+             <string>Token: </string>
+            </property>
+           </widget>
+          </item>
+          <item row="1" column="1">
+           <widget class="QLineEdit" name="edit_token">
+            <property name="maxLength">
+             <number>36</number>
+            </property>
+            <property name="echoMode">
+             <enum>QLineEdit::Password</enum>
+            </property>
+           </widget>
+          </item>
+          <item row="2" column="0">
+           <widget class="QLabel" name="web_signup_link">
+            <property name="text">
+             <string>Sign up</string>
+            </property>
+           </widget>
+          </item>
+          <item row="2" column="1">
+           <widget class="QLabel" name="web_token_info_link">
+            <property name="text">
+             <string>What is my token?</string>
+            </property>
+           </widget>
+          </item>
+         </layout>
+        </item>
+       </layout>
+      </widget>
+     </item>
+     <item>
+      <widget class="QGroupBox" name="groupBox">
+       <property name="title">
+        <string>Telemetry</string>
+       </property>
+       <layout class="QVBoxLayout" name="verticalLayout_2">
+        <item>
+         <widget class="QCheckBox" name="toggle_telemetry">
+          <property name="text">
+           <string>Share anonymous usage data with the Citra team</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QLabel" name="telemetry_learn_more">
+          <property name="text">
+           <string>Learn more</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <layout class="QGridLayout" name="gridLayoutTelemetryId">
+          <item row="0" column="0">
+           <widget class="QLabel" name="label_telemetry_id">
+             <property name="text">
+              <string>Telemetry ID:</string>
+             </property>
+           </widget>
+          </item>
+          <item row="0" column="1">
+           <widget class="QPushButton" name="button_regenerate_telemetry_id">
+            <property name="sizePolicy">
+             <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+               <horstretch>0</horstretch>
+               <verstretch>0</verstretch>
+             </sizepolicy>
+            </property>
+            <property name="layoutDirection">
+             <enum>Qt::RightToLeft</enum>
+            </property>
+            <property name="text">
+             <string>Regenerate</string>
+            </property>
+           </widget>
+          </item>
+         </layout>
+        </item>
+       </layout>
+      </widget>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <spacer name="verticalSpacer">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>20</width>
+       <height>40</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp
index c1ae0ccc898e176531a096e02b5fc3aabed4b978..8adbcfe865b9a8ff7fae0d79d62a6ca472aab047 100644
--- a/src/citra_qt/main.cpp
+++ b/src/citra_qt/main.cpp
@@ -48,6 +48,47 @@
 Q_IMPORT_PLUGIN(QWindowsIntegrationPlugin);
 #endif
 
+/**
+ * "Callouts" are one-time instructional messages shown to the user. In the config settings, there
+ * is a bitfield "callout_flags" options, used to track if a message has already been shown to the
+ * user. This is 32-bits - if we have more than 32 callouts, we should retire and recyle old ones.
+ */
+enum class CalloutFlag : uint32_t {
+    Telemetry = 0x1,
+};
+
+static void ShowCalloutMessage(const QString& message, CalloutFlag flag) {
+    if (UISettings::values.callout_flags & static_cast<uint32_t>(flag)) {
+        return;
+    }
+
+    UISettings::values.callout_flags |= static_cast<uint32_t>(flag);
+
+    QMessageBox msg;
+    msg.setText(message);
+    msg.setStandardButtons(QMessageBox::Ok);
+    msg.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+    msg.setStyleSheet("QLabel{min-width: 900px;}");
+    msg.exec();
+}
+
+void GMainWindow::ShowCallouts() {
+    static const QString telemetry_message =
+        tr("To help improve Citra, the Citra Team collects anonymous usage data. No private or "
+           "personally identifying information is collected. This data helps us to understand how "
+           "people use Citra and prioritize our efforts. Furthermore, it helps us to more easily "
+           "identify emulation bugs and performance issues. This data includes:<ul><li>Information"
+           " about the version of Citra you are using</li><li>Performance data about the games you "
+           "play</li><li>Your configuration settings</li><li>Information about your computer "
+           "hardware</li><li>Emulation errors and crash information</li></ul>By default, this "
+           "feature is enabled. To disable this feature, click 'Emulation' from the menu and then "
+           "select 'Configure...'. Then, on the 'Web' tab, uncheck 'Share anonymous usage data with"
+           " the Citra team'. <br/><br/>By using this software, you agree to the above terms.<br/>"
+           "<br/><a href='https://citra-emu.org/entry/telemetry-and-why-thats-a-good-thing/'>Learn "
+           "more</a>");
+    ShowCalloutMessage(telemetry_message, CalloutFlag::Telemetry);
+}
+
 GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) {
     Pica::g_debug_context = Pica::DebugContext::Construct();
     setAcceptDrops(true);
@@ -73,6 +114,9 @@ GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) {
 
     UpdateUITheme();
 
+    // Show one-time "callout" messages to the user
+    ShowCallouts();
+
     QStringList args = QApplication::arguments();
     if (args.length() >= 2) {
         BootGame(args[1]);
@@ -320,6 +364,8 @@ bool GMainWindow::LoadROM(const QString& filename) {
 
     const Core::System::ResultStatus result{system.Load(render_window, filename.toStdString())};
 
+    Core::Telemetry().AddField(Telemetry::FieldType::App, "Frontend", "Qt");
+
     if (result != Core::System::ResultStatus::Success) {
         switch (result) {
         case Core::System::ResultStatus::ErrorGetLoader:
diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h
index 360de2ced29cf4a1f110cefa556e5d035103f98b..d59a6d67d381c864c29ffb41b6fa81c5b61ca160 100644
--- a/src/citra_qt/main.h
+++ b/src/citra_qt/main.h
@@ -80,6 +80,8 @@ private:
     void BootGame(const QString& filename);
     void ShutdownGame();
 
+    void ShowCallouts();
+
     /**
      * Stores the filename in the recently loaded files list.
      * The new filename is stored at the beginning of the recently loaded files list.
diff --git a/src/citra_qt/ui_settings.h b/src/citra_qt/ui_settings.h
index 025c73f84b31fd5d9e8c97d877163f424a2555cb..d85c92765f6831078b28618c6525a4ad4f4cbb2d 100644
--- a/src/citra_qt/ui_settings.h
+++ b/src/citra_qt/ui_settings.h
@@ -48,6 +48,8 @@ struct Values {
 
     // Shortcut name <Shortcut, context>
     std::vector<Shortcut> shortcuts;
+
+    uint32_t callout_flags;
 };
 
 extern Values values;
diff --git a/src/core/settings.h b/src/core/settings.h
index ca657719aa09f448730bc29bc72ed883399e50b5..bf8014c5a752e1d88bc835517e8740db99e484f0 100644
--- a/src/core/settings.h
+++ b/src/core/settings.h
@@ -130,7 +130,10 @@ struct Values {
     u16 gdbstub_port;
 
     // WebService
+    bool enable_telemetry;
     std::string telemetry_endpoint_url;
+    std::string citra_username;
+    std::string citra_token;
 } extern values;
 
 // a special value for Values::region_value indicating that citra will automatically select a region
diff --git a/src/core/telemetry_session.cpp b/src/core/telemetry_session.cpp
index 94483f385295cb20a9e69cfb219b302fd2b84a61..104a16cc9e0c869003899d1028684cb653111ac3 100644
--- a/src/core/telemetry_session.cpp
+++ b/src/core/telemetry_session.cpp
@@ -3,8 +3,10 @@
 // Refer to the license.txt file included.
 
 #include <cstring>
+#include <cryptopp/osrng.h>
 
 #include "common/assert.h"
+#include "common/file_util.h"
 #include "common/scm_rev.h"
 #include "common/x64/cpu_detect.h"
 #include "core/core.h"
@@ -29,12 +31,65 @@ static const char* CpuVendorToStr(Common::CPUVendor vendor) {
     UNREACHABLE();
 }
 
+static u64 GenerateTelemetryId() {
+    u64 telemetry_id{};
+    CryptoPP::AutoSeededRandomPool rng;
+    rng.GenerateBlock(reinterpret_cast<CryptoPP::byte*>(&telemetry_id), sizeof(u64));
+    return telemetry_id;
+}
+
+u64 GetTelemetryId() {
+    u64 telemetry_id{};
+    static const std::string& filename{FileUtil::GetUserPath(D_CONFIG_IDX) + "telemetry_id"};
+
+    if (FileUtil::Exists(filename)) {
+        FileUtil::IOFile file(filename, "rb");
+        if (!file.IsOpen()) {
+            LOG_ERROR(Core, "failed to open telemetry_id: %s", filename.c_str());
+            return {};
+        }
+        file.ReadBytes(&telemetry_id, sizeof(u64));
+    } else {
+        FileUtil::IOFile file(filename, "wb");
+        if (!file.IsOpen()) {
+            LOG_ERROR(Core, "failed to open telemetry_id: %s", filename.c_str());
+            return {};
+        }
+        telemetry_id = GenerateTelemetryId();
+        file.WriteBytes(&telemetry_id, sizeof(u64));
+    }
+
+    return telemetry_id;
+}
+
+u64 RegenerateTelemetryId() {
+    const u64 new_telemetry_id{GenerateTelemetryId()};
+    static const std::string& filename{FileUtil::GetUserPath(D_CONFIG_IDX) + "telemetry_id"};
+
+    FileUtil::IOFile file(filename, "wb");
+    if (!file.IsOpen()) {
+        LOG_ERROR(Core, "failed to open telemetry_id: %s", filename.c_str());
+        return {};
+    }
+    file.WriteBytes(&new_telemetry_id, sizeof(u64));
+    return new_telemetry_id;
+}
+
 TelemetrySession::TelemetrySession() {
 #ifdef ENABLE_WEB_SERVICE
-    backend = std::make_unique<WebService::TelemetryJson>();
+    if (Settings::values.enable_telemetry) {
+        backend = std::make_unique<WebService::TelemetryJson>(
+            Settings::values.telemetry_endpoint_url, Settings::values.citra_username,
+            Settings::values.citra_token);
+    } else {
+        backend = std::make_unique<Telemetry::NullVisitor>();
+    }
 #else
     backend = std::make_unique<Telemetry::NullVisitor>();
 #endif
+    // Log one-time top-level information
+    AddField(Telemetry::FieldType::None, "TelemetryId", GetTelemetryId());
+
     // Log one-time session start information
     const s64 init_time{std::chrono::duration_cast<std::chrono::milliseconds>(
                             std::chrono::system_clock::now().time_since_epoch())
diff --git a/src/core/telemetry_session.h b/src/core/telemetry_session.h
index cf53835c3e755878a197b0654fea5ed445df2376..65613daae4dd9bf703c4849e6ca620d0d50bde64 100644
--- a/src/core/telemetry_session.h
+++ b/src/core/telemetry_session.h
@@ -35,4 +35,16 @@ private:
     std::unique_ptr<Telemetry::VisitorInterface> backend; ///< Backend interface that logs fields
 };
 
+/**
+ * Gets TelemetryId, a unique identifier used for the user's telemetry sessions.
+ * @returns The current TelemetryId for the session.
+ */
+u64 GetTelemetryId();
+
+/**
+ * Regenerates TelemetryId, a unique identifier used for the user's telemetry sessions.
+ * @returns The new TelemetryId that was generated.
+ */
+u64 RegenerateTelemetryId();
+
 } // namespace Core
diff --git a/src/web_service/telemetry_json.cpp b/src/web_service/telemetry_json.cpp
index a2d007e77975de7c7ebc792f5f3cc9e0cb66d375..6ad2ffcd4afe007459800ce03380670867270b7d 100644
--- a/src/web_service/telemetry_json.cpp
+++ b/src/web_service/telemetry_json.cpp
@@ -3,7 +3,6 @@
 // Refer to the license.txt file included.
 
 #include "common/assert.h"
-#include "core/settings.h"
 #include "web_service/telemetry_json.h"
 #include "web_service/web_backend.h"
 
@@ -81,7 +80,7 @@ void TelemetryJson::Complete() {
     SerializeSection(Telemetry::FieldType::UserFeedback, "UserFeedback");
     SerializeSection(Telemetry::FieldType::UserConfig, "UserConfig");
     SerializeSection(Telemetry::FieldType::UserSystem, "UserSystem");
-    PostJson(Settings::values.telemetry_endpoint_url, TopSection().dump());
+    PostJson(endpoint_url, TopSection().dump(), true, username, token);
 }
 
 } // namespace WebService
diff --git a/src/web_service/telemetry_json.h b/src/web_service/telemetry_json.h
index 39038b4f9ceadad24619fb683622648871f515e5..9e78c6803b799dfd18ccec52688d9cbf1d8106a2 100644
--- a/src/web_service/telemetry_json.h
+++ b/src/web_service/telemetry_json.h
@@ -17,7 +17,9 @@ namespace WebService {
  */
 class TelemetryJson : public Telemetry::VisitorInterface {
 public:
-    TelemetryJson() = default;
+    TelemetryJson(const std::string& endpoint_url, const std::string& username,
+                  const std::string& token)
+        : endpoint_url(endpoint_url), username(username), token(token) {}
     ~TelemetryJson() = default;
 
     void Visit(const Telemetry::Field<bool>& field) override;
@@ -49,6 +51,9 @@ private:
 
     nlohmann::json output;
     std::array<nlohmann::json, 7> sections;
+    std::string endpoint_url;
+    std::string username;
+    std::string token;
 };
 
 } // namespace WebService
diff --git a/src/web_service/web_backend.cpp b/src/web_service/web_backend.cpp
index 13e4555acd47465fd9ab91584b0f98d113dc828b..d28a3f757f89cf9ff0287929265abd583cd7bd56 100644
--- a/src/web_service/web_backend.cpp
+++ b/src/web_service/web_backend.cpp
@@ -2,51 +2,62 @@
 // Licensed under GPLv2 or any later version
 // Refer to the license.txt file included.
 
+#ifdef _WIN32
+#include <winsock.h>
+#endif
+
+#include <cstdlib>
+#include <thread>
 #include <cpr/cpr.h>
-#include <stdlib.h>
 #include "common/logging/log.h"
 #include "web_service/web_backend.h"
 
 namespace WebService {
 
 static constexpr char API_VERSION[]{"1"};
-static constexpr char ENV_VAR_USERNAME[]{"CITRA_WEB_SERVICES_USERNAME"};
-static constexpr char ENV_VAR_TOKEN[]{"CITRA_WEB_SERVICES_TOKEN"};
-
-static std::string GetEnvironmentVariable(const char* name) {
-    const char* value{getenv(name)};
-    if (value) {
-        return value;
-    }
-    return {};
-}
-
-const std::string& GetUsername() {
-    static const std::string username{GetEnvironmentVariable(ENV_VAR_USERNAME)};
-    return username;
-}
 
-const std::string& GetToken() {
-    static const std::string token{GetEnvironmentVariable(ENV_VAR_TOKEN)};
-    return token;
-}
+static std::unique_ptr<cpr::Session> g_session;
 
-void PostJson(const std::string& url, const std::string& data) {
+void PostJson(const std::string& url, const std::string& data, bool allow_anonymous,
+              const std::string& username, const std::string& token) {
     if (url.empty()) {
         LOG_ERROR(WebService, "URL is invalid");
         return;
     }
 
-    if (GetUsername().empty() || GetToken().empty()) {
-        LOG_ERROR(WebService, "Environment variables %s and %s must be set to POST JSON",
-                  ENV_VAR_USERNAME, ENV_VAR_TOKEN);
+    const bool are_credentials_provided{!token.empty() && !username.empty()};
+    if (!allow_anonymous && !are_credentials_provided) {
+        LOG_ERROR(WebService, "Credentials must be provided for authenticated requests");
         return;
     }
 
-    cpr::PostAsync(cpr::Url{url}, cpr::Body{data}, cpr::Header{{"Content-Type", "application/json"},
-                                                               {"x-username", GetUsername()},
-                                                               {"x-token", GetToken()},
-                                                               {"api-version", API_VERSION}});
+#ifdef _WIN32
+    // On Windows, CPR/libcurl does not properly initialize Winsock. The below code is used to
+    // initialize Winsock globally, which fixes this problem. Without this, only the first CPR
+    // session will properly be created, and subsequent ones will fail.
+    WSADATA wsa_data;
+    const int wsa_result{WSAStartup(MAKEWORD(2, 2), &wsa_data)};
+    if (wsa_result) {
+        LOG_CRITICAL(WebService, "WSAStartup failed: %d", wsa_result);
+    }
+#endif
+
+    // Built request header
+    cpr::Header header;
+    if (are_credentials_provided) {
+        // Authenticated request if credentials are provided
+        header = {{"Content-Type", "application/json"},
+                  {"x-username", username.c_str()},
+                  {"x-token", token.c_str()},
+                  {"api-version", API_VERSION}};
+    } else {
+        // Otherwise, anonymous request
+        header = cpr::Header{{"Content-Type", "application/json"}, {"api-version", API_VERSION}};
+    }
+
+    // Post JSON asynchronously
+    static cpr::AsyncResponse future;
+    future = cpr::PostAsync(cpr::Url{url.c_str()}, cpr::Body{data.c_str()}, header);
 }
 
 } // namespace WebService
diff --git a/src/web_service/web_backend.h b/src/web_service/web_backend.h
index 2753d3b68e161104e21eddcb6c2da1f045880ebb..d17100398676389de182a38821657d9447f3bdef 100644
--- a/src/web_service/web_backend.h
+++ b/src/web_service/web_backend.h
@@ -9,23 +9,15 @@
 
 namespace WebService {
 
-/**
- * Gets the current username for accessing services.citra-emu.org.
- * @returns Username as a string, empty if not set.
- */
-const std::string& GetUsername();
-
-/**
- * Gets the current token for accessing services.citra-emu.org.
- * @returns Token as a string, empty if not set.
- */
-const std::string& GetToken();
-
 /**
  * Posts JSON to services.citra-emu.org.
  * @param url URL of the services.citra-emu.org endpoint to post data to.
  * @param data String of JSON data to use for the body of the POST request.
+ * @param allow_anonymous If true, allow anonymous unauthenticated requests.
+ * @param username Citra username to use for authentication.
+ * @param token Citra token to use for authentication.
  */
-void PostJson(const std::string& url, const std::string& data);
+void PostJson(const std::string& url, const std::string& data, bool allow_anonymous,
+              const std::string& username = {}, const std::string& token = {});
 
 } // namespace WebService