Skip to main content

gamebanking (Python)

Server-side SDK for the Game Banking Engine. Python 3.9+, standard library only.

Game API keys move money. Use this SDK on your game servers only.

Install

pip install game-banking-sdk==0.5.0

Import the gamebanking module after install (pip package name and import name differ).

Quick start

import os
from gamebanking import GameBank, InsufficientFundsError

gb = GameBank(os.environ["GBE_API_KEY"], "https://your-engine.example.com")

player = gb.players.upsert(external_player_id="user-42", display_name="Ada Obi")
account = gb.accounts.open(bank_id=BANK_ID, player_id=player["id"])
gb.funding.create(
    account_id=account["id"], amount_minor=500_000, reason="salary",
    reference="payroll-2040-06-01-user-42",
    idempotency_key="payroll-2040-06-01-user-42",  # your stable ID: retrying never pays twice
)

who = gb.name_enquiry("058", "5915945174")
try:
    gb.transfers.create(source_account_id=account["id"], destination_account_number="5915945174", amount_minor=1_000)
except InsufficientFundsError as e:
    print("available:", e.details["available_balance"], "request:", e.request_id)

for line in gb.accounts.iter_transactions(account["id"]):
    print(line["direction"], line["amount_display"], line["narration"])

Responses are plain dicts, typed with TypedDicts in gamebanking.types for editor completion. Holds: gb.holds.place(...), capture(hold_id, amount_minor=...), release(hold_id). Reversals: gb.transactions.reverse(txn_id, reason=...). Clock: gb.clock.get(), configure(...), advance(...).

Fees and charges

from gamebanking import FeeChangedError

# Show the player what the transfer costs, then cap the fee at what they saw.
quote = gb.fees.quote(bank_id=BANK_ID, amount_minor=2_000_00)
try:
    r = gb.transfers.create(source_account_id=account["id"], destination_account_number="5915945174",
                            amount_minor=2_000_00, max_fee_minor=quote["fees_total_minor"])
    print(r["fees"], r["total_debit_minor"])  # each fee is its own ledger and statement line
except FeeChangedError:
    ...  # the schedule changed: quote again

# Configure fees (bank:write): ₦50 + 1%, capped at ₦500, on transfers of ₦1,000 or more.
gb.fees.create(BANK_ID, code="transfer_fee", name="Transfer fee", event="transfer", model="combined",
               fixed_minor=50_00, percentage_bps=100, max_fee_minor=500_00, min_amount_minor=1_000_00)
books = gb.banks.books(BANK_ID)  # fee_income_minor, levies_payable_minor

Merchants and cards

# A shop and its merchant account (card payments settle here).
shop = gb.merchants.create(external_merchant_id="shop:mama-put", name="Mama Put", category="food")
till = gb.accounts.open(bank_id=BANK_ID, merchant_id=shop["id"])

# Once per bank: turn on cards and add a program (bank:write).
gb.banks.update(BANK_ID, feature_flags={"cards": True})
gb.card_programs.create(BANK_ID, name="Classic", daily_limit_minor=50_000_00)

# Issue a card: card_number and cvv are shown only now.
issued = gb.cards.issue(account_id=account["id"])

# Pay: an approved payment is charged at once (status "captured"). A decline is a result, not an exception.
paid = gb.card_authorizations.authorize(card_id=issued["card"]["id"], merchant_account_id=till["id"], amount_minor=2_500_00)
if paid["authorization"]["status"] == "declined":
    show_player(paid["authorization"]["decline_code"])  # e.g. "insufficient_funds"

# Or hold first when the final amount or the sale isn't settled yet (a fuel pump, a pre-order).
held = gb.card_authorizations.authorize(card_id=issued["card"]["id"], merchant_account_id=till["id"], amount_minor=2_500_00, capture=False)
gb.card_authorizations.capture(held["authorization"]["id"], amount_minor=2_100_00)  # or release(); refund() later, in parts

gb.cards.report_lost(issued["card"]["id"], reason="stolen")  # final; no replacement is issued

Savings and fixed deposits

# Once per bank: turn savings on and define products (bank:write).
gb.banks.update(BANK_ID, feature_flags={"savings": True})
saver = gb.deposit_products.create(BANK_ID, code="easy_saver", name="Easy Saver", kind="savings",
                                   rate_tiers=[{"from_minor": 0, "rate_bps": 300}], withholding_tax_bps=1000,
                                   withdrawals_per_month=3, excess_withdrawal_action="forfeit_interest")
term = gb.deposit_products.create(BANK_ID, code="term", name="Term Deposit", kind="fixed_deposit",
                                  terms=[{"days": 90, "rate_bps": 800}])

# A savings account earns daily on the game clock and is paid monthly.
savings = gb.accounts.open(bank_id=BANK_ID, player_id=player["id"], product_id=saver["id"])
print(gb.accounts.interest(savings["id"])["accrued_interest_minor"])

# A fixed deposit (deposits:write); break it early with a quote first.
fd = gb.fixed_deposits.open(source_account_id=account["id"], product_id=term["id"], term_days=90, amount_minor=50_000_00)["fixed_deposit"]
quote = gb.fixed_deposits.break_quote(fd["id"])
gb.fixed_deposits.break_(fd["id"], max_penalty_minor=quote["penalty_minor"])  # PenaltyChangedError if it rose

Webhooks: debit and credit alerts

Every money movement on a player's account sends a signed account.debited or account.credited event to your endpoint. Verify it with the raw body:

from gamebanking import verify_webhook, WebhookVerificationError

# Register once (webhooks:write). The secret is shown only here.
ep = gb.webhooks.endpoints.create(url="https://game.example.com/gbe/webhooks", event_types=["account.debited", "account.credited"])

@app.post("/gbe/webhooks")  # Flask
def gbe_webhook():
    try:
        event = verify_webhook(request.get_data(), request.headers.get("GBE-Signature"), os.environ["GBE_WEBHOOK_SECRET"])
    except WebhookVerificationError:
        return "", 400
    if event["type"] == "account.debited":
        a = event["data"]  # amount_minor, charges, total_minor, balance_after, counterparty, sequence…
        notify_player(a["player_id"], f"-{a['total_display']} {a['narration'] or ''}")
    return "", 204  # answer within 10s; delivery is at least once, so dedupe on event["id"]

gb.webhooks.endpoints also has list, get, update (status="disabled" / "active"), delete, rotate_secret (the old secret keeps signing for 24 hours) and test. gb.webhooks.events has list / iter (the last 30 days, filterable by type and account_id, also a polling feed), get, deliveries and redeliver.

Setup and CI

from gamebanking import GameBankManagement

mgmt = GameBankManagement(os.environ["GBE_DEV_CREDENTIAL"], base_url)
game = mgmt.games.create(name="Lagos Hustle")
key = mgmt.api_keys.issue(game["id"], environment="sandbox", scopes=["account:read", "account:write", "transfer:create"])
print(key["secret"])  # shown once
mgmt.game(game["id"], "sandbox").banks.create(name="Eko Trust Bank", base_currency="NGN")

mgmt.games.update(id, name=..., slug=..., status=...) renames or suspends a game; mgmt.games.delete(id) deletes it and revokes its keys.

Behaviour

Retries, idempotency keys, error types and money helpers follow the shared contract in CONTRACT.md. The network error class is APIConnectionError, so it doesn't shadow Python's built-in ConnectionError. Client options: timeout (seconds), max_retries, on_response, sender (a custom HTTP function). to_minor("100.50", 2) and format_minor(10050, 2) convert amounts; floats are refused.

The client is synchronous. Call it from a thread pool (asyncio.to_thread) in async servers.

Development

Unit tests need no network:

PYTHONPATH=src python3 -m unittest discover -s tests

The contract test runs the full journey against a real engine. ci/docker-compose.yml starts one (PostgreSQL plus the engine's published image) on port 8080:

docker compose -f ci/docker-compose.yml up -d && ./ci/wait-for-engine.sh
GBE_BASE_URL=http://localhost:8080 PYTHONPATH=src python3 -m unittest discover -s tests
docker compose -f ci/docker-compose.yml down -v

To test against a local engine build instead, build its image and set GBE_ENGINE_IMAGE to that tag. CI (.github/workflows/ci.yml) runs both on every push.

Metadata

Release files for game-banking-sdk 0.5.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for game-banking-sdk 0.5.1
File Size Uploaded
game_banking_sdk-0.5.1.tar.gz 31.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for game-banking-sdk 0.5.1
File Interpreter ABI Platform
game_banking_sdk-0.5.1-py3-none-any.whl Python 3 none any Details

Total release size: 57.9 kB

Release files / game_banking_sdk-0.5.1.tar.gz

Download URL game_banking_sdk-0.5.1.tar.gz
Size 31.9 kB
Tags Source
SHA-256 checksum
How to use checksums
d5d8aedc97b90aef01e4b937c56aa1576d22f949bec5ded5386f57ee3af631a4
BLAKE2b-256 checksum
How to use checksums
fd5b15ac869535079de2bac38942f7c693dabf47da852d6c845e1aec87c53ef2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.10.7

Release files / game_banking_sdk-0.5.1-py3-none-any.whl

Download URL game_banking_sdk-0.5.1-py3-none-any.whl
Size 26.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
42d52cc9e66f43ca1e11313d5ae826eaae5319d5a74ffe880b2c5f694225cd15
BLAKE2b-256 checksum
How to use checksums
2f8b7d09a7201f1c252a1b38252447a043438faac06d1b074f467cc39fa0dd70
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.10.7

Release history Release notifications | RSS feed

0.5.3

2 release files

This release

0.5.1 This release

2 release files

0.5.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page