DEV Community

Payout Rail
Payout Rail

Posted on

Decision Trees in Production: When to Execute Plan B (ACH Return Handling)

Decision Trees in Production: When to Execute Plan B (ACH Return Handling)

The Fourth-and-Eight Problem in Payment Systems

A coach's decision to "go for it on fourth-and-eight if coverage is right" mirrors a critical pattern in payment infrastructure: conditional execution based on real-time signal analysis. In fintech, this translates directly to ACH return handling—knowing when to retry, when to switch rails, and when to escalate.

Your payout system, like a play call, needs to evaluate conditions in flight and execute the right branch. This article walks through building that decision tree for ACH returns.

Why ACH Returns Demand Conditional Logic

ACH transfers fail at predictable rates. NACHA data shows roughly 0.5–1% of B2B ACH transactions return. But "return" isn't monolithic. An R01 (insufficient funds) is recoverable in 3–5 days. An R03 (no account) is permanent.

Your code cannot treat all returns identically. You need conditional branches:

  • R01, R09 (insufficient funds, unsigned account): Retry after 2–3 days.
  • R03, R04 (no account, invalid account): Flag for manual review or switch to Visa Direct.
  • R07 (authorization revoked): Escalate; do not retry automatically.
  • R10 (unauthorized): Investigate with the originating customer.

Without this logic, you either retry permanently (wasting time and fees) or give up too early (leaving money on the table).

Building the Decision Tree

Here's a practical pattern:

async function handleAchReturn(returnCode, payoutRecord) {
  const returnMetadata = ACH_RETURN_CODES[returnCode];

  if (returnMetadata.recoverable && payoutRecord.retryCount < 2) {
    // Schedule retry after window (e.g., R01 after 3 days)
    await scheduleRetry(payoutRecord.id, returnMetadata.retryWindowDays);
    return { action: 'RETRY', nextAttempt: futureDate };
  }

  if (returnMetadata.requiresAltRail) {
    // Route to Visa Direct or RTP instead
    const altResult = await routeToVisaDirect(payoutRecord);
    return { action: 'ALT_RAIL', method: 'VISA_DIRECT', result: altResult };
  }

  if (returnMetadata.requiresManualReview) {
    // Escalate to ops team
    await createManualReviewTicket(payoutRecord, returnCode);
    return { action: 'MANUAL_REVIEW', escalated: true };
  }

  // Permanent failure
  return { action: 'FAIL', reason: returnMetadata.description };
}
Enter fullscreen mode Exit fullscreen mode

Key Return Codes & Conditions

Code Meaning Recoverable? Typical Window Recommended Action
R01 Insufficient funds Yes 3–5 days Retry once
R03 No account No N/A Manual review + alt rail
R04 Invalid account No N/A Manual review
R07 Authorization revoked No N/A Escalate; contact originator
R09 Unsigned account Yes 2–3 days Retry
R10 Unauthorized No N/A Investigate with customer
R29 Corporate account closed No N/A Manual review

Timing Matters: ACH Settlement Windows

ACH batches settle in windows:

  • Standard ACH: 1–2 business days.
  • Same-day ACH: 4 settlement windows per day (8:45 AM, 12:45 PM, 3:45 PM, 5:15 PM ET).

Returns typically land 1–2 business days after the original transfer. Your retry logic must account for this lag:

const retrySchedule = {
  R01: { delayDays: 3, maxAttempts: 2 },
  R09: { delayDays: 2, maxAttempts: 1 },
  R03: { delayDays: 0, maxAttempts: 0 }, // No retry
};
Enter fullscreen mode Exit fullscreen mode

When to Switch Rails

If ACH fails twice, consider Visa Direct or RTP:

  • Visa Direct: 30-min settlement, higher cost (~$0.25–0.50 per transaction), reversible for 10 days.
  • RTP: Near-instant, lower cost (~$0.05–0.15), available only at participating banks.

Decide at the second return:


javascript
if (payoutRecord.retryCount >= 2 && returnCode === 'R01') {
  return await routeToVisaDirect(pay

---

*Decoding ACH return codes programmatically? The [ACH Return Codes API](https://rapidapi.com/payoutrail-ach-return-codes/api/ach-return-codes-api?utm_source=nichestream&utm_medium=devto&utm_campaign=payoutrail-ach-returns) returns the full Nacha R01–R85 set with plain-language descriptions and handling guidance.*
Enter fullscreen mode Exit fullscreen mode

Top comments (0)