Game ideas & features

Your next feature

Player inbox

Targeted notifications and shared project broadcasts, with protected rewards and one social polling surface.

SDK developRequires a player session
Godot
Illustration of Player inbox
Your visible result

Your game displays targeted messages and broadcasts, marks them read, and claims each protected reward at most once.

Video coming later

This step does not have a video yet. The guide and MCP request are ready to use.

Player inbox

Prepare the same project

  1. Choose the same project in the dashboard as in your game configuration.

    Player inbox →
  2. No additional definition is needed for the first request. An empty result is valid until matching game data exists.

    Register the test player before publishing a plain-text broadcast in this project. Read the inbox with that same player.

    A broadcast reaches currently active players in this project who registered no later than publication. Previously inactive players can qualify after reactivation. Newer registrations are excluded.

Build this feature

You can preview the guide. Running it requires an active project key. Prepare project

SDK installed, project configuration imported and player signed in.

This SDK example uses develop. Check whether the required APIs exist in your installed SDK.

Complete the Quickstart connection with the intended player. Call run_reviewed_action() from an explicit button or scene action. Opening the file alone sends no request.

GDScript
res://scripts/inbox.gd
horizOn call
1extends Node
2​
3# Existing configured SDK and authenticated player session required.
4# Invoke run_reviewed_action explicitly after reviewing this example.
5func run_reviewed_action() -> void:
6 if not Horizon.auth.isSignedIn():
7 return
8 var user_id: String = Horizon.auth.getCurrentUser().userId
9 var loaded: Dictionary = await Horizon.inbox.getInbox(user_id, "")
10 if not loaded.success:
11 push_error(loaded.errorCode)
12 return
13 for entry in loaded.data.entries:
14 print(entry.textKey)
15 # The SDK materializes unchanged responses from its project/player cache.
16 # Respect loaded.data.pollAfterSeconds and stop refreshing with the game lifecycle.
17​
18 var claim_state := {"busy": false}
19 var claim_selected = func(entry_id: String) -> void:
20 if claim_state.busy:
21 return
22 var project_id := "YOUR_PROJECT_ID"
23 var path := "user://inbox_claim_" + project_id + "_" + user_id + ".txt"
24 if FileAccess.file_exists(path):
25 var previous := FileAccess.open(path, FileAccess.READ)
26 if previous == null:
27 return
28 var pending := previous.get_as_text()
29 previous.close()
30 if pending != entry_id:
31 return # Resolve the original claim first.
32 else:
33 var current: Dictionary = Horizon.inbox.getCurrentInbox()
34 if current.get("userId", "") != user_id:
35 return
36 var selected: Dictionary = {}
37 for entry in current.get("entries", []):
38 if entry.id == entry_id:
39 selected = entry
40 break
41 if selected.is_empty() or selected.claimed:
42 return
43 var attachment = selected.get("attachment")
44 if not attachment is Dictionary or attachment.get("values", []).is_empty():
45 return
46 # An existing identical pending ID bypasses this new-selection guard for safe replay.
47 var file := FileAccess.open(path, FileAccess.WRITE)
48 if file == null:
49 return
50 file.store_string(entry_id)
51 file.flush()
52 var saved_ok := file.get_error() == OK
53 file.close()
54 if not saved_ok:
55 return
56 claim_state.busy = true
57 var claimed: Dictionary = await Horizon.inbox.claim(user_id, entry_id)
58 claim_state.busy = false
59 if not claimed.success:
60 push_error(claimed.errorCode)
61 return
62 # Display claimed.data.inbox and replace values with claimed.data.playerValues.
63 # Never add the reward delta locally.
64 var current := FileAccess.open(path, FileAccess.READ)
65 if current != null:
66 var still_pending := current.get_as_text() == entry_id
67 current.close()
68 if still_pending:
69 DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
70 # Bind claim_selected to an explicit player action; do not claim automatically.
71 # Retry the same saved entry_id after an uncertain response.
72 # Other deliberate actions: await Horizon.inbox.markRead(user_id, entry_id)
73 # await Horizon.inbox.remove(user_id, entry_id) # Protects unclaimed rewards.
Open the complete SDK sample ↗

Test your new feature.

Your game displays targeted messages and broadcasts, marks them read, and claims each protected reward at most once.

Choose the same project in the dashboard as in your game configuration.

Endpoints
  • GET/inbox
  • POST/inbox/read
  • POST/inbox/claim
  • POST/inbox/remove
All endpoints in the API reference
Our suggestion for your next step

Give every round a high score.

Save the score after a round and show who's ahead. Keep using your connection and player session.