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
- Quick start
- Conventions
- GET /v1/images
- POST /v1/images
- GET /v1/info
- Extended info mode
- Response schemas
- GET /media/{key}/{WxH}.webp
- GET /v1/providers
9b. GET /v1/suggest
- GET /v1/sizes
- GET /health
- Tester UI
- Errors
- Providers and aliases
- How a source is chosen
- Size configuration
- Image pipeline
- Caching and public URLs
- Run and environment
- Catalog
- Worked example — Azure Roulette
- Worked example — Blood Suckers (extended)
- 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
| Rule | Detail |
|---|
| Game field | Query/body name or game (same meaning). |
| Provider field | Free text. Matched case-insensitively against id, label, and aliases. |
| Size string | WIDTHxHEIGHT, digits only, x case-insensitive. Range 16–4000 per side. Several sizes: comma or whitespace separated. |
| Default sizes | From config.json sizes, overridable with GAME_IMAGES_SIZES or per request sizes. |
| Output format | Always WebP. Default cover crop (center, LANCZOS). Optional user crop of the grabbed original first. Quality default 82. |
| Image array | One entry per requested size, same order as the size list. |
format=array | HTTP 200 body is only the images array. |
| Public URL | url is {publicBase}/media/{key}/{W}x{H}.webp. path is the same without host. |
| Scheme | http unless the request sends X-Forwarded-Proto: https. |
| Cache-Control | public, 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
| Name | Required | Type | Description |
|---|
name | yes* | string | Game title, e.g. azure roulette. |
game | yes* | string | Alias of name. Used if name is empty. |
provider | yes | string | Provider label or alias, e.g. pragmatic play live. |
sizes | no | string | Override size list, e.g. 400x400,400x300,300x400 or 900x1200. |
format | no | string | array → body is the images array only. Any other value → full object. Extended info is omitted in this mode. |
mode | no | string | extended (also info / full) attaches SlotCatalog stats in info. Images stay official / unwatermarked. |
extended | no | string | 1 / true / yes — same as mode=extended. |
crop | no | string | Optional box on the grabbed original: x,y,width,height. Omit for auto center-cover. |
cropUnit | no | string | px or ratio (0–1). If omitted: all four values ≤ 1 → ratio, else pixels. |
cropX cropY cropW cropH | no | number | Same 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" }
}
| Field | Required | Type | Description |
|---|
name | yes* | string | Game title. |
game | yes* | string | Alias of name. |
provider | yes | string | Provider. |
sizes | no | string or array | "400x400,900x1200" or [{ "id": "poster", "width": 900, "height": 1200 }, "400x400"]. |
format | no | string | "array" for a bare array. |
mode | no | string | "extended" to include info. |
extended | no | boolean | true — same as mode: "extended". |
crop | no | object or string | Optional. `{ "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" }.
| Name | Required | Type | Description |
|---|
name / game | yes | string | Game title. Aliases and reversed word order are tried (azure roulette → Roulette Azure). |
provider | no | string | Helps 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 with | Example |
|---|
| Query | mode=extended or extended=1 on /v1/images |
| JSON body | "mode": "extended" or "extended": true |
| Stats only | GET /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
{
"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" } ]
}
| Field | Type | Description |
|---|
game | string | Catalog title when known, otherwise the query name. |
query | string | Name you sent. |
provider | string | Canonical provider id (slug). |
providerLabel | string | Human label. |
source.url | string | Absolute URL of the downloaded original. |
source.kind | string | How it was found. See source kinds. |
source.symbol | string or null | Provider game id (PP 227, vs20olympgate, …). |
source.page | string or null | Marketing/catalog page if the image was scraped from HTML. |
images | array | One WebP per requested size. |
mode | string | Present when extended: "extended". |
info | object | Present when extended. See InfoObject. |
imagesError | string | Only if extended and official art failed. |
ImageObject (each images[] item)
| Field | Type | Description |
|---|
id | string | Size id (square, landscape, portrait, or {W}x{H}). |
size | string | {width}x{height} |
width | integer | Output width in pixels. |
height | integer | Output height in pixels. |
format | string | Always "webp". |
url | string | Absolute URL to the file. |
path | string | Path only, e.g. /media/{key}/400x400.webp. |
bytes | integer | File 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).
| Field | Type | Blood Suckers example |
|---|
found | boolean | true |
source | string | "slotcatalog" |
page | string | https://slotcatalog.com/en/slots/Blood-Suckers |
name | string | Blood Suckers |
providerName | string | NetEnt |
providerPage | string | https://slotcatalog.com/en/soft/NetEnt |
rtp | string | 98% |
rtpValue | number | 98 |
rtpRanges | boolean | true when the page has RTP Ranges! |
variance | string | Low |
maxWin | string | x1014.00 |
maxWinValue | number | 1014 |
betways | string | 25 |
betwaysValue | integer | 25 |
releaseDate | string | 2013-11-10 |
type | string | Video Slots |
layout | string | 5-3 |
minBet / maxBet | string | 0.25 / 50 |
features / themes | string[] | mechanic lists |
rating / ratingValue | string / number | 8.5 |
slotRank | string | from search sr when the table omits it |
cached | boolean | disk cache hit |
There is no image URL in info. JSON-LD image from SlotCatalog is discarded.
HTTP 200 body is ImageObject[] — the same three (or N) items, no wrapper.
Uncropped Stake original (JPEG/PNG/WebP as fetched). Used by the tester overlay.
Binary WebP produced by /v1/images with no user crop (auto center-cover).
| Piece | Rule |
|---|
key | 20 hex chars ([a-f0-9]{20}). SHA-256 prefix of `providerId\ |
WxH | Same widthxheight as requested, e.g. 400x400. |
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:
{ "error": "bad media path" } — path does not match the pattern.{ "error": "missing variant" } — key/size not generated yet.
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?" }
| Param | Required | Notes |
|---|
q | yes | Alias query / name. Min 2 characters. |
provider | no | Rank matching provider first. |
limit | no | Default 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.
| Method | Path | Notes |
|---|
| POST | /v1/capture | Body { "name", "provider", "slotUrl?" }. HTTP 202 job. |
| GET | /v1/capture | Recent jobs. |
| GET | /v1/capture/{id} | Status, file tree, kinds, counts. |
| GET | /v1/capture/{id}/bundle.zip | Full zip (original tree + MANIFEST.json). |
| POST | /v1/capture/{id}/bundle | Body { "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
| Path | Type |
|---|
GET / | Tester: searchable provider dropdown + game autocomplete every 2 keystrokes (Stake) → /v1/images. |
GET /index.html | Same page. |
GET /docs | Documentation index (EN / ES). |
GET /docs/en | This document as HTML. |
GET /docs/es | Spanish HTML. |
GET /docs/API.en.md | This markdown. |
GET /docs/API.es.md | Spanish markdown. |
GET /openapi.yaml | OpenAPI 3.1. |
GET /openapi.json | Same spec as JSON. |
Errors
All errors are JSON { "error": "..." } plus extra fields when useful. Cache-Control: no-store.
| HTTP | error | When |
|---|
| 400 | name and provider are required | Missing name/game or provider. |
| 400 | unknown provider | Provider string did not match. Extra: provider, known (label list). |
| 400 | invalid json | POST body is not JSON. |
| 400 | no valid sizes (expected 400x400,400x300,300x400) | sizes parsed to an empty list. |
| 400 | size out of range: {token} | Width or height below 16 or above 4000. |
| 404 | not found | Unknown route. |
| 404 | bad media path | /media/... shape is wrong. |
| 404 | missing variant | Media file not on disk. |
| 404 | no 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.
| id | Label | Aliases |
|---|
pragmatic-play | Pragmatic Play | pragmatic play, pragmatic, pp, pragmaticplay |
pragmatic-play-live | Pragmatic Play Live | pragmatic play live, pragmatic live, pp live, pragmaticplaylive, pragmatic play live casino |
kagaming | KAGaming | ka gaming, ka, kaga, kaga88, ka gaming kaga |
ruby-play | Ruby Play | rubyplay, ruby play, ruby |
evolution-gaming | Evolution Gaming | evolution, evolution gaming, evo, evolution live |
booming-games | Booming Games | booming, booming games |
100hp | 100HP | 100hp, 100 hp, 100hp gaming, 100 hp gaming |
ezugi | Ezugi | ezugi, ezugi live, ezugi gaming |
live-vegas | Live Vegas | live vegas, livevegas, vegas live |
netent | NetEnt | netent, 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:
| id | Hosts |
|---|
pragmatic-play, pragmatic-play-live | pragmaticplay.com, ppgames.net, pragmaticplay.net |
kagaming | kaga88.com, kagaming.com |
ruby-play | rubyplay.com |
evolution-gaming | games.evolution.com, evolution.com, egcdn.com |
booming-games | booming-games.com, boominggames.com |
100hp | 100hp.game, 100hp-games.com, casino.100hp.game |
ezugi | ezugi.com, games.evolution.com |
live-vegas | livevegas.live, live-vegas.com |
netent | netent.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
kind | Meaning |
|---|
stake-thumbnail | Only live kind: Stake thumbnailUrl (never SlotCatalog). |
Size configuration
Three layers, last applicable wins per request:
config.json → sizes- Process env
GAME_IMAGES_SIZES=400x400,400x300,300x400,900x1200 - 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
- Download original bytes (PNG/JPEG/WebP/GIF).
- PIL load + EXIF transpose.
ImageOps.fit cover to (width, height), LANCZOS, center (0.5, 0.5).- Flatten alpha onto
#0c0c10. - 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
| Path | Contents |
|---|
cache/originals/{key}.bin | Original download. |
cache/variants/{key}/{W}x{H}.webp | Rendered 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).
| Variable | Effect |
|---|
GAME_IMAGES_CONFIG | Path to JSON config (default ./config.json). |
GAME_IMAGES_PORT | Listen port. |
GAME_IMAGES_SIZES | Default size list, comma-separated WxH. |
GAME_IMAGES_PUBLIC_BASE | Absolute 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:
| Field | Value |
|---|
| Provider | NetEnt (/en/soft/NetEnt) |
| RTP | 98% (rtpRanges: true) |
| Variance | Low |
| Max Win | x1014.00 |
| Betways | 25 |
| Release Date | 2013-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