Steam SDK Setup Guide
This guide covers integrating Gaming Intelligence into Steam games using C++ and the Steamworks SDK.
Steam SDK Setup Guide
This guide covers integrating Gaming Intelligence into Steam games using C++ and the Steamworks SDK.
Prerequisites
| Requirement | Details |
|---|---|
| SDK Key | Console → Gaming → [Game Name] → Settings → SDK Configuration → SDK Key |
| Organization ID | Found in your Linkzly organization settings |
| Game ID | Console → Gaming → [Game Name] → Settings |
| Steamworks SDK | Available at partner.steamgames.com |
| C++ compiler | MSVC 2019+, GCC 9+, or Clang 10+ |
| OpenSSL or mbedTLS | For HMAC-SHA256 signing |
Platform Value
All events sent from Steam builds must include:
"platform": "steam"
Step 1: Player Identity via Steam
Use the Steam user's 64-bit SteamID as the player_id. This is stable, unique, and does not require additional account infrastructure.
#include "steam/steam_api.h"
#include <string>
std::string GetSteamPlayerId()
{
if (!SteamUser() || !SteamUser()->BLoggedOn())
return "";
uint64 steamId = SteamUser()->GetSteamID().ConvertToUint64();
return std::to_string(steamId);
}
Step 2: HTTP Integration
You have two options for sending HTTP requests. Choose the one that best fits your project.
Option A: SteamHTTP (no extra dependencies)
SteamHTTP is built into the Steamworks SDK and is the simplest choice if you are already initializing Steam.
#include "steam/steam_api.h"
#include "steam/isteamhttp.h"
#include <string>
class LinkzlyClient
{
public:
std::string sdkKey;
std::string orgId;
std::string gameId;
// Steam callback for async completion
CCallResult<LinkzlyClient, HTTPRequestCompleted_t> m_CallResult;
void SendEvents(const std::string& jsonBody)
{
ISteamHTTP* steamHttp = SteamHTTP();
if (!steamHttp) return;
HTTPRequestHandle hReq = steamHttp->CreateHTTPRequest(
k_EHTTPMethodPOST,
"https://gaming.linkzly.com/api/v1/gaming/events");
// Auth headers
steamHttp->SetHTTPRequestHeaderValue(hReq, "Authorization",
("Bearer " + sdkKey).c_str());
steamHttp->SetHTTPRequestHeaderValue(hReq, "X-Organization-ID",
orgId.c_str());
steamHttp->SetHTTPRequestHeaderValue(hReq, "X-Game-ID",
gameId.c_str());
steamHttp->SetHTTPRequestHeaderValue(hReq, "Content-Type",
"application/json");
// HMAC headers
std::string timestamp = GetTimestampMs();
std::string nonce = GenerateUUID();
std::string signature = ComputeHmacSha256(sdkKey, timestamp, nonce, jsonBody);
steamHttp->SetHTTPRequestHeaderValue(hReq, "X-Timestamp", timestamp.c_str());
steamHttp->SetHTTPRequestHeaderValue(hReq, "X-Nonce", nonce.c_str());
steamHttp->SetHTTPRequestHeaderValue(hReq, "X-Signature-256", signature.c_str());
// Body
steamHttp->SetHTTPRequestRawPostBody(
hReq,
"application/json",
(uint8*)jsonBody.c_str(),
(uint32)jsonBody.size());
// Send asynchronously
SteamAPICall_t hCall;
steamHttp->SendHTTPRequestAndSetHandle(hReq, &hCall);
m_CallResult.Set(hCall, this, &LinkzlyClient::OnHTTPComplete);
}
void OnHTTPComplete(HTTPRequestCompleted_t* pResult, bool bIOFailure)
{
if (bIOFailure || !pResult->m_bRequestSuccessful)
{
// Log error, optionally queue for retry
return;
}
// HTTP 202 = Accepted
if (pResult->m_eStatusCode == k_EHTTPStatusCode202Accepted)
{
// Events accepted
}
SteamHTTP()->ReleaseHTTPRequest(pResult->m_hRequest);
}
};
Note:
SteamAPI_RunCallbacks()must be called every frame (or on a timer) forCCallResultto fire. This is standard Steamworks practice.
Option B: libcurl (cross-platform, synchronous or async)
libcurl is a widely used option and works well if you are targeting multiple platforms beyond Steam or prefer a synchronous sending model.
#include <curl/curl.h>
#include <string>
static size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* out)
{
out->append((char*)contents, size * nmemb);
return size * nmemb;
}
void SendEventsWithCurl(
const std::string& sdkKey,
const std::string& orgId,
const std::string& gameId,
const std::string& jsonBody)
{
CURL* curl = curl_easy_init();
if (!curl) return;
std::string timestamp = GetTimestampMs();
std::string nonce = GenerateUUID();
std::string signature = ComputeHmacSha256(sdkKey, timestamp, nonce, jsonBody);
struct curl_slist* headers = nullptr;
headers = curl_slist_append(headers, ("Authorization: Bearer " + sdkKey).c_str());
headers = curl_slist_append(headers, ("X-Organization-ID: " + orgId).c_str());
headers = curl_slist_append(headers, ("X-Game-ID: " + gameId).c_str());
headers = curl_slist_append(headers, "Content-Type: application/json");
headers = curl_slist_append(headers, ("X-Timestamp: " + timestamp).c_str());
headers = curl_slist_append(headers, ("X-Nonce: " + nonce).c_str());
headers = curl_slist_append(headers, ("X-Signature-256: " + signature).c_str());
std::string responseBody;
curl_easy_setopt(curl, CURLOPT_URL,
"https://gaming.linkzly.com/api/v1/gaming/events");
curl_easy_setopt(curl, CURLOPT_POST, 1L);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, jsonBody.c_str());
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)jsonBody.size());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseBody);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
CURLcode res = curl_easy_perform(curl);
long httpCode = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
// httpCode == 202 means events accepted
}
Recommendation: Run libcurl requests on a background thread to avoid blocking the game loop.
Step 3: JSON Serialization
Use nlohmann/json (header-only) or rapidjson for building 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", "steam"},
{"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)
{
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("steam", alloc), alloc);
eventObj.AddMember("player_id", Value(playerId.c_str(), alloc), alloc);
eventObj.AddMember("session_id", Value(sessionId.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());
}
Step 4: HMAC Signing
HMAC signing is enabled by default. Disable at: Console → Gaming → [Game Name] → Settings → Security Settings → HMAC Signing Required → Off.
HMAC-SHA256 with OpenSSL
#include <openssl/hmac.h>
#include <openssl/sha.h>
#include <sstream>
#include <iomanip>
std::string ComputeHmacSha256(
const std::string& secretKey,
const std::string& timestamp,
const std::string& nonce,
const std::string& body)
{
// Signing string: "{timestamp}.{nonce}.{body}"
std::string signingString = timestamp + "." + nonce + "." + body;
unsigned char digest[EVP_MAX_MD_SIZE];
unsigned int digestLen = 0;
HMAC(EVP_sha256(),
secretKey.c_str(), (int)secretKey.size(),
(const unsigned char*)signingString.c_str(), signingString.size(),
digest, &digestLen);
std::ostringstream hex;
for (unsigned int i = 0; i < digestLen; i++)
hex << std::hex << std::setw(2) << std::setfill('0') << (int)digest[i];
return hex.str();
}
HMAC-SHA256 with mbedTLS (alternative)
#include "mbedtls/md.h"
#include <sstream>
#include <iomanip>
std::string ComputeHmacSha256MbedTLS(
const std::string& secretKey,
const std::string& timestamp,
const std::string& nonce,
const std::string& body)
{
std::string signingString = timestamp + "." + nonce + "." + body;
unsigned char output[32];
mbedtls_md_context_t ctx;
const mbedtls_md_info_t* mdInfo = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
mbedtls_md_init(&ctx);
mbedtls_md_setup(&ctx, mdInfo, 1);
mbedtls_md_hmac_starts(&ctx,
(const unsigned char*)secretKey.c_str(), secretKey.size());
mbedtls_md_hmac_update(&ctx,
(const unsigned char*)signingString.c_str(), signingString.size());
mbedtls_md_hmac_finish(&ctx, output);
mbedtls_md_free(&ctx);
std::ostringstream hex;
for (int i = 0; i < 32; i++)
hex << std::hex << std::setw(2) << std::setfill('0') << (int)output[i];
return hex.str();
}
HMAC headers:
| Header | Value |
|---|---|
X-Timestamp |
Unix timestamp in milliseconds |
X-Nonce |
UUID v4 |
X-Signature-256 |
HMAC-SHA256 hex of {timestamp}.{nonce}.{body} |
Replay window: 300 seconds.
Step 5: Session Management
Tie sessions to Steam overlay and game state changes rather than OS lifecycle:
// On game launch / after Steam authentication
void OnGameStart()
{
std::string playerId = GetSteamPlayerId();
std::string sessionId = GenerateUUID();
std::string body = BuildEventPayload(playerId, sessionId, "session_start", "1.0.0");
client.SendEvents(body);
}
// When player exits to main menu or quits
void OnGameEnd(const std::string& playerId, const std::string& sessionId)
{
std::string body = BuildEventPayload(playerId, sessionId, "session_end", "1.0.0");
client.SendEvents(body);
}
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 = std::gmtime(&t);
std::ostringstream oss;
oss << std::put_time(utc, "%Y-%m-%dT%H:%M:%SZ");
return oss.str();
}
std::string GenerateUUID()
{
// Simple random UUID v4 (use a proper library for production)
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);
// Set version 4 and variant bits
a = (a & 0xFFFFFFFFFFFF0FFFULL) | 0x0000000000004000ULL;
b = (b & 0x3FFFFFFFFFFFFFFFULL) | 0x8000000000000000ULL;
std::ostringstream ss;
ss << std::hex << std::setfill('0')
<< std::setw(8) << (uint32_t)(a >> 32) << "-"
<< std::setw(4) << (uint16_t)(a >> 16) << "-"
<< std::setw(4) << (uint16_t)(a & 0xFFFF) << "-"
<< std::setw(4) << (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:
{
"batch_id": "batch_abc123",
"events_received": 5,
"events_valid": 5,
"events_dropped": 0,
"trace_id": "trace_xyz789"
}
Troubleshooting
| Issue | Resolution |
|---|---|
| 401 Unauthorized | SDK Key is invalid; regenerate in Console |
| 400 Bad Request | Missing required fields: event_id, event_type, timestamp, platform, player_id, session_id |
| HMAC mismatch | Check system clock is synced; replay window is 300s |
SteamHTTP() returns null |
Ensure SteamAPI_Init() has been called successfully |
| Callbacks not firing | Call SteamAPI_RunCallbacks() each frame |
| SSL verification failure | Verify CA bundle is present for libcurl |
Next Steps
Was this helpful?
Help us improve our documentation