Exports And Integration Guide
Server and client exports, client events, legacy qb-banking compatibility exports, and the callback surface for developers integrating with oxide-banking.
Reference for the current oxide-banking integration surface.
Resource Names
Primary resource name (use this for all new integrations):
exports['oxide-banking']:<ExportName>(...)Legacy compatibility surface (for older QB-era resources only):
exports['qb-banking']:<ExportName>(...) -- works because the manifest declares provides { 'qb-banking' }oxide-banking supports QBCore, QBX, and ESX through o-link. All exports below work identically on every framework.
For framework-agnostic use across many resources, prefer the o-link banking namespace, which wraps the most-used exports:
local olink = exports['o-link']:olink()
olink.banking.AddAccountMoney('police', 5000, 'Fine paid')Server Exports
Signatures use Lua convention. ? after a type means optional/nilable. errorMessage? means the second return is a human-readable string only on failure.
Core / readiness
IsReady() -> boolean
Returns true once the banking system has finished its initial data load.
GetPlayerBankingData(citizenid) -> { account, creditScore, tier, loans, statements }
Aggregate banking profile for a citizen: player account row, credit score record, tier config, open loans keyed by loan id, and up to 100 recent statements (id, account_name, amount, reason, type, timestamp).
GetAccountData(accountName) -> table?
Returns a defensive shallow copy of a cached shared/job account (id, citizenid, account_name, account_balance, account_type, users). Returns nil if the account does not exist.
GetPlayerAccounts(citizenid) -> table[]
Every account the citizen owns or appears in the users list of, as { account_name, account_balance, account_type, is_owner }. Job accounts have no owner and an empty users list, so they are not included through job membership.
GetPlayerStatements(citizenid, limit = 50) -> table[]
Copy of up to limit most recent statements (id, account_name, amount, reason, statement_type, date). statement_type is 'deposit' or 'withdraw'; date is a Unix timestamp in seconds.
CanAfford(citizenid, amount, accountName) -> boolean
For checking (or nil) checks the player's bank balance via the o-link money bridge; for any other name checks the cached shared/job account balance.
Tiers
GetPlayerTier(citizenid) -> string
Current tier name (basic / premium / business), falling back to Config.DefaultTier.
UpgradePlayerTier(citizenid, newTier) -> boolean
Admin path. Force-sets the tier without checking requirements or charging fees.
GetAvailableTiers(citizenid) -> { [tierName] = table }
Every configured tier with its config fields (name, label, description, monthlyFee, interestRate, limits, features, requirements) annotated with isCurrent, isUpgrade, isDowngrade, meetsRequirements, requirementMessage, canUpgrade, canDowngrade. Requirements only read as met while the player is online.
GetTierLevel(tierName) -> number
Numeric hierarchy level: basic=1, premium=2, business=3, unknown=1.
CheckTierRequirements(citizenid, tierName) -> boolean, errorMessage?
Validates minBalance, creditScore, and jobBoss requirements for the named tier. Returns false, 'Player not online' for an offline player when the tier has requirements.
UpgradeAccountTier(citizenid, newTier) -> boolean, tierLabel | errorMessage
Player must be online. Verifies it is an upgrade and the requirements are met, and that the bank balance covers the first monthlyFee plus Config.Fees.services.accountUpgrade before charging either. Then charges both, resets the monthly fee date, and persists.
DowngradeAccountTier(citizenid, newTier = 'basic') -> boolean, tierLabel | errorMessage
Player must be online. Drops the player to a lower tier, charging Config.Fees.services.accountDowngrade if affordable (downgrade still proceeds if not).
Credit score
GetCreditScore(citizenid) -> number
Current numeric credit score (Config.CreditScore.minScore–maxScore, 300–850 by default). Creates a record at defaultScore if one does not exist.
UpdateCreditScore(citizenid, change, reason) -> number
Adds change to the player's credit event bonus, not to the score directly. The total bonus is clamped to ±Config.CreditScore.maxEventBonus and fades each banking day like any credit event, and the score is clamped to minScore–maxScore. Writes a credit history row and returns the new score. A change of 0 or nil changes nothing.
RecalculateCreditScore(citizenid) -> number, { factors, weightedScore, baseScore, eventBonus, thinFile, finalScore, oldScore }
Re-evaluates all five FICO-style factors (payment history 35%, utilization 30%, account age 15%, credit mix 10%, new inquiries 10%) into a base score, adds the decayed event bonus, and persists. A player with no loans and no loan payments gets defaultScore as their base (thinFile = true) while Config.CreditScore.thinFileUsesDefault is on.
GetCreditScoreBreakdown(citizenid, forceRecalc = false) -> { score, rating, stale, factors, suggestions, lastUpdated }
Returns per-factor sub-scores plus an improvement suggestion for any factor below 60. It reads the stored score unless forceRecalc is true, in which case it runs RecalculateCreditScore first. On a stored read (stale = true), suggestions leave out details and params, because the late-payment counts and utilization figures they quote only exist after a full recalculation. Each factor carries key, impact (high / medium / low) and a numeric weightValue next to the English label and weight.
GetCreditHistory(citizenid, limit = 50) -> table[]
Rows from bank_credit_history, most recent first.
RecordCreditEvent(citizenid, eventType, data) -> nil
Adds the event's delta to the player's credit event bonus and inserts a history row. Deltas come from Config.CreditScore.eventDeltas; loan_payment_late, loan_payment_missed and loan_collections read Config.LatePayment.creditScorePenalty, Config.LatePayment.missedPaymentPenalty and Config.Loans.collections.creditScoreImpact. Unknown event types are still logged with a 0 delta. data may be a table (stored as JSON) or a string, trimmed to 255 characters.
AddHardInquiry(citizenid, reason) -> nil
For resources that run their own credit check. Increments the player's stored hard-inquiry count, which the new-inquiries factor reads and the daily pass ages off after 14 days, and records a hard_inquiry credit event. The bank's own loan applications do not call this; they count through the 14-day application count instead.
Account freeze
FreezeAccount(citizenid, accountName, reason, frozenBy, frozenType = 'admin', duration?) -> boolean
Inserts a bank_frozen_accounts row keyed by citizenid or accountName with optional expiry (duration in seconds from now). The expiry is only checked when banking loads frozen accounts at startup; while the server runs, the freeze stays until UnfreezeAccount is called.
UnfreezeAccount(citizenid, accountName) -> boolean
Removes the freeze entry matching the citizenid or accountName.
SelfFreezeAccount(citizenid, duration, reason) -> boolean
Player-initiated freeze. duration accepts '1_hour', '24_hours', '7_days', 'indefinite'. Defaults invalid reasons to 'other'.
SelfUnfreezeAccount(citizenid) -> boolean, errorMessage?
Allows lifting a self-freeze only. Rejects admin-applied freezes. The bank screen cannot reach this while the account is frozen, because a frozen player cannot open the bank.
GetFreezeStatus(citizenid) -> { frozen, reason, expiresAt, frozenType, canSelfUnfreeze }
Freeze state with details from cache.
IsAccountLocked(citizenid) -> boolean, secondsRemaining?
Returns true with seconds remaining while a failed-PIN lockout is active. Config.Security.MaxLoginAttempts wrong ATM PINs lock PIN entry for Config.Security.LockoutDuration seconds. The lockout is held in memory, so a resource restart clears it.
Savings accounts
CreateSavingsAccount(citizenid, accountName, initialDeposit) -> boolean, { id, account_name, balance } | errorMessage
Player must be online. Requires a tier with features.savings. Validates against maxAccounts, requires an opening deposit of at least minimumBalance, and deducts the deposit from checking. A blank accountName becomes 'Savings N'.
GetSavingsAccounts(citizenid) -> table[]
Cached savings accounts: id, account_name, balance, interest_earned, created_at, the current effective annual interestRate, availableWithoutPenalty (the part that has cleared the notice period), unlockSchedule (up to 3 upcoming { amount, date } entries, date in epoch milliseconds), noticeDays, and penaltyRate.
DepositToSavings(citizenid, accountName, amount) -> boolean, newBalance | errorMessage
Player must be online. Moves amount (rounded down to a whole number) from checking into the named savings account and starts its notice period.
WithdrawFromSavings(citizenid, accountName, amount) -> boolean, { withdrawn, penalizedAmount, penalty, netAmount, newBalance } | errorMessage
Pulls from savings to checking, taking the oldest money in the account first. Anything drawn from deposits made within the last Config.AccountTypes.savings.withdrawalNoticeDays days is charged earlyWithdrawalPenalty; older money and earned interest are not. The account must be left either empty or at or above minimumBalance, and a withdrawal whose penalty would take the whole amount is refused. Works for an offline account holder.
Changed in 2.1.0
This function used to take a fourth applyPenalty argument. Whether the penalty applies is now decided from the account's own deposit history, so the argument no longer exists and is ignored if passed.
CloseSavingsAccount(citizenid, accountName) -> boolean, { finalAmount, refunded, penalty } | errorMessage
Refunds the remaining savings balance to checking and deletes the savings row. Charged the same way as a withdrawal: only money still inside the notice period pays the penalty.
Savings goals
CreateSavingsGoal(citizenid, goalName, targetAmount, category, targetDate?) -> boolean, { id, goal_name, target_amount, category } | errorMessage
Inserts a new active goal. Capped by Config.SavingsGoals.maxGoals. Falls back to 'other' for unknown categories. targetDate is optional and must be 'YYYY-MM-DD'; anything else is dropped.
GetSavingsGoals(citizenid) -> table[]
Active goals merged with up to 10 most recent completed goals, each with a progress percentage.
ContributeToGoal(citizenid, goalId, amount) -> boolean, { newAmount, progress, completed } | errorMessage
Player must be online. Transfers up to the goal's remaining amount from checking into the goal. Marks completed and notifies the player when reached.
WithdrawFromGoal(citizenid, goalId, amount) -> boolean, { newAmount, progress } | errorMessage
Player must be online. Withdraws from the goal back to checking. Flips completed back to false if the new amount drops below target.
DeleteSavingsGoal(citizenid, goalId) -> boolean, { refundedAmount } | errorMessage
Player must be online. Refunds the goal's current amount to checking and deletes the goal row.
Cards — issuance & lifecycle
IssueCardForAccount(source, accountName, pin, opts?) -> boolean, { cardNumber, cardNumberMasked, accountName } | errorKey
Validates PIN length (Config.Cards.PinLength), account access, card limits, and player funds; generates a unique card number, hashes the PIN, charges Config.Cards.OrderCost, adds a bank_card item to inventory. Optional opts = { cost, reason, replacingCard } charges cost instead of the order price and leaves replacingCard out of the card limit. Error keys include invalid_pin, no_access, insufficient_permission, card_limit_reached, not_enough_money, and inventory_full.
OrderReplacementCard(source, oldCardNumber, newPin) -> boolean, { cardNumber, cardNumberMasked, accountName } | errorReason
The caller must own the old card. Issues a new card for the same account, charged Config.Cards.ReplacementCost instead of the order price, then cancels the old card and removes its inventory item. If the new card cannot be issued, the old card is left as it was.
FreezeCard(cardNumber) -> boolean, errorReason?
Sets the card's status to frozen (only from active).
UnfreezeCard(cardNumber) -> boolean, errorReason?
Sets the card's status back to active (only from frozen).
CancelCard(cardNumber) -> boolean, errorReason?
Permanently marks an active or frozen card cancelled with cancelled_at timestamp. Not reactivatable. Does not remove the inventory item.
RevokeCard(cardNumber, reason) -> boolean
Sets the card's DB status to revoked with cancelled_at timestamp. Used for non-user-initiated removals (e.g. account closure). reason is only logged.
RevokeCardsForAccount(citizenid, accountName) -> nil
Revokes every card the citizen holds on the named account (for example after removing them from a shared account).
ChangeCardPIN(cardNumber, oldPin, newPin) -> boolean, errorReason?
Verifies the old PIN (auto-migrating legacy hashes), validates the new PIN is exactly 4 characters (regardless of Config.Cards.PinLength), stores the new SHA-256 hash.
Cards — queries & validation
ValidateCardAccess(citizenid, cardNumber) -> boolean, card | errorString
Confirms the card exists, is active, and the citizen still has access to its account.
ValidateCardPIN(cardNumber, pin) -> boolean, card | errorString
Verifies the PIN against the stored SHA-256 salted hash (auto-migrates legacy plain-text entries on first successful verify).
GetPlayerCards(citizenid) -> table[]
All active/frozen cards owned by the citizen, with masked/formatted numbers and account metadata.
GetPlayerInventoryCards(source) -> table[]
Walks the player's inventory for bank_card items, validates each, returns enriched metadata (masked number, account, daily limit fields, ownership). balance is only filled in when Config.Cards.ShowAccountBalanceInSelector is on. The card daily limit is stored but not enforced or counted in this version, so dailyUsed stays 0; withdrawals are limited by the tier's dailyWithdrawLimit.
GetCardsForAccount(accountName, citizenid?) -> string[]
Cached array of card numbers indexed by account name. For the personal checking account, citizenid is required and the result is limited to that character's cards; omitting it returns an empty array. Shared, job, and gang accounts ignore citizenid and return every card on the account.
GetAccountCardCount(accountName, citizenid?) -> number
Count of active and frozen cards on the account, following the same citizenid rules as GetCardsForAccount.
CanIssueCardForAccount(citizenid, accountName, replacingCard?) -> boolean, reason?
Checks the citizen's permission level against Config.Cards.SharedAccountMinPermission and ensures the account is below its MaxCardsPerAccountType limit. A replacingCard on the same account is not counted toward the limit.
HasAccountAccess(citizenid, accountName) -> boolean, permissionType
Returns true with 'owner' for personal checking and owned shared accounts, 'user' for listed shared-account users, and 'job' for any member of the job (not only the boss; the player must be online). Gangs always return false.
GetAccessibleAccounts(citizenid) -> table[]
Every account the player can access (personal checking + shared/job/gang) with account_type and permission.
MaskCardNumber(cardNumber) -> string
Formats a 16-digit number into the masked display form configured by Config.Cards.CardNumberMaskFormat (default **** **** **** XXXX).
FormatCardNumber(cardNumber) -> string
Formats a 16-digit number into four 4-digit groups separated by spaces.
Transactions — fees & previews
CalculateTransactionFee(citizenid, feeKey, amount) -> feeAmount, waiverReason?
Resolves the fee from Config.Fees (handling the percentage-based externalTransfer fee), then applies tier/balance waivers. Returns 0, 'Fees disabled' when Config.Fees.enabled is false. The bank does not charge the externalTransfer fee itself; call this if your resource wants to. For externalTransfer, a maxFee of 0 caps the fee at 0.
GetFeePreview(citizenid, feeKey, amount) -> { feeKey, feeDescription, amount, isWaived, waiverReason, transactionAmount, totalAmount }
UI-friendly fee preview including description and total.
CalculateATMFee(citizenid, atmType) -> feeAmount, waiverReason?
atmType is 'own', 'other', or 'outOfNetwork'. The bank's own ATMs are free and never call this; it is here for an ATM resource that wants to charge Config.Fees.transactions.atm.
CalculateWireTransferFee(citizenid, transferType = 'domestic') -> feeAmount, waiverReason?
Builds the fee key wireTransfer.<transferType> and resolves.
CheckDailyLimit(citizenid, limitType, amount) -> allowed, remaining
Checks whether adding amount would exceed the player's tier dailyWithdrawLimit or dailyTransferLimit.
GetExpeditedTransferFee() -> number
Configured expedited transfer fee (default 25) when Config.Fees.transactions.expeditedTransfer.enabled, else 0.
IsExpeditedTransferEnabled() -> boolean
Whether expedited transfers are enabled in config.
Transactions — wire & pending transfers
ProcessWireTransfer(fromCitizenId, toCitizenId, amount, transferType = 'domestic') -> boolean, message
Both players must be online. amount is whole dollars, at least $1 and at most Config.Transactions.MaxTransferAmount. Checks balance plus fee and the daily transfer limit, moves the money instantly (no Config.TransferDelays wait), applies the wire fee, updates daily transferred totals. If the recipient cannot be paid, the sender gets the money back and the call returns false, 'Transfer failed'. The bank screen has no wire transfer option; this export is the only way to send one.
CreatePendingTransfer(fromCitizenId, toCitizenId, amount, transferType, reason, expedited) -> result, statusString
Returns nil, 'instant' if no delay is configured for transferType ('external' or 'wire') or expedited == true. Otherwise inserts a bank_pending_transfers row and returns { id, scheduledCompletion, delayHours }, 'pending'. It does not take money from the sender; deduct it yourself first. The recipient is credited at the first hourly check after scheduledCompletion.
GetPendingTransfers(citizenid) -> table[]
Pending transfers in either direction, ordered by scheduled completion.
CancelPendingTransfer(citizenid, transferId) -> boolean, message
Only the sender can cancel, and only while the transfer is pending. Refunds the sender, marks the transfer cancelled, writes a refund statement.
Loans
ApplyForLoan(citizenid, loanType, amount, termDays, collateralType, collateralId) -> boolean, loanId | errorMessage, status?
Validates loan type/amount, checks daily loan cap, eligibility, calculates terms, inserts a pending loan, records a loan_opened credit event, then routes it: 'active' (funded now), 'pending_approval' (admin review for amounts at or over manualApprovalThreshold), 'processing' (funded after Config.LoanApproval.processingTime), or 'pending' (waiting for an admin). The fourth value is that status. If an automatic approval fails to pay out, returns false, errorMessage and the application stays pending.
CheckLoanEligibility(citizenid, loanType, amount) -> boolean, message, requirementsData
Validates min/max amount, active loan cap, the total borrowing limit (Config.Loans.exposure), credit score, account age, optional job-boss requirement (player must be online), and optional minIncome (checked against bank balance). On success requirementsData holds creditScore, accountAge, minBalance, and, when a borrowing limit applies, exposure ({ required, current, met } each). On failure it is nil.
CalculateLoanTerms(citizenid, loanType, amount, termDays) -> { principal, interestRate, rateModifier, termDays, totalInterest, totalAmount, paymentAmount, paymentsTotal, intervalDays, creditScore }?
Picks the credit-score rate modifier, computes the effective interest rate (floored at Config.Loans.minimumInterestRate), prorates interest over the term, and splits the total into paymentsTotal instalments intervalDays (Config.TimeSettings.LoanPaymentInterval) apart. A termDays that is not one of the product's termOptions is replaced by the first option. Returns nil for an unknown product.
GetPlayerLoans(citizenid) -> table[]
All loans in open statuses (pending, pending_approval, processing, approved, active, collections), read from the database and decorated with product label/icon, an externalManaged flag, instalments_total, interval_days, days_late, and late_fee_estimate.
GetLoanProducts(citizenid) -> { [productName] = { label, personalizedRate, requirements, meetsRequirements, ... } }
Every configured loan product with the player's personalized interest rate and per-requirement eligibility data.
MakeLoanPayment(citizenid, loanId, amount, options?) -> boolean, { amount, principalPaid, interestPaid, lateFeesPaid, interestRebate, remainingBalance, status, isLate } | errorMessage
Allocates payment to late fees first, then the balance, and never takes more than is owed. Works on active and collections loans, and for a borrower who is offline. Marks the loan paid if fully repaid. Loans registered by another resource are refused.
Only one payment runs on a loan at a time; a second call made while one is in progress returns false. A payment builds credit only when it finishes an instalment or clears the loan. options is used internally ({ payoff = true } applies the early-payoff interest rebate); leave it out.
PayOffLoan(citizenid, loanId) -> boolean, paymentResult | errorMessage
Computes the full payoff amount via CalculatePayoffAmount and submits it through MakeLoanPayment. Paying off before the term ends gives back the interest for the instalments that never ran.
CalculatePayoffAmount(loanId, citizenid?) -> { total, principal, lateFees, interestRebate }
Remaining balance, less the interest rebate, plus any current late fee (higher of flat or percent fee, capped at maxLateFee). Pass citizenid to have a loan that belongs to someone else read as not found (all zeros).
GetLoanPaymentHistory(loanId) -> table[]
All bank_loan_payments rows for the loan, most recent first.
StartCollections(citizenid, loanId) -> boolean
Marks the loan collections and records a loan_collections credit event worth Config.Loans.collections.creditScoreImpact (applied once, through the event). A loan in collections is no longer collected by the daily run; it is repaid by hand or by wage garnishment.
Vehicle financing
See Vehicle Financing Integration for the full integration contract.
RegisterVehicleLoan(data) -> boolean, loanId | errorMessage
data requires citizenid, plate, principal, totalAmount, paymentAmount, paymentsTotal; optional paymentsRemaining (defaults to paymentsTotal), interestRate (defaults to 0.06), and vehicleModel. Guards against duplicate active loans on the same plate. Inserts an externalManaged vehicle loan in active status with the upper-cased plate as collateral_id and a remaining balance of paymentAmount x paymentsRemaining, and records a loan_opened credit event.
RecordVehicleLoanPayment(plate, amount) -> boolean, { loanId, amount, remainingBalance, status, paymentsMade } | errorMessage
Finds the active vehicle loan by plate, decrements remaining amount, counts one payment, records the payment and a loan_payment_ontime credit event, marks paid if fully repaid.
CloseVehicleLoan(plate, reason = 'paid') -> boolean, errorMessage?
Marks the active vehicle loan paid or defaulted (when reason == 'repossessed'), zeros the remaining amount, records the appropriate credit event.
GetVehicleLoan(plate) -> loan?
Active/collections loan row for the plate from bank_loans, with notes JSON pre-decoded.
Interest
CalculateInterest(balance, dailyRate, days, method?) -> number
Returns 0 if interest disabled or balance below Config.Interest.minimumBalance. Otherwise computes simple (balance * rate * days) or compound (balance * ((1+rate)^days - 1)) interest, banker's-rounded. method defaults to Config.Interest.calculationMethod.
GetEffectiveInterestRate(citizenid, accountType) -> number
Tier annual rate × Config.Interest.accountTypeRates[accountType], divided by Config.TimeSettings.DaysPerYear to yield a daily rate.
ProcessLoginInterest(source) -> number
Runs automatically on login. Once at least one real day has passed since last_interest_payout, pays interest for the elapsed real days on the player's checking balance (capped at Config.Interest.maxInterestPayout per whole day) and on each shared account the player owns (capped at maxInterestPayout), updates last_interest_payout, notifies the player. Returns the total paid.
ProcessMonthlyFees(source) -> number
Runs automatically on login. After 30 real days since last_fee_date, charges the tier's monthlyFee × months due. Downgrades to basic (charging nothing) if the player cannot pay. Returns the amount charged.
Investments & CDs
CreateInvestment(citizenid, productType, amount) -> boolean, { id, product, maturityDate } | errorMessage
Player must be online. Enforces a 5-second per-product lock and DB duplicate check, validates the tier's investments feature, the product's tierRequired, the position limits (maxPositionsPerProduct, maxTotalPositions), min/max amount, and funds, then deducts funds and inserts the investment. product in the result is the product's label.
GetPlayerInvestments(citizenid) -> table[]
All open (active and matured) investment rows for the citizen, most recent first, each with an is_matured flag.
WithdrawInvestment(citizenid, investmentId) -> boolean, { payout, penalty, gain, matured } | errorMessage
Cashes out an open position. The server decides from the maturity date whether it is early; a matured position pays its full value. An early exit is charged the product's earlyWithdrawalPenalty on the earnings only, scaled down as the position approaches maturity. Atomically updates status to withdrawn, and pays out even if the player is offline.
Changed in 2.1.0
This function used to take a third early argument, and refused to cash out a position that had not matured unless early was true. The argument no longer exists and is ignored if passed.
PurchaseCD(citizenid, productType, amount) -> boolean, { id, maturityDate, maturityValue } | errorMessage
Player must be online. Requires a tier with the savings feature (Premium/Business on the shipped config), checks the position limits (maxPerProduct, maxTotal) and min/max deposit, deducts funds, inserts a bank_certificates row with prorated interest baked into maturity_value.
GetPlayerCDs(citizenid) -> table[]
All active or matured certificates of deposit for the citizen, each with an is_matured flag.
RedeemCD(citizenid, cdId) -> boolean, { payout, interest } | errorMessage
Pays out the CD's full maturity_value if the maturity date has passed. Rejects pre-maturity redemption (use EarlyWithdrawCD).
EarlyWithdrawCD(citizenid, cdId) -> boolean, { payout, penalty, interest } | errorMessage
For a CD that has not matured. Pays the deposit plus the interest accrued so far (prorated by time elapsed), less the product's earlyWithdrawalPenalty share of that accrued interest; interest is what the player keeps. Never pays less than the deposit. A matured CD is refused and must be redeemed with RedeemCD.
GetMarketState() -> { trend, modifier, dailyDrift, trendDaysRemaining, lastUpdate, previousDayChange, momentum }
Current market simulation state.
GetMarketHistory(days = Config.TimeSettings.DaysPerYear) -> table[]
Up to days records from bank_market_history (trend, modifier, day_number, recorded_at) in chronological order.
Security — 2FA
Enable2FA(citizenid, securityPin) -> boolean, backupCodes | errorMessage
Refused when Config.Security.Enable2FA is false or 2FA is already on. Hashes the 6-digit PIN with SHA-256+salt, generates 8 single-use backup codes, persists to bank_security_2fa. Returns the plaintext backup codes for the player to save.
Disable2FA(citizenid, verificationCode) -> boolean, errorMessage?
Requires a successful Verify2FA call first, then clears the secret and backup codes.
Verify2FA(citizenid, code) -> boolean, errorMessage?
Returns true straight away if the player has not turned 2FA on. Otherwise accepts either the 6-digit PIN (verified against the salted SHA-256 hash, with legacy-format migration) or one of the unused 8-character backup codes. Tracks failed attempts and locks out after 5 failures for 5 minutes.
Requires2FA(citizenid, amount) -> boolean
True when Config.Security.Enable2FA, amount >= Config.Security.TwoFactorThreshold, and the player has 2FA enabled. The bank's own withdrawals, transfers between accounts and transfers to other players use this and ask for the code on screen. Deposits do not. Exports that move money (RemoveMoney, ProcessWireTransfer, and so on) do not check it; a resource that wants a security-PIN step has to ask for it.
Get2FAStatus(citizenid) -> { enabled, backupCodesRemaining, threshold }
Enable state, count of unused backup codes, configured threshold.
GenerateBackupCodes(citizenid) -> boolean, codes | errorMessage
Replaces existing backup codes with 8 fresh single-use codes (invalidating the old set).
Security — fraud & alerts
TrackPlayerLogin(source, citizenid) -> nil
Captures the player's license: identifier and compares against bank_player_accounts.last_license. Alerts the player and logs a security event when a different device is detected.
CheckForFraud(citizenid, transactionType, amount, targetCitizenId) -> flagged, flags
Returns false, nil when Config.Security.EnableFraudDetection is off. Otherwise records the transaction for velocity tracking and emits flags for large_amount (at or above Config.Security.SuspiciousTransactionAmount) and rapid_transactions (5 or more in 60s). When either fires and targetCitizenId is someone the player has not paid before, a new_recipient flag is added as detail; it never fires on its own. Calls FlagSuspiciousActivity when any flag fires. The bank runs this on its own withdrawals, deposits and transfers to other players, and records those transfers in bank_transactions for the new_recipient check.
FlagSuspiciousActivity(citizenid, reason, data) -> nil
Writes a security log entry, notifies all online admins, prints to console, logs via the banking logger with priority = 'high'.
SendTransactionAlert(citizenid, transactionType, amount, details) -> nil
If alerts are enabled and amount is at or above the player's threshold and the type is enabled in preferences, fires a qb-banking:client:transactionAlert event to the player.
GetAlertPreferences(citizenid) -> { enabled, threshold, types }
Cached alert preferences, initializing with defaults from Config.Security.AlertThreshold if not present.
SetAlertPreferences(citizenid, prefs) -> boolean
Stores enabled, threshold, and types (deposit/withdraw/transfer/received) in the in-memory cache. Nothing is saved to the database, so preferences reset when the resource restarts.
Business — invoices
CreateInvoice(fromCitizenId, fromJob, toCitizenId, amount, description, dueDate) -> boolean, { invoiceId, invoiceNumber, dueDate } | errorMessage
Inserts into bank_invoices and notifies the recipient if they are online (and in the tablet app, if oxide-tablet is running). The invoice number is derived from the row id (first four letters of the job upper-cased, or PERS, then -YYYYMM- and the id padded to 5 digits), so two invoices raised at the same moment can never collide. If fromJob has no business account yet, one is created so the invoice can be paid into it.
amount is rounded down to a whole number, and anything not above 0 is rejected. Also rejected: a recipient who does not exist, a player invoicing themselves (personal invoices only), and a due date before today. description is trimmed to 255 characters. dueDate accepts 'YYYY-MM-DD' or a Unix timestamp, and falls back to Config.Business.InvoiceDueDays from now when it is missing or unreadable. Rejections for missing parties, a bad amount, or an unknown recipient are written to the server log with the arguments that caused them.
The per-player limits (InvoiceMaxAmount, MaxOutstandingInvoicesPerRecipient, rate limiting) apply only to invoices raised from the banking screen. Invoices raised by another resource are not subject to them.
PayInvoice(citizenid, invoiceId) -> boolean, { invoiceNumber, amountPaid, lateFee } | errorMessage
Charges the invoice amount plus any accrued late fee to the payer's bank and credits the business account or the player who raised it.
The invoice is claimed before the money moves, so a second attempt on an invoice already being paid is refused rather than charging twice. If the debit or the credit fails, the claim is released and the invoice stays payable.
SettleInvoice(invoice, payerIdentifier) -> boolean, { invoiceNumber, amountPaid, lateFee } | errorMessage
The same payment, taking an already-loaded invoice row. Used by automatic collection. The payer may be offline.
CancelInvoice(citizenid, invoiceId) -> boolean, message | errorMessage
Sets the invoice status to cancelled if the caller raised it and it is still pending or overdue. Notifies the recipient. Overdue invoices can be cancelled so that one raised by mistake can be withdrawn before automatic collection takes it.
GetInvoices(citizenid, jobName, filter) -> table[]
Invoices filtered by 'sent'/'received'/'pending'/'overdue' (default: all involving the citizen), enriched with from_name, to_name, is_sender. With 'sent', a jobName returns that job's invoices; without one, the citizen's personal invoices. The 'sent' filter returns up to 100 rows ordered by most recent activity, so an older invoice that was just paid still appears; the others return up to 50. 'received' rows that are still unpaid also carry late_fee_due, the late fee paying today would add.
Business — expenses & reports
RecordExpense(jobName, category, amount, description, recordedBy) -> boolean, { expenseId } | errorMessage
Validates category against Config.Business.ExpenseCategories, deducts from the job account, reimburses the recorder's bank, logs to bank_expenses.
GetExpenses(jobName, filters) -> table[]
Expense rows filtered by startDate, endDate, category, with pagination (limit, offset), enriched with recorded_by_name.
GetExpenseSummary(jobName, dateRange) -> { categories, grandTotal, startDate, endDate }
Groups expenses by category over a date range (defaults to current month). Adds percentage, label, icon from config.
GenerateIncomeStatement(jobName, startDate, endDate) -> { totalIncome, operatingExpenses, expenseBreakdown, netIncome, profitMargin, ... }
Sums job-account deposits (income) and expenses for the period. Defaults to current month.
GenerateBalanceReport(jobName) -> { balance, receivables, payables, netPosition, receivableCount }
Current job-account balance, sum of outstanding sent invoices (receivables), net position. Payables always 0.
GenerateCashFlowReport(jobName, startDate, endDate) -> { totalInflows, totalOutflows, netCashFlow, byMonth, startDate, endDate }
Sums statement deposits and withdrawals for the job account over the period, broken down by month.
Legacy qb-banking Compatibility Exports
These exist only for older resources that were built against qb-banking. New integrations should use the primary exports above regardless of framework.
CreatePlayerAccount(playerId, accountName, accountBalance, accountUsers) -> boolean, errorMessage?
playerId is the server ID of an online player. Creates a shared bank account owned by that player after validating the name and checking tier-imposed maxSharedAccounts. accountBalance becomes the starting balance; nothing is taken from the player. Persists to bank_accounts and caches.
CreateJobAccount(accountName, accountBalance) -> boolean
Creates a job-type bank account if it does not already exist (rehydrates cache if the DB row exists).
CreateGangAccount(accountName, accountBalance) -> boolean
Always returns false — gang accounts are not supported in oxide-banking. Use shared accounts instead.
CreateBankStatement(playerId, account, amount, reason, statementType, accountType) -> boolean
Accepts a server id (number) or citizenid (string). Title-cases the reason. Inserts a bank_statements row and prepends to the cached statements list (capped at 100).
AddMoney(accountName, amount, reason) -> boolean
Atomic UPDATE adding to the shared/job account balance, syncs cache, auto-creates a deposit statement. Delegates to ESXBridge for ESX job accounts when applicable. Returns false for an unknown or frozen account.
RemoveMoney(accountName, amount, reason) -> boolean
Atomic UPDATE with account_balance >= ? guard preventing overdrafts. Syncs cache and creates a withdraw statement on success. Returns false for an unknown or frozen account.
GetAccount(accountName) -> table?
Returns the cached account table directly (raw reference — do not mutate).
GetAccountBalance(accountName) -> number
Cached balance for the shared/job account, or 0 if missing.
AddGangMoney(accountName, amount, reason) -> boolean
Alias for AddMoney.
RemoveGangMoney(accountName, amount, reason) -> boolean
Alias for RemoveMoney.
GetGangAccount(accountName) -> table?
Alias for GetAccount.
GetGangAccountBalance(accountName) -> number
Alias for GetAccountBalance.
Client Exports
OpenBank() -> nil
Requests banking data via qb-banking:server:openBank, opens the NUI bank UI, starts the ESC close handler. Aborts if any UI is already open. Its optional first argument is used internally by the tablet app; leave it out.
CloseBank() -> nil
Resets all UI state flags (bank/ATM/card-selector/session), releases NUI focus, sends closeApp to the UI. If the bank is open inside the tablet app, closes the tablet instead.
OpenATM() -> nil
Verifies the player has a bank_card item, enters the card-selector flow (OpenCardSelector).
OpenCardSelector() -> nil
Fetches the player's inventory cards from the server. Auto-selects if only one card exists, otherwise shows the multi-card NUI selector.
CloseCardSelector() -> nil
Clears card selector state, releases NUI focus, sends closeCardSelector to the UI.
IsBankOpen() -> boolean
Bank UI open state.
IsATMOpen() -> boolean
ATM UI open state.
IsCardSelectorOpen() -> boolean
Card selector open state.
GetActiveATMSession() -> { cardNumber, accountName, accountType, dailyLimit, dailyUsed, dailyRemaining } | nil
Active ATM session populated when an ATM is opened with a selected card.
GetSelectedCard() -> card | nil
Card currently selected for an ATM session.
IsInBankZone() -> boolean
Cached bank-zone proximity flag. Always returns false when Config.UseTarget is enabled (zone tracking is bypassed).
IsNearATM() -> boolean, atmHandle?
Iterates Config.ATMModels and returns true with the entity handle if any ATM prop is within 1.5m of the player.
Client Events
qb-banking:client:useCard
Plays the card animation and progress bar, then opens the ATM flow if the player is within 1.5m of an ATM. This is what using the bank_card item triggers.
TriggerClientEvent('qb-banking:client:useCard', source)qb-banking:client:atmInteract
Same ATM flow without the distance check. Ignored if a bank, ATM, or card selector is already open.
qb-banking:client:transactionAlert
Sent by SendTransactionAlert. The client shows a notification and passes the alert to the banking screen.
RegisterNetEvent('qb-banking:client:transactionAlert', function(alert)
-- alert: { type = 'deposit'|'withdraw'|'transfer'|'received', amount, details, timestamp }
end)Callback Surface
Legacy lib.callback names still registered by the resource. New integrations should call exports directly instead of using these callbacks.
Bank, ATM, transfers, and tiers
qb-banking:server:openBankqb-banking:server:openATMqb-banking:server:withdrawqb-banking:server:depositqb-banking:server:internalTransferqb-banking:server:externalTransferqb-banking:server:orderCardqb-banking:server:openAccountqb-banking:server:renameAccountqb-banking:server:deleteAccountqb-banking:server:addUserqb-banking:server:removeUserqb-banking:server:getTierInfoqb-banking:server:upgradeTierqb-banking:server:downgradeTierqb-banking:server:getPlayerTierqb-banking:server:checkDailyLimitsqb-banking:server:getBalanceHistory
Cards
qb-banking:server:getInventoryCardsqb-banking:server:validateCardAndOpenATMqb-banking:server:getCardableAccountsqb-banking:server:orderCardForAccountqb-banking:server:selectCardForATMqb-banking:server:verifyCardPINqb-banking:server:getPlayerCardsqb-banking:server:freezeCardqb-banking:server:unfreezeCardqb-banking:server:changeCardPINqb-banking:server:cancelCardqb-banking:server:orderReplacementCard
Savings and recurring payments
qb-banking:server:createSavingsAccountqb-banking:server:getSavingsAccountsqb-banking:server:depositToSavingsqb-banking:server:quoteSavingsWithdrawalqb-banking:server:withdrawFromSavingsqb-banking:server:closeSavingsAccountqb-banking:server:createSavingsGoalqb-banking:server:getSavingsGoalsqb-banking:server:contributeToGoalqb-banking:server:withdrawFromGoalqb-banking:server:deleteSavingsGoalqb-banking:server:createRecurringPaymentqb-banking:server:getRecurringPaymentsqb-banking:server:cancelRecurringPaymentqb-banking:server:toggleRecurringPaymentqb-banking:server:updateRecurringPaymentqb-banking:server:getPendingTransfersqb-banking:server:cancelPendingTransferqb-banking:server:getTransferSettingsqb-banking:server:getTransferDelayInfo
Loans, credit, investments, and security
qb-banking:server:getLoansqb-banking:server:getLoanProductsqb-banking:server:checkLoanEligibilityqb-banking:server:getLoanTermsqb-banking:server:applyForLoanqb-banking:server:payLoanqb-banking:server:payOffLoanqb-banking:server:getLoanPaymentsqb-banking:server:getLoanPayoffAmountqb-banking:server:getCreditBreakdownqb-banking:server:getCreditHistoryqb-banking:server:recalculateCreditScoreqb-banking:server:getInvestmentsqb-banking:server:createInvestmentqb-banking:server:withdrawInvestmentqb-banking:server:getCDsqb-banking:server:purchaseCDqb-banking:server:redeemCDqb-banking:server:earlyWithdrawCDqb-banking:server:getMarketStateqb-banking:server:getMarketHistoryqb-banking:server:enable2FAqb-banking:server:disable2FAqb-banking:server:verify2FAqb-banking:server:get2FAStatusqb-banking:server:getBackupCodesqb-banking:server:regenerateBackupCodesqb-banking:server:getSecuritySettingsqb-banking:server:getSecurityLogqb-banking:server:getAlertPreferencesqb-banking:server:setAlertPreferencesqb-banking:server:selfFreezeAccountqb-banking:server:selfUnfreezeAccountqb-banking:server:getFreezeStatus
Business
qb-banking:server:recordExpenseqb-banking:server:getExpensesqb-banking:server:getExpenseSummaryqb-banking:server:createInvoiceqb-banking:server:payInvoiceqb-banking:server:cancelInvoiceqb-banking:server:getInvoicesqb-banking:server:getIncomeStatementqb-banking:server:getBalanceReportqb-banking:server:getCashFlowReportqb-banking:server:getExpenseCategoriesqb-banking:server:getJobActivity
Recommendation
Use exports['oxide-banking'] for new integration work.
Reserve exports['qb-banking'] and qb-banking:server:* callback names for legacy resources that still need them.
Admin Commands
Every oxide-banking admin command — player and job accounts, loans and credit, the banking schedule, failed refunds, debugging — and how to grant admin access.
Localization Guide
Translate the oxide-banking screen, ATM, prompts, command help, and notifications with ox_lib JSON locale files.