# Green Studios Integration API v1.2, as OpenAPI 3.1. # # Source: docs/45a_openapi_v1.yaml. Published as doc-integration-api/openapi.yaml by # scripts/build-doc-integration.py, which also checks it against the HTML guide # (docs/45a_API_publica_v1.html): every route and every free bet error code of the # guide must be here. The guide explains why each rule exists; this file states the # shape. # # Two directions, two servers: # - provider routes (/v1/*): implemented by Green Studios, called by the operator; # - wallet routes (/balance, /bet, /win, /rollback): implemented by the # operator at its wallet_url, called by Green Studios. openapi: 3.1.0 info: title: Green Studios Integration API v1.2 version: 1.2.0 description: | Seamless wallet: the operator's wallet is the only source of truth for balance. **All money is integer cents.** `1500` is 15.00 in the session currency. No monetary field accepts floating point. **One signature scheme, both directions:** `X-Signature: hex(HMAC-SHA256(body, secret))`, over the exact bytes sent. `timestamp` (milliseconds) goes inside the signed body; requests outside a 5 minute window are refused on both sides. **The two directions report errors differently, on purpose.** Provider routes (`/v1/*`) answer `4xx`/`502` with `{"status":"error","error_code","message"}`. Wallet routes answer business refusals as HTTP `200` with `status: "error"` and an `error_code`; a `5xx` from the wallet is a transport failure and is retried. servers: - url: https://api-games.greenstudios.com.br description: Green Studios (provider routes, /v1/*) tags: - name: provider description: operator → provider - name: freebets description: operator → provider, free bets - name: wallet description: provider → operator, at your wallet_url paths: /v1/health: get: tags: [provider] summary: Liveness and server clock (open, not signed) operationId: health security: [] responses: '200': description: alive content: application/json: schema: type: object required: [status, api_version, server_time] properties: status: {const: ok} api_version: {const: v1.2, description: 'contract version; routes stay under /v1'} server_time: {type: string, format: date-time} clock_skew_s: {type: number, description: accepted clock difference, in seconds} /v1/games: post: tags: [provider] summary: Catalogue of the games enabled for the operator operationId: games requestBody: required: true content: application/json: schema: allOf: - $ref: '#/components/schemas/Envelope' - type: object properties: currency: $ref: '#/components/schemas/Currency' description: filters the catalogue and turns on the limits limit: {type: integer, minimum: 1, maximum: 500, default: 100} offset: {type: integer, minimum: 0, default: 0} responses: '200': description: one page of the catalogue content: application/json: schema: type: object required: [status, operator, count, total, offset, games, currencies] properties: status: {const: ok} operator: {type: string} count: {type: integer, description: size of this page} total: {type: integer, description: size of the whole catalogue} offset: {type: integer} currencies: type: array items: {$ref: '#/components/schemas/Currency'} games: type: array items: {$ref: '#/components/schemas/Game'} '401': {$ref: '#/components/responses/InvalidSignature'} '429': {$ref: '#/components/responses/RateLimited'} /v1/launch: post: tags: [provider] summary: Opens a game for one player and returns its URL operationId: launch requestBody: required: true content: application/json: schema: allOf: - $ref: '#/components/schemas/Envelope' - type: object required: [game_id] properties: game_id: {type: string} player_id: {type: string, description: required unless demo} token: type: string minLength: 16 description: >- required unless demo. Random, opaque, new on every launch, never equal to player_id. currency: {$ref: '#/components/schemas/Currency'} language: {type: string, description: 'ISO 639-1: pt, en or es. Default en'} demo: {type: boolean, default: false} mobile: {type: boolean, default: false} lobby_url: {type: string, format: uri} deposit_url: {type: string, format: uri} brand: type: string pattern: '^[a-z0-9._-]{1,100}$' description: >- one of your brands (several sites, one integration): picks the theme and the splash. Unregistered brand = your operator's look. freebets: type: array maxItems: 10 description: >- free bets granted at session start. player_id comes from the launch; currency and game_id default to the launch values. Not on demo. items: {$ref: '#/components/schemas/LaunchFreebet'} responses: '200': description: the URL to load (iframe or redirect), valid for 15 minutes content: application/json: schema: type: object required: [status, url] properties: status: {const: ok} url: {type: string, format: uri} freebets: type: array description: one result per item of the request, in order; present only when the request had freebets items: oneOf: - allOf: - $ref: '#/components/schemas/Freebet' - type: object properties: status: {const: ok} - $ref: '#/components/schemas/LaunchFreebetError' '400': description: MISSING_GAME_ID, MISSING_TOKEN, MISSING_PLAYER_ID, INVALID_TOKEN or INVALID_REQUEST content: application/json: schema: {$ref: '#/components/schemas/ProviderError'} '401': {$ref: '#/components/responses/InvalidSignature'} '404': description: GAME_NOT_FOUND content: application/json: schema: {$ref: '#/components/schemas/ProviderError'} '429': {$ref: '#/components/responses/RateLimited'} /v1/freebets/grant: post: tags: [freebets] summary: Grants free rounds to one player description: >- Idempotent by freebet_id: the same freebet_id with the same fields returns the original grant; with different fields, 409 FREEBET_DUPLICATE. operationId: freebetGrant requestBody: required: true content: application/json: schema: allOf: - $ref: '#/components/schemas/Envelope' - $ref: '#/components/schemas/FreebetRequest' - type: object required: [freebet_id, player_id, game_id, currency, rounds, amount] responses: '200': description: the grant content: application/json: schema: allOf: - $ref: '#/components/schemas/Freebet' - type: object required: [status] properties: status: {const: ok} '400': {$ref: '#/components/responses/FreebetError'} '401': {$ref: '#/components/responses/InvalidSignature'} '403': {$ref: '#/components/responses/FreebetError'} '409': {$ref: '#/components/responses/FreebetError'} '429': {$ref: '#/components/responses/RateLimited'} '502': {$ref: '#/components/responses/FreebetError'} /v1/freebets/cancel: post: tags: [freebets] summary: Cancels a grant (idempotent) description: Rounds already played stay played. A repeated cancel returns the same 200. operationId: freebetCancel requestBody: required: true content: application/json: schema: allOf: - $ref: '#/components/schemas/Envelope' - type: object required: [freebet_id] properties: freebet_id: {$ref: '#/components/schemas/FreebetId'} responses: '200': description: cancelled content: application/json: schema: type: object required: [status, freebet_id, grant_id, state, cancelled_at] properties: status: {const: ok} freebet_id: {$ref: '#/components/schemas/FreebetId'} grant_id: {type: string} state: {const: cancelled} rounds_total: {type: integer} rounds_remaining: {type: integer} cancelled_at: {type: string, format: date-time} '400': {$ref: '#/components/responses/FreebetError'} '401': {$ref: '#/components/responses/InvalidSignature'} '403': {$ref: '#/components/responses/FreebetError'} '404': {$ref: '#/components/responses/FreebetError'} '409': {$ref: '#/components/responses/FreebetError'} '429': {$ref: '#/components/responses/RateLimited'} '502': {$ref: '#/components/responses/FreebetError'} /v1/freebets/status: post: tags: [freebets] summary: One grant (by freebet_id), or the grants of a player (by player_id) operationId: freebetStatus requestBody: required: true content: application/json: schema: allOf: - $ref: '#/components/schemas/Envelope' - oneOf: - type: object required: [freebet_id] properties: freebet_id: {$ref: '#/components/schemas/FreebetId'} - type: object required: [player_id] properties: player_id: {type: string, maxLength: 255} responses: '200': description: >- by freebet_id: the grant. By player_id: the player's grants, most recent first, up to 100. content: application/json: schema: oneOf: - allOf: - $ref: '#/components/schemas/Freebet' - type: object properties: status: {const: ok} - type: object required: [status, player_id, count, freebets] properties: status: {const: ok} player_id: {type: string} count: {type: integer} freebets: type: array maxItems: 100 items: {$ref: '#/components/schemas/Freebet'} '400': {$ref: '#/components/responses/FreebetError'} '401': {$ref: '#/components/responses/InvalidSignature'} '404': {$ref: '#/components/responses/FreebetError'} '429': {$ref: '#/components/responses/RateLimited'} '502': {$ref: '#/components/responses/FreebetError'} webhooks: # Requests Green Studios sends to POST {wallet_url}/. All of them carry the # WalletCommon fields and the X-Signature header. balance: post: tags: [wallet] summary: 'POST {wallet_url}/balance: token → player, balance, currency' description: >- Also the session check when the game opens: the player_id in the answer must be the owner of the token. operationId: walletBalance requestBody: required: true content: application/json: schema: allOf: - $ref: '#/components/schemas/WalletCommon' - type: object required: [player_id, token] properties: player_id: {type: string} token: {type: string} responses: '200': {$ref: '#/components/responses/WalletAnswer'} bet: post: tags: [wallet] summary: 'POST {wallet_url}/bet: debit' description: >- transaction_id is the idempotency key: the same id must never debit twice. On a free round, amount is 0, freebet_id identifies the grant and freebet_amount is the nominal stake (never debit it). In aggregate mode, the closing anchor has transaction_id "fb-". operationId: walletBet requestBody: required: true content: application/json: schema: allOf: - $ref: '#/components/schemas/WalletCommon' - type: object required: [player_id, token, amount, transaction_id, round_id, freebet_id] properties: player_id: {type: string} token: {type: string} amount: {$ref: '#/components/schemas/Cents'} transaction_id: {type: string} round_id: {type: string} freebet_id: {$ref: '#/components/schemas/WalletFreebetId'} freebet_amount: $ref: '#/components/schemas/Cents' description: nominal stake of a free round, for reporting only responses: '200': {$ref: '#/components/responses/WalletAnswer'} win: post: tags: [wallet] summary: 'POST {wallet_url}/win: credit' description: >- A zero amount is valid and must be accepted. On a free round, type is "freebet" and amount is the full payout. In aggregate mode, one credit with type "freebet_payout" closes the grant, even when the total is 0. operationId: walletWin requestBody: required: true content: application/json: schema: allOf: - $ref: '#/components/schemas/WalletCommon' - type: object required: [player_id, token, amount, transaction_id, bet_id, round_id, type, freebet_id] properties: player_id: {type: string} token: {type: string} amount: {$ref: '#/components/schemas/Cents'} transaction_id: {type: string} bet_id: {type: string, description: transaction_id of the /bet this credit closes} round_id: {type: string} type: type: string enum: [win, freebet, freebet_payout, zero_win] freebet_id: {$ref: '#/components/schemas/WalletFreebetId'} responses: '200': {$ref: '#/components/responses/WalletAnswer'} rollback: post: tags: [wallet] summary: 'POST {wallet_url}/rollback: refund of a /bet' description: Never sent for a free round, which debited nothing. operationId: walletRollback requestBody: required: true content: application/json: schema: allOf: - $ref: '#/components/schemas/WalletCommon' - type: object required: [player_id, token, amount, transaction_id, bet_id, round_id] properties: player_id: {type: string} token: {type: string} amount: {$ref: '#/components/schemas/Cents'} transaction_id: {type: string} bet_id: {type: string} round_id: {type: string} external_bet_id: {type: string, description: your id of the bet, when you returned one} responses: '200': {$ref: '#/components/responses/WalletAnswer'} components: securitySchemes: signature: type: apiKey in: header name: X-Signature description: hex(HMAC-SHA256(body, secret)), lowercase, over the exact bytes sent schemas: Currency: type: string pattern: '^[A-Z]{3}$' examples: [BRL] Cents: type: integer minimum: 0 description: integer cents Envelope: type: object required: [operator, timestamp] properties: operator: {type: string, description: your operator name} timestamp: {type: integer, description: milliseconds since the epoch; 5 minute window} Game: type: object required: [game_id, name, studio, category, demo, mobile, desktop, freebet] properties: game_id: {type: string} name: {type: string} studio: {type: string} category: {type: string} demo: {type: boolean} mobile: {type: boolean} desktop: {type: boolean} freebet: {type: boolean, description: the game accepts free bets} image_url: {type: string, format: uri, description: absent when the game has no artwork} currency: {$ref: '#/components/schemas/Currency'} min_bet: $ref: '#/components/schemas/Cents' description: only with currency; absent = unknown max_bet: $ref: '#/components/schemas/Cents' description: only with currency; absent = unknown max_win: $ref: '#/components/schemas/Cents' description: payout ceiling; only with currency; absent = unknown freebet_limits: {$ref: '#/components/schemas/FreebetLimits'} FreebetLimits: type: object description: >- Only with currency, on games with freebet true: the range /v1/freebets/grant accepts. A missing max_* field has no ceiling; a missing block means the range is unknown (do not grant). required: [min_amount, min_rounds] properties: min_amount: {$ref: '#/components/schemas/Cents'} max_amount: {$ref: '#/components/schemas/Cents'} min_rounds: {type: integer, minimum: 1} max_rounds: {type: integer} max_total: {$ref: '#/components/schemas/Cents'} max_expiry_days: {type: integer} FreebetId: type: string pattern: '^[A-Za-z0-9._:-]{1,128}$' description: your id of the grant, and its idempotency key WalletFreebetId: type: [string, 'null'] description: >- always present: null on a paid round; on a free round, your freebet_id (our grant_id when the grant came from our back office) FreebetRequest: type: object properties: freebet_id: {$ref: '#/components/schemas/FreebetId'} player_id: {type: string, maxLength: 255, description: the player does not need to have played} game_id: {type: string, description: a game with freebet true} currency: {$ref: '#/components/schemas/Currency'} rounds: {type: integer, minimum: 1} amount: type: integer minimum: 1 description: stake per round, in cents expires_at: {type: string, format: date-time, description: 'RFC 3339; default 72 hours'} LaunchFreebet: type: object required: [freebet_id, rounds, amount] properties: freebet_id: {$ref: '#/components/schemas/FreebetId'} game_id: {type: string, description: default the launched game} currency: {$ref: '#/components/schemas/Currency'} rounds: {type: integer, minimum: 1} amount: {type: integer, minimum: 1} expires_at: {type: string, format: date-time} LaunchFreebetError: type: object required: [freebet_id, status, error_code, message] properties: freebet_id: {type: string} status: {const: error} error_code: {$ref: '#/components/schemas/FreebetErrorCode'} message: {type: string} Freebet: type: object required: [freebet_id, grant_id, state, rounds_total, rounds_remaining] properties: freebet_id: {type: string} grant_id: {type: string} state: type: string enum: [granted, in_progress, completed, expired, cancelled] player_id: {type: string} game_id: {type: string} currency: {$ref: '#/components/schemas/Currency'} amount: {$ref: '#/components/schemas/Cents'} rounds_total: {type: integer} rounds_remaining: {type: integer} expires_at: {type: [string, 'null'], format: date-time} created_at: {type: [string, 'null'], format: date-time} FreebetErrorCode: type: string enum: - INVALID_REQUEST - GAME_NOT_SUPPORTED - CURRENCY_NOT_SUPPORTED - FREEBETS_NOT_ENABLED - FREEBET_NOT_FOUND - FREEBET_DUPLICATE - FREEBET_ALREADY_USED - FREEBET_EXPIRED - FREEBET_ALREADY_CANCELLED - INTERNAL_ERROR description: only INTERNAL_ERROR (502) is retryable, with the same request ProviderError: type: object required: [status, error_code, message] properties: status: {const: error} error_code: {type: string} message: {type: string} WalletCommon: type: object required: [timestamp, request_id, currency] properties: timestamp: {type: integer, description: milliseconds since the epoch} request_id: {type: string} currency: {$ref: '#/components/schemas/Currency'} game_id: {type: string, description: present when the game is known} WalletOk: type: object required: [status, balance] properties: status: {const: ok} balance: $ref: '#/components/schemas/Cents' description: after the operation; required on success currency: $ref: '#/components/schemas/Currency' description: currency of the account; send it on every response player_id: {type: string, description: owner of the token; required on /balance} transaction_id: {type: string, description: your id of the movement} WalletError: type: object required: [status, error_code] properties: status: {const: error} error_code: type: string enum: [INSUFFICIENT_FUNDS, TOKEN_EXPIRED, TOKEN_INVALID, PLAYER_NOT_FOUND, BET_NOT_FOUND, CURRENCY_MISMATCH, INTERNAL_ERROR] description: >- only INTERNAL_ERROR is retried by us, and so is any code we do not recognise error_message: {type: string} responses: InvalidSignature: description: >- Unknown or disabled operator, missing secret, wrong signature or timestamp out of the window: always the same answer. content: application/json: schema: type: object properties: status: {const: error} error_code: {const: INVALID_SIGNATURE} message: {type: string} RateLimited: description: too many requests; retry later content: application/json: schema: type: object properties: error: {const: rate_limited} message: {type: string} FreebetError: description: >- 400 INVALID_REQUEST, GAME_NOT_SUPPORTED, CURRENCY_NOT_SUPPORTED; 403 FREEBETS_NOT_ENABLED; 404 FREEBET_NOT_FOUND; 409 FREEBET_DUPLICATE, FREEBET_ALREADY_USED, FREEBET_EXPIRED, FREEBET_ALREADY_CANCELLED; 502 INTERNAL_ERROR (retry the same request) content: application/json: schema: type: object required: [status, error_code, message] properties: status: {const: error} error_code: {$ref: '#/components/schemas/FreebetErrorCode'} message: {type: string} WalletAnswer: description: >- Success, or a business refusal, both as HTTP 200. A 5xx is a transport failure and is retried. content: application/json: schema: oneOf: - $ref: '#/components/schemas/WalletOk' - $ref: '#/components/schemas/WalletError' security: - signature: []