Linkzly

Steam SDK Setup Guide

This guide covers integrating Gaming Intelligence into Steam games using C++ and the Steamworks SDK.

7 min read

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) for CCallResult to 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