Skip to main content
Cresora Commerce
Testing & Sandbox

ACH returns

Trigger NACHA return codes with the amount's cents, and handle them asynchronously.

An ACH debit does not fail in its own response. It is accepted, moves to SUBMITTED, and a return — if there is one — arrives later as a separate event. Sandbox lets you provoke a specific return code by choosing the amount's cents.

ℹNote

Routing numbers do not trigger returns. 021000021 is simply a valid test routing number; any valid test routing number behaves the same. The cents portion of the amount is the only trigger.

Cents to R-code

CentsCodeMeaning
.00—settles normally
.01R01Insufficient Funds
.02R02Account Closed
.03R03No Account
.04R04Invalid Account Number
.07R07Authorization Revoked
.08R08Payment Stopped
.10R10Customer Advises Unauthorized
.16R16Account Frozen
.20R20Non-Transaction Account

Any amount of $50,000 or more returns R01, whatever its cents.

ℹNote

This map is ACH-only. On the card rail the same cents mean something else entirely — .01 is "refer to issuer" there. See test cards.

Returns are asynchronous

The debit response tells you the entry was accepted, not that it was paid:

POST debit      -> 200, state: SUBMITTED
(bank processing)
return arrives  -> state: RETURNED + transaction.returned webhook

In production a return typically lands 1–3 banking days after submission. Sandbox is faster, but do not build a test that depends on a particular sandbox delay — poll for the state change or wait on the webhook.

ℹNote

Do not treat SUBMITTED as success in your reconciliation, your fulfilment trigger, or your UI. Funds are not guaranteed until the return window has passed.

The return webhook

transaction.returned is delivered in the standard envelope:

{
  "envelope_version": 1,
  "event_id": "0190a1e6-7081-7c9d-8e0f-3a4b5c6d7e8f",
  "event_type": "transaction.returned",
  "occurred_at": "2026-08-12T14:32:09Z",
  "partner_id": "0190a190-2b3c-7d4e-8f50-6a7b8c9d0e1f",
  "idempotency_key": "evt_0190a1e6-7081-7c9d-8e0f-3a4b5c6d7e8f",
  "data": {
    "transactionId": "0190a1e5-1111-7abc-8def-2a3b4c5d6e7f",
    "returnCode": "R01",
    "returnDescription": "Insufficient funds",
    "retryable": true,
    "stopsRecurring": false,
    "returnedAt": "2026-08-12T14:32:09Z"
  }
}

The transaction itself moves to state: RETURNED — confirm with GET /transactions/{transactionId} if you poll instead of consuming the webhook. (data carries the event's own fields, camelCase; the R-code is returnCode, not a decline_code.)

Handling rules:

  • Branch on event_type, never on payload shape.
  • idempotency_key is your dedup key — it is evt_ + the event_id (which is a bare UUID). Store it and ignore a repeat.
  • envelope_version is 1. Treat an unknown version as unprocessable rather than guessing.

R10 is terminal

ℹNote

R10 means the customer told their bank the debit was unauthorized. Stop retrying that account: NACHA does not permit re-presenting an entry returned as unauthorized. Resolve it with the customer and obtain a fresh authorization before debiting again.

R02, R03, R04, R16 and R20 all say the account itself cannot be debited — closed, missing, mistyped, frozen, or not a transaction account. Retrying the same details returns again; correct them first.

The full ACH contract, including authorization and consent requirements, is in the ACH guide.