pgbank-unofficial là một Python library chính thức phát hành trên PyPI giúp developers tích hợp với PGBank API một cách dễ dàng, an toàn và có nhiều tính năng production-ready. Thư viện hỗ trợ:
- Multi-account management với proxy/BrowserID linh hoạt per-account
- Auto-payment scheduling (flagship) — lập lịch chuyển khoản định kỳ, hỗ trợ cron/interval/conditional triggers
- Transaction history với query nâng cao, categorization, export Excel/CSV/JSON
- Webhook dispatcher để tích hợp với Discord, Telegram, custom HTTP endpoints
- Real-time monitoring cho balance threshold alerts và transaction detection
- Sync + Async API song song cho scripts lẫn FastAPI web apps
- CLI tool cho non-coders
Library hướng tới là de-facto standard cho mọi ai muốn tự động hoá ngân hàng PGBank bằng Python.
The library SHALL authenticate users against PGBank's web API using credentials and a pre-obtained BrowserID, and SHALL persist session tokens to disk so subsequent calls do not require re-authentication until the session expires.
- WHEN user calls
client.login(username="alice", password="xxx", browser_id="bid_xxx")on a fresh session - THEN library SHALL perform
login_step1and return anAuthResultindicating OTP is required - AND library SHALL return
otp_refandotp_tokenfor the user to submit OTP
- WHEN user calls
client.login(otp="123456")after receiving the OTP challenge - THEN library SHALL call
login_step2with the OTP and storedotp_ref/otp_token - AND library SHALL persist the resulting session to disk
- AND all subsequent API calls SHALL use the persisted session
- WHEN library starts with a valid session file on disk
- THEN library SHALL skip login and load the existing session
- AND library SHALL verify session validity with a lightweight API call (e.g.,
get_customer_info) - IF session is expired, THEN library SHALL automatically re-login if credentials are available, ELSE raise
SessionExpiredError
- WHEN user creates
PGBankClientorAccountwithoutbrowser_id - THEN library SHALL raise
MissingBrowserIDErrorimmediately
The library SHALL support managing multiple PGBank accounts simultaneously, each with its own credentials, optional proxy, and BrowserID configuration.
- WHEN user adds 2 accounts both using
browser_id="bid_shared"but differentproxyURLs - THEN library SHALL store both accounts and create separate HTTP clients per account
- AND each account SHALL use its own proxy when making API calls
- WHEN user adds 2 accounts with different
browser_ids but the sameproxy - THEN library SHALL create separate HTTP clients
- AND both accounts SHALL route through the shared proxy
- WHEN user adds an account with
proxy=None - THEN library SHALL make API calls directly without proxy
- WHEN user calls
manager.get_all_balances() - THEN library SHALL query balance for all registered accounts in parallel (with rate limiting)
- AND library SHALL return
dict[account_id, Balance]
- WHEN user calls
manager.health_check() - THEN library SHALL attempt lightweight API call on each account
- AND library SHALL return
dict[account_id, AccountStatus](ALIVE / LOCKED / OTP_REQUIRED / ERROR)
- WHEN user calls
manager.update_account(account_id, proxy="http://new:8080") - THEN library SHALL update the stored configuration
- AND subsequent API calls SHALL use the new proxy
The library SHALL expose typed methods to query account information and balances.
- WHEN user calls
client.get_customer_info() - THEN library SHALL return an
AccountInfodataclass withcustomer_id,customer_name,accounts(list ofBankAccount), andprimary_account
- WHEN user calls
client.get_balance() - THEN library SHALL return
Balance(account_number, available, total, currency, as_of)
- WHEN user calls
client.get_balance(account_number="1234567890") - THEN library SHALL return the balance for that specific account
- WHEN any monetary value is returned
- THEN library SHALL use
decimal.Decimaltype (never float) to avoid precision loss
The library SHALL support 3 types of transfers: domestic (intra-PGBank), NAPAS (inter-bank domestic), CITAD (inter-bank with CITAD network), and same-owner.
- WHEN user calls
client.transfer(from_account="123", to_account="456", amount=100000, bank_code="PGBANK") - THEN library SHALL perform init → verify → confirm flow
- AND library SHALL return
TransferResult(success=True, txn_id, timestamp, fee)
- WHEN user calls
client.transfer(...)with a non-PGBankbank_code(e.g., Vietcombank) - THEN library SHALL use the NAPAS transfer endpoints (init_napas/verify_napas/confirm_napas)
- WHEN PGBank API responds with "OTP_REQUIRED" during verify step
- AND user has set
otp_callbackon the client - THEN library SHALL call
otp_callback(prompt)and wait for OTP string - AND library SHALL retry verify with the provided OTP
- WHEN user calls
client.transfer(..., dry_run=True) - THEN library SHALL perform init + verify steps but NOT confirm
- AND library SHALL return
TransferResult(success=True, dry_run=True, ...)with simulated confirmation
- WHEN user calls
client.lookup_recipient(account="1234567890", bank_code="PGBANK") - THEN library SHALL return the recipient's full name as a string
- AND library SHALL cache the result for 24 hours
The library SHALL query, filter, search, categorize, and export transaction history across one or multiple accounts.
- WHEN user calls
history.query(HistoryQuery(from_date=..., to_date=..., min_amount=1000000)) - THEN library SHALL return
list[Transaction]matching all specified filters
- WHEN user calls
history.search("GRAB", account="alice") - THEN library SHALL perform fuzzy match on description, counterparty name, and counterparty account
- AND return matching transactions
- WHEN user calls
history.categorize(transactions)after adding rules like"GRAB" -> "Di chuyển" - THEN library SHALL apply rules to each transaction
- AND return
list[CategorizedTransaction]withcategoryfield populated
- WHEN user calls
export_to_excel(transactions, output_path="report.xlsx") - THEN library SHALL write a formatted Excel file with columns: Date, Account, Direction, Amount, Counterparty, Description, Category
- WHEN user calls
history.query_all_accounts(query) - THEN library SHALL fetch transactions from all registered accounts
- AND return
dict[account_id, list[Transaction]]
The library SHALL provide a scheduler to automate recurring, one-time, or conditional transfers.
- WHEN user calls
scheduler.add_job(Job(name="...", trigger=CronTrigger("0 9 5 * *"), action=...)) - THEN library SHALL persist the job to storage
- AND library SHALL compute
next_run_atbased on the cron expression
- WHEN the trigger fires (e.g., it's 9 AM on the 5th of the month)
- THEN library SHALL execute the action callable in a thread pool
- AND library SHALL record the run (success/failure, timestamp, error) to
job_history
- WHEN user creates
IntervalTrigger(days=1, jitter_seconds=600) - THEN library SHALL add random 0-600 second delay to each fire time
- AND library SHALL log the actual fire time for audit
- WHEN user creates
ConditionalTrigger(check_every=timedelta(hours=6), condition=lambda c: c.get_balance().available > 50000000) - THEN library SHALL poll the condition every 6 hours
- AND execute the action when condition returns True
- WHEN user calls
scheduler.pause_job(job_id) - THEN library SHALL mark the job as paused
- AND the trigger SHALL NOT fire until resumed
- WHEN a job fails 3 consecutive times (configurable threshold)
- THEN library SHALL automatically pause the job
- AND library SHALL dispatch a
JobEventnotification via the WebhookDispatcher
- WHEN user calls
scheduler.run_job_now(job_id) - THEN library SHALL execute the job immediately, bypassing the trigger
- AND return the
JobRunresult
- WHEN user creates
Job(..., dry_run=True) - THEN library SHALL execute the action in dry-run mode
- AND no actual transfer SHALL be performed
- WHEN a transfer job requires OTP
- THEN library SHALL call
scheduler.otp_callback(job, prompt) - AND wait for the returned OTP string
- WHEN a job would cause total transfers in a day to exceed
daily_limit_per_account(configurable, default 50M VND) - THEN library SHALL refuse to execute and log a warning
- AND mark the job run as
failedwith reasonDAILY_LIMIT_EXCEEDED
- WHEN library restarts
- THEN all jobs and their run history SHALL be reloaded from SQLite
- AND any jobs that were due during downtime SHALL fire immediately on restart (with jitter)
The library SHALL dispatch events (BalanceEvent, TransactionEvent, JobEvent) to registered subscribers and HTTP endpoints with retry logic.
- WHEN user calls
dispatcher.register_discord(webhook_url, events=[TransactionEvent]) - THEN library SHALL send a POST to the Discord URL whenever a TransactionEvent fires
- AND format the payload as a Discord embed message
- WHEN user calls
dispatcher.register_telegram(bot_token, chat_id, events=[BalanceEvent], template="...") - THEN library SHALL send the templated message via Telegram Bot API
- AND Jinja2 template variables SHALL be filled with event fields
- WHEN user calls
dispatcher.register_webhook(url, secret="my-secret", ...) - THEN library SHALL include
X-PGBank-Signatureheader with HMAC-SHA256(payload, secret)
- WHEN webhook delivery fails (non-2xx response or timeout)
- THEN library SHALL retry with exponential backoff (default: 2s, 4s, 8s, 16s, 32s)
- AND after max_retries (default 5), mark as
failed
- WHEN user calls
dispatcher.subscribe(TransactionEvent, my_async_func) - THEN library SHALL call
my_async_func(event)whenever a TransactionEvent fires
The library SHALL poll balances and transactions at user-defined intervals and dispatch events when changes are detected.
- WHEN user starts
BalancePoller(poll_interval=timedelta(minutes=5)) - THEN library SHALL fetch balance for each account every 5 minutes
- AND dispatch
BalanceEventwhen balance changes by more than 1000 VND (configurable threshold)
- WHEN user starts
TransactionMonitor(poll_interval=timedelta(minutes=2)) - THEN library SHALL fetch new transactions since last poll
- AND dispatch
TransactionEventfor each new transaction
- WHEN user calls
poller.alert_below(account_id, amount=100000) - THEN library SHALL dispatch a custom event when balance drops below 100000 VND
The library SHALL provide a pgbank CLI for command-line usage without writing Python code.
- WHEN user runs
pgbank account add --username alice --password xxx --browser-id bid_xxx [--proxy ...] [--nickname ...] - THEN library SHALL persist the account to the default storage
- AND print a success message
- WHEN user runs
pgbank balance [--account alice] - THEN library SHALL print a formatted table of balances
- WHEN user runs
pgbank schedule add --name "..." --cron "..." --account ... --to ... --amount ... - THEN library SHALL create a scheduled job and persist it
- WHEN user runs any
pgbankcommand with--json - THEN library SHALL output the result as JSON (for scripting)
The library SHALL persist all state (accounts, jobs, job history, cached data) to SQLite by default, and SHALL support swapping to alternative backends.
- WHEN user creates
PGBankManager()without specifying storage - THEN library SHALL use SQLite at
~/.pgbank_unofficial/data.db
- WHEN user creates
PGBankManager(storage=SQLiteStorage(path="/custom/path.db")) - THEN library SHALL store all data at the custom path
- WHEN library version is upgraded
- THEN library SHALL automatically apply any pending schema migrations on startup
The library SHALL provide full async equivalents of all sync APIs.
- WHEN user calls
await async_client.login(...) - THEN library SHALL perform authentication without blocking the event loop
- WHEN user calls
await async_client.transfer(...) - THEN library SHALL execute the transfer flow asynchronously
- WHEN user starts
AsyncAutoPaymentScheduler - THEN library SHALL use
asynciofor job execution and trigger evaluation - AND be safe to use in FastAPI/Starlette applications
The library SHALL raise specific, typed exceptions for different failure modes.
- WHEN PGBank API rejects credentials
- THEN library SHALL raise
AuthenticationErrorwith reason
- WHEN API call fails with
SESSION_EXPIREDerror code - THEN library SHALL attempt re-login (if credentials are available) or raise
SessionExpiredError
- WHEN API call exceeds the configured timeout (default 30s)
- THEN library SHALL raise
TimeoutError - AND retry policy (if enabled) SHALL be applied
- WHEN PGBank API returns HTTP 429
- THEN library SHALL respect
Retry-Afterheader - AND apply circuit breaker if sustained
The library SHALL emit structured logs at appropriate levels.
- WHEN user logs in
- THEN library SHALL log INFO with masked credentials (e.g.,
username=alice, password=***)
- WHEN a transfer is executed
- THEN library SHALL log INFO with all relevant fields EXCEPT full OTP or sensitive data
- WHEN a scheduler job fires
- THEN library SHALL log INFO with job_id, job_name, status, duration
This spec follows OpenSpec format. The capabilities listed above are the MUST-HAVE for v0.1.0+. Additional capabilities (e.g., Google Sheets integration, PDF export, advanced analytics) MAY be added in later versions.
- Mobile app push notifications
- Voice/TTS notifications (existing in current
apps/function_server/tts.pycan be added later) - QR code generation (existing in
apps/transfer/qr.pycan be added later) - Captcha solving (PGBank does not have captcha; explicitly excluded)
- Python: 3.9, 3.10, 3.11, 3.12
- OS: Windows, macOS, Linux
- Dependencies: requests, cryptography, click, pyyaml, jinja2 (see pyproject.toml)