Skip to main content

Overview

Expensify supports various payment methods for sending and receiving money, including direct bank transfers, debit cards, and the Expensify Wallet. This guide covers the core payment operations.

Payment Flow

The payment system in Expensify follows a robust pattern with optimistic updates, ensuring a smooth user experience even during network delays.

Optimistic Updates Pattern

All payment operations use Onyx optimistic updates:
  1. Optimistic Data - Updates UI immediately
  2. Success Data - Confirms the operation succeeded
  3. Failure Data - Reverts changes if the operation fails
const onyxData = {
    optimisticData: [
        {
            onyxMethod: Onyx.METHOD.MERGE,
            key: ONYXKEYS.RELEVANT_KEY,
            value: {
                isLoading: true,
                errors: null,
            },
        },
    ],
    successData: [
        {
            onyxMethod: Onyx.METHOD.MERGE,
            key: ONYXKEYS.RELEVANT_KEY,
            value: {
                isLoading: false,
            },
        },
    ],
    failureData: [
        {
            onyxMethod: Onyx.METHOD.MERGE,
            key: ONYXKEYS.RELEVANT_KEY,
            value: {
                isLoading: false,
                errors: getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'),
            },
        },
    ],
};

Payment Methods

Available Payment Types

Expensify supports multiple payment methods:
const PAYMENT_METHODS = {
    PERSONAL_BANK_ACCOUNT: 'bankAccount',
    DEBIT_CARD: 'debitCard',
    EXPENSIFY: 'Expensify',
};

const IOU_PAYMENT_TYPE = {
    ELSEWHERE: 'elsewhere',
    EXPENSIFY: 'Expensify',
    VBBA: 'vbba',
};

Payment Method Selection

Choosing the appropriate payment method based on context:
function getActivePaymentType(
    key: string,
    activeAdminPolicies: Policy[],
    latestBankItems: Record<string, BankAccountData>,
    policyID?: string
) {
    const policyFromContext = activeAdminPolicies.find((policy) => policy.id === policyID);
    
    // Check if it's a bank account
    if (key.startsWith('bankAccount')) {
        const bankAccountID = key.split('_')[1];
        const bankItem = latestBankItems[bankAccountID];
        
        if (bankItem?.type === CONST.BANK_ACCOUNT.TYPE.BUSINESS) {
            return {
                paymentType: CONST.IOU.PAYMENT_TYPE.VBBA,
                policyFromPaymentMethod: activeAdminPolicies.find(
                    (policy) => policy.id === bankItem.additionalData?.policyID
                ),
                shouldSelectPaymentMethod: false,
            };
        }
        
        return {
            paymentType: CONST.IOU.PAYMENT_TYPE.EXPENSIFY,
            shouldSelectPaymentMethod: false,
        };
    }
    
    // Debit card payment
    if (key.startsWith('fundID')) {
        return {
            paymentType: CONST.IOU.PAYMENT_TYPE.EXPENSIFY,
            shouldSelectPaymentMethod: false,
        };
    }
    
    return {
        policyFromContext,
        shouldSelectPaymentMethod: true,
    };
}

Sending Payments

Payment Confirmation

Before sending a payment, users can select their preferred payment method through a confirmation flow.
type ConfirmPaymentParams = {
    confirmPayment?: (paymentType: PaymentMethodType | undefined, additionalData?: Record<string, unknown>) => void;
};

function handlePaymentConfirmation(
    item: PaymentItemType,
    activeAdminPolicies: Policy[],
    latestBankItems: Record<string, BankAccountData>,
    policy: Policy | undefined,
    confirmPayment?: (paymentType: PaymentMethodType | undefined, additionalData?: Record<string, unknown>) => void
) {
    const {paymentType, policyFromPaymentMethod, policyFromContext, shouldSelectPaymentMethod} = 
        getActivePaymentType(item.key, activeAdminPolicies, latestBankItems, policy?.id);
    
    if (!shouldSelectPaymentMethod && paymentType) {
        const policyForPayment = policyFromPaymentMethod ?? policyFromContext;
        
        Navigation.navigate(
            ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(
                CONST.IOU.ACTION.PAY,
                CONST.IOU.TYPE.PAY,
                '',
                reportID,
                policyForPayment?.id,
            ),
            {
                iouPaymentType: paymentType,
                paymentMethod: item.key as PaymentMethod,
            },
        );
        
        if (paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY || 
            paymentType === CONST.IOU.PAYMENT_TYPE.VBBA) {
            return;
        }
    }
    
    confirmPayment?.(paymentType as PaymentMethodType, item?.additionalData);
}

Paying Money Requests

Handle payment of expense reports and money requests.
type PaymentData = {
    reportID: string;
    amount: number;
    paymentType: string;
    bankAccountID?: number;
    fundID?: number;
    payAsBusiness?: boolean;
};

function payMoneyRequestOnSearch(
    hash: number,
    paymentData: PaymentData[],
    currentSearchKey?: SearchKey
) {
    const optimisticData: OnyxUpdate[] = [
        {
            onyxMethod: Onyx.METHOD.MERGE,
            key: ONYXKEYS.COLLECTION.REPORT_METADATA,
            value: Object.fromEntries(
                paymentData.map((item) => [
                    `${ONYXKEYS.COLLECTION.REPORT_METADATA}${item.reportID}`,
                    {isActionLoading: true}
                ])
            ),
        },
    ];

    const successData: OnyxUpdate[] = [
        {
            onyxMethod: Onyx.METHOD.MERGE,
            key: ONYXKEYS.COLLECTION.REPORT_METADATA,
            value: Object.fromEntries(
                paymentData.map((item) => [
                    `${ONYXKEYS.COLLECTION.REPORT_METADATA}${item.reportID}`,
                    {isActionLoading: false}
                ])
            ),
        },
    ];

    if (currentSearchKey) {
        successData.push({
            onyxMethod: Onyx.METHOD.MERGE,
            key: currentSearchKey,
            value: {
                data: Object.fromEntries(
                    paymentData.map((item) => [
                        `${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`,
                        null
                    ])
                ),
            },
        });
    }

    const failureData: OnyxUpdate[] = [
        {
            onyxMethod: Onyx.METHOD.MERGE,
            key: ONYXKEYS.COLLECTION.REPORT_METADATA,
            value: Object.fromEntries(
                paymentData.map((item) => [
                    `${ONYXKEYS.COLLECTION.REPORT_METADATA}${item.reportID}`,
                    {isActionLoading: false}
                ])
            ),
        },
    ];

    if (currentSearchKey) {
        failureData.push({
            onyxMethod: Onyx.METHOD.MERGE,
            key: currentSearchKey,
            value: {
                data: Object.fromEntries(
                    paymentData.map((item) => [
                        `${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`,
                        {errors: getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')}
                    ])
                ),
            },
        });
    }

    API.write(
        WRITE_COMMANDS.PAY_MONEY_REQUEST_ON_SEARCH,
        {hash, paymentData: JSON.stringify(paymentData)},
        {optimisticData, successData, failureData},
    );
}

Invoice Payments

Handle invoice-specific payment parameters.
function getPayMoneyOnSearchInvoiceParams(
    policyID: string | undefined,
    payAsBusiness?: boolean,
    methodID?: number,
    paymentMethod?: PaymentMethod
): Partial<PaymentData> {
    const invoiceParams: Partial<PaymentData> = {
        payAsBusiness,
    };

    if (paymentMethod === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) {
        invoiceParams.bankAccountID = methodID;
    }

    if (paymentMethod === CONST.PAYMENT_METHODS.DEBIT_CARD) {
        invoiceParams.fundID = methodID;
    }

    return invoiceParams;
}

Receiving Payments

Setting Up for Receiving Payments

To receive payments, users must:
  1. Complete KYC verification
  2. Connect a bank account
  3. Activate the Expensify Wallet

Payment Receipt Confirmation

When a payment is received, the system automatically updates the relevant records:
// Optimistic update when payment is initiated
const optimisticData = [
    {
        onyxMethod: Onyx.METHOD.MERGE,
        key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
        value: {
            statusNum: CONST.REPORT.STATUS_NUM.REIMBURSED,
            stateNum: CONST.REPORT.STATE_NUM.APPROVED,
        },
    },
];

// Success confirmation
const successData = [
    {
        onyxMethod: Onyx.METHOD.MERGE,
        key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
        value: {
            lastVisibleActionCreated: DateUtils.getDBTime(),
        },
    },
];

Payment Status

Report Status After Payment

After a payment is processed, reports transition through different states:
const predictedNextStatus = 
    policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO 
        ? CONST.REPORT.STATUS_NUM.CLOSED 
        : CONST.REPORT.STATUS_NUM.OPEN;

Checking Payment Availability

const arePaymentsDisabled = 
    policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO;

Payment Verification

Magic Code Validation

Some payment operations require validation codes for security:
function processPaymentWithValidation(
    paymentID: number,
    validateCode: string
) {
    const parameters = {
        paymentID,
        validateCode,
    };

    const optimisticData: OnyxUpdate[] = [
        {
            onyxMethod: Onyx.METHOD.MERGE,
            key: ONYXKEYS.VALIDATE_ACTION_CODE,
            value: {
                validateCodeSent: null,
            },
        },
    ];

    API.write(WRITE_COMMANDS.PROCESS_PAYMENT, parameters, {
        optimisticData,
        successData,
        failureData,
    });
}

Payment Errors

Error Handling

Robust error handling ensures users are informed of payment failures:
const failureData: OnyxUpdate[] = [
    {
        onyxMethod: Onyx.METHOD.MERGE,
        key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
        value: {
            errors: getMicroSecondOnyxErrorWithTranslationKey('iou.error.genericPaymentError'),
            isLoadingPayment: false,
        },
    },
];

Common Error Scenarios

  • Insufficient funds - Not enough balance in wallet or account
  • Invalid payment method - Payment method expired or invalid
  • Network errors - Temporary connectivity issues
  • Validation errors - Incorrect validation codes
  • Policy restrictions - Payment disabled by policy settings

Payment History

Track payment history through transaction records:
const transaction = {
    transactionID: generateID(),
    reportID,
    amount,
    currency: CONST.CURRENCY.USD,
    created: DateUtils.getDBTime(),
    merchant: 'Payment Received',
    comment: {
        type: CONST.TRANSACTION.TYPE.PAYMENT,
    },
};

Best Practices

  • Always use optimistic updates for instant UI feedback
  • Implement proper error handling and rollback logic
  • Validate payment methods before processing
  • Log payment operations for audit trails
  • Use validation codes for sensitive operations
  • Never store card details in client-side storage
  • Implement rate limiting for payment attempts
  • Monitor for suspicious payment patterns
  • Show clear payment confirmation screens
  • Display payment status updates in real-time
  • Provide helpful error messages
  • Allow easy payment method switching

Reimbursements

Learn about expense reimbursement workflows

Invoices

Managing invoice payments

Bank Accounts

Connecting accounts for payments

Wallet

Managing your Expensify Wallet

Build docs developers (and LLMs) love