Linkzly

Epic Games Store SDK Setup Guide (C++)

---

12 min read

sidebar_label: Epic Games Store (C++) sidebar_category: Gaming SDK Guides

Epic Games Store SDK Setup Guide (C++)

This guide covers integrating Gaming Intelligence into Epic Games Store titles using C++ and the Epic Online Services (EOS) SDK. It covers EOS context setup, HTTP transport via the EOS HTTP interface and libcurl, player identification via EOS Product User IDs, HMAC request signing, and session lifecycle tied to EOS Authentication state.

Cross-platform note: The EOS SDK runs on Windows, macOS, Linux, PlayStation, Xbox, and Switch. This guide applies to any EGS title regardless of distribution platform. The integration patterns shown here are platform-agnostic. Linkzly's REST API runs alongside EOS — it does not replace any EOS functionality.


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
Epic Online Services SDK dev.epicgames.com/portal
Epic Developer Account Required for EOS product and client credential setup
OpenSSL or mbedTLS For HMAC-SHA256 request signing (OpenSSL ships with the EOS SDK on most platforms)
nlohmann/json or rapidjson Lightweight JSON library for event serialization

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 Epic Games Store titles must include "platform": "epic" in the event payload.


Architecture Overview

The Linkzly integration runs independently from EOS features. The recommended architecture is:

  1. Game thread: Tracks in-game events and enqueues them into a thread-safe buffer.
  2. Network thread: Dequeues batches, computes HMAC signatures, and POSTs to the Linkzly ingestion endpoint via the EOS HTTP interface or libcurl.
  3. EOS Auth lifecycle: Session start/end events are triggered by EOS Connect login and logout/expiration callbacks.

Call EOS_Platform_Tick() on every game loop frame to drive all EOS callbacks, including HTTP completion callbacks.


Step 1: EOS SDK Initialization

Initialize the EOS platform before calling any EOS interfaces:

#include "eos_sdk.h"
#include "eos_logging.h"

EOS_HPlatform g_EOSPlatformHandle = nullptr;

bool InitializeEOS(
    const char* productId,
    const char* sandboxId,
    const char* deploymentId,
    const char* clientId,
    const char* clientSecret,
    const char* productName,
    const char* productVersion)
{
    // 1. Initialize the EOS SDK
    EOS_InitializeOptions InitOptions = {};
    InitOptions.ApiVersion     = EOS_INITIALIZE_API_LATEST;
    InitOptions.ProductName    = productName;
    InitOptions.ProductVersion = productVersion;

    EOS_EResult InitResult = EOS_Initialize(&InitOptions);
    if (InitResult != EOS_EResult::EOS_Success &&
        InitResult != EOS_EResult::EOS_AlreadyConfigured)
    {
        // Log InitResult and return false
        return false;
    }

    // 2. Create the platform handle
    EOS_Platform_Options PlatformOptions = {};
    PlatformOptions.ApiVersion = EOS_PLATFORM_OPTIONS_API_LATEST;
    PlatformOptions.ProductId    = productId;
    PlatformOptions.SandboxId    = sandboxId;
    PlatformOptions.DeploymentId = deploymentId;
    PlatformOptions.ClientCredentials.ClientId     = clientId;
    PlatformOptions.ClientCredentials.ClientSecret = clientSecret;
    PlatformOptions.bIsServer    = EOS_FALSE;
    PlatformOptions.Flags        = EOS_PF_WINDOWS_ENABLE_OVERLAY_D3D9
                                 | EOS_PF_WINDOWS_ENABLE_OVERLAY_D3D10
                                 | EOS_PF_WINDOWS_ENABLE_OVERLAY_OPENGL;

    g_EOSPlatformHandle = EOS_Platform_Create(&PlatformOptions);
    return (g_EOSPlatformHandle != nullptr);
}

void ShutdownEOS()
{
    if (g_EOSPlatformHandle) {
        EOS_Platform_Release(g_EOSPlatformHandle);
        g_EOSPlatformHandle = nullptr;
    }
    EOS_Shutdown();
}

Call EOS_Platform_Tick(g_EOSPlatformHandle) every frame:

// In your game loop
void GameLoop()
{
    // ... other per-frame work ...
    if (g_EOSPlatformHandle)
        EOS_Platform_Tick(g_EOSPlatformHandle);
}

Step 2: EOS Player Identity (EOS Product User ID)

After a successful EOS_Connect_Login, use the EOS_ProductUserId as the Linkzly player_id. This ID is stable across sessions for a given Epic account and does not expose PII.

#include "eos_connect.h"
#include <string>

static std::string g_PlayerId;

std::string EOSProductUserIdToString(EOS_ProductUserId UserId)
{
    char Buffer[EOS_PRODUCTUSERID_MAX_LENGTH + 1] = {};
    int32_t BufferLen = static_cast<int32_t>(sizeof(Buffer));

    EOS_EResult Result = EOS_ProductUserId_ToString(UserId, Buffer, &BufferLen);
    if (Result == EOS_EResult::EOS_Success)
        return std::string(Buffer, BufferLen);

    return "";
}

// EOS_Connect_Login completion callback
EOS_DECLARE_CALLBACK(OnConnectLoginComplete,
    const EOS_Connect_LoginCallbackInfo* Data)
{
    if (Data->ResultCode == EOS_EResult::EOS_Success) {
        g_PlayerId = EOSProductUserIdToString(Data->LocalUserId);
        // Now safe to begin sending Linkzly events
        OnLinkzlySessionStart(g_PlayerId);
    }
}

void LoginWithEOS(EOS_HConnect ConnectHandle,
                  EOS_Connect_Credentials* Credentials)
{
    EOS_Connect_LoginOptions LoginOptions = {};
    LoginOptions.ApiVersion  = EOS_CONNECT_LOGIN_API_LATEST;
    LoginOptions.Credentials = Credentials;
    LoginOptions.UserLoginInfo = nullptr;

    EOS_Connect_Login(ConnectHandle, &LoginOptions,
        nullptr, OnConnectLoginComplete);
}

Step 3: HTTP Transport

You have two options for sending HTTP requests to the Linkzly ingestion endpoint. Choose the one that fits your project.


Option A: EOS HTTP Interface

The EOS HTTP interface routes requests through the Epic environment and respects Epic's network configuration. It is the recommended choice for EGS titles.

#include "eos_http.h"
#include <string>

// Completion callback — fired by EOS_Platform_Tick()
void LinkzlyHttpCallback(const EOS_HTTP_RequestCallbackInfo* Data)
{
    if (!Data) return;

    if (Data->ResponseCode == 202) {
        // Batch accepted — optionally parse Data->ResponseBody for batch_id and trace_id
    } else {
        // Log Data->ResponseCode and Data->ResponseBody for debugging
        // Re-queue events if the error is transient (5xx)
    }

    // Release the request handle after the callback fires
    EOS_HHTTPRequests HTTPHandle =
        EOS_Platform_GetHTTPRequestsInterface(g_EOSPlatformHandle);
    EOS_HTTP_ReleaseRequest(HTTPHandle, Data->RequestHandle);
}

void SendEventBatchEOS(
    const std::string& jsonBody,
    const std::string& sdkKey,
    const std::string& orgId,
    const std::string& gameId,
    const std::string& signature,
    const std::string& timestamp,
    const std::string& nonce)
{
    EOS_HHTTPRequests HTTPHandle =
        EOS_Platform_GetHTTPRequestsInterface(g_EOSPlatformHandle);

    // 1. Create the request
    EOS_HTTP_CreateRequestOptions CreateOpts = {};
    CreateOpts.ApiVersion = EOS_HTTP_CREATEREQUEST_API_LATEST;
    EOS_HHTTPRequest HTTPRequest = EOS_HTTP_CreateRequest(HTTPHandle, &CreateOpts);

    // 2. Set method to POST
    EOS_HTTP_SetMethodOptions MethodOpts = {};
    MethodOpts.ApiVersion = EOS_HTTP_SETMETHOD_API_LATEST;
    MethodOpts.Method     = EOS_HTTP_EMethod::EOS_HTTP_EMethod_Post;
    EOS_HTTP_SetMethod(HTTPRequest, &MethodOpts);

    // 3. Set URL
    EOS_HTTP_SetURLOptions URLOpts = {};
    URLOpts.ApiVersion = EOS_HTTP_SETURL_API_LATEST;
    URLOpts.URL        = "https://gaming.linkzly.com/api/v1/gaming/events";
    EOS_HTTP_SetURL(HTTPRequest, &URLOpts);

    // 4. Add authentication and signing headers
    EOS_HTTP_AddRequestHeader(HTTPRequest, "Authorization",
        ("Bearer " + sdkKey).c_str());
    EOS_HTTP_AddRequestHeader(HTTPRequest, "X-Organization-ID", orgId.c_str());
    EOS_HTTP_AddRequestHeader(HTTPRequest, "X-Game-ID",         gameId.c_str());
    EOS_HTTP_AddRequestHeader(HTTPRequest, "Content-Type",      "application/json");
    EOS_HTTP_AddRequestHeader(HTTPRequest, "X-Signature-256",   signature.c_str());
    EOS_HTTP_AddRequestHeader(HTTPRequest, "X-Timestamp",       timestamp.c_str());
    EOS_HTTP_AddRequestHeader(HTTPRequest, "X-Nonce",           nonce.c_str());

    // 5. Set body
    EOS_HTTP_SetBodyOptions BodyOpts = {};
    BodyOpts.ApiVersion       = EOS_HTTP_SETBODY_API_LATEST;
    BodyOpts.Body             = jsonBody.c_str();
    BodyOpts.BodyLengthBytes  = static_cast<uint32_t>(jsonBody.size());
    EOS_HTTP_SetBody(HTTPRequest, &BodyOpts);

    // 6. Send asynchronously
    EOS_HTTP_SendRequestOptions SendOpts = {};
    SendOpts.ApiVersion        = EOS_HTTP_SENDREQUEST_API_LATEST;
    SendOpts.RequestCallbackFn = LinkzlyHttpCallback;
    SendOpts.ClientData        = nullptr;
    EOS_HTTP_SendRequest(HTTPRequest, &SendOpts);
}

Required: EOS_Platform_Tick() must be called on every game loop frame for LinkzlyHttpCallback to fire. Without this, HTTP callbacks never execute.


Option B: libcurl

libcurl is a common alternative for studios that already have a libcurl integration or that need synchronous or background-thread HTTP. Run libcurl requests on a dedicated network thread to avoid blocking the game loop.

#include <curl/curl.h>
#include <string>

static size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* out)
{
    out->append(static_cast<char*>(contents), size * nmemb);
    return size * nmemb;
}

bool SendEventBatchCurl(
    const std::string& jsonBody,
    const std::string& sdkKey,
    const std::string& orgId,
    const std::string& gameId,
    const std::string& signature,
    const std::string& timestamp,
    const std::string& nonce)
{
    CURL* curl = curl_easy_init();
    if (!curl) return false;

    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-Signature-256: " + signature).c_str());
    headers = curl_slist_append(headers,
        ("X-Timestamp: " + timestamp).c_str());
    headers = curl_slist_append(headers,
        ("X-Nonce: " + nonce).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, static_cast<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);

    return (res == CURLE_OK && httpCode == 202);
}

Step 4: HMAC Signing

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.

The signing string format is: {timestamp}.{nonce}.{body}

OpenSSL Implementation

OpenSSL ships with the EOS SDK on most platforms and is the recommended choice:

#include <openssl/hmac.h>
#include <string>
#include <sstream>
#include <iomanip>

std::string ComputeHmacSha256(
    const std::string& signingSecret,
    const std::string& timestamp,
    const std::string& nonce,
    const std::string& body)
{
    // Build the 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(),
         signingSecret.data(), static_cast<int>(signingSecret.size()),
         reinterpret_cast<const unsigned char*>(signingString.data()),
         signingString.size(),
         digest, &digestLen);

    std::ostringstream hex;
    for (unsigned int i = 0; i < digestLen; i++)
        hex << std::hex << std::setw(2) << std::setfill('0')
            << static_cast<int>(digest[i]);

    return hex.str();
}

mbedTLS Implementation (Alternative)

Use mbedTLS if OpenSSL is not available in your build environment:

#include "mbedtls/md.h"
#include <string>
#include <sstream>
#include <iomanip>

std::string ComputeHmacSha256_mbedTLS(
    const std::string& signingSecret,
    const std::string& timestamp,
    const std::string& nonce,
    const std::string& body)
{
    std::string signingString = timestamp + "." + nonce + "." + body;

    unsigned char hmac[32];
    const mbedtls_md_info_t* mdInfo =
        mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);

    mbedtls_md_hmac(
        mdInfo,
        reinterpret_cast<const unsigned char*>(signingSecret.data()),
        signingSecret.size(),
        reinterpret_cast<const unsigned char*>(signingString.data()),
        signingString.size(),
        hmac);

    std::ostringstream hex;
    for (int i = 0; i < 32; i++)
        hex << std::hex << std::setw(2) << std::setfill('0')
            << static_cast<int>(hmac[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 are rejected.


Step 5: JSON Serialization

#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",     "epic"},
                {"player_id",    playerId},
                {"session_id",   sessionId},
                {"sdk_version",  "1.0.0"},
                {"game_version", gameVersion}
            }
        })}
    };
    return payload.dump();
}

Step 6: Session Lifecycle

Tie session start and end to EOS Connect authentication state:

#include "eos_connect.h"
#include <string>

static std::string g_SessionId;

// Called after EOS Connect Login succeeds (see Step 2)
void OnLinkzlySessionStart(const std::string& playerId)
{
    g_SessionId = GenerateUUID();

    std::string body      = BuildEventPayload(
        playerId, g_SessionId, "session_start", "1.0.0");
    std::string timestamp = GetTimestampMs();
    std::string nonce     = GenerateUUID();
    std::string signature = ComputeHmacSha256(
        "// YOUR_SIGNING_SECRET", timestamp, nonce, body);

    SendEventBatchEOS(body, "// YOUR_SDK_KEY",
        "// YOUR_ORG_ID", "// YOUR_GAME_ID",
        signature, timestamp, nonce);
}

// EOS Auth Expiration callback — fired when the user's EOS token expires
EOS_DECLARE_CALLBACK(OnAuthExpiration,
    const EOS_Connect_AuthExpirationCallbackInfo* Data)
{
    std::string body      = BuildEventPayload(
        g_PlayerId, g_SessionId, "session_end", "1.0.0");
    std::string timestamp = GetTimestampMs();
    std::string nonce     = GenerateUUID();
    std::string signature = ComputeHmacSha256(
        "// YOUR_SIGNING_SECRET", timestamp, nonce, body);

    SendEventBatchEOS(body, "// YOUR_SDK_KEY",
        "// YOUR_ORG_ID", "// YOUR_GAME_ID",
        signature, timestamp, nonce);
}

// Register the callback after EOS Connect Login succeeds
void RegisterAuthExpirationCallback(EOS_HConnect ConnectHandle)
{
    EOS_Connect_AddNotifyAuthExpirationOptions Opts = {};
    Opts.ApiVersion = EOS_CONNECT_ADDNOTIFYAUTHEXPIRATION_API_LATEST;

    EOS_Connect_AddNotifyAuthExpiration(ConnectHandle, &Opts,
        nullptr, OnAuthExpiration);
}

// On application exit
void OnGameExiting()
{
    std::string body      = BuildEventPayload(
        g_PlayerId, g_SessionId, "session_end", "1.0.0");
    std::string timestamp = GetTimestampMs();
    std::string nonce     = GenerateUUID();
    std::string signature = ComputeHmacSha256(
        "// YOUR_SIGNING_SECRET", timestamp, nonce, body);

    // Prefer libcurl for the exit flush since EOS_Platform_Tick() won't run
    SendEventBatchCurl(body, "// YOUR_SDK_KEY",
        "// YOUR_ORG_ID", "// YOUR_GAME_ID",
        signature, timestamp, nonce);
}

Exit flush: Because EOS_Platform_Tick() stops running when the game exits, the EOS HTTP interface may not deliver the final session_end event. Use libcurl (synchronous) for the exit flush to guarantee delivery.


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()
{
    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"
}

Standard Event Types

Event Type Description
session_start Player begins a game session
session_end Player ends a game session
level_start Player enters a level
level_complete Player completes a level
purchase In-game purchase
ad_impression Ad shown to player
custom Any custom event

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 Confirm the signing string is {timestamp}.{nonce}.{body} with no extra characters. Check system clock is NTP-synced; replay window is 300 seconds
EOS HTTP callback never fires Ensure EOS_Platform_Tick() is called every frame; without it, no EOS callbacks execute
EOS_Platform_GetHTTPRequestsInterface returns null EOS platform was not successfully initialized; check EOS_Platform_Create return value
EOS_ProductUserId_ToString fails Ensure EOS_Connect_Login completed successfully before calling
libcurl CURLE_SSL_CONNECT_ERROR Verify CURLOPT_SSL_VERIFYPEER is 1L and a valid CA bundle is available
Events dropped on exit Use libcurl for the final session_end flush; EOS HTTP won't deliver callbacks after EOS_Platform_Tick() stops

Next Steps

Was this helpful?

Help us improve our documentation