Xbox SDK Setup Guide (GDK C++)
---
sidebar_label: Xbox (GDK C++) sidebar_category: Gaming SDK Guides
Xbox SDK Setup Guide (GDK C++)
This guide covers integrating Gaming Intelligence into Xbox Series X|S and Xbox One games using C++ and the Microsoft Game Development Kit (GDK). It demonstrates HTTP transport via WinHTTP, HMAC-SHA256 signing via BCrypt, event serialization, session lifecycle management, and offline queuing patterns suitable for Xbox certification.
GDK Note: This guide targets the Microsoft Game Development Kit (GDK) for Xbox Series X|S and Xbox One. Legacy XDK developers should adapt the HTTP transport section to use
XCurlor the XDK-equivalent HTTP primitives, and replace BCrypt calls with the equivalent XDK crypto API.
Prerequisites
Before you begin, have the following ready:
| Requirement | Where to Find It |
|---|---|
| SDK Key | Linkzly Console → Gaming → [Your Game] → Settings → SDK Configuration → SDK Key |
| Organization ID | Linkzly Console → Organization Settings |
| Game ID | Linkzly Console → Gaming → [Your Game] → Settings → General Settings |
| Signing Secret | Linkzly Console → Gaming → [Your Game] → Settings → SDK Configuration → Signing Secret |
| Microsoft GDK | Microsoft Game Dev portal (partner access required) |
| WinHTTP or XCurl | WinHTTP ships with the GDK; XCurl is the GDK-native curl wrapper |
| BCrypt | BCrypt.h ships with the GDK and Windows SDK; no additional dependency needed |
| rapidjson or nlohmann/json | Lightweight JSON serializer for building event payloads |
Your SDK key is masked by default in the console. Use the eye icon to reveal it or the copy button to copy it directly.
Platform Value
All events sent from Xbox builds must include "platform": "xbox" in the event payload.
Architecture Overview
Xbox certification (the Xbox Requirements, or XRs) prohibits blocking the game's main thread on network I/O. The recommended pattern is:
- Game thread: Enqueues events into a thread-safe in-memory buffer using
std::mutexor a lock-free queue. - Network thread: Dequeues events in batches, serializes them, computes the HMAC signature, and POSTs to the ingestion endpoint via WinHTTP.
- Suspend/resume handling: Flush and pause network activity when the title suspends; resume sending after the title resumes.
Step 1: Player Identity via Xbox
Use the Xbox user's XUID (Xbox User ID) as the player_id. The XUID is a stable 64-bit identifier tied to the player's Xbox Live account.
#include <XUser.h>
#include <string>
std::string GetXboxPlayerId(XUserHandle userHandle)
{
uint64_t xuid = 0;
HRESULT hr = XUserGetId(userHandle, &xuid);
if (FAILED(hr)) {
// User may not be signed in — fall back to a locally generated UUID
return "";
}
char buf[32];
snprintf(buf, sizeof(buf), "%llu", static_cast<unsigned long long>(xuid));
return std::string(buf);
}
If your title does not require Xbox Live sign-in, generate a UUID on first launch, persist it to the title's local storage via XPersistentLocalStorage, and use that as player_id.
Step 2: HTTP Transport via WinHTTP
WinHTTP is the recommended HTTP library for GDK titles. It is included in the GDK and supports TLS 1.2+ out of the box.
Initialization
Open a session handle once at startup and reuse it across all requests:
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <winhttp.h>
#include <string>
#include <vector>
#pragma comment(lib, "winhttp.lib")
// Store these as members of your networking class
HINTERNET g_hSession = nullptr;
void InitWinHTTP()
{
g_hSession = WinHttpOpen(
L"Linkzly-GDK-SDK/1.0",
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
WINHTTP_NO_PROXY_NAME,
WINHTTP_NO_PROXY_BYPASS,
0);
// g_hSession is nullptr on failure; handle gracefully
}
void ShutdownWinHTTP()
{
if (g_hSession) {
WinHttpCloseHandle(g_hSession);
g_hSession = nullptr;
}
}
Sending an Event Batch
Call this from your dedicated network thread:
struct LinkzlyCredentials
{
std::string sdkKey;
std::string orgId;
std::string gameId;
};
bool SendEventBatch(
const LinkzlyCredentials& creds,
const std::string& jsonBody,
const std::string& timestamp,
const std::string& nonce,
const std::string& signature)
{
if (!g_hSession) return false;
// Helper: convert narrow string to wide for WinHTTP
auto ToWide = [](const std::string& s) -> std::wstring {
return std::wstring(s.begin(), s.end());
};
// 1. Open connection to the ingestion host
HINTERNET hConnect = WinHttpConnect(
g_hSession,
L"gaming.linkzly.com",
INTERNET_DEFAULT_HTTPS_PORT,
0);
if (!hConnect) return false;
// 2. Open a POST request (HTTPS)
HINTERNET hRequest = WinHttpOpenRequest(
hConnect,
L"POST",
L"/api/v1/gaming/events",
nullptr,
WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES,
WINHTTP_FLAG_SECURE);
if (!hRequest) {
WinHttpCloseHandle(hConnect);
return false;
}
// 3. Build headers
std::wstring headers =
L"Content-Type: application/json\r\n"
L"Authorization: Bearer " + ToWide(creds.sdkKey) + L"\r\n" +
L"X-Organization-ID: " + ToWide(creds.orgId) + L"\r\n" +
L"X-Game-ID: " + ToWide(creds.gameId) + L"\r\n" +
L"X-Timestamp: " + ToWide(timestamp) + L"\r\n" +
L"X-Nonce: " + ToWide(nonce) + L"\r\n" +
L"X-Signature-256: " + ToWide(signature) + L"\r\n";
WinHttpAddRequestHeaders(
hRequest,
headers.c_str(),
static_cast<DWORD>(headers.size()),
WINHTTP_ADDREQ_FLAG_ADD);
// 4. Send request with body
BOOL bResult = WinHttpSendRequest(
hRequest,
WINHTTP_NO_ADDITIONAL_HEADERS, 0,
const_cast<char*>(jsonBody.c_str()),
static_cast<DWORD>(jsonBody.size()),
static_cast<DWORD>(jsonBody.size()),
0);
if (bResult)
bResult = WinHttpReceiveResponse(hRequest, nullptr);
// 5. Read HTTP status code
bool accepted = false;
if (bResult)
{
DWORD statusCode = 0;
DWORD statusSize = sizeof(statusCode);
WinHttpQueryHeaders(
hRequest,
WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
WINHTTP_HEADER_NAME_BY_INDEX,
&statusCode, &statusSize,
WINHTTP_NO_HEADER_INDEX);
accepted = (statusCode == 202);
// For 5xx errors, consider re-queuing the batch for retry
}
WinHttpCloseHandle(hRequest);
WinHttpCloseHandle(hConnect);
return accepted;
}
Thread safety:
WinHttpConnectandWinHttpOpenRequestare not thread-safe with the sameHINTERNETsession handle. Create per-thread connection handles, or serialize access with a mutex.
Step 3: HMAC Signing via BCrypt
HMAC signing is enabled by default for all new games. To disable it during development: Console → Gaming → [Your Game] → Settings → Security Settings → HMAC Signing Required → Off.
BCrypt (BCrypt.h) is part of the Windows Cryptography API: Next Generation (CNG) and ships with the GDK. No additional libraries are required.
#include <bcrypt.h>
#include <string>
#include <vector>
#include <sstream>
#include <iomanip>
#pragma comment(lib, "bcrypt.lib")
// Signing string format: "{timestamp}.{nonce}.{body}"
std::string ComputeHmacSha256(
const std::string& signingSecret,
const std::string& timestamp,
const std::string& nonce,
const std::string& body)
{
std::string signingString = timestamp + "." + nonce + "." + body;
BCRYPT_ALG_HANDLE hAlg = nullptr;
NTSTATUS status = BCryptOpenAlgorithmProvider(
&hAlg,
BCRYPT_SHA256_ALGORITHM,
nullptr,
BCRYPT_ALG_HANDLE_HMAC_FLAG); // Required for HMAC mode
if (!BCRYPT_SUCCESS(status)) return "";
// Query the output hash size (32 bytes for SHA-256)
DWORD hashSize = 0;
DWORD resultSize = 0;
BCryptGetProperty(hAlg, BCRYPT_HASH_LENGTH,
reinterpret_cast<PUCHAR>(&hashSize), sizeof(DWORD), &resultSize, 0);
std::vector<UCHAR> hashBytes(hashSize);
// Create the hash object, supplying the secret key
BCRYPT_HASH_HANDLE hHash = nullptr;
status = BCryptCreateHash(
hAlg, &hHash,
nullptr, 0,
reinterpret_cast<PUCHAR>(const_cast<char*>(signingSecret.data())),
static_cast<ULONG>(signingSecret.size()),
0);
if (BCRYPT_SUCCESS(status))
status = BCryptHashData(
hHash,
reinterpret_cast<PUCHAR>(const_cast<char*>(signingString.data())),
static_cast<ULONG>(signingString.size()),
0);
if (BCRYPT_SUCCESS(status))
status = BCryptFinishHash(hHash, hashBytes.data(), hashSize, 0);
if (hHash) BCryptDestroyHash(hHash);
BCryptCloseAlgorithmProvider(hAlg, 0);
if (!BCRYPT_SUCCESS(status)) return "";
// Hex-encode
std::ostringstream hex;
for (DWORD i = 0; i < hashSize; i++)
hex << std::hex << std::setw(2) << std::setfill('0')
<< static_cast<int>(hashBytes[i]);
return hex.str();
}
HMAC headers:
| Header | Value |
|---|---|
X-Timestamp |
Unix timestamp in milliseconds (e.g., 1714000000000) |
X-Nonce |
UUID v4, unique per request |
X-Signature-256 |
HMAC-SHA256 hex of {timestamp}.{nonce}.{body} |
Replay window: 300 seconds. Requests with timestamps older than 5 minutes from the server clock are rejected.
Step 4: JSON Serialization
Use nlohmann/json (header-only) or rapidjson to build event payloads.
Using nlohmann/json
#include "nlohmann/json.hpp"
using json = nlohmann::json;
std::string BuildEventPayload(
const std::string& playerId,
const std::string& sessionId,
const std::string& eventType,
const std::string& gameVersion)
{
json payload = {
{"events", json::array({
{
{"event_id", GenerateUUID()},
{"event_type", eventType},
{"timestamp", GetCurrentISOTimestamp()},
{"platform", "xbox"},
{"player_id", playerId},
{"session_id", sessionId},
{"sdk_version", "1.0.0"},
{"game_version", gameVersion}
}
})}
};
return payload.dump();
}
Using rapidjson
#include "rapidjson/document.h"
#include "rapidjson/writer.h"
#include "rapidjson/stringbuffer.h"
using namespace rapidjson;
std::string BuildEventPayloadRapid(
const std::string& playerId,
const std::string& sessionId,
const std::string& eventType,
const std::string& gameVersion)
{
Document doc;
doc.SetObject();
auto& alloc = doc.GetAllocator();
Value eventsArray(kArrayType);
Value eventObj(kObjectType);
eventObj.AddMember("event_id",
Value(GenerateUUID().c_str(), alloc), alloc);
eventObj.AddMember("event_type",
Value(eventType.c_str(), alloc), alloc);
eventObj.AddMember("timestamp",
Value(GetCurrentISOTimestamp().c_str(), alloc), alloc);
eventObj.AddMember("platform",
Value("xbox", alloc), alloc);
eventObj.AddMember("player_id",
Value(playerId.c_str(), alloc), alloc);
eventObj.AddMember("session_id",
Value(sessionId.c_str(), alloc), alloc);
eventObj.AddMember("game_version",
Value(gameVersion.c_str(), alloc), alloc);
eventsArray.PushBack(eventObj, alloc);
doc.AddMember("events", eventsArray, alloc);
StringBuffer buffer;
Writer<StringBuffer> writer(buffer);
doc.Accept(writer);
return std::string(buffer.GetString(), buffer.GetSize());
}
Step 5: Session Lifecycle
Tie session start/end to the GDK application lifecycle:
#include <XGameRuntimeInit.h>
#include <string>
static std::string g_PlayerId;
static std::string g_SessionId;
static LinkzlyCredentials g_Creds = {
"// YOUR_SDK_KEY",
"// YOUR_ORG_ID",
"// YOUR_GAME_ID"
};
static std::string g_SigningSecret = "// YOUR_SIGNING_SECRET";
void OnGameLaunched(XUserHandle userHandle)
{
g_PlayerId = GetXboxPlayerId(userHandle);
g_SessionId = GenerateUUID();
std::string body = BuildEventPayloadRapid(
g_PlayerId, g_SessionId, "session_start", "1.0.0");
std::string timestamp = GetTimestampMs();
std::string nonce = GenerateUUID();
std::string signature = ComputeHmacSha256(
g_SigningSecret, timestamp, nonce, body);
// Dispatch on network thread
EnqueueForNetwork(body, timestamp, nonce, signature);
}
// Called when Game Bar suspends the title or the console enters low-power
void OnTitleSuspend()
{
// Flush queued events asynchronously before acknowledging suspend
FlushQueuedEventsAsync();
// Xbox certification: must complete suspend acknowledgment within budget
}
// Called on resume — continue the existing session
void OnTitleResume()
{
// No new session_start; just resume sending queued events
ResumeNetworkThread();
}
void OnGameExiting()
{
std::string body = BuildEventPayloadRapid(
g_PlayerId, g_SessionId, "session_end", "1.0.0");
std::string timestamp = GetTimestampMs();
std::string nonce = GenerateUUID();
std::string signature = ComputeHmacSha256(
g_SigningSecret, timestamp, nonce, body);
EnqueueForNetwork(body, timestamp, nonce, signature);
WaitForNetworkFlush(2000); // Block up to 2 seconds on exit
}
Step 6: Offline Event Queuing
Connectivity may be unavailable when a title first launches or during transient network interruptions. Retain events in memory and flush on the next successful connection.
#include <deque>
#include <string>
#include <mutex>
struct PendingBatch
{
std::string body;
std::string timestamp;
std::string nonce;
std::string signature;
};
class LinkzlyEventQueue
{
public:
static const size_t MAX_QUEUE_SIZE = 500;
void Enqueue(PendingBatch batch)
{
std::lock_guard<std::mutex> lock(m_mutex);
if (m_queue.size() >= MAX_QUEUE_SIZE)
m_queue.pop_front();
m_queue.push_back(std::move(batch));
}
// Call from the dedicated network thread
void Flush(const LinkzlyCredentials& creds)
{
std::lock_guard<std::mutex> lock(m_mutex);
while (!m_queue.empty())
{
const auto& front = m_queue.front();
bool ok = SendEventBatch(
creds, front.body, front.timestamp, front.nonce, front.signature);
if (ok)
m_queue.pop_front();
else
break; // Stop on failure; retry on next network tick
}
}
private:
std::deque<PendingBatch> m_queue;
std::mutex m_mutex;
};
Helper Utilities
#include <chrono>
#include <sstream>
#include <iomanip>
#include <random>
#include <ctime>
std::string GetTimestampMs()
{
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
return std::to_string(ms);
}
std::string GetCurrentISOTimestamp()
{
auto now = std::chrono::system_clock::now();
std::time_t t = std::chrono::system_clock::to_time_t(now);
std::tm utc = {};
gmtime_s(&utc, &t); // Windows-safe; use gmtime_r on Linux
std::ostringstream oss;
oss << std::put_time(&utc, "%Y-%m-%dT%H:%M:%SZ");
return oss.str();
}
std::string GenerateUUID()
{
static std::random_device rd;
static std::mt19937_64 gen(rd());
static std::uniform_int_distribution<uint64_t> dis;
uint64_t a = dis(gen), b = dis(gen);
a = (a & 0xFFFFFFFFFFFF0FFFULL) | 0x0000000000004000ULL;
b = (b & 0x3FFFFFFFFFFFFFFFULL) | 0x8000000000000000ULL;
std::ostringstream ss;
ss << std::hex << std::setfill('0')
<< std::setw(8) << static_cast<uint32_t>(a >> 32) << "-"
<< std::setw(4) << static_cast<uint16_t>(a >> 16) << "-"
<< std::setw(4) << static_cast<uint16_t>(a) << "-"
<< std::setw(4) << static_cast<uint16_t>(b >> 48) << "-"
<< std::setw(12) << (b & 0x0000FFFFFFFFFFFFULL);
return ss.str();
}
Batch Response
A successful batch submission returns HTTP 202 Accepted with a JSON body:
{
"success": true,
"batch_id": "batch_01jk...",
"events_received": 10,
"events_valid": 10,
"events_dropped": 0,
"trace_id": "trace_abc...",
"server_timestamp": "2025-04-15T12:00:00Z"
}
Xbox Certification Checklist
Before submitting for Xbox certification, verify the following:
| Check | Requirement |
|---|---|
| No blocking network calls on main thread | All WinHTTP calls run on a dedicated network thread |
| Suspend handling completes on time | Flush events before acknowledging suspend; do not block |
| Player identity obtained correctly | Use XUserGetId or a persisted UUID fallback |
| TLS certificate validation enabled | Do not disable cert checks in WinHttpSetOption |
| SDK Key not embedded in plaintext in release | Use secure storage or build-time secrets management |
| Event queue bounded | Cap the in-memory queue to prevent unbounded memory growth |
Troubleshooting
| Issue | Resolution |
|---|---|
| 401 Unauthorized | Verify SDK Key, Org ID, and Game ID are correct with no extra whitespace |
| 400 Bad Request | All required fields must be present: event_id, event_type, timestamp, platform, player_id, session_id |
| HMAC mismatch | Check system clock is NTP-synced; replay window is 300 seconds. Confirm signing string: {timestamp}.{nonce}.{body} |
WinHttpOpenRequest returns null |
Ensure WINHTTP_FLAG_SECURE is set and the session handle is valid |
WinHttpSendRequest returns false |
Call GetLastError() — common codes: ERROR_WINHTTP_NAME_NOT_RESOLVED (DNS), ERROR_WINHTTP_SECURE_FAILURE (TLS) |
BCrypt STATUS_INVALID_PARAMETER |
Confirm the algorithm was opened with BCRYPT_ALG_HANDLE_HMAC_FLAG |
| Events lost on suspend | Flush the event queue before acknowledging the suspend callback |
Next Steps
Was this helpful?
Help us improve our documentation