# How to interact with Nakama from headless servers

**URL:** https://heroiclabs.com/docs/nakama/guides/concepts/headless-server-auth/
**Summary:** Connect a headless game server to Nakama using the server-to-server HTTP key, with or without player context.
**Keywords:** headless server, dedicated server, server to server, http key, authentication, fleet
**Categories:** nakama, concepts

---


# How to interact with Nakama from headless servers

If you run headless game servers, whether through a service like GameLift or Edgegap or on your own hardware, you may need to communicate with Nakama directly. This guide walks through how to authenticate and call Nakama, both without a player session and in the context of a specific player.

If you're looking for how Nakama integrates with server orchestration platforms, see the [GameLift](/docs/nakama/guides/concepts/gamelift-integration/) and [Edgegap](/docs/nakama/guides/concepts/edgegap-integration/) guides. Here, we cover the reverse direction: how a server instance communicates with Nakama.

What this guide covers:

1. [Two ways to call Nakama](#two-ways-to-call-nakama)
2. [Call Nakama without player context](#call-nakama-without-player-context)
3. [Call Nakama in a player context](#call-nakama-in-a-player-context)
4. [Common use cases](#common-use-cases)

## Two ways to call Nakama

A headless server can call Nakama in one of two contexts: without a player identity, or in the context of a specific player.

### Without player context, with the HTTP key

The server authenticates with the HTTP key and calls an RPC. Nakama receives the request without a user context.

Use this for tasks such as fetching match configuration, maps, and other game-scoped data. It can also handle player-context work when passing that player's ID to a custom RPC. We'll cover this later.

### In a player's context, with a session token

The server can mint a player session token through an HTTP-key-authenticated RPC. It can then use the session token for a specific player and can call Nakama using the same SDK methods as a game client. 

Use this for server-side access to persist player data, such as progression, inventory, and scores.

Let's look at both approaches in practice. The first example covers server-to-server communication. If you need to access player data, skip ahead to the [player context example](#call-nakama-in-a-player-context).

## Call Nakama without player context

Let's use an example where you want to fetch match configurations from a headless server. You'll use Nakama's HTTP key for server-to-server communication, following these steps:

1. **Configure the HTTP key.** Set `runtime.http_key` in your Nakama configuration. The default value is `defaulthttpkey`, but you should change it for a production deployment.

2. **Create an RPC.** Register a `get_match_config` RPC that returns the configuration the headless server needs to start the match. Add a guard so the RPC only runs for server callers. If a request includes a user ID, reject it.

3. **Call the RPC at server boot.** The headless server calls the RPC using the HTTP key:

   ```text
   POST /v2/rpc/get_match_config?http_key=...&unwrap
   ```

   The `unwrap` parameter returns the RPC response as JSON rather than as a JSON-encoded string.

We've listed the steps here, for more details on server-to-server RPCs, including a complete RPC implementationsee, see [Server-to-Server](/docs/nakama/server-framework/runtime-examples/server-to-server/).


## Call Nakama in a player context

When the server needs to work with a specific player's data, you have two options: 
- Option 1: Mint a session token for that player 
- Option 2: Pass the player ID to a custom RPC

### Option 1. Mint a player session token

Here's how the flow works step by step:

**1. Create an RPC to mint the token** 

Create an RPC, such as `mint_player_token`, that takes a player ID and returns a session token.

The headless server calls it using the HTTP key:

```text
POST /v2/rpc/mint_player_token?http_key=...&unwrap
{userId}
```

The RPC uses [`nk.AuthenticateTokenGenerate()`](/docs/nakama/server-framework/go-runtime/function-reference/#AuthenticateTokenGenerate) to issue a real Nakama session token for that player:
{{< code filename="mint.go" hideable="false" >}}
```go
const tokenLifetimeSec = 300

type mintRequest struct {
	UserId string `json:"userId"`
}

func RpcMintPlayerToken(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
	if userId, ok := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string); ok && userId != "" {
		return "", runtime.NewError("server-to-server calls only", 7)
	}

	var req mintRequest
	if err := json.Unmarshal([]byte(payload), &req); err != nil {
		return "", runtime.NewError("invalid input", 3)
	}

	account, _ := nk.AccountGetId(ctx, req.UserId)

	token, exp, _ := nk.AuthenticateTokenGenerate(
		req.UserId,
		account.User.Username,
		// Absolute Unix timestamp, not a duration, so 0 falls back to session.token_expiry_sec.
		time.Now().Unix()+tokenLifetimeSec,
		map[string]string{"actor": "headless"},
	)

	return fmt.Sprintf(`{"token":%q,"expiresAt":%d}`, token, exp), nil
}
```
{{< /code >}}

**2. Call the RPC and use the token with the Nakama SDK**

The headless server calls the RPC with the HTTP key, passing the player ID. Nakama returns a session token for that player. The headless server can now use the Nakama SDK, in this example the C# SDK, with the player's session. The SDK methods are the same ones you would use from a Unity client.

{{< code filename="ServerMatchEnd.cs" hideable="false" >}}
```csharp
using Nakama;
using Nakama.TinyJson;

public class MintedToken
{
    public string token { get; set; }
    public long expiresAt { get; set; }
}

var response = await client.RpcAsync(
    httpKey,
    "mint_player_token",
    new { userId = player.UserId }.ToJson()
);

var minted = response.Payload.FromJson<MintedToken>();
var session = Session.Restore(minted.token);

// WriteLeaderboardRecordAsync is part of the SDK
await client.WriteLeaderboardRecordAsync(session, "kills", player.Kills);
```
{{< /code >}}

A couple of things to note:
- Set the token expiry to cover the expected gameplay session. Your headless server keeps the token for each player and reuses it throughout the play session, such as a match.
- Keep the HTTP key server-side. Make sure the HTTP key is only supplied to the server build.

### Option 2. Pass the user ID in the server-to-server RPC

Another option is to keep the headless server authenticated with the HTTP key and pass the player ID to a custom RPC.

How it works:
- The headless server calls the RPC using the HTTP key.
- The server includes the player ID in the request payload.
- Nakama runs the custom RPC without a player session.
- The RPC passes the player ID to the Nakama runtime function that performs the operation.
- Nakama performs the operation for that player.

This works because Nakama runtime functions that act on a player take that player's ID as an explicit parameter. The parameter name varies by function:

- [`WalletUpdate(ctx, userID, changeset, ...)`](/docs/nakama/server-framework/go-runtime/function-reference/#WalletUpdate)
- [`AccountUpdateId(ctx, userID, username, ...)`](/docs/nakama/server-framework/go-runtime/function-reference/#AccountUpdateId)
- [`NotificationSend(ctx, userID, subject, ...)`](/docs/nakama/server-framework/go-runtime/function-reference/#NotificationSend)
- [`StorageList(ctx, callerID, userID, collection, ...)`](/docs/nakama/server-framework/go-runtime/function-reference/#StorageList)
- [`LeaderboardRecordWrite(ctx, id, ownerID, ...)`](/docs/nakama/server-framework/go-runtime/function-reference/#LeaderboardRecordWrite)

For example, the server can send a score and player ID to an RPC:
```text
POST /v2/rpc/submit_score?http_key=...&unwrap
{"userId":"player-123","score":5000}
```

The RPC then passes that userId to LeaderboardRecordWrite:
```text
nk.LeaderboardRecordWrite(ctx, "kills", userId, ...)
```

Here's what the custom RPC could look like:

{{< code filename="scores.go" hideable="false" >}}
```go
const leaderboardId = "kills"

type submitScoreRequest struct {
	UserId   string `json:"userId"`
	Username string `json:"username"`
	Score    int64  `json:"score"`
	MatchId  string `json:"matchId"`
}

func RpcSubmitScore(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, payload string) (string, error) {
	if userId, ok := ctx.Value(runtime.RUNTIME_CTX_USER_ID).(string); ok && userId != "" {
		return "", runtime.NewError("server-to-server calls only", 7)
	}

	var req submitScoreRequest
	if err := json.Unmarshal([]byte(payload), &req); err != nil {
		return "", runtime.NewError("invalid input", 3)
	}

	metadata := map[string]interface{}{"matchId": req.MatchId}

	if _, err := nk.LeaderboardRecordWrite(ctx, leaderboardId, req.UserId, req.Username, req.Score, 0, metadata, nil); err != nil {
		return "", runtime.NewError("failed to write record", 13)
	}

	logger.Info("wrote %d for %s from match %s", req.Score, req.UserId, req.MatchId)

	return fmt.Sprintf(`{"written":true,"userId":%q}`, req.UserId), nil
}
```
{{< /code >}}

The tradeoff is that you have to expose each operation through your own RPC. A wallet update, inventory read, XP grant, or friends list would each require its own RPC and payload contract.

For example:
```text
POST /v2/rpc/submit_score?http_key=...      {userId, score}
POST /v2/rpc/update_wallet?http_key=...     {userId, delta}
POST /v2/rpc/read_inventory?http_key=...    {userId}
POST /v2/rpc/grant_xp?http_key=...          {userId, amount}
```
Each RPC adds another piece of server code to maintain.

## Common use cases

Besides the examples above, other scenarios where your headless server may need to make calls to Nakama include:
- **Signal instance lifecycle.** An instance scaling down calls an RPC with the HTTP key and no player context, telling Nakama to stop routing new players to it, then shuts down after existing matches finish.
- **Batch writes for multiple players.** For example, when a match ends, the server may send results for all players in one RPC, including kills, XP, currency and so on. Make the write idempotent so the server can safely queue and retry it if Nakama is unavailable.
- **Ops tooling.** Maintenance scripts use the HTTP key to call the same server-side RPCs as your game servers. For privileged operations, isolate this traffic with a dedicated Nakama node and key.
- **Satori flags, live events, and server events.** If you're looking to query Satori from your dedicated server, for example to fetch feature flags or live events, have it query Nakama first. Nakama handles authentication with Satori internally and returns the data to your server. The same applies for sending server-side analytics events: send your events to Nakama, and Nakama publishes them to Satori with [`ServerEventsPublish`](/docs/nakama/server-framework/go-runtime/function-reference/#ServerEventsPublish).
