Game Images API

Game Images API

Version: 1.1 Base URL (public): https://grabbr.jailbroken.tech Local: http://127.0.0.1:8790 Content type: application/json; charset=utf-8 (except media and HTML) Auth: none CORS: Access-Control-Allow-Origin: * on every response. OPTIONS is allowed on all routes (GET, POST, OPTIONS, header Content-Type).

Given a game name and a provider, the API always takes lobby art from Stake (thumbnailUrl on mediumrare/imgix), cover-crops it, and returns WebP files at configurable pixel sizes. GraphQL for that lookup always uses Chile egress.

Default sizes: 400×400, 400×300, 300×400. Any extra size (for example 900×1200) is a query/body/env/config.json change — the renderer does not care how many sizes you ask for.


Contents

  1. Quick start
  2. Conventions
  3. GET /v1/images
  4. POST /v1/images
  5. GET /v1/info
  6. Extended info mode
  7. Response schemas
  8. GET /media/{key}/{WxH}.webp
  9. GET /v1/providers

9b. GET /v1/suggest

  1. GET /v1/sizes
  2. GET /health
  3. Tester UI
  4. Errors
  5. Providers and aliases
  6. How a source is chosen
  7. Size configuration
  8. Image pipeline
  9. Caching and public URLs
  10. Run and environment
  11. Catalog
  12. Worked example — Azure Roulette
  13. Worked example — Blood Suckers (extended)
  14. OpenAPI

Quick start

# Full object (game + source + images[])
curl -sS 'https://grabbr.jailbroken.tech/v1/images?name=azure%20roulette&provider=pragmatic%20play%20live'

# Bare array of the three images
curl -sS 'https://grabbr.jailbroken.tech/v1/images?name=azure%20roulette&provider=pragmatic%20play%20live&format=array'

# Extra size tomorrow
curl -sS 'https://grabbr.jailbroken.tech/v1/images?name=azure%20roulette&provider=pragmatic%20play%20live&sizes=400x400,400x300,300x400,900x1200'

# Extended stats (RTP, variance, max win, …) + official images
curl -sS 'https://grabbr.jailbroken.tech/v1/images?name=Blood%20Suckers&provider=NetEnt&mode=extended'

# Stats only
curl -sS 'https://grabbr.jailbroken.tech/v1/info?name=Blood%20Suckers&provider=NetEnt'

# POST
curl -sS -X POST https://grabbr.jailbroken.tech/v1/images \
  -H 'content-type: application/json' \
  -d '{"name":"azure roulette","provider":"Pragmatic Play Live"}'

Spanish documentation: API.es.md · OpenAPI: openapi.yaml


Conventions

RuleDetail
Game fieldQuery/body name or game (same meaning).
Provider fieldFree text. Matched case-insensitively against id, label, and aliases.
Size stringWIDTHxHEIGHT, digits only, x case-insensitive. Range 16–4000 per side. Several sizes: comma or whitespace separated.
Default sizesFrom config.json sizes, overridable with GAME_IMAGES_SIZES or per request sizes.
Output formatAlways WebP. Default cover crop (center, LANCZOS). Optional user crop of the grabbed original first. Quality default 82.
Image arrayOne entry per requested size, same order as the size list.
format=arrayHTTP 200 body is only the images array.
Public URLurl is {publicBase}/media/{key}/{W}x{H}.webp. path is the same without host.
Schemehttp unless the request sends X-Forwarded-Proto: https.
Cache-Controlpublic, max-age=86400 on HTTP 200; no-store on errors.

GET /v1/images

Resolve official art and return WebP variants.

URL: /v1/images

Query parameters

NameRequiredTypeDescription
nameyes*stringGame title, e.g. azure roulette.
gameyes*stringAlias of name. Used if name is empty.
provideryesstringProvider label or alias, e.g. pragmatic play live.
sizesnostringOverride size list, e.g. 400x400,400x300,300x400 or 900x1200.
formatnostringarray → body is the images array only. Any other value → full object. Extended info is omitted in this mode.
modenostringextended (also info / full) attaches SlotCatalog stats in info. Images stay official / unwatermarked.
extendednostring1 / true / yes — same as mode=extended.
cropnostringOptional box on the grabbed original: x,y,width,height. Omit for auto center-cover.
cropUnitnostringpx or ratio (0–1). If omitted: all four values ≤ 1 → ratio, else pixels.
cropX cropY cropW cropHnonumberSame box as four query fields instead of crop=.

\* One of name or game must be non-empty.

Optional crop is applied to the Stake original before each size is cover-fit. Auto variants stay at /media/{key}/{W}x{H}.webp. Cropped variants use /media/{key}/{cropToken}/{W}x{H}.webp so they do not overwrite auto files. The untouched original is GET /media/{key}/original. Response includes original (width/height/path) and crop (null when unused).

Example

GET /v1/images?name=azure%20roulette&provider=pragmatic%20play%20live HTTP/1.1
Host: 127.0.0.1:8790

See worked example for a measured 200 body.


POST /v1/images

Same handler as GET. JSON body.

URL: /v1/images Header: Content-Type: application/json

Body

{
  "name": "azure roulette",
  "game": "optional, used if name is missing",
  "provider": "Pragmatic Play Live",
  "sizes": "400x400,400x300,300x400",
  "format": "array",
  "crop": { "x": 40, "y": 20, "width": 280, "height": 280, "unit": "px" }
}
FieldRequiredTypeDescription
nameyes*stringGame title.
gameyes*stringAlias of name.
provideryesstringProvider.
sizesnostring or array"400x400,900x1200" or [{ "id": "poster", "width": 900, "height": 1200 }, "400x400"].
formatnostring"array" for a bare array.
modenostring"extended" to include info.
extendednobooleantrue — same as mode: "extended".
cropnoobject or stringOptional. `{ "x", "y", "width", "height", "unit": "px"

Invalid JSON → 400 { "error": "invalid json" }.

curl -sS -X POST https://grabbr.jailbroken.tech/v1/images \
  -H 'content-type: application/json' \
  -d '{
    "name": "Gates of Olympus",
    "provider": "Pragmatic Play",
    "sizes": [{"id":"square","width":400,"height":400},{"id":"poster","width":900,"height":1200}]
  }'

GET /v1/info

SlotCatalog-style metadata only. No images.

URL: /v1/info Also POST /v1/info with JSON { "name", "provider" }.

NameRequiredTypeDescription
name / gameyesstringGame title. Aliases and reversed word order are tried (azure roulette → Roulette Azure).
providernostringHelps pick the right row (NetEnt vs a Megaways remake).

HTTP 200 when a page is matched; 404 { "info": { "found": false, … } } when search misses.

curl -sS 'https://grabbr.jailbroken.tech/v1/info?name=Blood%20Suckers&provider=NetEnt'

Extended info mode

Same catalog service as SlotCatalog game pages (provider, RTP, variance, max win, betways, release date, plus type / layout / bets / features / themes / rating when present).

Images always come from Stake (thumbnailUrl / imgix). SlotCatalog thumbs are watermarked and never used. Host slotcatalog.com is banned as an image source.

Enable withExample
Querymode=extended or extended=1 on /v1/images
JSON body"mode": "extended" or "extended": true
Stats onlyGET /v1/info

If SlotCatalog has no hit, stats may come from Stake or a generated stub. images[] is still Stake-only. HTTP 404 when Stake has no matching thumbnail. format=array stays images-only.

Search uses SlotCatalog typeahead (/json.php?blck=topSrch). The game page #attributes table and VideoGame JSON-LD are parsed. HTML is cached under cache/slotcatalog/ for 7 days. Stake kuratorGameQuery is the metadata+thumbnail fallback; that call always uses Chile egress.


Response schemas

Full object (format omitted)

{
  "game": "Roulette Azure",
  "query": "azure roulette",
  "provider": "pragmatic-play-live",
  "providerLabel": "Pragmatic Play Live",
  "source": {
    "url": "https://mediumrare.imgix.net/....",
    "kind": "stake-thumbnail",
    "page": "https://stake1017.com/casino/games/..."
  },
  "images": [ { "...": "ImageObject" } ]
}
FieldTypeDescription
gamestringCatalog title when known, otherwise the query name.
querystringName you sent.
providerstringCanonical provider id (slug).
providerLabelstringHuman label.
source.urlstringAbsolute URL of the downloaded original.
source.kindstringHow it was found. See source kinds.
source.symbolstring or nullProvider game id (PP 227, vs20olympgate, …).
source.pagestring or nullMarketing/catalog page if the image was scraped from HTML.
imagesarrayOne WebP per requested size.
modestringPresent when extended: "extended".
infoobjectPresent when extended. See InfoObject.
imagesErrorstringOnly if extended and official art failed.

ImageObject (each images[] item)

FieldTypeDescription
idstringSize id (square, landscape, portrait, or {W}x{H}).
sizestring{width}x{height}
widthintegerOutput width in pixels.
heightintegerOutput height in pixels.
formatstringAlways "webp".
urlstringAbsolute URL to the file.
pathstringPath only, e.g. /media/{key}/400x400.webp.
bytesintegerFile size of that WebP.

InfoObject (info when mode=extended, and /v1/info)

Shape of a SlotCatalog attributes block. Null when the page has no value (N/A is stored as null).

FieldTypeBlood Suckers example
foundbooleantrue
sourcestring"slotcatalog"
pagestringhttps://slotcatalog.com/en/slots/Blood-Suckers
namestringBlood Suckers
providerNamestringNetEnt
providerPagestringhttps://slotcatalog.com/en/soft/NetEnt
rtpstring98%
rtpValuenumber98
rtpRangesbooleantrue when the page has RTP Ranges!
variancestringLow
maxWinstringx1014.00
maxWinValuenumber1014
betwaysstring25
betwaysValueinteger25
releaseDatestring2013-11-10
typestringVideo Slots
layoutstring5-3
minBet / maxBetstring0.25 / 50
features / themesstring[]mechanic lists
rating / ratingValuestring / number8.5
slotRankstringfrom search sr when the table omits it
cachedbooleandisk cache hit

There is no image URL in info. JSON-LD image from SlotCatalog is discarded.

Bare array (format=array)

HTTP 200 body is ImageObject[] — the same three (or N) items, no wrapper.


GET /media/{key}/original

Uncropped Stake original (JPEG/PNG/WebP as fetched). Used by the tester overlay.

GET /media/{key}/{WxH}.webp

Binary WebP produced by /v1/images with no user crop (auto center-cover).

PieceRule
key20 hex chars ([a-f0-9]{20}). SHA-256 prefix of `providerId\
WxHSame widthxheight as requested, e.g. 400x400.

GET /media/{key}/{cropToken}/{WxH}.webp

Same sizes after an optional user crop. cropToken looks like c-px-40-20-280-280 or c-r-0p1000-0p0500-0p8000-0p9000. Auto files are not overwritten.

Headers on 200: Content-Type: image/webp, CORS, Cache-Control: public, max-age=86400.

404 JSON:

Call /v1/images first; then use url / path from the response. Do not invent keys.


GET /v1/suggest

Stake casino typeahead (kuratorGameQuery, Chile GraphQL). Used by the tester autocomplete (fires every 2 characters).

URL: /v1/suggest Also POST JSON { "q", "provider?", "limit?" }

ParamRequiredNotes
qyesAlias query / name. Min 2 characters.
providernoRank matching provider first.
limitnoDefault 20, max 50.
curl -sS 'https://grabbr.jailbroken.tech/v1/suggest?q=sw&provider=pragmatic-play'

games[]: name, slug, provider, providerLabel, thumbnailUrl, page, type. Empty list if nothing matches (HTTP 200).


Capture (REA v2)

/v1/images now includes slot.url (Stake game page). Capture opens that URL in the Chile Chrome, clicks Play, maps every capturable response (iframes + CDN: images, audio, configs, sprites, JS, HTML) and rebuilds host/path as on the original slot.

MethodPathNotes
POST/v1/captureBody { "name", "provider", "slotUrl?" }. HTTP 202 job.
GET/v1/captureRecent jobs.
GET/v1/capture/{id}Status, file tree, kinds, counts.
GET/v1/capture/{id}/bundle.zipFull zip (original tree + MANIFEST.json).
POST/v1/capture/{id}/bundleBody { "paths": ["host/a.png"] } selected zip.
GET/v1/capture/{id}/file?path=One file.

Poll until status is done or error. Tester: mode Capture.


GET /v1/providers

Lists canonical ids, labels, and aliases.

GET /v1/providers HTTP/1.1
{
  "providers": [
    {
      "id": "pragmatic-play-live",
      "label": "Pragmatic Play Live",
      "aliases": [
        "pragmatic play live",
        "pragmatic live",
        "pp live",
        "pragmaticplaylive",
        "pragmatic play live casino"
      ]
    }
  ]
}

GET /v1/sizes

Default size list currently loaded (after config.json + GAME_IMAGES_SIZES).

{
  "sizes": [
    { "id": "square", "width": 400, "height": 400 },
    { "id": "landscape", "width": 400, "height": 300 },
    { "id": "portrait", "width": 300, "height": 400 }
  ]
}

GET /health

Liveness. No dependency check on upstream CDNs.

{
  "ok": true,
  "service": "game-images-api",
  "images": "stake",
  "chile": { "ok": true, "requireChile": true, "city": "Pucón", "countryCode": "CL" },
  "stakeCdp": true
}

images is always "stake". stakeCdp is the headed Chromium used for GraphQL (Chile egress).


Tester UI

PathType
GET /Tester: searchable provider dropdown + game autocomplete every 2 keystrokes (Stake) → /v1/images.
GET /index.htmlSame page.
GET /docsDocumentation index (EN / ES).
GET /docs/enThis document as HTML.
GET /docs/esSpanish HTML.
GET /docs/API.en.mdThis markdown.
GET /docs/API.es.mdSpanish markdown.
GET /openapi.yamlOpenAPI 3.1.
GET /openapi.jsonSame spec as JSON.

Errors

All errors are JSON { "error": "..." } plus extra fields when useful. Cache-Control: no-store.

HTTPerrorWhen
400name and provider are requiredMissing name/game or provider.
400unknown providerProvider string did not match. Extra: provider, known (label list).
400invalid jsonPOST body is not JSON.
400no valid sizes (expected 400x400,400x300,300x400)sizes parsed to an empty list.
400size out of range: {token}Width or height below 16 or above 4000.
404not foundUnknown route.
404bad media path/media/... shape is wrong.
404missing variantMedia file not on disk.
404no official image found for game='…' provider=…Resolver exhausted. Extra: game, provider (canonical id).
502(exception message)Download, decode, or render failed.

Unknown provider example:

{
  "error": "unknown provider",
  "provider": "unknown",
  "known": [
    "Pragmatic Play",
    "Pragmatic Play Live",
    "KAGaming",
    "Ruby Play",
    "Evolution Gaming",
    "Booming Games",
    "100HP",
    "Ezugi",
    "Live Vegas",
    "NetEnt"
  ]
}

Providers and aliases

Send any alias; the API stores the id.

idLabelAliases
pragmatic-playPragmatic Playpragmatic play, pragmatic, pp, pragmaticplay
pragmatic-play-livePragmatic Play Livepragmatic play live, pragmatic live, pp live, pragmaticplaylive, pragmatic play live casino
kagamingKAGamingka gaming, ka, kaga, kaga88, ka gaming kaga
ruby-playRuby Playrubyplay, ruby play, ruby
evolution-gamingEvolution Gamingevolution, evolution gaming, evo, evolution live
booming-gamesBooming Gamesbooming, booming games
100hp100HP100hp, 100 hp, 100hp gaming, 100 hp gaming
ezugiEzugiezugi, ezugi live, ezugi gaming
live-vegasLive Vegaslive vegas, livevegas, vegas live
netentNetEntnetent, net ent

Matching is case-insensitive, hyphen/space insensitive for the id, then exact alias, then contains for aliases of length ≥ 5.

Official domains used when ranking search hits:

idHosts
pragmatic-play, pragmatic-play-livepragmaticplay.com, ppgames.net, pragmaticplay.net
kagamingkaga88.com, kagaming.com
ruby-playrubyplay.com
evolution-gaminggames.evolution.com, evolution.com, egcdn.com
booming-gamesbooming-games.com, boominggames.com
100hp100hp.game, 100hp-games.com, casino.100hp.game
ezugiezugi.com, games.evolution.com
live-vegaslivevegas.live, live-vegas.com
netentnetent.com, netentcloud.com

Trusted extra CDNs (after official hosts): agstatic.com, primeapi.com, softswiss.net. Banned image hosts: slotcatalog.com (watermark).


How a source is chosen

Images are always Stake lobby thumbnailUrl (imgix/mediumrare). SlotCatalog, PP CDN, Bing, and studio pages are not used as image sources. GraphQL always via Chile IP (headed Chromium). HTTP 404 if Stake has no matching game or the thumb cannot be downloaded.

Source kinds

kindMeaning
stake-thumbnailOnly live kind: Stake thumbnailUrl (never SlotCatalog).

Size configuration

Three layers, last applicable wins per request:

  1. config.json → sizes
  2. Process env GAME_IMAGES_SIZES=400x400,400x300,300x400,900x1200
  3. Request sizes (query or JSON)

config.json example:

{
  "sizes": [
    { "id": "square", "width": 400, "height": 400 },
    { "id": "landscape", "width": 400, "height": 300 },
    { "id": "portrait", "width": 300, "height": 400 }
  ]
}

Adding 900×1200 tomorrow without code:

GAME_IMAGES_SIZES=400x400,400x300,300x400,900x1200 python3 server.py

Or one call:

curl -sS 'https://grabbr.jailbroken.tech/v1/images?name=azure%20roulette&provider=pp%20live&sizes=900x1200'

Reload: GET /v1/sizes reads config/env on every request (no process restart needed for config.json or env if you restart; config.json is re-read per request).


Image pipeline

  1. Download original bytes (PNG/JPEG/WebP/GIF).
  2. PIL load + EXIF transpose.
  3. ImageOps.fit cover to (width, height), LANCZOS, center (0.5, 0.5).
  4. Flatten alpha onto #0c0c10.
  5. Save WebP quality=webpQuality (default 82), method=6.

Cover means the frame is filled; overflow is cropped. A 325× landscape PP tile upscaled to 900×1200 is still a valid WebP of those pixel dimensions.


Caching and public URLs

PathContents
cache/originals/{key}.binOriginal download.
cache/variants/{key}/{W}x{H}.webpRendered WebP.

key = sha256(providerId|normalizedName|sourceUrl)[:20].

/v1/images writes those files, then returns URLs. /media/... only reads.

publicBase in config or GAME_IMAGES_PUBLIC_BASE prefixes url. Empty → http(s)://{Host}.

cacheTtlSec in config.json is reserved; eviction is not implemented. Delete cache/ to rebuild.


Run and environment

python3 /opt/hallallow/apps/game-images-api/server.py

Default bind: 0.0.0.0:8790. Dependency: Pillow (requirements.txt).

VariableEffect
GAME_IMAGES_CONFIGPath to JSON config (default ./config.json).
GAME_IMAGES_PORTListen port.
GAME_IMAGES_SIZESDefault size list, comma-separated WxH.
GAME_IMAGES_PUBLIC_BASEAbsolute prefix for images[].url (CDN / reverse proxy).

Other config.json keys: host, port, publicBase, webpQuality, fit (cover), userAgent, timeoutSec (upstream HTTP, default 18s), sizes.

Behind TLS termination, send X-Forwarded-Proto: https so url is https://....


Catalog

File: data/catalog.json. Optional seed so live games resolve without a marketing page.

{
  "games": [
    {
      "provider": "pragmatic-play-live",
      "names": ["azure roulette", "roulette azure", "roulette 1 azure"],
      "symbol": "227",
      "title": "Roulette Azure"
    }
  ]
}

Match: exact name (score 100), contains (70+), token overlap (60+). Minimum score 60. Names are normalized (lowercase, hyphens → spaces).

Add a row when you know a PP/Evolution id. No restart required; catalog is read on resolve.


Worked example — Azure Roulette

Measured on this host against Pragmatic Play CDN rec/325/227.png.

Request

curl -sS 'https://grabbr.jailbroken.tech/v1/images?name=azure%20roulette&provider=pragmatic%20play%20live'

Response (HTTP 200)

{
  "game": "Roulette Azure",
  "query": "azure roulette",
  "provider": "pragmatic-play-live",
  "providerLabel": "Pragmatic Play Live",
  "source": {
    "url": "https://mediumrare.imgix.net/....",
    "kind": "stake-thumbnail",
    "page": "https://stake1017.com/casino/games/..."
  },
  "images": [
    {
      "id": "square",
      "size": "400x400",
      "width": 400,
      "height": 400,
      "format": "webp",
      "url": "https://grabbr.jailbroken.tech/media/1f976b03a7d9dde1cb54/400x400.webp",
      "path": "/media/1f976b03a7d9dde1cb54/400x400.webp",
      "bytes": 28968
    },
    {
      "id": "landscape",
      "size": "400x300",
      "width": 400,
      "height": 300,
      "format": "webp",
      "url": "https://grabbr.jailbroken.tech/media/1f976b03a7d9dde1cb54/400x300.webp",
      "path": "/media/1f976b03a7d9dde1cb54/400x300.webp",
      "bytes": 26408
    },
    {
      "id": "portrait",
      "size": "300x400",
      "width": 300,
      "height": 400,
      "format": "webp",
      "url": "https://grabbr.jailbroken.tech/media/1f976b03a7d9dde1cb54/300x400.webp",
      "path": "/media/1f976b03a7d9dde1cb54/300x400.webp",
      "bytes": 22324
    }
  ]
}

format=array returns that images array only. sizes=900x1200,400x400 added a 900×1200 WebP (70384 bytes) plus 400×400.

Second measured game: Gates of Olympus / Pragmatic Play → vs20olympgate on the same rec/325 CDN, three WebPs with exact pixel sizes.


Worked example — Blood Suckers (extended)

Stats match the SlotCatalog attributes table for Blood Suckers. Images, if present, are still official (never /userfiles/image/games/… on slotcatalog.com).

curl -sS 'https://grabbr.jailbroken.tech/v1/images?name=Blood%20Suckers&provider=NetEnt&mode=extended'

info includes:

FieldValue
ProviderNetEnt (/en/soft/NetEnt)
RTP98% (rtpRanges: true)
VarianceLow
Max Winx1014.00
Betways25
Release Date2013-11-10

Stats-only:

curl -sS 'https://grabbr.jailbroken.tech/v1/info?name=Blood%20Suckers&provider=NetEnt'

OpenAPI

Machine-readable spec: openapi.yaml (also GET /openapi.yaml and GET /openapi.json).

Client generation (example):

npx @redocly/cli build-docs https://grabbr.jailbroken.tech/openapi.yaml