Understanding payment statuses is critical for properly handling charges and payouts. Every payment moves through a defined lifecycle with specific status transitions, and knowing when payments can be modified—or when they become unstoppable—directly impacts your application’s payment logic and user experience.
What This Reference Covers
This comprehensive guide provides everything you need to handle payment statuses correctly:
Status lifecycles : The complete flow from creation to terminal states
The pending threshold : Why pending status is the point of no return
Failed vs reversed : Critical distinction between pre-funding and post-funding failures
ACH return codes : All R-codes and S-codes with their meanings and timing
Status reasons : Complete catalog of failure reasons and their sources
Status/Source/Reason matrix : Valid combinations showing what actually occurs
Implementation patterns : Code examples for monitoring and handling status changes
Whether you’re building retry logic, handling disputes, or reconciling returns, this reference ensures you respond appropriately to every payment state.
Payment Lifecycle Overview
The happy path for payments follows this progression:
Created
Payment successfully created and awaiting verification
Scheduled
Payment passed verifications and risk scoring, queued for processing
Pending
Payment sent to network - CANNOT BE STOPPED
Paid
Payment successfully funded
Critical Understanding: Once a payment reaches pending status, it has been submitted to the payment network and cannot be stopped, held, or cancelled. The payment must complete its network processing.
Status Values
Complete Status Reference
Status Description Can Modify? Terminal? createdPayment created, awaiting verification ✅ No scheduledVerified and queued for processing ✅ No pendingSent to network ❌ No on_holdPaused for review ✅* No paidSuccessfully funded ❌ No† failedDeclined/returned before funding ❌ Yes reversedReturned after funding completed ❌ Yes cancelledTerminated before network submission ❌ Yes
*Only user-initiated holds can be released by the user; Watchtower holds require Straddle authorization
†Can still transition to reversed if return occurs post-funding
Understanding Failed vs Reversed
Status When It Occurs Money Movement Example failedBefore funding completes Funds deposited in your account NSF during processing reversedAfter funding completes Funds deposited then withdrawn from your account Dispute filed after payment
Status Sources
Sources identify where status changes originate:
Source Description When Used systemAutomated system processing Normal status progression (created → scheduled → pending → paid) watchtowerInternal fraud/risk system Balance checks, fraud screening, validation failures bank_declineACH network return Returns from banks customer_disputeCustomer disputed with their bank Unauthorized claims (R05, R07, R10, R11, R29) user_actionManual action via API User-initiated holds, cancels, releases
Status/Source/Reason Matrix
Success States
Status Source Reason Context createdsystemokPayment created and awaiting verification scheduledsystemokPayment validated and scheduled pendingsystemokPayment submitted to ACH network paidsystemokPayment successfully funded
Hold States
Status Source Reason Context on_holdwatchtowerrisk_reviewFlagged by risk system (paykey or customer under review) on_holdwatchtoweramount_too_largeExceeds max transaction or daily limits on_holduser_actionuser_requestManually held by user via API
Failed States (Pre-Funding)
Status Source Reason Context failedwatchtowerinsufficient_fundsPre-submission balance check detected insufficient funds failedwatchtowerpayment_blockedBlocked by Straddle fraud screening failedwatchtowerinvalid_paykeyPaykey validation failed (blocked, restricted, or customer not found) failedwatchtowerpayment_stoppedPayment rail assignment failed failedwatchtowerduplicate_entryDuplicate payment detected during ID assignment failedbank_declineinsufficient_fundsACH return R01/R09 failedbank_declineclosed_bank_accountACH return R02 failedbank_declineinvalid_bank_accountACH return R03/R04/R20 failedbank_declineinvalid_routingACH return R13 failedbank_declinefrozen_bank_accountACH return R16 failedbank_declineowner_deceasedACH return R14/R15 failedbank_declinepayment_stoppedACH return R08 failedbank_declinepayout_refusedACH return R23 failedbank_declineduplicate_entryACH return R24 failedbank_declineother_network_returnOther ACH return codes failedcustomer_disputedisputedACH return R05/R07/R10/R11/R29 (before funding) faileduser_actionuser_requestManually failed by user via API
Reversed States (Post-Funding)
Status Source Reason Context reversedbank_declineinsufficient_fundsLate ACH return R01/R09 reversedbank_declineclosed_bank_accountLate ACH return R02 reversedbank_declineinvalid_bank_accountLate ACH return R03/R04/R20 reversedbank_declineinvalid_routingLate ACH return R13 reversedbank_declinefrozen_bank_accountLate ACH return R16 reversedbank_declineowner_deceasedLate ACH return R14/R15 reversedbank_declinepayment_stoppedLate ACH return R08 reversedbank_declinepayout_refusedLate ACH return R23 reversedbank_declineduplicate_entryLate ACH return R24 reversedbank_declineother_network_returnOther late ACH returns reversedcustomer_disputedisputedACH return R05/R07/R10/R11/R29 (after funding)
Cancelled States
Status Source Reason Context cancelleduser_actionuser_requestManually cancelled by user via API
Cancellation only occurs via user action in the dashboard or API
Important: Same Reason, Different Sources
Some reason codes can originate from different sources depending on when they’re detected:
insufficient_funds
Source When Detected Description watchtowerPre-submission Balance check via Plaid detected insufficient funds before network submission bank_declinePost-submission ACH network returned R01/R09 after payment was submitted to bank
Key distinction:
Watchtower : Proactive check using real-time balance data — payment never leaves Straddle
Bank Decline : Reactive return from the ACH network after the payment was sent
Status Reasons
Active Reason Codes
These reason codes are actively used by the system:
Reason Code Description Sources okNormal/successful status systeminsufficient_fundsCustomer’s account has insufficient funds watchtower, bank_declineclosed_bank_accountBank account is closed bank_declineinvalid_bank_accountAccount cannot be located or is invalid bank_declineinvalid_routingInvalid ACH routing number bank_declinefrozen_bank_accountAccount frozen or OFAC hold bank_declineowner_deceasedAccount owner is deceased bank_declinedisputedCustomer notified bank payment was unauthorized customer_disputepayment_stoppedCustomer placed stop payment order watchtower, bank_declinerisk_reviewPayment under review by Straddle watchtowerpayment_blockedBlocked by fraud screening watchtowerinvalid_paykeyPaykey blocked due to previous failures watchtoweramount_too_largeExceeds maximum allowed amount watchtowerduplicate_entryDuplicate transaction detected watchtower, bank_declineuser_requestUser-requested action user_actionother_network_returnOther network return codes bank_declinepayout_refusedReceiver refused credit bank_decline
ACH Return Codes
Standard ACH Returns (R-Codes)
Code Reason Message R01 insufficient_fundsThe customer’s account has insufficient funds R09 insufficient_fundsUncollected funds R02 closed_bank_accountThe bank account is closed R03 invalid_bank_accountThe bank account could not be located R04 invalid_bank_accountBank account number invalid (missing digits) R13 invalid_routingInvalid ACH routing number R20 invalid_bank_accountAccount not eligible for transaction activity R05 disputedDebit to consumer account using corporate consent R07 disputedAuthorization revoked by customer R10 disputedCustomer advises not authorized R11 disputedCheck truncation entry return R29 disputedCorporate customer notified bank unauthorized R08 payment_stoppedCustomer placed stop payment order R14 owner_deceasedRepresentative payee deceased R15 owner_deceasedBeneficiary/account holder deceased R16 frozen_bank_accountAccount frozen or OFAC hold R23 payout_refusedReceiver refused credit R24 duplicate_entryDuplicate entry R06+ other_network_returnVarious other return reasons
Straddle Internal Codes (S-Codes)
Code Reason Message When Applied S01 payment_blockedInvalid routing number Pre-origination S02 payment_blockedKnown bad account number Pre-origination S10 payment_blockedInvalid account number Pre-origination S11 payment_blockedPrevious R02, R03, R04, R16, or R20 Pre-origination S12 payment_blockedPrevious R05, R07, R08, R10, R11, or R29 Pre-origination S13 payment_blockedInvalid ODFI credentials Pre-origination
Status Details Structure
Every payment includes detailed status information:
{
"status" : "failed" ,
"status_details" : {
"message" : "The customer's account has insufficient funds to cover this payment." ,
"reason" : "insufficient_funds" ,
"source" : "bank_decline" ,
"code" : "R01" ,
"changed_at" : "2024-10-02T14:30:00Z"
},
"status_history" : [
{
"status" : "created" ,
"message" : "Payment successfully created and awaiting verification." ,
"reason" : "ok" ,
"source" : "system" ,
"changed_at" : "2024-10-01T10:00:00Z"
},
{
"status" : "scheduled" ,
"message" : "Payment successfully validated and scheduled." ,
"reason" : "ok" ,
"source" : "system" ,
"changed_at" : "2024-10-01T10:05:00Z"
},
{
"status" : "pending" ,
"message" : "Payment successfully originated to network." ,
"reason" : "ok" ,
"source" : "system" ,
"changed_at" : "2024-10-01T14:00:00Z"
},
{
"status" : "failed" ,
"message" : "The customer's account has insufficient funds." ,
"reason" : "insufficient_funds" ,
"source" : "bank_decline" ,
"code" : "R01" ,
"changed_at" : "2024-10-02T14:30:00Z"
}
]
}
Handling Status Changes
Monitoring for Status Changes
async function handleStatusChange ( payment ) {
const { status , status_details } = payment ;
switch ( status ) {
case 'created' :
// Payment created, awaiting verification
console . log ( 'Payment created, will be verified soon' );
break ;
case 'scheduled' :
// Passed verification, queued for processing
console . log ( 'Payment verified and scheduled' );
// Last chance to cancel if needed
break ;
case 'pending' :
// CRITICAL: Payment sent to network, cannot stop
console . log ( '⚠️ Payment sent to network - CANNOT BE STOPPED' );
await notifyPendingPayment ( payment );
break ;
case 'on_hold' :
// Check source to determine if we can release
if ( status_details . source === 'user_action' ) {
console . log ( 'User hold - can release when ready' );
} else if ( status_details . source === 'watchtower' ) {
console . log ( 'Risk hold - cannot release via API' );
await notifyCompliance ( payment );
}
break ;
case 'paid' :
// Successfully funded
console . log ( '✓ Payment successfully funded' );
await fulfillOrder ( payment );
// Note: Can still reverse later
break ;
case 'failed' :
// Failed before funding
console . log ( `✗ Payment failed: ${ status_details . reason } ` );
await handleFailure ( payment );
break ;
case 'reversed' :
// Returned after funding
console . log ( `⚠️ Payment reversed: ${ status_details . reason } ` );
await handleReversal ( payment );
break ;
case 'cancelled' :
// Terminated before network submission
console . log ( 'Payment cancelled' );
break ;
}
}
Handling Specific Failure Reasons
async function handlePaymentFailure ( payment ) {
const { reason , code , source } = payment . status_details ;
// Determine if retry is possible
const retryableReasons = [
'insufficient_funds' , // May succeed later with funds
'duplicate_entry' // Timing issue, may succeed
];
const nonRetryableReasons = [
'closed_bank_account' ,
'invalid_bank_account' ,
'invalid_routing' ,
'disputed' ,
'payment_stopped' ,
'owner_deceased' ,
'frozen_bank_account' ,
'invalid_paykey' ,
'payment_blocked'
];
if ( retryableReasons . includes ( reason )) {
// Can retry after addressing issue
await scheduleRetry ( payment );
} else if ( nonRetryableReasons . includes ( reason )) {
// Cannot retry - need new payment method or investigation
await requestNewPaymentMethod ( payment . customer_id );
}
// Log return code for reconciliation
if ( code ) {
await logReturnCode ( payment . id , code , reason );
}
}
Common Scenarios
Successful Payment Flow
created → system / ok - Payment initialized
scheduled → system / ok - Verification passed, risk approved
pending → system / ok - Sent to ACH network
paid → system / ok - Funds successfully transferred
Pre-Submission Balance Check Failure
created → system / ok
Balance check detects insufficient funds
failed → watchtower / insufficient_funds
No money moved, payment never sent to bank
NSF After Submission (ACH Return)
created → scheduled → pending
Bank returns with R01
failed → bank_decline / insufficient_funds / code: R01
No money moved (failed before funding)
Dispute After Funding
created → scheduled → pending → paid
Customer disputes with their bank
Bank sends R10 return
reversed → customer_dispute / disputed / code: R10
Money was moved, then returned
Risk Hold and Release
created → system / ok - Payment initialized
Watchtower flags for review
on_hold → watchtower / risk_review
Manual review in dashboard
If approved: released → scheduled → normal flow
If declined: → failed or cancelled
User-Initiated Hold
created → system / ok
User calls hold API
on_hold → user_action / user_request
User can release when ready
Released → continues processing
Best Practices
Monitor Pending Status Once pending, payments cannot be stopped. Plan accordingly.
Check Both Source and Reason The same reason can come from different sources with different implications.
Handle Failed vs Reversed Different workflows for pre-funding failures vs post-funding reversals.
Track Return Codes Log ACH return codes for reconciliation and pattern analysis.
Next Steps