Skip to main content

game-banking-sdk

Python distribution 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.3

Quick start

import os
from gamebanking import GameBank, InsufficientFundsError  # module inside game-banking-sdk

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.3

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.3
File Size Uploaded
game_banking_sdk-0.5.3.tar.gz 32.0 kB Details

Built distribution (wheel)

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

Total release size: 57.9 kB

Release files / game_banking_sdk-0.5.3.tar.gz

Download URL game_banking_sdk-0.5.3.tar.gz
Size 32.0 kB
Tags Source
SHA-256 checksum
How to use checksums
d8de295b21ca65fb65ae0e7d628685a09684e1b5e1a54f2a54d9e68c1badd505
BLAKE2b-256 checksum
How to use checksums
4529f9f6748ac35a7302d2af87cef99b4b5658b08a26049330075c399955778a
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.3-py3-none-any.whl

Download URL game_banking_sdk-0.5.3-py3-none-any.whl
Size 26.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7779a583b37f2d0c4e4b82db6b127c9adafb8ef2928da69e328dbbbe5f9b0cdb
BLAKE2b-256 checksum
How to use checksums
f32482220a6b925bde0b7b2fd2903f8d4d354cdabb6e991f8fb254324d273119
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

This release

0.5.3 This release

2 release files

0.5.1

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