ACH Best Practices
Guidelines for maximizing ACH transaction success rates
ACH Best Practices
Follow these best practices to ensure successful ACH transactions, reduce returns, and provide a great customer experience.
Bank Account Validation
1. Validate Routing Numbers
Always validate routing numbers before submitting transactions:
- Format: Must be exactly 9 numeric digits
- Check Digit: Use the ABA routing number algorithm to verify validity
- Real-Time: Validate during data entry to catch errors early
// Example: Basic routing number format validation
function isValidRoutingFormat(routing) {
return /^\d{9}$/.test(routing);
}
// Example: ABA check digit validation
function isValidRoutingCheckDigit(routing) {
if (!/^\d{9}$/.test(routing)) return false;
const weights = [3, 7, 1, 3, 7, 1, 3, 7];
let sum = 0;
for (let i = 0; i < 8; i++) {
sum += parseInt(routing[i]) * weights[i];
}
const checkDigit = (10 - (sum % 10)) % 10;
return checkDigit === parseInt(routing[8]);
}2. Validate Account Numbers
- Length: Must be between 4 and 17 digits
- Format: Numeric characters only
- Display: Show last 4 digits for customer confirmation
3. Match Account Types
Ensure the account type matches the actual bank account:
| Account Type | When to Use |
|---|---|
personalChecking | Individual's checking account |
personalSavings | Individual's savings account |
corporateChecking | Business checking account |
corporateSavings | Business savings account |
Tip: Using the wrong account type may cause transaction failures or returns.
Customer Authorization
1. Obtain Proper Authorization
ACH regulations require explicit customer authorization before debiting their account. Include:
- Clear disclosure of the amount to be debited
- Frequency of debits (one-time vs. recurring)
- Cancellation policy and how to revoke authorization
- Contact information for questions or disputes
2. Store Authorization Records
Keep records of:
- Date and time of authorization
- IP address (for online authorizations)
- Authorization text shown to customer
- Customer's acceptance confirmation
3. ACH Mandate Text
For some processors (like Braintree), include an achMandate field with the authorization text:
{
"achMandate": "I authorize [Your Company] to debit my bank account for the amount of $XX.XX on [date]. I understand that I can revoke this authorization by contacting [email protected]."
}Required Information by Account Type
Personal Accounts
{
"payment": {
"bankAccount": {
"type": "personalChecking",
"account": "123456789",
"routing": "987654321",
"bankCountry": "USA"
}
},
"firstName": "John",
"lastName": "Doe"
}Required: firstName and lastName (root-level fields)
Corporate Accounts
{
"payment": {
"bankAccount": {
"type": "corporateChecking",
"account": "123456789",
"routing": "987654321",
"bankCountry": "USA"
}
},
"accountBusinessName": "Acme Corporation"
}Required: accountBusinessName
Transaction Timing
1. Understand Settlement Times
- ACH transactions take 1-3 business days to settle
- Returns can occur up to 60 days after the transaction
- Plan your fulfillment accordingly
2. Set Customer Expectations
Communicate clearly:
- "Funds will be debited within 1-3 business days"
- "Your order will ship after payment confirmation"
- "You'll receive an email when the payment is complete"
3. Handle Business Days
ACH network operates on banking business days:
- Processing: Monday - Friday (excluding federal holidays)
- Cut-off times: Transactions submitted after cut-off process next business day
- Weekend submissions: Process on the next business day
Webhook Implementation
1. Always Use Webhooks
Never rely on synchronous responses for ACH transaction status:
// Subscribe to these events
const achWebhookEvents = [
'transaction.pending', // Transaction submitted
'transaction.approved', // Transaction settled
'transaction.declined', // Transaction failed
'transaction.errored' // Processing error
];2. Handle All Status Changes
function handleTransactionWebhook(event) {
switch (event.type) {
case 'transaction.approved':
// Fulfill order, update records
fulfillOrder(event.data.transactionId);
break;
case 'transaction.declined':
// Notify customer, request new payment
handleDecline(event.data);
break;
case 'transaction.errored':
// Log error, alert support team
handleError(event.data);
break;
case 'transaction.pending':
// Update UI to show processing
updateOrderStatus(event.data.transactionId, 'processing');
break;
}
}3. Implement Retry Logic
For failed webhooks, implement exponential backoff:
Attempt 1: Immediate
Attempt 2: 1 minute
Attempt 3: 5 minutes
Attempt 4: 30 minutes
Attempt 5: 2 hours
Error Prevention
1. Pre-validate All Fields
Check before submission:
| Field | Validation |
|---|---|
| Routing Number | 9 digits, valid check digit |
| Account Number | 4-17 digits |
| Account Type | Valid enum value |
| Bank Country | "USA" |
| First/Last Name | Required for personal accounts |
| Business Name | Required for corporate accounts |
2. Handle Common Errors Gracefully
| Error | Customer Message |
|---|---|
| Invalid routing number | "Please check your routing number. It should be 9 digits and can be found on the bottom of your check." |
| Invalid account number | "Please verify your account number. It should be between 4 and 17 digits." |
| ACH not supported | "Bank account payments are not available for this merchant." |
3. Provide Clear Instructions
Help customers find their bank details:
- Include an image showing where to find routing/account numbers on a check
- Explain the difference between checking and savings accounts
- Offer alternative payment methods if ACH fails
Recurring Payments
1. Use Multi-Use Tokens
Tokenize bank accounts for recurring billing:
POST /v1/transactions?tokenize=true
2. Handle Failed Recurring Payments
Implement a retry strategy:
- Immediate retry: Try once more in case of temporary failure
- Delayed retry: Try again in 3-5 days
- Customer notification: Alert customer after 2 failures
- Grace period: Allow time for customer to update payment method
- Suspension: Suspend service after multiple failures
3. Keep Tokens Updated
- Notify customers before token expiration
- Provide easy way to update bank account information
- Remove tokens when customers close accounts
Security Best Practices
1. Never Log Sensitive Data
// BAD - Don't log account numbers
console.log('Processing payment for account:', accountNumber);
// GOOD - Log only safe identifiers
console.log('Processing payment for transaction:', transactionId);2. Use Tokenization
- Never store raw bank account numbers
- Use Preczn tokens for all storage and processing
- Tokenize immediately upon collection
3. Validate Webhook Signatures
Always verify webhook signatures before processing:
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}Testing
1. Use Test Account Numbers
| Scenario | Account | Routing |
|---|---|---|
| Approved | 123456789 | 987654321 |
| Declined | 000000000 | 888888888 |
| Error | 111111111 | 999999999 |
2. Test All Scenarios
- Successful transactions
- Declined transactions
- Network errors
- Webhook delivery
- Refund processing
- Partial refunds
3. Test Edge Cases
- Maximum amount transactions
- Minimum amount transactions
- International characters in names
- Special characters in business names
Summary Checklist
Before going live, verify:
- Routing number validation implemented
- Account number validation implemented
- Account type selection available
- Customer authorization captured and stored
- ACH mandate included (if using Braintree)
- Webhooks configured and tested
- Error handling implemented
- Customer-facing error messages defined
- Retry logic for recurring payments
- Security measures in place
- Test scenarios completed
Updated 2 days ago
