Send batch and non-player events from your server
Many of the events you want to track aren’t produced on a player’s device. Match results, purchase validations, and other authoritative outcomes are decided on your server, and some events aren’t tied to any single player at all. Sending these from the server keeps them trustworthy and lets you record activity that no individual client is responsible for.
This guide walks you through how to use the Server-Event API to publish batch events spanning many identities (or none) in a single request.
Using the Server-Event API
#
Other ways of sending events, such as the client SDKs or Nakama’s satori.EventsPublish function, operate on a single authenticated player at a time. The Server-Event API differs in two ways:
- It accepts a batch of events spanning multiple identities, or none, in a single request.
- It doesn’t rely on a player session, so you authenticate with your API key directly rather than a player’s session token.
The Server-Events endpoint is v1/server-event. To send events using this API, the full URL you need to call is:
<<your-satori-server-url>>/v1/server-event
Authorization
#
The Server-Event API authenticates with an API key. Use a different API key for each event source to differentiate them in your data lake exports if needed.
To authorize your requests, use Basic Auth with your API key as the username and an empty password. See Console API for examples.
The Server-Event API expects an array of events under the events object in the request body. The following sample contains two events:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| {
"events":
[
{
"name": "purchaseCompleted",
"identityId": "00000000-0000-0000-0000-000000000001",
"timestamp": "2025-07-15T12:00:00.00Z",
"value": "",
"metadata": {
"test": "false",
"amount": "20",
"currency": "GBP"
}
},
{
"name": "packageDropped",
"timestamp": "2025-07-15T12:00:00.00Z",
"value": "3",
"metadata": {
"matchId": "C5B60A25-66E3-4924-B462-9B7E380B1E0D",
"dropNumber": "3"
}
}
]
}
|
All the possible fields in an event are detailed in the following table:
| Field Name | Type | Format | Required | Description |
|---|
name | string | – | Yes | Event name. |
id | string | – | No | Optional event ID assigned by the client, used to de-duplicate in retransmission scenarios. If not supplied, the server assigns a unique ID. |
metadata | object | – | No | Event metadata, if any. Keys and values are strings. |
value | string | – | No | Optional value. |
timestamp | string | date-time | Yes | The time when the event was triggered on the producer side. |
identity_id | string | – | No | The identity ID associated with the event. |
session_id | string | – | No | The session ID associated with the event. |
session_issued_at | string | int64 | No | The session “issued at” timestamp. |
session_expires_at | string | int64 | No | The session “expires at” timestamp. |
Batch acceptance behavior
#
When events are sent in a batch, each event is evaluated independently. A batch is not accepted or rejected as a single unit. This means valid events in the same batch can still be accepted even if other events are rejected. Rejected events can be reviewed in the debugger.
Use cases
#
In addition to supporting multiple identities, the Server-Event API offers several other use cases that can enhance event collection for your game. These are also available when you use other event publishing methods, however, this guide will focus on how they can be useful with the Server-Event API.
Sending events without an identity (non-player events)
#
Satori allows events to be ingested without any identity. These are called non-player events, and are useful for events tied to a group, game, or match rather than a player. For example, a package drop in a large-scale multiplayer game (like Fortnite) is a non-player event.
Non-player events are stored in Satori and are used to update metrics. However, the events are not listed under any identities as they are not bound to an identity. Instead, those events are passed to data lake adaptors and will be available for you to access from the data lake’s portal.
Sending events without a session
#
Satori, regardless of the source of the event, matches events with the most relevant session ID if an event is sent without any session information. Although this behavior is not specific to the Server-Event API, it’e useful when sending events from a server. If you’re tracking events from both clients and servers, this feature of Satori makes managing sessions more convenient.
Example events
#
These examples show how to send events in three common scenarios.
First, a purchaseCompleted event for a specific player, sent when the store validates an in-app purchase:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| {
"events":
[
{
"name": "purchaseCompleted",
"identityId": "00000000-0000-0000-0000-000000000001",
"timestamp": "2025-07-15T12:00:00.00Z",
"value": "",
"metadata": {
"test": "false",
"amount": "20",
"currency": "GBP"
}
}
]
}
|
This event carries no session information, so Satori links it to the latest available session, provided that session falls within the period configured by event.sessionless_events_grace_period_sec.
Next, an event sent during a match. In a co-op multiplayer shooter, the server reports a new enemy spawned for the game instance:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| {
"events":
[
{
"name": "enemySpawned",
"timestamp": "2025-07-15T12:00:00.00Z",
"value": "5",
"metadata": {
"matchId": "0078EC13-FDE4-44E7-990C-3ABE180B6298",
"partyId": "8D47AF50-8EAF-4C2E-B721-D39A06F9F5E3",
"spawnCount": "5",
"gameTimeSec": "528"
}
}
]
}
|
This event has no identity set, so Satori doesn’t bind it to one. Instead, it forwards the event to your configured data lake for storage and analysis.
Finally, a custom levelCompleted event, this time sent with a session ID:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| {
"events":
[
{
"name": "levelCompleted",
"identityId": "00000000-0000-0000-0000-000000000001",
"timestamp": "2025-07-15T12:00:00.00Z",
"sessionId": "2513FC77-3B8D-487B-9709-18E0A27F0ECB",
"sessionIssuedAt": "1752577200",
"sessionExpiresAt": "1752663600",
"value": "10",
"metadata": {
"levelId": "10",
"retryCount": "2"
}
}
]
}
|
When you include a sessionId, Satori uses that session and shows its issued-at and expires-at timestamps on the event in the dashboard. This lets you manage session IDs yourself instead of relying on the latest Satori session.
Code samples
#
The following samples send the three example events (purchaseCompleted, enemySpawned, and levelCompleted) to the Server-Event API. In each, replace <your-satori-server> with your server URL and <your-api-key> with your API key.
cURL
#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| curl --location '<your-satori-server>/v1/server-event' \
--user '<your-api-key>:' \
--header 'Content-Type: application/json' \
--data '{
"events":
[
{
"name": "purchaseCompleted",
"identityId": "00000000-0000-0000-0000-000000000001",
"timestamp": "2025-07-15T12:00:00.00Z",
"value": "",
"metadata": {
"test": "false",
"amount": "20",
"currency": "GBP"
}
},
{
"name": "enemySpawned",
"timestamp": "2025-07-15T12:00:00.00Z",
"value": "5",
"metadata": {
"matchId": "0078EC13-FDE4-44E7-990C-3ABE180B6298",
"partyId": "8D47AF50-8EAF-4C2E-B721-D39A06F9F5E3",
"spawnCount": "5",
"gameTimeSec": "528"
}
},
{
"name": "levelCompleted",
"identityId": "00000000-0000-0000-0000-000000000001",
"timestamp": "2025-07-15T12:00:00.00Z",
"sessionId": "2513FC77-3B8D-487B-9709-18E0A27F0ECB",
"sessionIssuedAt": "1752577200",
"sessionExpiresAt": "1752663600",
"value": "10",
"metadata": {
"levelId": "10",
"retryCount": "2"
}
}
]
}'
|
Server-side code
#
The following snippet sends the same events from your server. Select your language using the dropdown at the top of the page.
Server
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
| package main
import (
"bytes"
"encoding/base64"
"fmt"
"net/http"
)
func main() {
url := "http://<your-satori-server>/v1/server-event"
username := "<your-api-key>"
password := ""
auth := base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
jsonData := []byte(`{
"events": [
{
"name": "purchaseCompleted",
"identityId": "00000000-0000-0000-0000-000000000001",
"timestamp": "2025-07-15T12:00:00.00Z",
"value": "",
"metadata": {
"test": "false",
"amount": "20",
"currency": "GBP"
}
},
{
"name": "enemySpawned",
"timestamp": "2025-07-15T12:00:00.00Z",
"value": "5",
"metadata": {
"matchId": "0078EC13-FDE4-44E7-990C-3ABE180B6298",
"partyId": "8D47AF50-8EAF-4C2E-B721-D39A06F9F5E3",
"spawnCount": "5",
"gameTimeSec": "528"
}
},
{
"name": "levelCompleted",
"identityId": "00000000-0000-0000-0000-000000000001",
"timestamp": "2025-07-15T12:00:00.00Z",
"sessionId": "2513FC77-3B8D-487B-9709-18E0A27F0ECB",
"sessionIssuedAt": "1752577200",
"sessionExpiresAt": "1752663600",
"value": "10",
"metadata": {
"levelId": "10",
"retryCount": "2"
}
}
]
}`)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil {
panic(err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Basic "+auth)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println("Response Status:", resp.Status)
}
|
Server
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
| local http = require("socket.http")
local ltn12 = require("ltn12")
local mime = require("mime") -- Ensure LuaSocket supports this
local username = "<your-api-key>"
local password = ""
local auth = mime.b64(username .. ":" .. password)
local json = [[
{
"events": [
{
"name": "purchaseCompleted",
"identityId": "00000000-0000-0000-0000-000000000001",
"timestamp": "2025-07-15T12:00:00.00Z",
"value": "",
"metadata": { "test": "false", "amount": "20", "currency": "GBP" }
},
{
"name": "enemySpawned",
"timestamp": "2025-07-15T12:00:00.00Z",
"value": "5",
"metadata": {
"matchId": "0078EC13-FDE4-44E7-990C-3ABE180B6298",
"partyId": "8D47AF50-8EAF-4C2E-B721-D39A06F9F5E3",
"spawnCount": "5",
"gameTimeSec": "528"
}
},
{
"name": "levelCompleted",
"identityId": "00000000-0000-0000-0000-000000000001",
"timestamp": "2025-07-15T12:00:00.00Z",
"sessionId": "2513FC77-3B8D-487B-9709-18E0A27F0ECB",
"sessionIssuedAt": "1752577200",
"sessionExpiresAt": "1752663600",
"value": "10",
"metadata": { "levelId": "10", "retryCount": "2" }
}
]
}
]]
local response = {}
local res, code, headers, status = http.request{
url = "http://<your-satori-server>/v1/server-event",
method = "POST",
headers = {
["Authorization"] = "Basic " .. auth,
["Content-Type"] = "application/json",
["Content-Length"] = tostring(#json)
},
source = ltn12.source.string(json),
sink = ltn12.sink.table(response)
}
print("Status:", status)
print("Response body:", table.concat(response))
|
Server
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
| const fetch = require("node-fetch"); // Only needed in Node.js
const username = "<your-api-key>";
const password = "";
const auth = Buffer.from(`${username}:${password}`).toString("base64");
const body = {
events: [
{
name: "purchaseCompleted",
identityId: "00000000-0000-0000-0000-000000000001",
timestamp: "2025-07-15T12:00:00.00Z",
value: "",
metadata: {
test: "false",
amount: "20",
currency: "GBP"
}
},
{
name: "enemySpawned",
timestamp: "2025-07-15T12:00:00.00Z",
value: "5",
metadata: {
matchId: "0078EC13-FDE4-44E7-990C-3ABE180B6298",
partyId: "8D47AF50-8EAF-4C2E-B721-D39A06F9F5E3",
spawnCount: "5",
gameTimeSec: "528"
}
},
{
name: "levelCompleted",
identityId: "00000000-0000-0000-0000-000000000001",
timestamp: "2025-07-15T12:00:00.00Z",
sessionId: "2513FC77-3B8D-487B-9709-18E0A27F0ECB",
sessionIssuedAt: "1752577200",
sessionExpiresAt: "1752663600",
value: "10",
metadata: {
levelId: "10",
retryCount: "2"
}
}
]
};
fetch("http://<your-satori-server>/v1/server-event", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Basic ${auth}`
},
body: JSON.stringify(body)
})
.then(res => res.text())
.then(text => console.log("Response:", text))
.catch(err => console.error("Error:", err));
|
See also
#