Skip to content

Direct HTTP Ingestion (protobuf)

To send telemetry without an official SDK, POST a protobuf-encoded TelemetryBatch directly to the ingest endpoint. Most integrations should use the official Unity, UE5, or Godot SDK instead. This page is the low-level path for engines without an SDK or teams with their own delivery pipeline.

FieldValue
Method and URLPOST https://ingest.framedash.dev/v1/events
Content-Typeapplication/x-protobuf
X-API-KeyAn Ingest key with the events:write scope
X-SDK-VersionSDK version identifier (e.g. custom-1.0.0)

Set Content-Length to match the actual body size. If you gzip the body, add Content-Encoding: gzip.

Keep the request body (after gzip compression, if used) at or below 126,000 bytes. A larger body is rejected with 413. A Content-Type other than application/x-protobuf, or a Content-Encoding other than gzip, returns 415. A missing Content-Length returns 411. The endpoint returns 202 with { "status": "accepted" } after it accepts the batch for asynchronous processing. This is not confirmation that every event passed downstream validation or was durably stored; a decodable batch can still contain invalid events that are discarded later.

Ingested events take about 20-30 seconds to appear in builds, queries, and the dashboard. A short delay does not mean ingestion failed. When testing a custom sender end to end, give one event a unique marker such as ingest_probe_<unique-id> and replace <unique-id> with a UUID or similar value for each run. After waiting, query that exact marker with a Full key carrying the data:admin scope. A Read-only key’s analytics:read scope can access aggregate analytics APIs, but it cannot run the raw-SQL framedash query used here:

Terminal window
framedash query --api-key-file full.key --project-id <project-id> \
"SELECT count(), max(timestamp) FROM events WHERE event_name = 'ingest_probe_<unique-id>'"

Only a count greater than zero, with a timestamp from the current run, confirms durable storage. Use a new marker for every test so an older test event cannot produce a false positive.

The body is the byte string of an encoded framedash.v1.TelemetryBatch message (proto3). api_key and sdk_version travel in HTTP headers (X-API-Key, X-SDK-Version), not in the payload. The full schema definition follows.

syntax = "proto3";
package framedash.v1;
// 3D position vector for spatial analytics (heatmaps, player tracking).
message Vector3 {
float x = 1;
float y = 2;
float z = 3;
}
// Source of the telemetry event.
enum TelemetrySource {
TELEMETRY_SOURCE_UNSPECIFIED = 0;
TELEMETRY_SOURCE_PLAYER = 1;
TELEMETRY_SOURCE_AUTOMATED = 2;
}
// Core telemetry event emitted by game SDKs.
// Each event maps to a single row in the ClickHouse `framedash.events` table.
message GameTelemetryEvent {
string event_name = 1;
// Unix epoch in microseconds. Stored as DateTime64(6) in ClickHouse.
int64 timestamp_us = 2;
string session_id = 3;
// Explicit player identifier — the only PII the SDK may collect.
string player_id = 4;
Vector3 position = 5;
string map_id = 6;
reserved 7;
reserved "zone_id";
float fps = 8;
float frame_time_ms = 9;
int64 memory_used_bytes = 10;
float gpu_time_ms = 11;
// Free-form string key-value pairs for game-specific context.
map<string, string> attributes = 12;
// Free-form numeric metrics for game-specific measurements.
map<string, double> metrics = 13;
// Source of the telemetry event.
TelemetrySource source = 14;
string build_id = 15;
string platform = 16;
string engine_version = 17;
// Camera orientation in degrees.
// camera_yaw: horizontal rotation (0 = North, clockwise, [0, 360)).
// camera_pitch: vertical rotation (-90 = straight down, +90 = straight up).
// 'optional' enables has-field presence tracking — distinguishes
// "not sent by SDK" (absent) from "facing North at level" (0, 0).
optional float camera_yaw = 18;
optional float camera_pitch = 19;
// CPU game thread time in milliseconds (logic, AI, physics).
// 0 = not collected (old SDK or unavailable).
float game_thread_ms = 20;
// CPU render thread time in milliseconds (draw commands, culling).
// 0 = not collected (old SDK or unavailable).
float render_thread_ms = 21;
}
// Batch wrapper sent by SDKs via POST /v1/events.
// api_key and sdk_version are transmitted in HTTP headers (X-API-Key, X-SDK-Version),
// not in the Protobuf payload.
message TelemetryBatch {
repeated GameTelemetryEvent events = 1;
}

Compile the .proto with protoc (or buf), build a TelemetryBatch with the generated code, and serialize it to bytes. Write the serialized bytes to a file and POST it with curl --data-binary.

Terminal window
# 1. Compile the proto (Python shown)
protoc --python_out=. telemetry.proto
# 2. In your app code, build a TelemetryBatch and write it to batch.bin
# 3. POST it
curl -X POST https://ingest.framedash.dev/v1/events \
-H "X-API-Key: fd_your_ingest_key_here" \
-H "X-SDK-Version: custom-1.0.0" \
-H "Content-Type: application/x-protobuf" \
--data-binary @batch.bin

Each event must satisfy the Data Model ingest validation limits. timestamp_us must fall within the last 30 days and no more than 48 hours in the future, event_name and session_id must be non-empty, string and map fields must stay within their bounds, and numeric metrics must be finite. The official SDKs clamp these values client-side before flushing.