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-emptygame_nameis strongly recommended); game_codevalues match those sent in Optimove Personalize real-time events;newis a JSON Boolean, not the strings"true"or"false";rtpis between0and1;- at least one of
desktop,mobileandtabletis1; - all three platform values are explicit
0or1, nevernull; - every
tagsandgame_featuresobject 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.jsonA 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.jsonThe 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:
| Status | Meaning | Required action |
|---|---|---|
STARTED | Processing has not finished | Keep polling with backoff; do not upload another snapshot |
COMPLETED | The processing function reached a successful terminal state | Reconcile the exported game-code count and set before marking the run successful or starting another upload |
FAILED | Processing failed | Alert, 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.
Updated about 2 hours ago
