Game Images API
Versión: 1.1 URL base (público): https://grabbr.jailbroken.tech Local: http://127.0.0.1:8790 Content-Type: application/json; charset=utf-8 (salvo media y HTML) Auth: ninguna CORS: Access-Control-Allow-Origin: * en todas las respuestas. OPTIONS permitido (GET, POST, OPTIONS, header Content-Type).
Con un nombre de juego y un proveedor, la API toma siempre el arte de lobby de Stake (thumbnailUrl en mediumrare/imgix), lo recorta en modo cover y devuelve archivos WebP en tamaños de píxel configurables. El GraphQL de esa búsqueda sale siempre por IP de Chile.
Tamaños por defecto: 400×400, 400×300, 300×400. Cualquier tamaño extra (por ejemplo 900×1200) se agrega por query, body, env o config.json — el renderer no limita cuántos tamaños pedís.
Contenido
- Inicio rápido
- Convenciones
- GET /v1/images
- POST /v1/images
- GET /v1/info
- Modo de información extendida
- Esquemas de respuesta
- GET /media/{key}/{WxH}.webp
- GET /v1/providers
9b. GET /v1/suggest
- GET /v1/sizes
- GET /health
- UI de prueba
- Errores
- Proveedores y alias
- Cómo se elige la fuente
- Configuración de tamaños
- Pipeline de imagen
- Caché y URLs públicas
- Arranque y entorno
- Catálogo
- Ejemplo medido — Azure Roulette
- Ejemplo medido — Blood Suckers (extendido)
- OpenAPI
Inicio rápido
# Objeto completo (juego + source + images[])
curl -sS 'https://grabbr.jailbroken.tech/v1/images?name=azure%20roulette&provider=pragmatic%20play%20live'
# Solo el array de las tres imágenes
curl -sS 'https://grabbr.jailbroken.tech/v1/images?name=azure%20roulette&provider=pragmatic%20play%20live&format=array'
# Tamaño extra
curl -sS 'https://grabbr.jailbroken.tech/v1/images?name=azure%20roulette&provider=pragmatic%20play%20live&sizes=400x400,400x300,300x400,900x1200'
# Stats extendidos (RTP, varianza, max win, …) + imágenes oficiales
curl -sS 'https://grabbr.jailbroken.tech/v1/images?name=Blood%20Suckers&provider=NetEnt&mode=extended'
# Solo stats
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"}'
Documentación en inglés: API.en.md · OpenAPI: openapi.yaml
Convenciones
| Regla | Detalle |
|---|
| Campo de juego | Query/body name o game (mismo significado). |
| Campo de proveedor | Texto libre. Match case-insensitive contra id, label y alias. |
| String de tamaño | ANCHOxALTO, solo dígitos, x insensible a mayúsculas. Rango 16–4000 por lado. Varios tamaños: separados por coma o espacio. |
| Tamaños default | De config.json → sizes, override con GAME_IMAGES_SIZES o sizes por request. |
| Formato de salida | Siempre WebP. Default recorte cover (centro, LANCZOS). Crop opcional del original grabado antes. Calidad default 82. |
| Array de imágenes | Una entrada por tamaño pedido, mismo orden que la lista. |
format=array | El body HTTP 200 es solo el array images. |
| URL pública | url = {publicBase}/media/{key}/{W}x{H}.webp. path es lo mismo sin host. |
| Scheme | http salvo que el request mande X-Forwarded-Proto: https. |
| Cache-Control | public, max-age=86400 en HTTP 200; no-store en errores. |
GET /v1/images
Resuelve arte oficial y devuelve variantes WebP.
URL: /v1/images
Query parameters
| Nombre | Requerido | Tipo | Descripción |
|---|
name | sí* | string | Título del juego, p. ej. azure roulette. |
game | sí* | string | Alias de name. Se usa si name está vacío. |
provider | sí | string | Label o alias del proveedor, p. ej. pragmatic play live. |
sizes | no | string | Override de tamaños, p. ej. 400x400,400x300,300x400 o 900x1200. |
format | no | string | array → el body es solo el array de imágenes. Cualquier otro valor → objeto completo. En este modo no se incluye info. |
mode | no | string | extended (también info / full) agrega stats de SlotCatalog en info. Las imágenes siguen siendo oficiales, sin marca de agua. |
extended | no | string | 1 / true / yes — igual que mode=extended. |
crop | no | string | Caja opcional sobre el original grabado: x,y,ancho,alto. Si se omite, cover centrado. |
cropUnit | no | string | px o ratio (0–1). Si se omite: los cuatro valores ≤ 1 → ratio, si no píxeles. |
cropX cropY cropW cropH | no | number | La misma caja en cuatro query params. |
\* Uno de name o game tiene que venir con valor.
El crop opcional se aplica al original de Stake antes del cover-fit de cada tamaño. Las variantes auto quedan en /media/{key}/{W}x{H}.webp. Las recortadas usan /media/{key}/{cropToken}/{W}x{H}.webp. El original sin tocar: GET /media/{key}/original. La respuesta incluye original y crop (null si no se usó).
Ejemplo
GET /v1/images?name=azure%20roulette&provider=pragmatic%20play%20live HTTP/1.1
Host: 127.0.0.1:8790
El body 200 medido está en ejemplo medido.
POST /v1/images
Mismo handler que GET. Body JSON.
URL: /v1/images Header: Content-Type: application/json
Body
{
"name": "azure roulette",
"game": "opcional, se usa si falta name",
"provider": "Pragmatic Play Live",
"sizes": "400x400,400x300,300x400",
"format": "array",
"crop": { "x": 40, "y": 20, "width": 280, "height": 280, "unit": "px" }
}
| Campo | Requerido | Tipo | Descripción |
|---|
name | sí* | string | Título del juego. |
game | sí* | string | Alias de name. |
provider | sí | string | Proveedor. |
sizes | no | string o array | "400x400,900x1200" o [{ "id": "poster", "width": 900, "height": 1200 }, "400x400"]. |
format | no | string | "array" para un array pelado. |
mode | no | string | "extended" para incluir info. |
extended | no | boolean | true — igual que mode: "extended". |
crop | no | object o string | Opcional. `{ "x", "y", "width", "height", "unit": "px" |
JSON inválido → 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
Solo metadata estilo SlotCatalog. Sin imágenes.
URL: /v1/info También POST /v1/info con JSON { "name", "provider" }.
| Nombre | Requerido | Tipo | Descripción |
|---|
name / game | sí | string | Título. Se prueban alias y orden invertido (azure roulette → Roulette Azure). |
provider | no | string | Ayuda a elegir la fila correcta (NetEnt vs un remake Megaways). |
HTTP 200 si hay página; 404 { "info": { "found": false, … } } si el search no pega.
curl -sS 'https://grabbr.jailbroken.tech/v1/info?name=Blood%20Suckers&provider=NetEnt'
El mismo tipo de ficha que la página de SlotCatalog (proveedor, RTP, varianza, max win, betways, fecha de salida, más type / layout / apuestas / features / themes / rating si están).
Las imágenes siempre salen de Stake (thumbnailUrl / imgix). Los thumbs de SlotCatalog llevan marca de agua y no se usan. El host slotcatalog.com está prohibido como fuente de imagen.
| Cómo activarlo | Ejemplo |
|---|
| Query | mode=extended o extended=1 en /v1/images |
| Body JSON | "mode": "extended" o "extended": true |
| Solo stats | GET /v1/info |
Si SlotCatalog no tiene hit, los stats pueden salir de Stake o de un stub generated. images[] sigue siendo solo Stake. HTTP 404 cuando Stake no tiene thumbnail. format=array sigue siendo solo imágenes.
Search: typeahead SlotCatalog (/json.php?blck=topSrch). Se parsea la tabla #attributes y el JSON-LD VideoGame. HTML cacheado en cache/slotcatalog/ 7 días. Fallback de metadata+thumb: Stake kuratorGameQuery, siempre por IP de Chile.
Esquemas de respuesta
{
"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" } ]
}
| Campo | Tipo | Descripción |
|---|
game | string | Título de catálogo si existe; si no, el nombre consultado. |
query | string | Nombre que enviaste. |
provider | string | Id canónico del proveedor (slug). |
providerLabel | string | Label humano. |
source.url | string | URL absoluta del original descargado. |
source.kind | string | Cómo se encontró. Ver kinds de fuente. |
source.symbol | string o null | Id del juego en el proveedor (PP 227, vs20olympgate, …). |
source.page | string o null | Página de marketing/catálogo si se scrapeo HTML. |
images | array | Un WebP por tamaño pedido. |
mode | string | Si extendido: "extended". |
info | object | Si extendido. Ver InfoObject. |
imagesError | string | Solo si extendido y falló el arte oficial. |
ImageObject (cada ítem de images[])
| Campo | Tipo | Descripción |
|---|
id | string | Id del tamaño (square, landscape, portrait, o {W}x{H}). |
size | string | {width}x{height} |
width | integer | Ancho de salida en píxeles. |
height | integer | Alto de salida en píxeles. |
format | string | Siempre "webp". |
url | string | URL absoluta del archivo. |
path | string | Solo path, p. ej. /media/{key}/400x400.webp. |
bytes | integer | Tamaño en bytes de ese WebP. |
InfoObject (info con mode=extended, y /v1/info)
Forma de la tabla de atributos de SlotCatalog. null si la página no tiene valor (N/A → null).
| Campo | Tipo | Ejemplo Blood Suckers |
|---|
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 si la página tiene 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[] | listas de mecánicas |
rating / ratingValue | string / number | 8.5 |
cached | boolean | hit de caché en disco |
No hay URL de imagen en info. El image del JSON-LD de SlotCatalog se descarta.
El body HTTP 200 es ImageObject[] — los mismos tres (o N) ítems, sin wrapper.
Original de Stake sin recortar (JPEG/PNG/WebP tal como se bajó). Lo usa el overlay del tester.
WebP binario generado por /v1/images sin crop de usuario (cover centrado).
| Pieza | Regla |
|---|
key | 20 hex ([a-f0-9]{20}). Prefijo SHA-256 de `providerId\ |
WxH | Mismo anchoxalto pedido, p. ej. 400x400. |
Mismos tamaños después de un crop opcional. cropToken tipo c-px-40-20-280-280. No pisa los archivos auto.
Headers en 200: Content-Type: image/webp, CORS, Cache-Control: public, max-age=86400.
404 JSON:
{ "error": "bad media path" } — el path no matchea el patrón.{ "error": "missing variant" } — esa key/tamaño todavía no se generó.
Primero llamá /v1/images; después usá url / path de la respuesta. No inventes keys.
GET /v1/suggest
Typeahead de casino Stake (kuratorGameQuery, GraphQL Chile). Lo usa el autocomplete del tester (dispara cada 2 caracteres).
URL: /v1/suggest También POST JSON { "q", "provider?", "limit?" }
| Param | Req | Notas |
|---|
q | sí | Alias query / name. Mínimo 2 caracteres. |
provider | no | Prioriza ese proveedor. |
limit | no | Default 20, máx 50. |
curl -sS 'https://grabbr.jailbroken.tech/v1/suggest?q=sw&provider=pragmatic-play'
games[]: name, slug, provider, providerLabel, thumbnailUrl, page, type. Lista vacía si no hay match (HTTP 200).
Capture (REA v2)
/v1/images incluye slot.url (página del juego en Stake). Capture abre esa URL en el Chrome Chile, hace click en Play, mapea cada respuesta capturable (iframes + CDN: imágenes, audio, configs, sprites, JS, HTML) y rearmá el árbol host/path como el slot original.
| Método | Path | Notas |
|---|
| POST | /v1/capture | Body { "name", "provider", "slotUrl?" }. HTTP 202 job. |
| GET | /v1/capture | Jobs recientes. |
| GET | /v1/capture/{id} | Status, árbol, kinds, counts. |
| GET | /v1/capture/{id}/bundle.zip | Zip entero (árbol original + MANIFEST.json). |
| POST | /v1/capture/{id}/bundle | Body { "paths": ["host/a.png"] } zip seleccionado. |
| GET | /v1/capture/{id}/file?path= | Un archivo. |
Poll hasta status done o error. Tester: modo Capture.
GET /v1/providers
Lista ids canónicos, labels y alias.
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
Lista de tamaños actualmente cargada (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 chequea CDNs upstream.
{
"ok": true,
"service": "game-images-api",
"images": "stake",
"chile": { "ok": true, "requireChile": true, "city": "Pucón", "countryCode": "CL" },
"stakeCdp": true
}
images es siempre "stake". stakeCdp es el Chromium headed (Chile) usado para GraphQL.
UI de prueba
| Path | Tipo |
|---|
GET / | Tester: dropdown de proveedores con buscador + autocomplete de juego cada 2 teclas (Stake) → /v1/images. |
GET /index.html | La misma página. |
GET /docs | Índice de documentación (EN / ES). |
GET /docs/en | Este documento en HTML (inglés). |
GET /docs/es | Este documento en HTML (español). |
GET /docs/API.en.md | Markdown en inglés. |
GET /docs/API.es.md | Markdown en español. |
GET /openapi.yaml | OpenAPI 3.1. |
GET /openapi.json | La misma spec en JSON. |
Errores
Todos los errores son JSON { "error": "..." } más campos extra cuando aplica. Cache-Control: no-store.
| HTTP | error | Cuándo |
|---|
| 400 | name and provider are required | Falta name/game o provider. |
| 400 | unknown provider | El string de proveedor no matcheó. Extra: provider, known (lista de labels). |
| 400 | invalid json | El body POST no es JSON. |
| 400 | no valid sizes (expected 400x400,400x300,300x400) | sizes parseó a lista vacía. |
| 400 | size out of range: {token} | Ancho o alto menor que 16 o mayor que 4000. |
| 404 | not found | Ruta desconocida. |
| 404 | bad media path | Forma de /media/... incorrecta. |
| 404 | missing variant | El archivo no está en disco. |
| 404 | no official image found for game='…' provider=… | El resolver se quedó sin fuentes. Extra: game, provider (id canónico). |
| 502 | (mensaje de excepción) | Falló descarga, decode o render. |
Ejemplo de proveedor desconocido:
{
"error": "unknown provider",
"provider": "unknown",
"known": [
"Pragmatic Play",
"Pragmatic Play Live",
"KAGaming",
"Ruby Play",
"Evolution Gaming",
"Booming Games",
"100HP",
"Ezugi",
"Live Vegas",
"NetEnt"
]
}
Proveedores y alias
Mandá cualquier alias; la API guarda el id.
| id | Label | Alias |
|---|
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 |
El match es case-insensitive, id con o sin guiones/espacios, después alias exacto, después contains para alias de longitud ≥ 5.
Dominios oficiales para rankear hits de búsqueda:
| 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 |
CDN extra de confianza (después de hosts oficiales): agstatic.com, primeapi.com, softswiss.net.
Cómo se elige la fuente
Las imágenes siempre salen de Stake: thumbnailUrl del lobby (imgix/mediumrare). SlotCatalog, CDN de PP, Bing y páginas de estudio no se usan como fuente de imagen. GraphQL siempre por IP de Chile (Chromium headed). HTTP 404 si Stake no tiene el juego o el thumb no se puede bajar.
Los stats extendidos siguen pudiendo venir de SlotCatalog; las tres WebP siguen siendo Stake.
Kinds de fuente
kind | Significado |
|---|
stake-thumbnail | Único kind en vivo: thumbnailUrl de Stake (nunca SlotCatalog). |
Configuración de tamaños
Tres capas; gana la última aplicable por request:
config.json → sizes- Env del proceso
GAME_IMAGES_SIZES=400x400,400x300,300x400,900x1200 sizes del request (query o JSON)
Ejemplo config.json:
{
"sizes": [
{ "id": "square", "width": 400, "height": 400 },
{ "id": "landscape", "width": 400, "height": 300 },
{ "id": "portrait", "width": 300, "height": 400 }
]
}
Agregar 900×1200 mañana sin tocar código:
GAME_IMAGES_SIZES=400x400,400x300,300x400,900x1200 python3 server.py
O en un solo llamado:
curl -sS 'https://grabbr.jailbroken.tech/v1/images?name=azure%20roulette&provider=pp%20live&sizes=900x1200'
GET /v1/sizes relee config.json en cada request.
Pipeline de imagen
- Descarga bytes originales (PNG/JPEG/WebP/GIF).
- PIL load + EXIF transpose.
ImageOps.fit cover a (width, height), LANCZOS, centro (0.5, 0.5).- Aplana alpha sobre
#0c0c10. - Guarda WebP
quality=webpQuality (default 82), method=6.
Cover llena el recuadro; el excedente se recorta. El thumb de Stake (imgix ~800px) se recorta a cada tamaño pedido.
Caché y URLs públicas
| Path | Contenido |
|---|
cache/originals/{key}.bin | Original descargado. |
cache/variants/{key}/{W}x{H}.webp | WebP renderizado. |
key = sha256(providerId|nombreNormalizado|sourceUrl)[:20].
/v1/images escribe esos archivos y devuelve URLs. /media/... solo lee.
publicBase en config o GAME_IMAGES_PUBLIC_BASE prefija url. Vacío → http(s)://{Host}.
cacheTtlSec en config.json está reservado; no hay eviction. Borrá cache/ para regenerar.
Arranque y entorno
python3 /opt/hallallow/apps/game-images-api/server.py
Bind default: 0.0.0.0:8790. Dependencia: Pillow (requirements.txt).
| Variable | Efecto |
|---|
GAME_IMAGES_CONFIG | Path al JSON de config (default ./config.json). |
GAME_IMAGES_PORT | Puerto. |
GAME_IMAGES_SIZES | Lista default, WxH separados por coma. |
GAME_IMAGES_PUBLIC_BASE | Prefijo absoluto de images[].url (CDN / reverse proxy). |
Otras keys de config.json: host, port, publicBase, webpQuality, fit (cover), userAgent, timeoutSec (HTTP upstream, default 18s), sizes.
Detrás de TLS, mandá X-Forwarded-Proto: https para que url salga https://....
Catálogo
Archivo: data/catalog.json. Semilla opcional para que juegos live resuelvan sin página de marketing.
{
"games": [
{
"provider": "pragmatic-play-live",
"names": ["azure roulette", "roulette azure", "roulette 1 azure"],
"symbol": "227",
"title": "Roulette Azure"
}
]
}
Match: nombre exacto (score 100), contains (70+), overlap de tokens (60+). Mínimo 60. Nombres normalizados (minúsculas, guiones → espacios).
Agregá una fila cuando sepas el id PP/Evolution. No hace falta restart; el catálogo se lee en cada resolve.
Ejemplo medido — Azure Roulette
Medido en este host contra la CDN de Pragmatic Play rec/325/227.png.
Request
curl -sS 'https://grabbr.jailbroken.tech/v1/images?name=azure%20roulette&provider=pragmatic%20play%20live'
Respuesta (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 devuelve solo ese array images. sizes=900x1200,400x400 agregó un WebP 900×1200 (70384 bytes) más 400×400.
Segundo juego medido: Gates of Olympus / Pragmatic Play → vs20olympgate en la misma CDN rec/325, tres WebP con tamaños de píxel exactos.
Ejemplo medido — Blood Suckers (extendido)
Los stats coinciden con la tabla de atributos de Blood Suckers. Las imágenes, si hay, siguen siendo oficiales (nunca /userfiles/image/games/… en slotcatalog.com).
curl -sS 'https://grabbr.jailbroken.tech/v1/images?name=Blood%20Suckers&provider=NetEnt&mode=extended'
info incluye:
| Campo | Valor |
|---|
| Provider | NetEnt (/en/soft/NetEnt) |
| RTP | 98% (rtpRanges: true) |
| Variance | Low |
| Max Win | x1014.00 |
| Betways | 25 |
| Release Date | 2013-11-10 |
Solo stats:
curl -sS 'https://grabbr.jailbroken.tech/v1/info?name=Blood%20Suckers&provider=NetEnt'
OpenAPI
Spec machine-readable: openapi.yaml (también GET /openapi.yaml y GET /openapi.json).
Generar clientes (ejemplo):
npx @redocly/cli build-docs https://grabbr.jailbroken.tech/openapi.yaml