# 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](#quick-start)
2. [Conventions](#conventions)
3. [GET /v1/images](#get-v1images)
4. [POST /v1/images](#post-v1images)
5. [GET /v1/info](#get-v1info)
6. [Extended info mode](#extended-info-mode)
7. [Response schemas](#response-schemas)
8. [GET /media/{key}/{WxH}.webp](#get-mediakeywxhwebp)
9. [GET /v1/providers](#get-v1providers)
9b. [GET /v1/suggest](#get-v1suggest)
10. [GET /v1/sizes](#get-v1sizes)
11. [GET /health](#get-health)
12. [Tester UI](#tester-ui)
13. [Errors](#errors)
14. [Providers and aliases](#providers-and-aliases)
15. [How a source is chosen](#how-a-source-is-chosen)
16. [Size configuration](#size-configuration)
17. [Image pipeline](#image-pipeline)
18. [Caching and public URLs](#caching-and-public-urls)
19. [Run and environment](#run-and-environment)
20. [Catalog](#catalog)
21. [Worked example — Azure Roulette](#worked-example-azure-roulette)
22. [Worked example — Blood Suckers (extended)](#worked-example-blood-suckers-extended)
23. [OpenAPI](#openapi)

---

## Quick start

```bash
# 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`](API.es.md) · OpenAPI: [`openapi.yaml`](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

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

See [worked example](#worked-example-azure-roulette) for a measured 200 body.

---

## POST /v1/images

Same handler as GET. JSON body.

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

### Body

```json
{
  "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"|"ratio" }` or `"x,y,w,h"`. Omit for auto center-cover. |

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

```bash
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.

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

---

## Extended info mode

Same catalog service as [SlotCatalog game pages](https://slotcatalog.com/en/slots/Blood-Suckers) (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

### Full object (`format` omitted)

```json
{
  "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-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](#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.

### 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).

| Piece | Rule |
|-------|------|
| `key` | 20 hex chars (`[a-f0-9]{20}`). SHA-256 prefix of `providerId\|normalizedName\|sourceUrl`. |
| `WxH` | Same `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:**

- `{ "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. |

```bash
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.

```http
GET /v1/providers HTTP/1.1
```

```json
{
  "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`).

```json
{
  "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.

```json
{
  "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:

```json
{
  "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**:

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

`config.json` example:

```json
{
  "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:

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

Or one call:

```bash
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

| 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

```bash
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.

```json
{
  "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**

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

**Response (HTTP 200)**

```json
{
  "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](https://slotcatalog.com/en/slots/Blood-Suckers). Images, if present, are still official (never `/userfiles/image/games/…` on slotcatalog.com).

```bash
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:

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

---

## OpenAPI

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

Client generation (example):

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