Welcome to the documentation for the GameAnalytics Collector REST API. ⚡️

A Collector is a GameAnalytics server (one of many) that receive and collect game events being submitted using the HTTP protocol.
All official GameAnalytics SDK’s are using this same API.

API basics

Game Keys

The following keys are needed when submitting events.

  • game key
  • secret key

The game key is the unique identifier for the game.
The secret key is used to protect the events from being altered as they travel to our servers.

To obtain these keys it is needed to register/create your game at our GameAnalytics tool. Locate them in the game settings after you create the game in your account.

API Endpoints

HTTP and HTTPS are supported.
Data is submitted and received as JSON strings.
Gzip is supported and strongly recommended.


API endpoint for production


API endpoint for sandbox

  • Use the sandbox-api endpoint and sandbox keys during the implementation phase.
  • Switch to the production endpoint and production keys once the integration is completed.

Create your production keys in our GameAnalytics tool.

Do not use production keys on the sandbox-api; it will only authorise the sandbox game keys.

Sandbox Keys

Key Code
Game Key 5c6bcb5402204249437fb5a7a80a4959
Secret Key 16813a12f718bc5c620f56944e1abc3ea13ccbac

Event Processing

Events will be registered at the realtime dashboard after a few minutes.

Once the time reaches 00:00 UTC all events for yesterday (the last 24hours) will be aggregated and thereby visible in all other areas of our tool.


It is highly recommended to gzip the data when submitting.

  • Set the header Content-Encoding header to gzip
  • Gzip the events JSON string and add the data to the POST payload
  • Calculate the HMAC Authorization header using the gzipped data

The collector has a POST size limit of 1MB.
Read more on the troubleshooting section.

Look at the code example for gzip and HMAC in python below. We also provide a full example with python at the end of this document.


Authentication is handled by specifying the Authorization header for the request.

The authentication value is a HMAC SHA-256 digest of the raw body content from the request using the secret key (private key) as the hashing key and then encoding it using base64.
Look at the code examples for shell, python and C#.





Header Value Comment
Authorization HMAC HASH The authentication hash.
Content Type application/json Required.
Content Encoding gzip Optional. Set only if payload is gzipped.
Content Length [ length of payload ] Optional. Set if possible.


2 routes are supported.

  • Init
  • Events

The API version is part of the URL. Currently it’s v2.
Replace the <game_key> in the URL with your specific game key.

Validation for JSON body content is described later in the documentation.


POST /v2/<game_key>/init

The init call should be requested whenever a new session starts or at least when the game is launched.

Use the init call to determine if submitting events should be disabled and to adjust client timestamp.

Do not send any events if the init call did not reply correctly or the enabled flag is not true. If a faulty implementation is detected (straining the servers) GameAnalytics can disable using this field. This rarely happens. It could be on certain devices that might cause issues and pollute the data. If the game is still submitting (not respecting the flag) then it might be entirely disabled.





The POST request should contain a valid JSON object as in the body containing the following fields.

Field Description
Platform A string representing the platform of the SDK, e.g. “ios”
os_version A string representing the OS version, e.g. “ios 8.1”
sdk_version Custom solutions should ALWAYS use the string “rest api v2”

The server response is a JSON object with the following fields.

Field Description
enabled A boolean. Events should ONLY be sent if this field is present and set to true. If not true then deactivate.
server_ts An integer timestamp of the current server time in UTC (seconds since EPOCH).
flags An array of strings. Not used at the moment. In the future this could contain flags set by GA servers to control SDK behaviour. Make sure the code does not break if this contain values in the future.

Adjust client timestamp

The server_ts should be used if the client clock is not configured correctly.

  • Compare the value with the local client timestamp.
  • Store the offset in seconds.
  • Use this offset whenever an event is added to adjust the local timestamp (client_ts) for the event


POST /v2/<game_key>/events

The events route is for submitting events.

The POST body payload is a JSON list containing 0 or more event objects. Like this example:

Events always need to be in a list even if you are sending only 1 event.

An event object contain all data related to a specific event triggered. Each type of event is defined by a unique category field and each require specific fields to be defined.

If the status code 200 is returned then the request succeeded and all events were collected.

Read more about error responses (looking for the reason when validation should fail) in the troubleshooting guide.

When your game implementation is running it should cache events (local db recommended) and periodically (each 20 seconds for example) submit all stored events using the events route.

When offline the code should also queue events and submit once the connection is restored. Recommended to define a maximum size for the cache that (when exceeded) will activate a trim deleting all events for the oldest sessions.






Try it yourself now!

Install the Postman app or Chrome extension and click the button to import some request examples to run.

Run Postman

Event Types

Events are JSON objects with a certain set of required fields as well as optional fields. The exact requirements of a valid event depends on its category. Here are the available categories (event types).

  • user (session start)
  • session_end
  • business
  • progression
  • resource
  • design
  • troubleshooting
  • error

All the events share (inherit) a list of fields that we call the default annotations.

Default annotations (shared)

Each event has unique fields defining the event. But all events need to include shared fields called default annotations.

The default annotation fields define information like os_version, platform etc. and they need to be included in each event object. Some are required and some are optional.

It is recommended to create a method collecting the shared default annotations and returning a dictionary. When an event is triggered this method should be called and the specific event fields (for the event type) should be added to the dictionary. The merged dictionary is then used to create the JSON event object.

Field Validation Type Required Description
device short string Yes examples: “iPhone6.1”, “GT-I9000”. If not found then “unknown”.
v integer Yes Reflects the version of events coming in to the collectors. Current version is 2.
user_id string Yes Use the unique device id if possible. For Android it’s the AID. Should always be the same across game launches.
client_ts client ts integer Yes Timestamp when the event was created (put in queue/database) on the client. This timestamp should be a corrected one using an offset of time from server_time.

  1. The SDK will get the server TS on the init call (each session) and then calculate a difference (within some limit) from the local time and store this ‘offset’.
  2. When each event is created it should calculate/adjust the ‘client_ts’ using the ‘offset’.
sdk_version sdk version Yes The SDK is submitting events to the servers. For custom solutions ALWAYS use “rest api v2”.
os_version os version Yes Operating system version. Like “android 4.4.4”, “ios 8.1”.
manufacturer short string Yes Manufacturer of the hardware the game is played on. Like “apple”, “samsung”, “lenovo”.
platform platform Yes The platform the game is running. Platform is often a subset of os_version like “android”, “windows” etc.
session_id session id Yes Generate a random lower-case string matching the UUID format. Example:
session_num unsigned integer Yes The SDK should count the number of sessions played since it was installed (storing locally and incrementing). The amount should include the session that is about to start.
limit_ad_tracking boolean No Send true if detected. Very important to always check this when using iOS idfa.
logon_gamecenter boolean No Send true if detected. Logged in to GameCenter.
logon_gameplay boolean No Send true if detected. Logged in to Google Play.
jailbroken boolean No If detected that device is jailbroken (hacked) or not. Should only be sent when true
android_id string No Send this if on Android and the google_aid is not available (e.g. Android phones without the play store)
googleplus_id string No Send if found.
facebook_id string No Send if found. Should be stored cross-session and sent along always after that.
gender gender string No Send if found. Should be stored cross-session and sent along always after that.
facebook_id string No Send if found. Should be stored cross-session and sent along always after that.
birth_year birthyear integer No Send if found. Should be stored cross-session and sent along always after that.
custom_01 short string No Send Custom dimension 1 if that is currently active/set.
custom_02 short string No Send Custom dimension 2 if that is currently active/set.
custom_03 short string No Send Custom dimension 3 if that is currently active/set.
build short string No Send if needed. A build version. Should be set before any events are sent.
engine_version engine version No Send if using engine. examples: “unreal 4.7” or “unity 5.6.10”
ios_idfv string No Send if iOS. Apple’s identifier for vendors. This is unique per app/game.
connection_type connection type string No Send if found. This will give the connection status of a device – how the device is connected to the internet (or if not). (offline, wwan, wifi, lan)
ios_idfa string No Send if iOS. Apple’s identifier for advertisers. This is the same across apps/games. Send this always (on iOS) and make sure to ALWAYS check if user has enabled “limited_ad_tracking”. If so then add the field mentioned elsewhere and targeting for that idfa will not happen.
google_aid string No Send if Android. Google’s identifier for advertisers. This is the same across apps/games. Read more about it here.


Do something like the following to track this value.

  • Use a persistent DB like SqlLite for storing queued events and other information.
  • Create a table called key_value with key & value columns. Use this column for storing all variables cross game-launch.
  • For counting session number use a key called session_num.
  • When a session is started this row should be retrieved. Increment the value and update row.

custom_01, custom_02, custom_03

Read more about Custom Dimensions here.

JSON validation schema.
The collector servers will validate these fields and reject any event not passing.

User (session start)

As session is the concept of a user spending a period of time focused on a game.

The user event acts like a session start. It should always be the first event in the first batch sent to the collectors and added each time a session starts.

Field Unique Limit Description/Validation
category Yes user

+ add the default annotation fields

JSON validation schema.
The collector servers will validate these fields and reject any event not passing.

Session end

Whenever a session is determined to be over the code should always attempt to add a session end event and submit all pending events immediately.

Only one session end event per session should be activated.

Field Required Description/Validation
category Yes session_end
length Yes Session length in seconds

+ add the default annotation fields

On mobile devices the session normally stops when a user tap the home button and the app is not visible.

On certain phones (like Android) there is also a pause and stop event where the session will end on stop but also if pause has been active for 20 seconds or so.

Different platforms implement varying behaviour but all with the same goal – to end the session when the user is no longer focused on the game.

Session length

Session length is the amount of seconds spent focused on a game. Whenever a session starts the current timestamp should be stored in a variable. When session end is triggered this values is used to calculate the session length.

Detecting missing session end on game launch

Sometimes the session end event could not be added as the game closed without giving time to finish processing. It is recommended to implement code that is able to detect this on game launch and add the missing session end with correct session length.

This should be solved using a local storage that will work cross session/game-launch. In our official SDK implementation we use SqlLite. The following practise describe how to detect and submit a missing session end.

  • Use a persistent DB like SqlLite for storing queued events and other information.
  • Create a table called session_end with the columns session_start_ts, session_id & last_event_default_annotations.
  • Each time (in that session) an event is added to the queue (excluding session end) this row is updated on the column last_event_default_annotations containing the shared default annotations for the particular event (including the field client_ts for the event). If the event added is a session end, then simply delete the row matching the session.
  • On game launch query the session_end table. If an entry exists then there is a session that did not have time to add a session end. Look inside the last_event_default_annotations and find client_ts. Use that value with the session_start_ts to calculate session length. Use the last_event_default_annotations to create the session end event. Add event and submit.

JSON validation schema.
The collector servers will validate these fields and reject any event not passing.


Business events are for real-money purchases.

Field Required Description/Validation
category Yes business
event_id Yes A 2 part event id.
❗️ Read about unique value limitations here.
amount Yes The amount of the purchase in cents (integer)
currency Yes Currency need to be a 3 letter upper case string to pass validation.
In addition the currency need to be a valid currency for correct rate/conversion calculation at a later stage. Look at the following link for a list valid currency values.
transaction_num Yes Similar to the session_num. Store this value locally and increment each time a business event is submitted during the lifetime (installation) of the game/app.
cart_type No A string representing the cart (the location) from which the purchase was made. Could be menu_shop or end_of_level_shop.
❗️ Read about unique value limitations here.
receipt_info No A JSON object that can contain 3 fields: store, receipt and signature. Used for payment validation of receipts. Currently purchase validation is only supported for iOS and Android stores.
For iOS the store is apple and the receipt is base64 encoded. For Android the store is google_play and the receipt is base64 encoded + the IAP signature is also required.

Look at the validation schema later to see the structure of this receipt_info object.

+ add the default annotation fields

itemType & itemId

The itemType is like a category/folder for items and the ItemId is an identifier for what has been purchased. They are separated by a semicolon.


  • BlueGemPack:GameAnalytics.blue_gems_50
  • BlueGemPack:GameAnalytics.blue_gems_100
  • BlueGemPack:GameAnalytics.blue_gems_500
  • Boost:MegaBoost
  • Boost:SmallBoost

In the GameAnalytics tool it is possible to select the itemId and get detailed information. But it is also possible to select the itemType and thereby get aggregated values for all itemIds within. This could be visualised like a histogram for each or simply showing the total revenue for all BlueGemPacks over time.

Transaction number

Do something like the following to track this value:

  • Use a persistent DB like SqlLite for storing queued events and other information.
  • Create a table called key_value with key value columns. Use this column for storing all variables cross game-launch.
  • For the counting transactions use a key called transaction_num.
  • Each time a business event is triggered this row is retrieved, value is incremented and row is updated.

Purchase validation

The result of validating receipts is monetization metrics being divided into valid and non-valid in the GameAnalytics tool.

Currently purchase validation is only supported for iOS and Android stores. We are working on adding more ways (stores) for validating purchases. This feature is not meant to provide validation inside the game to block hackers. It is intended to provide valid numbers in GameAnalytics by flagging business events from monetizer hacks. Simply exclude the receipt_info field when using stores that are not supported yet.
JSON validation schema.
The collector servers will validate these fields and reject any event not passing.


Resource events are for tracking the flow of virtual currency registering the amounts users are spending (sink) and receiving (source) for a specified virtual currency.

Field Required Description/Validation
category Yes resource
event_id Yes A 4 part event id string.
❗️ Read about unique value limitations here.
amount Yes The amount of the in game currency (float). This value should be negative if flowType is Sink. For instance, if the players pays 100 gold for a level, the corresponding resource event will have the amount -100 added in this field.

+ add the default annotation fields


Flow type is an enum with only 2 possible string values.

  • Sink means spending virtual currency on something.
  • Source means receiving virtual currency from some action.


A custom string defining the type of resource (currency) used in the event. Some examples of possible values.

  • Boost
  • Coins
  • Gems
  • Lives
  • Stars

itemType & itemId

The itemType functions like a category for the ItemId values.
The purpose/meaning of these values are different when using Sink or Source.

  • Sink item values should represent what the virtual currency was spent on.
  • Source item values should represent in what way the virtual currency was earned.
Examples flowType virtualCurrency itemType itemId
Life used to play level Sink life continuity startLevel
Star used to continue level Sink star continuity resumeLevel
Gold spent to buy rainbow boost Sink gold boost rainbowBoost
Earned a life by watching a video ad Source life rewardedVideo gainLifeAdColony
Bought gold with real money* Source gold purchase goldPack100

* When buying virtual currency for real money a business event should also be sent.

The resource event should not be spammed. Imagine an infinite runner game where coins are picked up quickly. Instead of submitting an event on each coin pickup the code should collect the gathered coins until the level is over and submit the amount.

JSON validation schema.
The collector servers will validate these fields and reject any event not passing


Progression events are used to track attempts at completing levels in order to progress in a game. There are 3 types of progression events.

  • Start
  • Fail
  • Complete
Field Required Description/Validation
category Yes progression
event_id Yes A 2-4 part event id.
❗️ Read about unique value limitations here.
attempt_num No The number of attempts for this level. Add only when Status is “Complete” or “Fail”. Increment each time a progression attempt failed for this specific level.
score No An optional player score for attempt. Only sent when Status is “Fail” or “Complete”.

+ add the default annotation fields


event_id attempt_num score
Fail:PirateIsland:Sandyhills 1 1234
Complete:PirateIsland:Sandyhills 2 1234


The progression evnetId will end up in the tool as a selectable metric with drilldown into a hierarchy. For example…

  • Progression > Fail > PirateIsland > Sandyhills View Fails on specific level SandyHills on PirateIsland.
  • Progression > Complete > PirateIsland > (all). View Completes for all levels on PirateIsland.

It is possible to use 1, 2 or 3 values depending on your game. For example…

  • PirateWorld:PirateIsland:SandyHills
  • PirateIsland:SandyHills
  • SandyHills


The Start progression event should be called when a user is starting an attempt at completing a specific level. The attempt will stop once Fail or Complete is called.

When a Start event is called the progression event_id (excluding the progression status) should be stored locally. For example Start:PirateIsland:SandyHills should store PirateIsland:SandyHills locally.

If the Start event is called when there is an ongoing attempt already in progress the code should add a Fail event for that attempt, before adding the new Start event.


The Fail progression event should be called when a user did not complete an ongoing level attempt. Add a score value if needed.

  • The user ran out of time or did not get enough points. Score screen is often shown.
  • The user is exiting the game.
  • A Start event was called during an ongoing attempt.


The Complete progression event should be called when a user did complete a level attempt. Add a score value if needed.

Handling attempt_num

The attempt_num is the number of times the user performed an attempt at completing a specific level (tracked for each progression event id). Once a complete is registered the counting is reset. Do something like the following to track the incrementing of progression attempts.

  • Use a persistent DB like SqlLite for storing queued events and other information.
  • Create a table called progression with progression_event_id attempt_num columns.
  • On progression Fail look inside the table for the specified progression_event_id
    • if it’s there then increment attempt_num for the row by 1 and use this value in the event
    • if it’s not there then create row for progression_event_id with attempt_num equal to 1 and use 1 in the event
  • On progression Complete look inside the table for the specified progression_event_id
    • if it’s there then get the attempt_num value, use this value+1 in the event and delete the row
    • if it’s not there then use 1 as the attempt_num

JSON validation schema.
The collector servers will validate these fields and reject any event not passing.


Every game is unique! Therefore it varies what information is needed to track for each game. Some needed events might not be covered by our other event types and the design event is available for creating a custom metric using an event id hierarchy.

Field Required Description/Validation
category Yes design
event_id Yes A 1-5 part event id.
❗️ Read about unique value limitations here.
value No Optional value. float.

+ add the default annotation fields


GamePlay:Kill:[monster_type] and value equal to points gained. In the GameAnalytics explorer tool you can now select the following metrics.

Metric Selection Description
GamePlay (all) Show all count/sum etc. from all events beneath GamePlay. These metrics are not that useful, but GamePlay is defined as a group to contain all gameplay related metrics. Other root groups could be Ads, Performance, UI etc.
GamePlay:Kill (all) Show count/sum etc. for all things tracked under GamePlay:Kill. Show either aggregated count/sum over time or histogram with bars for each monster_type. All points earned by killing (over time), the average points earned by killing (over time) or number of times something was killed (count).
GamePlay:Kill:AlienSmurf As the above, but only showing the specific monster_type.
GamePlay:Kill:(AlienSmurf + HumanRaider) almost same as above. Show both AlienSmurf and HumanRaider values in chart and values.

This example was a simple part of what the Explore tool can provide. Design events are used for many other things like Funnels, Segments, Cohorts etc.

The design events grant more freedom for deep hierarchies and therefore these have a limitation; custom dimension and progression filters will not work on design events. Therefore you cannot (at the moment) filter a selected design event metric by custom or progression dimensions in the tool.

Read more about limits, creating optimal event id’s and what you should track at our documentation page here.

The design event id structure (with the colon separation) represents a tree hierarchy. It is very important to not generate an excessive amount of unique nodes possible in the event hierarchy tree.

A bad implementation example.

Potential unique values for each.
monster_type=100 armor_used=300 weapon_used=300 damage_done=5000

100 * 300 * 300 * 5000 = possible nodes.

This is far too many.
The damage should be put as a value and not in the event string. Even though we remove the damage string it will still be 90M nodes. The processing will be blocked when doing this and could cause other problems when browsing our tool.

Only use deep hierarchies when using values with a small distinct pool of possible values. Do NOT use any dynamic values inside the string like timestamps or other data that change over time.

The maximum amount of unique nodes generated should be around 10k.

JSON validation schema.
The collector servers will validate these fields and reject any event not passing.


An Error event should be sent whenever something horrible has happened in your code – some Exception/state that is not intended.

The following error types are supported:

  • debug
  • info
  • warning
  • error
  • critical

Do not send more than 10 error events pr. game launch!
The error events can put a high load on the device the game is running on if every Exception is submitted. This is due to Exceptions having a lot of data and that they can be fired very frequently (1000/second is possible).

The GameAnalytics servers will block games that send excessive amounts of Error events.

Simple solution
Keep track of how many is sent and stop sending when the threshold is reached.

More advanced
Keep track of each type of Exception sent in a list. If an error event match a type already sent then ignore. If 10 types have been sent then stop sending. This will ensure that 10 similar Error events fired quickly will not result in other types not being discovered.

The idea is that developers should discover an error in the GameAnalytics tool and then fix the cause by submitting a new version of the app. Even with the limit of 10 error events this should still be possible.

Field Required Description/Validation
category Yes error
severity Yes The type of error severity.
message Yes Stack trace or other information detailing the error. Can be an empty string.

+ add the default annotation fields

The error event support large stack traces and therefore these events have a limitation; custom dimension and progression filters will not work on error events. Therefore you cannot (at the moment) filter a selected error event metric by custom or progression dimensions in the tool.

JSON validation schema.
The collector servers will validate these fields and reject any event not passing.

Custom Dimensions

Specifying custom dimensions will enable filters in the GameAnalytics tool. These filters can be used on other metrics (like DAU or Revenue for example) to view only numbers for when that dimension value was active.
To enable these you have to use the fields custom_01  custom_02 custom_03.

Example for tracking player class

  1. player changes class to ninja and this is registered locally (variable and local db) as custom_01
  2. an event is triggered and custom_01 value is set in the event
  3. player changes class to wizard and this is registered locally (variable and local db) as custom_01
  4. an event is triggered and custom_01 value is set in the event
  5. session is ending and the session end event also get the custom_01 value
  6. session starts (for example game launch)
    • custom_01 (+ the other 2) is retrieved from local db (if found)
    • the user event (session start) get the retrieved custom_01 value added
  7. an event is triggered and custom_01 value is set in the event
  8. player removes class and custom_01 is set to empty (also in db)
  9. Custom_01 is not added on any further events

This will allow dimension custom_01 to have the filter values ninja and wizard available for selection in the tool. For example visualising Daily Active Users (DAU) filtered by players who were playing a ninja.

Use only custom dimensions if the need is for filtering all other data.

Often a design event is a much better fit and will not use the more expensive and limited custom dimensions. Use a design event if you need counting and/or value aggregation for that specific action. For example when the player chooses a class you could track a design event with class:ninja or class:wizard and even include a value (like money in the bank or XP at the time). This will give you counts, sum, mean, histogram etc.

  • Think about the need you have for filtering data
  • Decide what area each custom dimension should track
  • Decide on a finite list of dimension values for each. Using too many will block your game
  • Don’t use all 3 straight away unless you are certain of how to use it

DO NOT use custom dimensions for these

As they are already tracked or can be tracked in other ways.

  • device information
  • os version
  • gender
  • age
  • platform
  • level progression (use progression)
  • never use float or timestamps or other dynamic content

Popular usage for custom dimensions

  • player class (ninja, wizard)
  • player affiliation (horde, alliance)
  • player persona (social, power_gamer)
  • player level (1_4, 5_8, 9_12 .. 97_100)


These are the technical limitations currently for sending events.

  • size of POST request
  • event field validation
  • frequency of unique values

Size of POST request

The collector has a POST size limit of 1MB in the request body.
Read more in the troubleshooting section on 413 status codes.

Event field validation

When events are submitted they will each be validated. These validation rules are listed by each event in this documentation.
Read more in the troubleshooting section on 400 status code.

Frequency of unique values

GameAnalytics have some additional limitation regarding the frequency of unique values for certain events. If these thresholds are exceeded during the day then the GameAnalytics servers will start to throttle the game.


When a game is being throttled it means that certain processing of the raw data has stopped. This is often due to events containing too many unique values that result in aggregation being hard/impossible.

It is worth specifying that the collection of events is not suspended.

The result in the tool will be metrics flatlining. Many core metrics will still be there; like DAU etc. But event types (design, progression etc.) will not be updated.

How to solve it?

This happens rarely. Contact support and get information about why the throttle was activated and how to fix the implementation to avoid it.


These are the recommended unique value limitations.

Default annotations

Field Unique Limit
build 100
platform 30
device 500
os_version 255
progression 100 pr. event part
custom_01 50
custom_02 50
custom_03 50

Business Event

Field Unique Limit
event_id (itemType) 100
event_id (itemId) 100
cart_type 10

Resource Event

Field Unique Limit
event_id (virtualCurrency) 100
event_id (itemType) 100
event_id (itemId) 100

Design Event

Field Unique Limit
event_id (entire string) 50000*

* This is a very large threshold. The amount of tree-nodes generated will also affect if the game will be throttled. Having this many is not recommended and it can affect the GameAnalytics tool experience (downloading that much information to the browser).


The servers validate fields and reject events that do not pass. Therefore it is valuable to know which field(s) did not pass validation and the reason why.

The most common HTTP response status codes 200, 401, 400.


200 : OK
The request went well. All possible events sent were collected successfully.


Authorization could not be verified. Either the game keys are not correct or the implementation is not done properly for calculating the HMAC hash for the Authentication header.


The collector has a size limit of 1MB in the request body.
If the post body is less than 2 times the max limit (between 1M and 2MB) you get a 413 response code.
If it is bigger than that you will get a closed connection from the collector.

Therefore when submitting a large amount of events the code should split them up. Use multiple requests one after the other. Again it is highly recommended to use gzip as this will reduce the size significantly.


This can happen in the following scenarios.

  • the data sent was not valid JSON (unable to decode)
  • the JSON data sent was not a list (read more here)
  • the JSON contents (events) failed validation

In the first 2 cases there will be very little information in the response.

The last case will happen when one or more events fail to match it’s validation schema.

The servers will then not collect the failed events but still collect the valid events. Then reply with a JSON string containing a list of objects for each event that failed. Each error object will contain information about all the fields that might have failed validation for that specific event and also include the event fields that were submitted.

When receiving a 400 status code during implementation please review the response JSON to ascertain if it’s valid and a list. Then review what fields did not pass validation and fix.

If a batch contains two events (A and B) and A fails validation but B passes then A will be rejected and B collected.

Due to this – do not resubmit event data when a 400 response is received.

Look at the following 400 response error reply snippet example in JSON:

Re-submitting events?

When queued events are sent to the collector servers they should each be marked locally as being submitted.

If the attempt failed due to no connection (no network etc.) or 413 (body too large) then the being submitted events should be put into queue again. If you get a 413 then try to split the events into even smaller batches and then submit. For all other responses (200, 401, 400 etc.) these events should be wiped.

Do not keep events and resubmit based on other reasons than offline or the 413 response.

In production a 400 response should be logged and the implementation fixed for the next time the game is released.

Character encoding

Make sure your character encoding is UTF-8 for strings.
Certain event_id strings require specific characters validated by matching a regex.

An example is the progression event_id string. This requires a match for this regex:

The validation for this event_id can fail if using UTF-8 strings dynamically (like level names). These might contain other characters then just a-Z and numbers. Make sure to encode strings to support the requirements.

Users are reporting issues with failing authentication using the UTF-8 standard in Java. In this case make sure you encode to the ISO-8859-1 standard instead.

Server Implementation

It is possible to submit events from a server on behalf of clients. A scenario could be a multiplayer server keeping track of sessions for all clients connected.

Even though this is possible it is not recommended.

Country lookup by IP

The GameAnalytics collectors will inspect the request IP and perform a GEO look-up for country information.

Note that GameAnalytics will not store the IP on our servers. It is only used to resolve to a country abbreviation string like US.

If all the events are submitted by a single server then the country for all users would be the same (the country the server is located in). This can be solved by forwarding the client IP in the request when sending events.

This is done using the standard X-Forwarded-For HTTP header. For example if your client has the IP then the header to be included in the request should look like this…


Read more about this header on wikipedia.

Request per user

As you can submit many events in the same request (a batch) it would be tempting to send events from multiple users at the same time. This is not recommended as you specify one IP per request in the header X-Forwarded-For and thus all the events submitted will be annotated with the country resolved by that single IP.

It is needed to submit a request per user and specify the IP. A way to obtain this on the server would be to…

  • collect intended events for users inside buckets per IP (user)
  • activate a synchronous loop (one after the other) that submit events for each bucket (user) using the IP in the forward header
  • wait a few seconds (15-20) and activate the loop again submitting events collected

This will make sure each user is resolved properly by country and our servers are not spammed with requests.

Session End

The server should keep track of the session time for each user. When it is detected that a user is no longer playing the game it is important to add (submit) a session end event for that user and session_id.

Python Example

Download a Python example here using the sandbox api and sandbox game key and secret. The code is implementing several key areas.

  • HMAC Authorization hash
  • gzipping
  • simple memory queue (use something like SqlLite instead)
  • server-time adjustment
  • default annotations function
  • init / event routes etc.

Run the example from the terminal:

Example steps

  • make an init call
    • check if disabled
    • calculate client timestamp offset from server
  • start a session
  • add a user event (session start) to queue
  • add a business event + some design events to queue
  • add some design events to queue
  • add session_end event to queue
  • submit events in queue