Skip to main content
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:
1

Created

Payment successfully created and awaiting verification
2

Scheduled

Payment passed verifications and risk scoring, queued for processing
3

Pending

Payment sent to network - CANNOT BE STOPPED
4

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

StatusDescriptionCan Modify?Terminal?
createdPayment created, awaiting verificationNo
scheduledVerified and queued for processingNo
pendingSent to networkNo
on_holdPaused for review✅*No
paidSuccessfully fundedNo†
failedDeclined/returned before fundingYes
reversedReturned after funding completedYes
cancelledTerminated before network submissionYes
*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

StatusWhen It OccursMoney MovementExample
failedBefore funding completesFunds deposited in your accountNSF during processing
reversedAfter funding completesFunds deposited then withdrawn from your accountDispute filed after payment

Status Sources

Sources identify where status changes originate:
SourceDescriptionWhen Used
systemAutomated system processingNormal status progression (created → scheduled → pending → paid)
watchtowerInternal fraud/risk systemBalance checks, fraud screening, validation failures
bank_declineACH network returnReturns from banks
customer_disputeCustomer disputed with their bankUnauthorized claims (R05, R07, R10, R11, R29)
user_actionManual action via APIUser-initiated holds, cancels, releases

Status/Source/Reason Matrix

Success States

StatusSourceReasonContext
createdsystemokPayment created and awaiting verification
scheduledsystemokPayment validated and scheduled
pendingsystemokPayment submitted to ACH network
paidsystemokPayment successfully funded

Hold States

StatusSourceReasonContext
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)

StatusSourceReasonContext
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)

StatusSourceReasonContext
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

StatusSourceReasonContext
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

SourceWhen DetectedDescription
watchtowerPre-submissionBalance check via Plaid detected insufficient funds before network submission
bank_declinePost-submissionACH 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 CodeDescriptionSources
okNormal/successful statussystem
insufficient_fundsCustomer’s account has insufficient fundswatchtower, bank_decline
closed_bank_accountBank account is closedbank_decline
invalid_bank_accountAccount cannot be located or is invalidbank_decline
invalid_routingInvalid ACH routing numberbank_decline
frozen_bank_accountAccount frozen or OFAC holdbank_decline
owner_deceasedAccount owner is deceasedbank_decline
disputedCustomer notified bank payment was unauthorizedcustomer_dispute
payment_stoppedCustomer placed stop payment orderwatchtower, bank_decline
risk_reviewPayment under review by Straddlewatchtower
payment_blockedBlocked by fraud screeningwatchtower
invalid_paykeyPaykey blocked due to previous failureswatchtower
amount_too_largeExceeds maximum allowed amountwatchtower
duplicate_entryDuplicate transaction detectedwatchtower, bank_decline
user_requestUser-requested actionuser_action
other_network_returnOther network return codesbank_decline
payout_refusedReceiver refused creditbank_decline

ACH Return Codes

Standard ACH Returns (R-Codes)

CodeReasonMessage
R01insufficient_fundsThe customer’s account has insufficient funds
R09insufficient_fundsUncollected funds
R02closed_bank_accountThe bank account is closed
R03invalid_bank_accountThe bank account could not be located
R04invalid_bank_accountBank account number invalid (missing digits)
R13invalid_routingInvalid ACH routing number
R20invalid_bank_accountAccount not eligible for transaction activity
R05disputedDebit to consumer account using corporate consent
R07disputedAuthorization revoked by customer
R10disputedCustomer advises not authorized
R11disputedCheck truncation entry return
R29disputedCorporate customer notified bank unauthorized
R08payment_stoppedCustomer placed stop payment order
R14owner_deceasedRepresentative payee deceased
R15owner_deceasedBeneficiary/account holder deceased
R16frozen_bank_accountAccount frozen or OFAC hold
R23payout_refusedReceiver refused credit
R24duplicate_entryDuplicate entry
R06+other_network_returnVarious other return reasons

Straddle Internal Codes (S-Codes)

CodeReasonMessageWhen Applied
S01payment_blockedInvalid routing numberPre-origination
S02payment_blockedKnown bad account numberPre-origination
S10payment_blockedInvalid account numberPre-origination
S11payment_blockedPrevious R02, R03, R04, R16, or R20Pre-origination
S12payment_blockedPrevious R05, R07, R08, R10, R11, or R29Pre-origination
S13payment_blockedInvalid ODFI credentialsPre-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

  1. createdsystem / ok - Payment initialized
  2. scheduledsystem / ok - Verification passed, risk approved
  3. pendingsystem / ok - Sent to ACH network
  4. paidsystem / ok - Funds successfully transferred

Pre-Submission Balance Check Failure

  1. createdsystem / ok
  2. Balance check detects insufficient funds
  3. failedwatchtower / insufficient_funds
  4. No money moved, payment never sent to bank

NSF After Submission (ACH Return)

  1. createdscheduledpending
  2. Bank returns with R01
  3. failedbank_decline / insufficient_funds / code: R01
  4. No money moved (failed before funding)

Dispute After Funding

  1. createdscheduledpendingpaid
  2. Customer disputes with their bank
  3. Bank sends R10 return
  4. reversedcustomer_dispute / disputed / code: R10
  5. Money was moved, then returned

Risk Hold and Release

  1. createdsystem / ok - Payment initialized
  2. Watchtower flags for review
  3. on_holdwatchtower / risk_review
  4. Manual review in dashboard
  5. If approved: released → scheduled → normal flow
  6. If declined: → failed or cancelled

User-Initiated Hold

  1. createdsystem / ok
  2. User calls hold API
  3. on_holduser_action / user_request
  4. User can release when ready
  5. 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