Gaming Inventory Quickstart

This guide checks and uploads one complete Gaming Inventory snapshot, then monitors processing and reconciles the exported Inventory view.

Before continuing, read Full-snapshot replacement semantics. A file containing one game is appropriate only for a brand whose intended complete catalogue contains exactly one game.

1. Set your connection values

The base URL must include the versioned Gaming Inventory base path and must not end with /.

export GAMING_INVENTORY_BASE_URL='https://api.opti-x.optimove.net/inventory/v1'
export SERVICE_API_KEY='{{service_api_key}}'
export BRAND_KEY='{{brand_key}}'

Run this workflow from a secure server or CI job. Do not expose the API key in client-side code.

2. Create the complete snapshot

Save the intended full catalogue as games.json:

{
  "SLOT001": {
    "game_code": "SLOT001",
    "game_name": "Adventure Quest",
    "game_category": "Slots",
    "game_class": "Video Slots",
    "game_type": "Five Reel",
    "desktop": 1,
    "mobile": 1,
    "tablet": 1,
    "rtp": 0.96,
    "themes": ["Adventure"],
    "launch_date": "17/05/2025",
    "new": false,
    "supplier": "Example Games",
    "default_order": 10,
    "image_url": "https://cdn.example.com/games/slot001.jpg",
    "tags": [
      { "exclusive": false }
    ],
    "skin_id": ["main"],
    "regulation": ["GB"]
  }
}

Production files must contain every game that should remain available for the brand. The root key SLOT001 exactly matches game_code.

Before sending the file, check at minimum that:

  • the root is a non-empty JSON object;
  • every root key is unique and equals its nested game_code;
  • every game has a non-empty game_code (a non-empty game_name is strongly recommended);
  • game_code values match those sent in Optimove Personalize real-time events;
  • new is a JSON Boolean, not the strings "true" or "false";
  • rtp is between 0 and 1;
  • at least one of desktop, mobile and tablet is 1;
  • all three platform values are explicit 0 or 1, never null;
  • every tags and game_features object has exactly one key/value pair; and
  • removals compared with the last successful snapshot are intentional.

3. Validate the complete file

curl --silent --show-error --fail-with-body \
  --request POST \
  --url "$GAMING_INVENTORY_BASE_URL/validate" \
  --header "x-api-key: $SERVICE_API_KEY" \
  --header "x-brand-key: $BRAND_KEY" \
  --header 'content-type: application/json' \
  --data-binary @games.json

A file accepted by the current API validator returns HTTP 200:

{
  "validation_errors": []
}

A file rejected by the current API validator returns HTTP 400. Errors are grouped under the top-level game key:

{
  "validation_errors": [
    {
      "SLOT001": [
        {
          "loc": ["new"],
          "msg": "value is not a valid boolean",
          "type": "value_error.strictbool"
        }
      ]
    }
  ]
}

Do not upload while validation_errors contains any entry. A 200 is not proof that the file satisfies the complete intended contract: the current validator does not enforce every declared requirement, range, format, or root-level rule. Your integration must apply every structural local check above—root-key equality, a non-empty catalogue, expected game counts, and intentional removals—and is strongly encouraged to verify the recommended values (game_name, RTP range, and platform completeness).

4. Request a temporary upload form

curl --silent --show-error --fail-with-body \
  --request GET \
  --url "$GAMING_INVENTORY_BASE_URL/upload" \
  --header "x-api-key: $SERVICE_API_KEY" \
  --header "x-brand-key: $BRAND_KEY" \
  --output upload-form.json

The response contains an upload URL and a set of signed form fields:

{
  "url": "https://temporary-upload.example.com/",
  "fields": {
    "key": "brand/123/inventory/games.json",
    "policy": "<opaque-signed-policy>",
    "credential": "<opaque-value>",
    "date": "20260101T120000Z",
    "signature": "<opaque-signature>"
  }
}

The exact set of returned fields can vary and their values are opaque. Submit every field unchanged, exactly as received. The form expires after one hour; request a new one if it expires.

5. Upload using the temporary form

The following Python example dynamically forwards all returned form fields and adds the file as the final multipart field. It requires Python 3 and the requests package.

Save it as upload_inventory.py:

import json
from pathlib import Path

import requests

BASE_URL = "https://api.opti-x.optimove.net/inventory/v1"
API_KEY = "{{service_api_key}}"
BRAND_KEY = "{{brand_key}}"
FILE_PATH = Path("games.json")

api_headers = {
    "x-api-key": API_KEY,
    "x-brand-key": BRAND_KEY,
}

form_response = requests.get(
    f"{BASE_URL}/upload",
    headers=api_headers,
    timeout=30,
)
form_response.raise_for_status()
upload_form = form_response.json()

with FILE_PATH.open("rb") as inventory_file:
    upload_response = requests.post(
        upload_form["url"],
        data=upload_form["fields"],
        files={
            "file": (
                "games.json",
                inventory_file,
                "application/json",
            )
        },
        timeout=120,
    )

upload_response.raise_for_status()

result = {
    "upload_status": upload_response.status_code,
}
Path("upload-result.json").write_text(
    json.dumps(result, indent=2),
    encoding="utf-8",
)
print(json.dumps(result, indent=2))

The file upload goes to the temporary URL, not to the Gaming Inventory API base URL. Do not send x-api-key or x-brand-key to the temporary upload host.

A successful upload confirms file transfer only—it does not confirm processing. Obtain the processing trace ID from /logs/recent, as described next.

6. Find and store the trace ID

Request the brand's recent traces:

curl --silent --show-error --fail-with-body \
  --request GET \
  --url "$GAMING_INVENTORY_BASE_URL/logs/recent" \
  --header "x-api-key: $SERVICE_API_KEY" \
  --header "x-brand-key: $BRAND_KEY"

Processing is asynchronous, so the trace may take a short time to appear. Retry with backoff. Select the new trace created after your recorded upload time and store its traceId. Because uploads must not overlap, there should be only one unaccounted-for trace for that brand.

The implementation queries the previous 24 hours but stops after approximately 1,000 raw main-and-step log items. A busy brand can therefore receive only a subset. Do not use this endpoint as your long-term trace store.

7. Poll the trace to a terminal status

Replace {{trace_id}} with the stored value:

curl --silent --show-error --fail-with-body \
  --request GET \
  --url "$GAMING_INVENTORY_BASE_URL/logs/trace/{{trace_id}}" \
  --header "x-api-key: $SERVICE_API_KEY" \
  --header "x-brand-key: $BRAND_KEY"

The response contains mainLog and steps:

{
  "traceId": "example-version-id",
  "mainLog": {
    "traceId": "example-version-id",
    "recordType": "MAIN",
    "brandKey": "123",
    "fileName": "brand/123/inventory/games.json",
    "fileSize": 4281,
    "status": "COMPLETED",
    "processedRows": 1,
    "duration": 6842,
    "timestamp": "2026-01-01T12:00:05+00:00"
  },
  "steps": []
}

Interpret mainLog.status as follows:

StatusMeaningRequired action
STARTEDProcessing has not finishedKeep polling with backoff; do not upload another snapshot
COMPLETEDThe processing function reached a successful terminal stateReconcile the exported game-code count and set before marking the run successful or starting another upload
FAILEDProcessing failedAlert, inspect errorMessage and failed steps, then follow the recovery procedure

Do not treat the upload HTTP status or the appearance of a trace as success. mainLog.status: "COMPLETED" is the successful terminal processing result, but it does not prove that every intended large deletion was applied. Complete the post-processing reconciliation before closing the run.

See Monitoring and recovery for production polling, alerting and failure recovery.


Did this page help you?