C# (HttpClient) examples — Ludex ingestion
Bare .NET HttpClient — no Ludex SDK. Targets POST /v1/ingest/event and POST /v1/ingest/batch with JSON and 202 success responses.
Add System.Text.Json (included in modern .NET) for serialization.
Configuration
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
const string BaseUrl = "https://ingest.ludexstudio.com"; // or the URL Ludex provides
const string ApiKey = Environment.GetEnvironmentVariable("LUDEX_API_KEY")!;
const string ProjectId = Environment.GetEnvironmentVariable("LUDEX_PROJECT_ID")!;
const string EnvironmentName = Environment.GetEnvironmentVariable("LUDEX_ENVIRONMENT")!;
EnvironmentName must match the credential’s stored environment id (same as the environment field in JSON).
Build a shared HttpClient
Reuse one client per process (socket pooling). Set the API key on default headers or per request.
using var http = new HttpClient { BaseAddress = new Uri(BaseUrl.TrimEnd('/') + "/") };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ApiKey);
Single event (scope: ingest:events)
Use explicit dictionary keys so the JSON matches the API (event, snake_case fields) without relying on naming policies:
var payload = new Dictionary<string, object?>
{
["project_id"] = ProjectId,
["environment"] = EnvironmentName,
["event"] = new Dictionary<string, object?>
{
["event_name"] = "level_complete",
["timestamp"] = DateTime.UtcNow.ToString("o"),
["player_id"] = "player-uuid-123",
["session_id"] = Guid.NewGuid().ToString(),
["platform"] = "Windows",
["properties"] = new Dictionary<string, object?> { ["level"] = 3, ["score"] = 1200 },
["sdk"] = new Dictionary<string, string>
{
["name"] = "csharp-httpclient-example",
["version"] = "1.0.0",
},
["event_id"] = Guid.NewGuid().ToString(),
},
};
var json = JsonSerializer.Serialize(payload);
using var req = new HttpRequestMessage(HttpMethod.Post, "v1/ingest/event")
{
Content = new StringContent(json, Encoding.UTF8, "application/json"),
};
req.Headers.TryAddWithoutValidation("Idempotency-Key", $"idem-{Guid.NewGuid():N}");
req.Headers.TryAddWithoutValidation("X-Correlation-Id", "corr-demo-001");
using var resp = await http.SendAsync(req);
var body = await resp.Content.ReadAsStringAsync();
resp.EnsureSuccessStatusCode(); // throws if not 2xx — ingestion uses 202
Console.WriteLine((int)resp.StatusCode);
Console.WriteLine(body);
Batch (scope: ingest:batch)
var batch = new Dictionary<string, object?>
{
["project_id"] = ProjectId,
["environment"] = EnvironmentName,
["events"] = new object[]
{
new Dictionary<string, object?>
{
["event_name"] = "match_started",
["timestamp"] = DateTime.UtcNow.ToString("o"),
["session_id"] = "sess-1",
["properties"] = new Dictionary<string, object?> { ["mode"] = "ranked" },
},
new Dictionary<string, object?>
{
["event_name"] = "match_ended",
["timestamp"] = DateTime.UtcNow.ToString("o"),
["session_id"] = "sess-1",
["properties"] = new Dictionary<string, object?> { ["duration_sec"] = 900 },
},
},
};
var batchJson = JsonSerializer.Serialize(batch);
using var batchReq = new HttpRequestMessage(HttpMethod.Post, "v1/ingest/batch")
{
Content = new StringContent(batchJson, Encoding.UTF8, "application/json"),
};
using var batchResp = await http.SendAsync(batchReq);
var batchBody = await batchResp.Content.ReadAsStringAsync();
batchResp.EnsureSuccessStatusCode();
Console.WriteLine(batchBody);
Notes
- 202 Accepted is success; do not treat non-200 as failure if you only check
StatusCode == HttpStatusCode.OK. - On 429 / 5xx, apply backoff and retry; on 401 / 403 / 422, fix credentials or payload.
- Keep
event_idstable for the same logical event when retrying after timeouts.