Friends
Add a social graph to your game: friend codes, username search, accept/decline requests, live online presence, and blocking — all from the client SDK.
Friends gives your game a social graph without you standing up a friends service of your own. Players find each other by name or by a short shareable code, send requests, see who's online right now, and block anyone they don't want to hear from. Everything is driven from the client SDK with the calling player's own secret, so a client can only ever act on its own social graph.
The model
- Scoped to one game + environment. A friendship lives inside a
single game and a single environment (
testorlive, derived from the API key). Friends made in yourtestbuild never leak intolive, and players in one game are invisible to another. - Two ways to add. A player can be added by a username search (display-name match within the game) or by their friend code — a short, 6-character shareable string that's stable for the life of the player. Codes are handy for "add me" links, QR codes, or reading a code out loud.
- Requests must be accepted.
addcreates a pending request the other player has to accept. The one exception is a reciprocal add: if the target had already requested the caller, the secondaddauto-accepts and the friendship is live immediately. - Live presence via heartbeat. While a player is active the client
sends a lightweight heartbeat (~every 30s). Friends see their
onlineflag,lastActiveAt, and an optional free-formstatuslabel ("in_match", "lobby", …). Presence has a short TTL: stop beating and the player quietly goes offline. - Blocking. Blocking a player tears down any friendship or pending request between the two and hides each from the other's search and presence.
Friends is a client-SDK feature. Every call is authorized by the
client_sdk API key (Authorization: Bearer <key>) plus the
player's own secret (X-Player-Secret: <secret>), so a player can
only read and modify their own friends, requests, and blocks. The
acting player is the :externalId in the path. Server-side tooling
can address a different player by passing { as } on any SDK method.
End to end with the TypeScript SDK
Everything below hangs off kraty.friends.*. The SDK resolves the
active player automatically (see the
TypeScript SDK guide), so
you never pass the player id yourself.
Get and share your friend code
import { Kraty } from '@kraty/sdk';
const kraty = new Kraty({ apiKey: '<your-client-sdk-key>' });
// Generated on first use, then stable forever. Show it in a
// "your code" UI or drop it into a share sheet.
const { friendCode, displayIdentity } = await kraty.friends.getCode();
console.log(`Add me: ${friendCode}`); // e.g. "K7F2Q9"Find someone by name
// Search other players by display name within this game + environment.
// Blocked players are omitted; each hit reports your relationship.
const results = await kraty.friends.search('shadow', { limit: 10 });
for (const r of results) {
// relationship: 'none' | 'friends' | 'request_incoming' | 'request_outgoing'
console.log(r.displayIdentity?.name, r.externalPlayerId, r.relationship);
}Send a friend request
You can add by friend code or by externalPlayerId — pass exactly one.
// By the code they shared with you:
const byCode = await kraty.friends.add({ friendCode: 'K7F2Q9' });
// Or by a player id you already have (e.g. from a search hit):
const byId = await kraty.friends.add({ externalPlayerId: 'player_88' });
if (byCode.status === 'pending') {
// They must accept. `byCode.request` is the outgoing request row.
console.log('Request sent:', byCode.request?.requestId);
} else {
// Reciprocal auto-accept: they'd already requested you.
console.log('Instant friend:', byCode.friend?.externalPlayerId);
}The call throws KratyApiError on the friend-specific error codes —
cannot_friend_self (400), already_friends (409),
friend_code_invalid (404), or player_blocked (403):
import { KratyApiError } from '@kraty/sdk';
try {
await kraty.friends.add({ friendCode: userInput });
} catch (err) {
if (err instanceof KratyApiError) {
switch (err.code) {
case 'friend_code_invalid': return toast('That code doesn’t exist.');
case 'already_friends': return toast('You’re already friends.');
case 'cannot_friend_self': return toast('You can’t add yourself.');
case 'player_blocked': return toast('Unable to add this player.');
}
}
throw err;
}Review and respond to requests
// Both directions in one call.
const { incoming, outgoing } = await kraty.friends.listRequests();
// Accept an incoming request → returns the new Friend.
if (incoming.length > 0) {
const friend = await kraty.friends.accept(incoming[0].requestId);
console.log('Now friends with', friend.externalPlayerId);
}
// Decline an incoming request you don't want.
await kraty.friends.decline(incoming[1]?.requestId);
// Cancel an outgoing request you sent by mistake.
await kraty.friends.cancelRequest(outgoing[0]?.requestId);List friends with live presence
const friends = await kraty.friends.list();
for (const f of friends) {
const where = f.status ? ` — ${f.status}` : '';
const seen = f.online
? 'online'
: f.lastActiveAt
? `last seen ${new Date(f.lastActiveAt).toLocaleString()}`
: 'offline';
console.log(`${f.displayIdentity?.name ?? f.externalPlayerId}: ${seen}${where}`);
console.log(` friends since ${f.friendsSince}`);
}Broadcast your own presence
Call heartbeat on a timer (about every 30 seconds) while the player
is active. Presence expires automatically once the heartbeats stop, so
there's no explicit "go offline" call — closing the app is enough.
// Kick off a heartbeat loop for as long as the player is in-app.
const beat = setInterval(() => {
void kraty.friends.heartbeat({ status: 'lobby' });
}, 30_000);
// Update the status label whenever the player's activity changes.
await kraty.friends.heartbeat({ status: 'in_match' });
// Clear the label but stay online.
await kraty.friends.heartbeat({ status: null });
// Stop beating when the player leaves; presence lapses on its own.
clearInterval(beat);Remove a friend
await kraty.friends.remove('player_88'); // pass the friend's externalPlayerIdBlock and unblock
// Block by code or by id. Removes any friendship / pending request
// and hides you both from each other's search + presence.
await kraty.friends.block({ externalPlayerId: 'player_99' });
// See who you've blocked.
const blocked = await kraty.friends.listBlocks();
for (const b of blocked) {
console.log(b.displayIdentity?.name, 'blocked at', b.blockedAt);
}
// Lift the block.
await kraty.friends.unblock('player_99');The same flow over raw REST
Every endpoint lives under /sdk/v1, takes the client_sdk bearer
key plus the player's secret, and wraps success in { "data": … }.
The :externalId segment is the acting player.
Send a request
POST /sdk/v1/players/{externalId}/friends/requests
Authorization: Bearer <your-client-sdk-key>
X-Player-Secret: <player-secret>
Content-Type: application/json
{ "friendCode": "K7F2Q9" }A brand-new request returns 201:
{
"data": {
"status": "pending",
"request": {
"requestId": "frq_…",
"direction": "outgoing",
"player": {
"externalPlayerId": "player_88",
"displayIdentity": { "name": "ShadowStrike", "avatar": null, "country": "PT" }
},
"createdAt": "2026-07-17T12:00:00.000Z"
}
}
}If the target had already requested you, it auto-accepts and returns
200 with the friendship instead:
{
"data": {
"status": "accepted",
"friend": {
"externalPlayerId": "player_88",
"displayIdentity": { "name": "ShadowStrike", "avatar": null, "country": "PT" },
"friendsSince": "2026-07-17T12:00:01.000Z",
"online": true,
"lastActiveAt": "2026-07-17T12:00:00.000Z",
"status": "lobby"
}
}
}Accept a request
POST /sdk/v1/players/{externalId}/friends/requests/{requestId}/accept
Authorization: Bearer <your-client-sdk-key>
X-Player-Secret: <player-secret>{
"data": {
"friend": {
"externalPlayerId": "player_88",
"displayIdentity": { "name": "ShadowStrike", "avatar": null, "country": "PT" },
"friendsSince": "2026-07-17T12:00:01.000Z",
"online": false,
"lastActiveAt": null,
"status": null
}
}
}List friends
GET /sdk/v1/players/{externalId}/friends
Authorization: Bearer <your-client-sdk-key>
X-Player-Secret: <player-secret>{
"data": {
"friends": [
{
"externalPlayerId": "player_88",
"displayIdentity": { "name": "ShadowStrike", "avatar": null, "country": "PT" },
"friendsSince": "2026-07-17T12:00:01.000Z",
"online": true,
"lastActiveAt": "2026-07-17T12:34:56.000Z",
"status": "in_match"
}
]
}
}Refresh presence
POST /sdk/v1/players/{externalId}/presence
Authorization: Bearer <your-client-sdk-key>
X-Player-Secret: <player-secret>
Content-Type: application/json
{ "status": "in_match" }{
"data": {
"online": true,
"lastActiveAt": "2026-07-17T12:34:56.000Z",
"status": "in_match"
}
}The full endpoint list — friend code, search, requests (list / accept / decline / cancel), friends (list / remove), and blocks (list / add / remove) — is in the SDK API reference.
React to the social graph from your backend
Three webhook kinds let your studio backend follow the social graph (for analytics, anti-abuse, or push notifications) without polling:
| Event kind | Fires when |
|---|---|
friend.request_sent | A player sends a friend request (the pending case; a reciprocal auto-accept fires friend.request_accepted instead). |
friend.request_accepted | A request is accepted — whether explicitly via accept or through a reciprocal auto-accept. |
friend.removed | A friendship ends, via remove or because one side blocked the other. |
Subscribe to them from your game's Webhooks tab exactly like any other kind; see Webhooks → Social graph for the per-kind payloads plus the shared envelope, signing, and retry model.
SDK availability
The Friends surface is mirrored across the client SDKs so the method names line up 1:1:
- TypeScript (
@kraty/sdk) —kraty.friends.*, documented above. - Flutter and Unity already ship the same surface
(
kraty.friends.getCode()in Dart,kraty.Friends.GetCodeAsync()in C#). - iOS and Android native SDKs will mirror this exact surface when they land; the wire contract is stable, so anything you build against REST today keeps working.
// Flutter client SDK
final code = await kraty.friends.getCode();
await kraty.friends.add(FriendTarget.code('K7F2Q9'));
final friends = await kraty.friends.list();// Unity client SDK
var code = await kraty.Friends.GetCodeAsync();
await kraty.Friends.AddAsync(FriendTarget.Code("K7F2Q9"));
var friends = await kraty.Friends.ListAsync();