# Games Management

> **API Reference**: <https://api.play.fun/api-reference>

## Authentication

### Generate HMAC Signature

**POST** `/user/hmac-signature`

Generate an HMAC signature for authenticating API requests. This endpoint is used to generate the `Authorization` header required for protected endpoints.

**Request Body:**

```json
{
  "secretKey": "your-secret-key",
  "method": "POST",
  "path": "/games",
  "apiKey": "your-api-key"
}
```

**Response:**

```json
{
  "data": {
    "signature": "HMAC-SHA256 apiKey=your-api-key signature=abc123... timestamp=1234567890"
  }
}
```

**Example:**

```javascript
async function getSignature(method, path) {
  const response = await fetch('https://api.play.fun/user/hmac-signature', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      secretKey: 'your-secret-key',
      method: method,
      path: path,
      apiKey: 'your-api-key',
    }),
  });

  const result = await response.json();
  return result.data.signature;
}
```

***

## Games Endpoints

### Get All Games

**GET** `/games`

Retrieve a list of all games with optional pagination, sorting, and filtering.

**Query Parameters:**

* `limit` (optional, default: 50, max: 100) - Number of games to retrieve
* `offset` (optional, default: 0) - Number of games to skip for pagination
* `sort` (optional, default: 'desc') - Sort order: `asc` or `desc`
* `sortBy` (optional, default: 'totalRewardsPoolValueUsd') - Field to sort by:
  * `totalRewardsAllocatedUsd`
  * `name`
  * `createdAt`
  * `estimatedDailyRewardsUsd`
  * `totalRewardsPoolValueUsd`
* `query` (optional) - Search query to filter games by name or description
* `include_extra` (optional, default: false) - Include token association information

**Example Request:**

```bash
curl "https://api.play.fun/games?limit=10&offset=0&sortBy=totalRewardsPoolValueUsd&sort=desc"
```

**Example Response:**

```json
{
  "games": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "name": "Awesome Game",
      "description": "A fun play-to-earn game",
      "gameUrl": "https://example.com/game",
      "imageUrl": "https://cdn.opengameprotocol.com/games/image.png",
      "totalRewardsPoolValueUsd": "15000.50",
      "isHtmlGame": true,
      "hasSdk": true
    }
  ],
  "offset": 0,
  "hasMore": true,
  "total": 158
}
```

**JavaScript Example:**

```javascript
async function getAllGames(limit = 50, offset = 0) {
  const params = new URLSearchParams({
    limit: limit.toString(),
    offset: offset.toString(),
    sortBy: 'totalRewardsPoolValueUsd',
    sort: 'desc',
  });

  const response = await fetch(`https://api.play.fun/games?${params}`);

  return await response.json();
}
```

***

### Register a New Game

**POST** `/games` 🔒

Register a new game on the platform. Requires HMAC authentication.

**Headers:**

```
Content-Type: multipart/form-data
x-auth-provider: hmac
Authorization: HMAC-SHA256 apiKey=... signature=... timestamp=...
```

**Form Data Fields:**

* `name` (required) - Game name
* `description` (required) - Game description
* `gameUrl` (required) - URL where the game is hosted
* `platform` (required) - Platform type: `WEB`, `DESKTOP`, `MOBILE`, or `CONSOLE`
* `isHTMLGame` (optional) - Whether the game is an HTML5 game
* `iframable` (optional) - Whether the game can be embedded in an iframe
* `image` (optional) - Game thumbnail image file
* `coverImage` (optional) - Game cover image file
* `base64Image` (optional) - Base64 encoded game image (alternative to file upload)
* `base64CoverImage` (optional) - Base64 encoded cover image (alternative to file upload)
* `twitter` (optional) - Twitter/X profile URL
* `discord` (optional) - Discord server URL
* `telegram` (optional) - Telegram group URL
* `maxScorePerSession` (optional) - Maximum points per gaming session
* `maxSessionsPerDay` (optional) - Maximum number of sessions per day
* `maxCumulativePointsPerDay` (optional) - Maximum total points per day
* `hidden` (optional, default: false) - Game will not appear in listings but can be played via direct link. Useful for testing play.fun integrations before making the game public.
* `orgRewardsSplit` (optional) - Organization rewards split configuration (see detailed explanation below)
* `jwksUrl` (optional) - JSON Web Key Set URL for authentication

#### Image Upload (`image` and `coverImage`)

Games require at least one image (thumbnail). You can provide images using either file uploads or base64 encoded strings.

**Option 1: File Upload (Recommended for larger images)**

Upload image files using `multipart/form-data`:

```javascript
const formData = new FormData();
formData.append('name', 'My Game');
formData.append('description', 'Game description');
formData.append('gameUrl', 'https://mygame.com');
formData.append('platform', 'WEB');

// Add image file from file input
const imageFile = document.querySelector('#imageInput').files[0];
formData.append('image', imageFile);

// Optionally add cover image
const coverFile = document.querySelector('#coverInput').files[0];
formData.append('coverImage', coverFile);
```

**Option 2: Base64 Encoded Strings (For smaller images or when file upload isn't available)**

Provide images as base64 data URIs:

```javascript
const formData = new FormData();
formData.append('name', 'My Game');
formData.append('description', 'Game description');
formData.append('gameUrl', 'https://mygame.com');
formData.append('platform', 'WEB');

// Base64 encoded image with data URI
formData.append('base64Image', 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...');

// Optionally add base64 cover image
formData.append('base64CoverImage', 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEA...');
```

**Image Requirements:**

* **Required:** At least one image must be provided (`image` file OR `base64Image` string)
* **Priority:** If both `image` file and `base64Image` are provided, the file upload takes precedence
* **Formats:** jpeg, jpg, png, gif, webp
* **Size Limit (Base64):** Maximum 5MB per image when using base64 encoding
* **Size Limit (File Upload):** Check API limits (typically larger than base64)
* **Data URI Format:** Must be in format `data:image/{format};base64,{data}`
  * ✅ Valid: `data:image/png;base64,iVBORw0KGg...`
  * ✅ Valid: `data:image/jpeg;base64,/9j/4AAQ...`
  * ❌ Invalid: Missing `data:image/` prefix
  * ❌ Invalid: Missing `;base64,` separator

**Example Error Responses:**

Missing image:

```json
{
  "statusCode": 400,
  "message": "No image provided",
  "error": "Bad Request"
}
```

Invalid base64 format:

```json
{
  "statusCode": 400,
  "message": "Invalid base64 image format",
  "error": "Bad Request"
}
```

Unsupported image format:

```json
{
  "statusCode": 400,
  "message": "Unsupported image format: bmp. Supported formats: jpeg, jpg, png, gif, webp",
  "error": "Bad Request"
}
```

Image too large:

```json
{
  "statusCode": 400,
  "message": "Image file size exceeds 5MB limit. Please use a smaller image or the UPLOAD flow for larger files.",
  "error": "Bad Request"
}
```

#### Organization Rewards Split (`orgRewardsSplit`)

The `orgRewardsSplit` field allows you to distribute creator rewards among multiple team members, developers, or partners. This is useful for games developed by studios, teams, or platforms with multiple contributors.

**Format:**

```json
{
  "userId1": 5000,
  "userId2": 3000,
  "userId3": 2000
}
```

**Key Concepts:**

1. **Basis Points (BPS):** Values are expressed in basis points where 10,000 BPS = 100%
   * 10000 BPS = 100%
   * 5000 BPS = 50%
   * 2500 BPS = 25%
   * 100 BPS = 1%
2. **Total Must Equal 10,000:** The sum of all basis points must exactly equal 10,000
   * ✅ Valid: `{ "user1": 6000, "user2": 4000 }` (6000 + 4000 = 10000)
   * ❌ Invalid: `{ "user1": 6000, "user2": 3000 }` (6000 + 3000 = 9000, missing 1000)
   * ❌ Invalid: `{ "user1": 6000, "user2": 5000 }` (6000 + 5000 = 11000, exceeds 10000)
3. **User Identifiers:** Keys can be in the following formats. **Important:** All identifiers must correspond to users who have logged into Play Fun via Privy (or will be created automatically):
   * **OGP User IDs** (UUID format): `"550e8400-e29b-41d4-a716-446655440000"` - Direct OGP user ID
   * **Privy IDs**: `"did:privy:abc123..."` - Full Privy user ID
   * **Solana Wallet Addresses**: `"sol:9qdvVLY3vLhmvV7uBkLJsQKcHDjxhoUWJ9uZASYEfwwC"` or `"solana:..."` - Wallet linked to a Privy account
   * **Ethereum Wallet Addresses**: `"eth:0x123..."` or `"ethereum:0x123..."` - Wallet linked to a Privy account
   * **Email Addresses**: `"email:user@example.com"` - Email linked to a Privy account
   * **Twitter/X Handles**: `"twitter:username"` or `"x:username"` - Twitter/X account
4. **Important User Requirements:**
   * **For wallet/email identifiers:** The user must have previously logged into Play Fun using Privy with that wallet or email, OR the system will automatically create a Privy user for them
   * **For OGP User IDs:** Must be an existing user in the Play Fun system
   * **For Privy IDs:** Must be a valid Privy user ID
   * All team members will receive their split of creator rewards automatically when the game generates revenue
5. **Ownership Verification:**
   * If you don't own the game domain (verified via API key meta tag), the `orgRewardsSplit` will be overridden to 100% to you
   * To use custom splits, you must first claim ownership by adding the verification meta tag to your game
6. **Default Behavior:** If not provided or empty, defaults to 100% (10000 BPS) to the game creator

**Example: Equal Split Between 3 Developers**

```json
{
  "orgRewardsSplit": {
    "550e8400-e29b-41d4-a716-446655440000": 3333,
    "did:privy:abc123def456": 3333,
    "sol:9qdvVLY3vLhmvV7uBkLJsQKcHDjxhoUWJ9uZASYEfwwC": 3334
  }
}
```

**Example: Studio + Developer Split (70% / 30%)**

```json
{
  "orgRewardsSplit": {
    "studio-user-id": 7000,
    "developer-user-id": 3000
  }
}
```

**Example: Publisher Platform + 5 Game Developers**

```json
{
  "orgRewardsSplit": {
    "platform-publisher-id": 5000,
    "game-dev-1": 1000,
    "game-dev-2": 1000,
    "game-dev-3": 1000,
    "game-dev-4": 1000,
    "game-dev-5": 1000
  }
}
```

**Common Use Cases:**

* **Game Studios:** Split rewards between studio owner and developers
* **Publishing Platforms:** Take a platform fee and distribute remainder to game creators
* **Collaborative Projects:** Fairly distribute rewards among team members
* **Outsourced Development:** Pay developers and contractors from game rewards
* **Multi-Team Games:** Split between art, programming, and design teams

**Error Responses:**

Invalid split (doesn't sum to 10000):

```json
{
  "statusCode": 400,
  "message": "Total org rewards split must equal 10000 bps",
  "error": "Bad Request"
}
```

Split exceeds 10000:

```json
{
  "statusCode": 400,
  "message": "Total org rewards split exceeds 10000 bps",
  "error": "Bad Request"
}
```

Invalid user identifier:

```json
{
  "statusCode": 400,
  "message": "Unable to resolve playerId: invalid-id",
  "error": "Bad Request"
}
```

**Example Response:**

```json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "My Awesome Game",
  "description": "A fantastic play-to-earn experience",
  "gameUrl": "https://mygame.com",
  "imageUrl": "https://cdn.opengameprotocol.com/games/550e8400.png",
  "createdAt": "2025-11-28T12:00:00.000Z"
}
```

**Full JavaScript Example:**

See [register-game-example.md](https://docs.play.fun/build-on-play.fun/api-endpoints/register-game-example) for a complete implementation.

**Example with orgRewardsSplit:**

```javascript
async function registerGameWithTeamSplit(gameData) {
  const signature = await getSignature('POST', '/games');

  const formData = new FormData();
  formData.append('name', gameData.name);
  formData.append('description', gameData.description);
  formData.append('gameUrl', gameData.gameUrl);
  formData.append('platform', 'WEB');
  formData.append('isHTMLGame', 'true');

  // Add image file
  if (gameData.imageFile) {
    formData.append('image', gameData.imageFile);
  }

  // Define team split: 50% studio, 30% lead dev, 20% artist
  const teamSplit = {
    'studio-user-id-uuid': 5000, // 50%
    'lead-dev-user-id-uuid': 3000, // 30%
    'artist-user-id-uuid': 2000, // 20%
  };

  // Important: FormData requires JSON string for objects
  formData.append('orgRewardsSplit', JSON.stringify(teamSplit));

  const response = await fetch('https://api.play.fun/games', {
    method: 'POST',
    headers: {
      'x-auth-provider': 'hmac',
      Authorization: signature,
    },
    body: formData,
  });

  if (!response.ok) {
    const errorText = await response.text();
    throw new Error(`Registration failed: ${response.status} - ${errorText}`);
  }

  return await response.json();
}

// Usage example
const gameData = {
  name: 'Team Adventure Game',
  description: 'An epic adventure created by our studio',
  gameUrl: 'https://teamgame.com',
  imageFile: imageBlob, // File or Blob object
};

try {
  const result = await registerGameWithTeamSplit(gameData);
  console.log('Game registered successfully:', result);
  console.log('Rewards will be split among team members automatically');
} catch (error) {
  console.error('Failed to register game:', error.message);
}
```

***

### Update an Existing Game

**POST** `/games/update/{gameId}` 🔒

Update an existing game's metadata and images. Requires HMAC authentication.

**Path Parameters:**

* `gameId` (required) - UUID of the game to update

**Headers & Form Data:** Same as Register Game endpoint. Additionally supports:

* `hidden` (optional) - Game will not appear in listings but can be played via direct link. Useful for testing play.fun integrations before making the game public.

**Example:**

```javascript
async function updateGame(gameId, updates) {
  const signature = await getSignature('POST', `/games/update/${gameId}`);

  const formData = new FormData();
  formData.append('name', updates.name);
  formData.append('description', updates.description);
  formData.append('gameUrl', updates.gameUrl);
  formData.append('platform', updates.platform);

  if (updates.imageFile) {
    formData.append('image', updates.imageFile);
  }

  const response = await fetch(`https://api.play.fun/games/update/${gameId}`, {
    method: 'POST',
    headers: {
      'x-auth-provider': 'hmac',
      Authorization: signature,
    },
    body: formData,
  });

  return await response.json();
}
```

***

### Get Game by ID

**GET** `/games/id/{id}`

Retrieve detailed information about a specific game.

**Path Parameters:**

* `id` (required) - UUID of the game

**Query Parameters:**

* `include_tokens` (optional, default: false) - Include associated token information

**Example Request:**

```bash
curl "https://api.play.fun/games/id/550e8400-e29b-41d4-a716-446655440000?include_tokens=true"
```

**Example Response:**

```json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "Awesome Game",
  "description": "A fun play-to-earn game",
  "gameUrl": "https://example.com/game",
  "imageUrl": "https://cdn.opengameprotocol.com/games/image.png",
  "coverImage": "https://cdn.opengameprotocol.com/games/cover.png",
  "isHtmlGame": true,
  "iframable": true,
  "platform": "WEB",
  "twitter": "https://twitter.com/awesomegame",
  "discord": "https://discord.gg/awesomegame",
  "totalRewardsPoolValueUsd": "15000.50",
  "totalRewardsAllocatedUsd": "5000.25",
  "tokens": [
    {
      "address": "TokenMint123...",
      "symbol": "GAME",
      "name": "Game Token",
      "imageUrl": "https://cdn.opengameprotocol.com/tokens/game.png"
    }
  ]
}
```

**JavaScript Example:**

```javascript
async function getGameById(gameId, includeTokens = false) {
  const params = includeTokens ? '?include_tokens=true' : '';
  const response = await fetch(`https://api.play.fun/games/id/${gameId}${params}`);
  return await response.json();
}
```

***

### Get My Games

**GET** `/games/me` 🔒

Retrieve games created by the authenticated user. Requires HMAC authentication.

**Headers:**

```
x-auth-provider: hmac
Authorization: HMAC-SHA256 apiKey=... signature=... timestamp=...
```

**Query Parameters:**

* `limit` (optional, default: 50) - Number of games to retrieve
* `offset` (optional, default: 0) - Number of games to skip
* `sort` (optional, default: 'desc') - Sort order: `asc` or `desc`
* `include_extra` (optional, default: false) - Include additional game information

**Example Request:**

```javascript
async function getMyGames(limit = 50, offset = 0) {
  const signature = await getSignature('GET', '/games/me');

  const params = new URLSearchParams({
    limit: limit.toString(),
    offset: offset.toString(),
    sort: 'desc',
  });

  const response = await fetch(`https://api.play.fun/games/me?${params}`, {
    headers: {
      'x-auth-provider': 'hmac',
      Authorization: signature,
    },
  });

  return await response.json();
}
```

**Example Response:**

```json
[
  {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "My Game 1",
    "description": "First game",
    "gameUrl": "https://game1.com",
    "imageUrl": "https://cdn.opengameprotocol.com/games/game1.png",
    "totalRewardsPoolValueUsd": "10000.00"
  },
  {
    "id": "660e8400-e29b-41d4-a716-446655440001",
    "name": "My Game 2",
    "description": "Second game",
    "gameUrl": "https://game2.com",
    "imageUrl": "https://cdn.opengameprotocol.com/games/game2.png",
    "totalRewardsPoolValueUsd": "5000.00"
  }
]
```

***

### Toggle Game Visibility

**POST** `/games/toggle-visibility/{id}` 🔒

Toggle the visibility of a game (show/hide from public listings). Requires HMAC authentication and game ownership.

**Path Parameters:**

* `id` (required) - UUID of the game

**Headers:**

```
x-auth-provider: hmac
Authorization: HMAC-SHA256 apiKey=... signature=... timestamp=...
```

**Example Request:**

```javascript
async function toggleGameVisibility(gameId) {
  const signature = await getSignature('POST', `/games/toggle-visibility/${gameId}`);

  const response = await fetch(`https://api.play.fun/games/toggle-visibility/${gameId}`, {
    method: 'POST',
    headers: {
      'x-auth-provider': 'hmac',
      Authorization: signature,
    },
  });

  return await response.json();
}
```

**Example Response:**

```json
{
  "success": true,
  "message": "Game visibility toggled successfully",
  "game": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "My Game",
    "hidden": true
  }
}
```

***

### Claim Game Ownership

**POST** `/games/claim-ownership/{gameId}` 🔒

Claim ownership of a game by verifying control of the game's domain. Requires HMAC authentication.

**Path Parameters:**

* `gameId` (required) - UUID of the game to claim

**Headers:**

```
x-auth-provider: hmac
Authorization: HMAC-SHA256 apiKey=... signature=... timestamp=...
```

**How it works:**

1. The game creator must add a verification meta tag to their game's HTML
2. The verification tag contains the user's API key
3. This endpoint verifies the tag exists and matches the authenticated user

**Example Request:**

```javascript
async function claimGameOwnership(gameId) {
  const signature = await getSignature('POST', `/games/claim-ownership/${gameId}`);

  const response = await fetch(`https://api.play.fun/games/claim-ownership/${gameId}`, {
    method: 'POST',
    headers: {
      'x-auth-provider': 'hmac',
      Authorization: signature,
    },
  });

  return await response.json();
}
```

**Verification Meta Tag:** Add this to your game's HTML `<head>` section:

```html
<meta name="x-ogp-key" content="your-api-key-here" />
```

**Example Response:**

```json
{
  "success": true,
  "message": "Game ownership claimed successfully",
  "game": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "ownerId": "user-id-123",
    "ownershipValidated": true
  }
}
```

***

## Batch Operations

### Get Games by Token IDs

**POST** `/games/batch/token-ids`

Retrieve games associated with multiple token addresses in a single request. Maximum 100 token IDs per request.

**Request Body:**

```json
{
  "tokenIds": ["TokenMintAddress1...", "TokenMintAddress2...", "TokenMintAddress3..."]
}
```

**Example Response:**

```json
{
  "TokenMintAddress1...": {
    "games": [
      {
        "id": "game-id-1",
        "imageUrl": "https://cdn.opengameprotocol.com/games/game1.png",
        "name": "Game One"
      },
      {
        "id": "game-id-2",
        "imageUrl": "https://cdn.opengameprotocol.com/games/game2.png",
        "name": "Game Two"
      }
    ],
    "total": 2
  },
  "TokenMintAddress2...": {
    "games": [
      {
        "id": "game-id-3",
        "imageUrl": "https://cdn.opengameprotocol.com/games/game3.png",
        "name": "Game Three"
      }
    ],
    "total": 1
  }
}
```

**JavaScript Example:**

```javascript
async function getGamesByTokenIds(tokenIds) {
  const response = await fetch('https://api.play.fun/games/batch/token-ids', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ tokenIds }),
  });

  return await response.json();
}
```

***

### Get Games by Game IDs

**POST** `/games/batch/ids`

Retrieve multiple games by their IDs in a single request. Maximum 100 game IDs per request.

**Request Body:**

```json
{
  "gameIds": ["550e8400-e29b-41d4-a716-446655440000", "660e8400-e29b-41d4-a716-446655440001"]
}
```

**Example Response:**

```json
[
  {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "Game One",
    "description": "First game",
    "gameUrl": "https://game1.com",
    "imageUrl": "https://cdn.opengameprotocol.com/games/game1.png",
    "isHtmlGame": true
  },
  {
    "id": "660e8400-e29b-41d4-a716-446655440001",
    "name": "Game Two",
    "description": "Second game",
    "gameUrl": "https://game2.com",
    "imageUrl": "https://cdn.opengameprotocol.com/games/game2.png",
    "isHtmlGame": true
  }
]
```

**JavaScript Example:**

```javascript
async function getGamesByIds(gameIds) {
  const response = await fetch('https://api.play.fun/games/batch/ids', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ gameIds }),
  });

  return await response.json();
}
```

***

## Error Responses

All endpoints may return the following error responses:

**400 Bad Request:**

```json
{
  "statusCode": 400,
  "message": "Invalid request parameters",
  "error": "Bad Request"
}
```

**401 Unauthorized:**

```json
{
  "statusCode": 401,
  "message": "Invalid or missing authentication",
  "error": "Unauthorized"
}
```

**403 Forbidden:**

```json
{
  "statusCode": 403,
  "message": "You do not have permission to perform this action",
  "error": "Forbidden"
}
```

**404 Not Found:**

```json
{
  "statusCode": 404,
  "message": "Game not found",
  "error": "Not Found"
}
```

**500 Internal Server Error:**

```json
{
  "statusCode": 500,
  "message": "An error occurred while processing your request",
  "error": "Internal Server Error"
}
```

***

## Rate Limits

The API implements rate limiting to ensure fair usage:

* **Default Rate Limit:** 60 requests per minute per IP address
* **Authenticated Endpoints:** May have higher limits based on your account

If you exceed the rate limit, you'll receive a `429 Too Many Requests` response:

```json
{
  "statusCode": 429,
  "message": "Too many requests, please try again later",
  "error": "Too Many Requests"
}
```

***

## Best Practices

1. **Cache Responses:** Cache game data locally to reduce API calls
2. **Use Batch Endpoints:** When fetching multiple games, use batch endpoints instead of individual requests
3. **Handle Errors Gracefully:** Always implement proper error handling
