Your game's server
If your game has a server (a multiplayer backend, a dedicated server, a service that keeps accounts), it can report achievements and stats to Moymo itself. A player cannot fake what your server sends, so this is the way to report what must be real. See Fair play for when to use it.
You need nothing new in the game except one call: the game asks Moymo for a ticket and gives it to your server, so your server knows which Moymo player it is talking to.
1. Your game ──► Moymo for Windows: "a ticket, please" ──► Moymo
2. Your game ──► your server: the ticket
3. Your server ──► Moymo: "whose ticket is this?" ──► player pl_8kq2…
4. Your server ──► Moymo: "pl_8kq2… unlocked champion"- Make a server key. In the Developer Portal, open your game, the Server keys tab, New server key. Name it after the server that will use it ("EU servers"), and pick Sandbox or Live. The key is shown once: Copy it into your server's configuration. A sandbox key starts with
mk_test_, a live key withmk_live_. - Ask for a ticket in the game. When the player connects to your server, request a ticket and send it along (see below).
- Verify the ticket on your server. Your server sends the ticket to Moymo and gets the player's id back. Keep the id with your own account for that player: it never changes.
- Report. Unlock achievements and set stats for that player id, whenever your server decides they are earned.
In the game: request a ticket
Moymo for Windows
Players' tickets come from Moymo for Windows, from its next release on. Until then, test with the tickets of moymo dev (below).
The ticket arrives a moment later, on the game thread. It is valid for 5 minutes and can be verified once, so request a new one each time the player connects.
Request a ticket
csharp
using Moymo;
Identity.RequestTicket(result =>
{
if (result.Status == TicketStatus.Ok)
myServer.SendMoymoTicket(result.Ticket); // your own protocol
});cpp
moymo::Identity::RequestTicket([](const moymo::TicketResult& result) {
if (result.status == moymo::TicketStatus::Ok) {
MyServer::SendMoymoTicket(result.ticket); // your own protocol
}
});In Unreal Engine, the Blueprint node Request Moymo Ticket does the same, with On Ticket.
Status | Means |
|---|---|
Ok | Here is the ticket. |
NotSignedIn | Moymo for Windows is not running, or nobody is signed in to it. Play on without Moymo. |
NotAllowed | The player has not allowed any module for your game. |
Unavailable | Moymo could not be reached. Try again later. |
A ticket says nothing about the player but who they are for your game: no name, no e-mail.
On your server: verify the ticket
RequestPOST
/tickets/verify bash
curl https://moymo.net/api/server/v1/tickets/verify \
-H "Authorization: Bearer $MOYMO_SERVER_KEY" \
-H "Content-Type: application/json" \
-d '{"ticket": "mt_2f9c…"}'js
const response = await fetch('https://moymo.net/api/server/v1/tickets/verify', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.MOYMO_SERVER_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ ticket })
})
if (!response.ok) {
const problem = await response.json()
throw new Error(`${response.status} ${problem.code}: ${problem.message}`)
}
const { player } = await response.json()csharp
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization =
new("Bearer", Environment.GetEnvironmentVariable("MOYMO_SERVER_KEY"));
var response = await http.PostAsJsonAsync(
"https://moymo.net/api/server/v1/tickets/verify", new { ticket });
response.EnsureSuccessStatusCode();
var verified = await response.Content.ReadFromJsonAsync<VerifiedTicket>();
// record VerifiedTicket(string Player, string Environment, DateTimeOffset ExpiresAt);python
import os
import requests
response = requests.post(
"https://moymo.net/api/server/v1/tickets/verify",
headers={"Authorization": f"Bearer {os.environ['MOYMO_SERVER_KEY']}"},
json={"ticket": ticket},
)
response.raise_for_status()
player = response.json()["player"]Response200OKJSON
json
{
"player": "pl_8kq2m4x7t1v9c3b5n6z0r2w4y8",
"environment": "live",
"expiresAt": "2026-11-06T12:05:00Z"
}player is the same for this person in your game every time, and another game sees a different id for them. Store it with your own account for the player. If the player withdraws their consent and later allows your game again, they get a new id.
A ticket that is wrong, expired or already used answers 400 with TICKET_INVALID, TICKET_EXPIRED or TICKET_USED. Ask the game for a new one.
Report an achievement
RequestPUT
/players/{player}/achievements/ bash
curl -X PUT https://moymo.net/api/server/v1/players/pl_8kq2m4x7t1v9c3b5n6z0r2w4y8/achievements/champion \
-H "Authorization: Bearer $MOYMO_SERVER_KEY"js
await fetch(`https://moymo.net/api/server/v1/players/${player}/achievements/champion`, {
method: 'PUT',
headers: { Authorization: `Bearer ${process.env.MOYMO_SERVER_KEY}` }
})csharp
await http.PutAsync(
$"https://moymo.net/api/server/v1/players/{player}/achievements/champion", null);python
requests.put(
f"https://moymo.net/api/server/v1/players/{player}/achievements/champion",
headers={"Authorization": f"Bearer {os.environ['MOYMO_SERVER_KEY']}"},
)Response200OKJSON
json
{ "unlocked": true, "firstTime": true, "unlockedAt": "2026-11-06T12:01:13Z" }Unlocking twice is harmless: the second answer says "firstTime": false. So when you are not sure a call arrived, send it again.
Report a stat
RequestPUT
/players/{player}/stats/ Shellbash
curl -X PUT https://moymo.net/api/server/v1/players/pl_8kq2m4x7t1v9c3b5n6z0r2w4y8/stats/season_wins \
-H "Authorization: Bearer $MOYMO_SERVER_KEY" \
-H "Content-Type: application/json" \
-d '{"value": 10}'Response200OKJSON
json
{ "value": 10, "unlocked": ["champion"] }Send the stat's whole value as your server knows it, not the change. Moymo keeps it as its kind says: a counter never goes down, a max keeps the highest, a min the lowest, a flag stays true. unlocked lists the achievements whose rules the new value met.
A set stat takes one member at a time: POST /players/{player}/stats/{stat}/members with {"symbol": "smithy"}.
When the player has not allowed it
If the player has not allowed achievements for your game (stats count as part of achievements), the answer is 403 with NOT_ALLOWED. Nothing is kept. Send it again the next time the player connects: every call is safe to repeat, and your server still knows what the player earned.
Testing without Moymo for Windows
moymo dev hands out tickets too. They work only with an mk_test_ key, and their player ids start with pl_dev_. For such a player, Moymo checks every call as it would for a real one (the key, the names, the values, the rules) and answers the same way, but keeps nothing. So you can test your server before anyone has Moymo for Windows.
Server SDKs
The calls above are plain HTTPS, so any language can make them. For Node.js and .NET there are small libraries that also retry politely after 429 and 503. They are in the SDK's repository, in servers/node and servers/dotnet; they are not on npm or NuGet yet.
Unlock with a server SDK
js
import { MoymoServer } from '@moymo/server'
const moymo = new MoymoServer(process.env.MOYMO_SERVER_KEY) // mk_test_… or mk_live_…
const { player } = await moymo.verifyTicket(ticket)
await moymo.unlockAchievement(player, 'champion')
await moymo.setStat(player, 'season_wins', 10)csharp
using Moymo.Server;
var moymo = new MoymoServer(Environment.GetEnvironmentVariable("MOYMO_SERVER_KEY")
?? throw new InvalidOperationException("MOYMO_SERVER_KEY is not set"));
var verified = await moymo.VerifyTicketAsync(ticket);
await moymo.UnlockAchievementAsync(verified.Player, "champion");
await moymo.SetStatAsync(verified.Player, "season_wins", 10);Every call is in the Server API reference.