API and MCP access are included with a Business account.
One seat included, scoped keys for every member, and a shared monthly quota.
Business API v1
The Business API gives programmatic access to the same market intelligence Game Oracle renders in the app: catalogue search, game reports, Game Gap market segments, outlier leaderboards and Concept Compass projects. It is available to every member of an active Business account.
Base URL: https://game-oracle.com/api/business/v1
Every endpoint returns JSON. Bodies are JSON unless noted (image search also accepts multipart uploads).
Authentication
Pass your API key in the Authorization header on every request:
Authorization: Bearer go_biz_<key> Keys are created and revoked from Account Settings. Each key is scoped (see below) and belongs to the member who created it; every member of a Business account may hold up to five active keys.
Scopes
| Scope | Grants |
|---|---|
search:read | All /search/* endpoints |
games:read | /games/{app_id}, /games/reports and the jobs they create |
gaps:read | /game-gap/* endpoints and the jobs they create |
outliers:read | /outliers |
concepts:write | All /concept-compass/* endpoints and the jobs they create |
Job endpoints (/jobs/*) require the scope of the job’s type. A key missing a scope receives:
{
"success": false,
"error": "Key missing required scope: games:read",
"code": "INSUFFICIENT_SCOPE"
} Rate Limits & Quotas
| Control | Default |
|---|---|
| Per-minute rate limit (per key) | 60 requests/minute |
| Monthly quota (per Business account) | 10,000 units/month |
The monthly quota is shared by every key on the account. Most requests cost one unit. The exceptions:
- Batch report jobs cost one unit per id at submission (ids that do not exist are refunded). A cancelled job refunds its unprocessed items.
- Concept Compass costs 8 units to create a project and 3 units to update its competitors.
- Polling and reference data (
GET /jobs/{id},GET /jobs/{id}/results,GET /search/fields, everyGET /reference/*) are free — rate-limited but never consuming quota.
A request that would push the account over its quota is refused with 429 QUOTA_EXCEEDED before any work is done.
Staying under the rate limit
The per-minute limit is per key, and every request counts towards it — including free ones and, over MCP, the connection handshake. Two habits keep an integration clear of 429 RATE_LIMITED:
- Pace on the headers. Every response carries
X-RateLimit-RemainingandX-RateLimit-Reset(Unix seconds). WhenRemainingreaches one or two, wait untilResetbefore the next call rather than retrying on a 429. ARetry-Afterheader accompanies every 429. - Rotate keys for parallel work. Each member of the account can hold up to five keys and every key has its own window, while the monthly quota is shared. Give each worker, agent or environment its own key (from Account Settings) instead of fanning one key out across them.
Batch endpoints exist for the same reason: one POST /games/reports with 100 ids is one request against the window, where 100 GET /games/{app_id} calls would be a hundred.
Response headers
| Header | Description |
|---|---|
X-RateLimit-Limit | Per-minute cap for this key |
X-RateLimit-Remaining | Requests left in the current window |
X-RateLimit-Reset | Unix timestamp when the window resets |
X-Quota-Limit | The account’s monthly quota |
X-Quota-Used | Units consumed this month across all keys |
Error Model
{
"success": false,
"error": "Human-readable message",
"code": "MACHINE_READABLE_CODE",
"details": [{ "path": "appIds", "message": "Too big: expected array to have <=100 items" }]
} details is present on validation errors only.
| Status | Codes |
|---|---|
| 400 | VALIDATION_ERROR |
| 401 | INVALID_KEY |
| 403 | INSUFFICIENT_SCOPE, SUBSCRIPTION_REQUIRED, SEAT_REVOKED |
| 404 | NOT_FOUND |
| 409 | JOB_NOT_FINISHED, JOB_NOT_ACTIVE, JOB_IN_PROGRESS |
| 429 | RATE_LIMITED, QUOTA_EXCEEDED, TOO_MANY_ACTIVE_JOBS |
| 500 | SERVER_ERROR |
| 502 | UPSTREAM_ERROR |
| 503 | SERVICE_UNAVAILABLE, CONCURRENCY_LIMIT (retry after the Retry-After header) |
Search
Five endpoints share one result format. All are POST with a JSON body.
| Endpoint | Ranks by |
|---|---|
/search/title | Similarity to an existing game (appId) |
/search/description | Similarity to a free-text description (query) |
/search/image | Visual similarity to one or more images |
/search/attributes | No ranking — structured filters only, sortable |
/search/combined | Rank fusion of any mix of title, description and images |
Common body fields
| Field | Type | Default | Notes |
|---|---|---|---|
nNeighbours | integer 1–10000 | 1000 | Semantic searches only: how many closest games to consider |
page | integer ≥ 1 | 1 | |
pageSize | integer 1–100 | 25 | |
filters | object | — | The Data Explorer advanced filters (below) |
attributes | object | — | Free-form filters over any catalogue field (below) |
sort | { field, direction } | app_id asc | Attribute search only; semantic searches are always ordered by distance |
filters — Data Explorer advanced filters
Identical to the controls in the Data Explorer. Lists accept arrays or comma-separated strings; ranges accept [min, max] or "min,max".
| Field | Type / values |
|---|---|
includeTags, excludeTags | string[] |
releasedOrUnreleased | Released | Unreleased | Both |
earlyAccess, aiDisclosure | Include | Exclude | Only |
pricingType | Free | Paid | Both |
devType | Self-Published | AAA | All |
minSteamDBScore, minPercPositiveReviews | number 0–100 |
minReviewCount, minPeerRank, minUniquenessScore | number ≥ 0 |
excludeNSFW | boolean |
releaseDateStart, releaseDateEnd | YYYY-MM-DD |
wishlistRange, unitsSoldRange, revenueRange, devLifetimeRevenueRange, pubLifetimeRevenueRange, initialPriceRange, latestPriceRange | [min, max] |
gameEngine | string (substring match) |
attributes — any catalogue field
attributes is a map of field name → condition, where a condition is a bare value (equality), an array ($in), or an operator object:
{
"attributes": {
"is_free": false,
"meta_tags": ["Roguelike", "Deckbuilder"],
"scores.uniqueness_score": { "$gte": 6.5 },
"store_release_date.year": { "$in": [2025, 2026] },
"title": { "$regex": "farm", "$options": "i" }
}
} Operators: $eq $ne $gt $gte $lt $lte $in $nin $exists $all $regex. $regex is a case-insensitive substring match and is only accepted on text fields; $all requires every listed value (tags, languages). Up to 40 fields per request.
GET /search/fields returns every filterable field with its accepted operators and whether it is sortable. Fields include every column of the games table (app_id, title, initial_price, latest_price, is_free, early_access, coming_soon, age_days, game_engine, ai_disclosure, …), the satellite groups (review_count.*, review_state.*, scores.*, estimated_units_sold.*, estimated_revenue.*, estimated_wishlist.*, developers_lifetime_revenue.*, store_release_date.*, latest_release_date.*, age_normalised_rank.*, pc_min_requirements.*, followers) and the relations (meta_tags, developers, publishers, supported_languages, platform_windows, platform_linux, image_urls).
When filters and attributes constrain the same field, their operators are merged and attributes wins on conflicts.
Reference data — the values you can search for
Guessing tag names or engine strings is the usual way to get an empty result. These endpoints return the live vocabularies so a client (or an agent) can build a valid query first. All of them are free of quota and need any active key, whatever its scopes.
| Endpoint | Returns |
|---|---|
GET /search/fields | Every filterable/sortable field with its operators, plus filters — the enumerated options of each filters key — and reference links to the endpoints below |
GET /reference/tags | Every Steam tag the catalogue uses: all_tags, genres and themes (grouped { name, tags[] }), visuals, player_support, nsfw, steam_features, for attributes.meta_tags and filters.includeTags |
GET /reference/values/{field} | Distinct values of one low-cardinality field, most common first ({ value, games }): game_engine, age_category, publication_era, controller_support, supported_languages |
GET /reference/segments | The vocabulary of GET /game-gap/segments: dimensionalities, the live subcategory list per dimensionality, sortBy keys, exclusion flags, range parameters |
GET /reference/concept-options | The tag lists, enums, defaults and limits POST /concept-compass/projects validates against |
GET /reference/features | Search the ~470 game design features (“building blocks”) by name or definition (q, category, offset; 50 per page). Their ids are what featureIds accepts |
GET /reference/values/game_engine
→ { "field": "game_engine", "attribute": "game_engine",
"values": [{ "value": "Unity", "games": 28711 }, { "value": "Unreal Engine", "games": 9147 }, …] } Values are returned exactly as stored so they match in attributes (some supported_languages entries carry a leading space, for example — use the strings as given). Lists are cached server-side for ten minutes.
Title search
POST /search/title
{ "appId": 413150, "nNeighbours": 500, "filters": { "pricingType": "Paid" }, "pageSize": 50 } The queried game is always first with distance: 0. 404 NOT_FOUND if the app id is unknown.
Description search
POST /search/description
{ "query": "A cozy farming sim where you rebuild a lighthouse town", "nNeighbours": 1000 } Image search
JSON form — public https:// image URLs and/or inline base64:
POST /search/image
{
"imageUrls": ["https://example.com/capsule.png"],
"images": [{ "data": "<base64>", "mimeType": "image/png" }],
"nNeighbours": 1000
} Multipart form — files in images (repeat the field), with optional string fields nNeighbours, page, pageSize and JSON-encoded filters, attributes, sort:
curl -X POST https://game-oracle.com/api/business/v1/search/image
-H "Authorization: Bearer go_biz_…"
-F "images=@screenshot1.png" -F "images=@screenshot2.png"
-F "nNeighbours=500" -F 'filters={"excludeNSFW":true}' Up to 5 images, 10 MB each. Image URLs must be public https:// addresses. This search can take up to a few minutes when the image service is cold; the request waits.
Attribute search
POST /search/attributes
{
"attributes": { "coming_soon": true, "estimated_wishlist.mid": { "$gte": 20000 } },
"filters": { "excludeNSFW": true },
"sort": { "field": "estimated_wishlist.mid", "direction": "desc" },
"pageSize": 100
} Combined search
POST /search/combined
{
"title": { "appId": 413150 },
"description": { "query": "farming sim with a mystery" },
"image": { "imageUrls": ["https://example.com/capsule.png"] },
"weights": { "title": 0.5, "description": 0.3, "image": 0.2 },
"nNeighbours": 1000,
"filters": { "releasedOrUnreleased": "Released" }
} Each source is queried in parallel and the rankings fused (reciprocal rank fusion, the same as the Data Explorer). weights overrides the default split. With more than one source, distance is a normalised rank score (0 = top), not a cosine distance.
Result format
{
"success": true,
"data": [
{
"app_id": 413150,
"title": "Stardew Valley",
"…": "every games-table field plus review_count, review_state, scores, estimated_* ranges, release dates, meta_tags, developers, publishers, platforms, supported_languages, …",
"socials": { "app_id": 413150, "twitter": "…", "discord": "…" },
"distance": 0,
"source": "title"
}
],
"meta": { "page": 1, "pageSize": 25, "total": 812, "candidates": 1000 }
} total is the number of games matching your filters (within the nNeighbours candidates for semantic searches); candidates is how many the semantic source returned before filtering.
Game reports
GET /games/{app_id}
The full report the /games/{app_id} page shows:
| Field | Contents |
|---|---|
game | Every catalogue field for the game |
socials | Store and social links |
tags | The game’s tags grouped: genres, themes, visuals, playerSupport, steamFeatures, other |
benchmarks | Steam-wide baselines every stat is compared against |
segments | The market segments the game belongs to (with displayTitle) |
playerHistory | Monthly average and peak concurrent players |
reviewSummary | Review analysis: overall, positives[], negatives[] |
similarGames | The 100 most similar games, closest first, each with distance |
generation | Whether reviewSummary was served from cache or produced on this request |
Review analysis is produced on first request and cached for later ones, so a report may take a few seconds the first time.
POST /games/reports — batch
POST /games/reports
{ "appIds": [413150, 1145360, 1794680] } Up to 100 ids. Returns 202 Accepted with a job (see Jobs). Costs one unit per id; unknown ids are marked skipped and refunded.
Game Gap
GET /game-gap/segments
Search market segments with the same filters as the Game Gap page. All parameters are query-string values.
| Parameter | Values |
|---|---|
dimensionality | Both | 2D | 3D |
subcategory | A subcategory name, or __NULL__ for top-level segments |
keyword | Substring match on the segment’s keywords |
sortBy | demand_score | avg_uniqueness_score | median_estimated_steam_sales (default: top picks) |
excludeGoliaths, excludeStampede, excludeAchievementFarming, excludeCultFollowing, excludeNsfw (or excludeNSFW), excludeFranchiseDominated | true to exclude that segment type |
minPercComingSoon/maxPercComingSoon, minPercIndie/maxPercIndie, minPercVpos/maxPercVpos | fractions 0–1 |
minMedianSales/maxMedianSales, minMedianWishlists/maxMedianWishlists, minMedianRevenue/maxMedianRevenue, minMedianDevLifetimeRevenue/maxMedianDevLifetimeRevenue, minMedianUniquenessScore/maxMedianUniquenessScore, minMedianAge/maxMedianAge | numbers |
clusterIds | Comma-separated ids (≤100) — fetch specific segments instead of filtering |
page, pageSize | pageSize ≤ 50, default 20 |
GET /reference/segments lists the live subcategories and every accepted parameter. Each row is the segment list item (cluster_id, title, summary, keywords, total, demand_score, avg_uniqueness_score, median_estimated_steam_sales, median_wishlist_estimate, warning flags, …) plus displayTitle. Segments that have never been titled get one generated for the first ten on the page; the rest use a keyword-derived displayTitle until a later request fills them in.
GET /game-gap/segments/{cluster_id}
The full report the /game-gap/{cluster_id} page shows:
| Field | Contents |
|---|---|
segment | The segment row: composition percentages, medians, trend figures, warning flags, app_ids |
title | Display title |
games | Every game in the segment (full catalogue rows) |
overview | detailed_summary, five_similar_features, market_gaps, biggest_opportunities, biggest_challenges, developer_fit |
sentiment | Player sentiment: overview, positives[], negatives[] |
benchmarks | Steam-wide baselines |
metrics | Derived exactly as the page derives them: warnings, badges (missed opportunity / underserved audience), performance medians vs Steam, launchEfficiency, hollowHitAppIds, topOutlierAppIds, gameEngines |
generation | Whether overview / sentiment came from cache or were produced on this request |
Overview and sentiment are produced on first request and cached for 30 days.
POST /game-gap/reports — batch
POST /game-gap/reports
{ "clusterIds": ["v2-both-cluster-3", "v2-2d-cluster-17"] } Up to 100 ids; 202 Accepted with a job. One unit per id.
Outliers
GET /outliers?listType=upcoming_games|released_games
The /leaderboards/outliers lists: the top 100 games by uniqueness, ranked by estimated wishlists (upcoming_games) or SteamDB score (released_games). Rows: rank, app_id, title, steam_db_score, uniqueness_score, estimated_units_sold, estimated_units_sold_steam, wishlist_estimate, capsule_img_url, coming_soon, free_to_play.
Concept Compass
POST /concept-compass/projects
Create a project from the same inputs the Concept Compass wizard collects. The project appears in your Concept Compass project list immediately and is filled in as the job runs.
POST /concept-compass/projects
{
"name": "Lighthouse Keeper",
"engine": "Godot",
"teamSize": 2,
"scope": "Small",
"shortDescription": "A cozy farming sim where you rebuild a lighthouse town and uncover its past.",
"visualStyle": "2D",
"artStyle": ["Pixel Graphics"],
"playerPerspective": ["Top-Down"],
"themesGenres": ["Farming Sim", "Cozy", "Mystery"],
"aesthetics": "Warm palette, soft synth soundtrack",
"gameplay": "Daily loop of farming, fishing and restoring buildings",
"narrative": "Letters from the previous keeper reveal why the town emptied",
"playerExperience": "Calm with a slow-burn mystery",
"featureIds": ["f_a7c36776d58e41d4"]
} name and shortDescription (≥10 characters) are required; visualStyle, artStyle, playerPerspective and themesGenres accept the wizard’s tag lists; everything else defaults as in the wizard. Reference images are not accepted over the API.
featureIds (optional) names 1–5 core features — the design pillars the concept is built around — using ids from GET /reference/features. They sharpen the competitor search (games verified to share them are considered first) and are woven into every generated section. An unknown id is a 400 listing details.unknownIds, and nothing is created or charged.
Returns 202 Accepted:
{
"success": true,
"data": {
"jobId": "…",
"type": "concept_create",
"status": "queued",
"stage": "landscape",
"projectId": "…",
"pollUrl": "/api/business/v1/jobs/…",
"reportUrl": "/api/business/v1/concept-compass/projects/…",
"items": [
{ "id": "landscape", "status": "pending" },
{ "id": "resources", "status": "pending" },
"…"
]
}
} Costs 8 units. The job runs the competitor landscape first (retrieval, relevance evaluation), then the report sections in parallel; a section that fails is left empty and reported in items.
GET /concept-compass/projects/{projectId}
While a job is writing to the project: 202 with { "status": "generating", "job": { … } } and a Retry-After header. Otherwise 200 with the full report:
| Field | Contents |
|---|---|
input | The wizard fields |
competitors[] | Each competitor’s full catalogue row plus distance, relevance (Very/Somewhat), relevanceReason, discoveryMethod |
headline | uniquenessScore, sales, reviews, revenue, wishlists, wishlistConversion (mid/low/high), priceRange, counts, validPublisherIds |
projection | Per-outcome forecasts (reviews, units, revenue, wishlists) with distribution quantiles, evidence quality and the evidence accounting behind each |
boxleiterMultipliers | The review-to-sales multiplier range used for the cross-check |
resources | suggestions (technical components, visuals, audio, level design, hard problems, search/asset keywords) and suggestedPublishers |
marketing | suggestedSteamDescription, ideas[], suggestedTags |
competition | summary, playerSentiment |
gdd | draft, markdown, generatedAt, editedAt, inspirationCompetitorIds |
generation | generatedFromCompetitorIds and drift (how far the competitor list has moved since) |
POST /concept-compass/update-competitors/{projectId}
POST /concept-compass/update-competitors/{projectId}
{ "competitorIds": [413150, 1145360, 1794680] } Replaces the competitor list (up to 100 ids; unknown ids are dropped). Added games have their similarity measured against your concept; removed games are dropped. The competition summary and player sentiment are regenerated; projections and headline statistics are derived on read and update automatically. Returns 202 with a job; 409 JOB_IN_PROGRESS if the project already has a running job. Costs 3 units.
Jobs
Batch reports and Concept Compass generation run as jobs. A job passes through queued → running → one of completed, partial (some items failed), failed or cancelled. Work starts within seconds of submission; a full batch of 100 reports typically finishes in a few minutes. Jobs and their results are retained for 7 days.
At most 5 jobs per account may be queued or running at once (429 TOO_MANY_ACTIVE_JOBS).
GET /jobs/{jobId} — poll (free)
{
"success": true,
"data": {
"jobId": "…",
"type": "segment_reports",
"status": "running",
"progress": { "total": 40, "done": 12, "failed": 0, "skipped": 1 },
"items": [{ "id": "v2-both-cluster-3", "status": "done" }, "…"],
"createdAt": "…",
"updatedAt": "…",
"completedAt": null,
"expiresAt": "…",
"resultsUrl": "/api/business/v1/jobs/…/results"
}
} Poll every few seconds; polling never consumes quota.
GET /jobs/{jobId}/results?page&pageSize — results (free)
Available once the job is terminal (409 JOB_NOT_FINISHED before that). Each entry carries the item’s status and, for done items, the full report — the same object GET /games/{app_id} or GET /game-gap/segments/{cluster_id} returns. pageSize is at most 25 because each entry is a full report.
{
"success": true,
"data": [
{
"id": "413150",
"status": "done",
"error": null,
"report": { "game": { "…": "…" }, "reviewSummary": { "…": "…" } }
},
{ "id": "999999", "status": "skipped", "error": "Not found", "report": null }
],
"meta": { "page": 1, "pageSize": 10, "total": 3, "jobStatus": "completed" }
} For Concept Compass jobs the results endpoint returns the job and its reportUrl; fetch the report from there.
DELETE /jobs/{jobId} — cancel
Cancels a queued or running job. Items not yet processed are marked skipped and their units refunded (refundedUnits in the response).
MCP
The same operations are available over the Model Context Protocol at POST /api/business/v1/mcp, authenticated with the same key. MCP tools additionally accept compact: true and fields: [...] to trim their output for an agent’s context window. See the MCP server guide for client setup, the tool list and the output-shaping options.