> ## Documentation Index
> Fetch the complete documentation index at: https://docs.spritz.finance/llms.txt
> Use this file to discover all available pages before exploring further.

# ACH debit user experience

> Handle every ACH debit state in your UI, email, and push notifications.

An ACH debit deposit has two independent timelines:

* the bank debit;
* the crypto delivery.

Never collapse them into one spinner. Crypto can arrive before the debit settles, and a
settled debit can still be returned later.

## Source of truth

Use the successful create response for the first screen. After that, use current API
reads:

* `GET /v1/deposits/{depositId}` for the bank and crypto lifecycle;
* `GET /v1/funding-sources/{sourceId}` for whether the bank can be used again;
* `GET /v1/integrator/ach-debit/returns/{returnId}` for return handling.

Subscribe to `onramp.created`, `onramp.updated`, `onramp.completed`,
`achDebitReturn.created`, and `achDebitReturn.updated` to synchronize current state.
Subscribe separately to `achDebit.authorized`, `achDebit.deliveryProgress`,
`achDebit.delivered`, `achDebit.refunded`, and `achDebit.returned` for push
notifications. Every webhook can arrive more than once or out of order.

For an `onramp.*` event:

1. Fetch the current on-ramp by its webhook `id`.
2. If `source.depositId` is present, fetch that deposit with the user's authorization.
3. Update your stored resource and UI idempotently.

Do not infer a push notification from these generic events. Use the semantic
`achDebit.*` event's immutable snapshot, stable `eventId`, and per-deposit `sequence`.

For an `achDebitReturn.*` event, fetch the return with integrator authentication. Use
its `depositId` and `sourceId` to refresh the related resources.

## Amounts to display

| UI value                 | Field or calculation                                      |
| ------------------------ | --------------------------------------------------------- |
| Total bank debit         | `totalDebitAmountUsd`                                     |
| Crypto purchased         | `principalAmountUsd` and `asset`                          |
| Fee paid by the user     | `userFeeUsd`                                              |
| Crypto confirmed onchain | `confirmedReleasedAmountUsd`                              |
| Crypto still deliverable | `max(0, principalAmountUsd - confirmedReleasedAmountUsd)` |
| Blockchain proof         | `payoutTxHash`, when present                              |
| Bank progress            | `debitStatus`                                             |
| Crypto delivery progress | `releaseStatus`                                           |

`releasedAmountUsd` includes releases submitted to the blockchain but not necessarily
confirmed. Do not tell the user that amount was delivered. Use
`confirmedReleasedAmountUsd` for delivered amounts and `payoutTxHash` as proof when it
is available.

## Deposit choice

Keep the amount screen simple. Offer at most two choices:

| Choice                | API priority | User promise                                                                                         |
| --------------------- | ------------ | ---------------------------------------------------------------------------------------------------- |
| **Standard**          | `normal`     | The full amount is delivered after the bank debit settles.                                           |
| **Fastest available** | `high`       | Spritz delivers as much as currently available before settlement and the remainder after settlement. |

Do not expose W1/W2, risk tiers, `high.maxAmountUsd`, or `high.maxEarlyReleaseAmountUsd` as
separate product concepts. Use the maximum for the selected priority only to validate
the amount. Then prepare the deposit and show the exact returned
`instantPortionUsd`, `settlementPortionUsd`, `userFeeUsd`, and `totalDebitAmountUsd`
before the user authorizes it.

For example, render a prepared $600 Fastest available deposit as “$100 delivered
early; \$500 after the bank debit settles.” The user chooses the delivery option, not
the split.

Also show the network and shortened wallet address. Use the funding source's
`institution.name` and `accountNumberLast4` when available; omit either value when it
is absent instead of showing a placeholder.

Shorten wallet addresses to the first 6 and last 4 characters, separated by `…` (for
example `AbCd12…9XyZ`). Keep the full address available to copy and in an accessible
label. Never use the shortened form as a unique key or identity check.

Format `availableAt` and `clearsAt` in the user's app locale and configured timezone
(fall back to the device timezone), including a short timezone name. Use “after \[date]”
for `availableAt` and exact clear times. When `clearsAtIsEstimate` is `true`, use
“around \[date]” so the UI does not turn a forecast into a promise.

Use these grammatical bank labels in copy:

| Available fields          | Phrase                                                    |
| ------------------------- | --------------------------------------------------------- |
| Institution and last four | `from Chase ••••6789` / `to Chase ••••6789`               |
| Institution only          | `from Chase` / `to Chase`                                 |
| Last four only            | `from bank account ••••6789` / `to bank account ••••6789` |
| Neither                   | `from your bank account` / `to your bank account`         |

## State machines

The top-level `status` is useful for lists. The two component statuses explain what is
actually happening.

### Bank debit

| `debitStatus` | Meaning                                                      | User-facing treatment                                                                                                        |
| ------------- | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
| `authorized`  | The user authorized the debit; it has not been submitted.    | “Bank debit authorized.”                                                                                                     |
| `submitting`  | Spritz is submitting the debit.                              | “Bank debit processing.”                                                                                                     |
| `submitted`   | The bank debit is in flight.                                 | “Bank debit processing.”                                                                                                     |
| `settled`     | The debit settled, but it can still be returned later.       | “Bank debit settled.” Do not call it irreversible.                                                                           |
| `returned`    | The bank returned the debit.                                 | Use the return record's `userAction`.                                                                                        |
| `failed`      | The debit failed before settlement. The deposit is terminal. | “The bank debit failed. No new debit was started.” Do not retry the existing deposit or show a retry button from this state. |

`debitFailureCode` and `debitFailureReason` are diagnostic provider values, not a closed
public enum. Log them for support. Do not build user copy or control flow around them.

### Crypto delivery

| `releaseStatus` | Meaning                                                      | User-facing treatment                                 |
| --------------- | ------------------------------------------------------------ | ----------------------------------------------------- |
| `not_started`   | No crypto release has been queued.                           | Pending only while delivery can still progress.       |
| `queued`        | A release is waiting or being submitted.                     | “Crypto delivery pending.” No promised deadline.      |
| `partial`       | Some, but not all, crypto is confirmed.                      | Show delivered and remaining amounts separately.      |
| `completed`     | The full principal is confirmed onchain.                     | “Crypto delivered.” Show `payoutTxHash` when present. |
| `failed`        | The release stopped before the full principal was delivered. | Label the remainder “not delivered,” not “pending.”   |

`releaseFailureCode` and `releaseFailureReason` are diagnostic provider values. Do not
branch user copy on them.

### Top-level deposit

| `status`             | Meaning in a list or history view                                         |
| -------------------- | ------------------------------------------------------------------------- |
| `authorized`         | The user authorized the bank debit.                                       |
| `processing`         | The debit or crypto delivery is still progressing.                        |
| `partially_released` | Some crypto is confirmed and some remains pending.                        |
| `completed`          | Crypto delivery completed. The bank can still return the debit later.     |
| `failed`             | The debit or crypto release failed. Inspect both component statuses.      |
| `refunded`           | Spritz initiated a refund of the bank debit; bank processing time varies. |
| `returned`           | The bank returned the debit. Fetch the return record.                     |

Do not infer the component state from `status`. For example, a deposit can have a
settled bank debit and a failed crypto release, or completed crypto delivery and a bank
debit that later returns.

## Screens and messages

Use these states as the minimum complete frontend contract:

| Current state                                | Suggested message                                                                    |
| -------------------------------------------- | ------------------------------------------------------------------------------------ |
| Create succeeded                             | “You authorized a $506.00 debit to purchase $500.00 of USDC.”                        |
| `processing`, nothing confirmed              | “Your bank debit is processing. Crypto delivery is pending.”                         |
| Some crypto confirmed, delivery can continue | “$100.00 of USDC delivered. $400.00 remains pending.”                                |
| All crypto confirmed, debit not settled      | “\$500.00 of USDC delivered. Your bank debit is still processing.”                   |
| All crypto confirmed, debit settled          | “Crypto delivered and bank debit settled.”                                           |
| Release failed with nothing confirmed        | “This deposit could not be completed. No crypto was delivered.”                      |
| Release failed after partial confirmation    | “$100.00 of USDC was delivered. The remaining $400.00 was not delivered.”            |
| Debit failed before settlement               | “The bank debit failed. No new debit was started. Contact support if you need help.” |
| `refunded`                                   | “A \$506.00 refund to your bank was initiated. Bank processing times vary.”          |
| `returned`                                   | Explain the returned debit, delivered crypto, and the next action from `userAction`. |

Do not call an authorized debit “withdrawn,” “submitted,” or “completed.” Do not call a
partial delivery complete. Keep completed deposits visible in history because a later
ACH return can change them.

For high-priority deposits, render `priority`, `releaseDecisionMode`, both portions,
`userFeeUsd`, and `totalDebitAmountUsd` from the **create response**. They can differ
from the earlier preparation. A partially instant result or downgrade to normal is a
successful deposit, not an error.

## Pre-deposit UI copy

These outcomes happen before a deposit exists. Show them in the active flow; do not
send email or push. Replace bracketed values with current structured API fields.
Private risk, provider, reserve, and integration-policy inputs are not returned; never
infer them or put raw diagnostics in user copy.

“Contact support” always means the support channel inside **your app**. Configure one
`supportHref` before launch and use it for every support CTA below. Your support team
escalates to Spritz with the user ID, funding-source/deposit ID when one exists, and the
response's `X-Request-ID` / `X-Correlation-ID`. Do not send the end user to an
undocumented Spritz operations channel.

### Bank linking and funding sources

| Outcome                                                                             | Message                                                                                                                                                                     | CTA                                             |
| ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
| Source `pending`                                                                    | “We’re verifying this bank account.”                                                                                                                                        | **Check again**                                 |
| Source `review_required` / ownership uncertain                                      | “We couldn’t confirm this bank account automatically. ACH deposits are paused while we review it.”                                                                          | **Contact support**                             |
| `ownership_mismatch`                                                                | “The name on this bank account doesn’t match your verified profile.”                                                                                                        | **Link a different bank**                       |
| `PLAID_ACCOUNT_ALREADY_LINKED`                                                      | “This bank account is already linked. Choose it to continue.”                                                                                                               | **Choose bank account**                         |
| `PLAID_ACCOUNT_REROUTED` / source `rerouted`                                        | “This bank account is temporarily unavailable. Use a different bank account or try again after \[availableAt].” Omit the date when structured `availableAt` is unavailable. | **Choose another bank**                         |
| `PLAID_ACCOUNT_BLOCKED`, source `disabled`, or `permanent: true`                    | “This bank account can’t be used for ACH deposits.”                                                                                                                         | **Choose another bank** and **Contact support** |
| `PLAID_ACCOUNT_LIMIT_EXCEEDED`                                                      | “You’ve reached the linked bank account limit. Choose an existing bank account.”                                                                                            | **Choose bank account**                         |
| `PLAID_ACCOUNT_LINK_INCOMPLETE`                                                     | “We couldn’t finish linking this bank account.”                                                                                                                             | **Contact support**                             |
| `PLAID_DUPLICATE_IDENTITY`                                                          | “We couldn’t link this bank account. Use your existing Spritz account or contact support.”                                                                                  | **Contact support**                             |
| `PLAID_DUPLICATE_BANK_ACCOUNT`                                                      | “This bank account is already linked to another Spritz account.”                                                                                                            | **Contact support**                             |
| `PLAID_ACCOUNT_HOLDER_UNAVAILABLE`                                                  | “We couldn’t verify ownership of this bank account.”                                                                                                                        | **Link a different bank**                       |
| `PLAID_VERIFIED_IDENTITY_UNAVAILABLE`                                               | “Your verified profile needs attention before you can link a bank account.”                                                                                                 | **Contact support**                             |
| `PLAID_LINK_FAILED`, `PLAID_SANDBOX_LINK_UNAVAILABLE`, `PLAID_IDENTITY_UNAVAILABLE` | “We couldn’t link this bank account right now. Try again or choose a different bank.”                                                                                       | **Try again** and **Choose another bank**       |
| Source `deleted`                                                                    | “This bank account was removed.”                                                                                                                                            | **Link a bank account**                         |

### Amount, availability, and deposit decisions

| Outcome                                                                                             | Message                                                                                                    | CTA                                                                       |
| --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| `validation_error`                                                                                  | “Check the highlighted information and try again.”                                                         | Focus the returned `field`                                                |
| `minimum_deposit`                                                                                   | “Enter at least \$\[minAmountUsd].”                                                                        | Keep amount entry open                                                    |
| `transaction_limit`                                                                                 | “Enter no more than \$\[maxAmountUsd].”                                                                    | Keep amount entry open                                                    |
| Settlement/exposure limit with `clearsAt`                                                           | “Your current ACH deposit limit is \$\[maxAmountUsd]. You can try again after \[clearsAt].”                | **Try later**                                                             |
| Settlement/exposure limit without `clearsAt`                                                        | “Your current ACH deposit limit is \$\[maxAmountUsd].”                                                     | Enter an allowed amount or choose the documented `suggestedAction`        |
| `rail_halted`, `ach_debit_program_halted`, `ach_debit_new_user_paused`, `new_user_admission_paused` | “ACH bank deposits are temporarily unavailable.”                                                           | **Choose another deposit method**                                         |
| `source_pending`                                                                                    | “This bank account is still being verified.”                                                               | **Check again**                                                           |
| `source_review_required`, `risk_review_required`                                                    | “This bank deposit needs review.”                                                                          | **Contact support**                                                       |
| `source_rerouted`, `risk_rerouted`                                                                  | “This bank account is temporarily unavailable for deposits.”                                               | **Choose another bank**; show **Try after \[availableAt]** when present   |
| `source_disabled`, `source_not_eligible`                                                            | “This bank account can’t be used for ACH deposits.”                                                        | **Choose another bank**                                                   |
| `source_not_found`                                                                                  | “This bank account is no longer available.”                                                                | Re-list and **Choose another bank**                                       |
| `ach_debit_source_identity_ambiguous`                                                               | “We found a problem with your linked bank accounts.”                                                       | **Contact support**                                                       |
| `ach_debit_source_unsettled_limit`                                                                  | “A debit from this bank account is still processing.”                                                      | **Try again after it settles**                                            |
| `risk_rejected`                                                                                     | “This bank deposit is unavailable.”                                                                        | **Choose another deposit method**                                         |
| `risk_evaluation_unavailable`, `integration_unavailable`                                            | “We couldn’t authorize this bank deposit right now.”                                                       | **Try again later**                                                       |
| `ach_debit_user_access_restricted`, `ach_debit_user_access_disabled`                                | “ACH bank deposits are unavailable for your account.”                                                      | **Contact support**                                                       |
| `preparation_expired`                                                                               | “This authorization expired. Review and authorize the deposit again.”                                      | **Review deposit**                                                        |
| `preparation_not_found`                                                                             | “We couldn’t find this authorization. Review and authorize the deposit again.”                             | **Review deposit**                                                        |
| `preparation_already_used`                                                                          | “This authorization was already used. Check your deposit history before trying again.”                     | **View deposit history**                                                  |
| `integration_not_approved`, `integration_amount_limit`, `integration_usage_limit`                   | “ACH bank deposits are temporarily unavailable.”                                                           | **Choose another deposit method**; notify your operator to contact Spritz |
| `integration_state_conflict`                                                                        | “We couldn’t confirm whether your deposit was authorized. Check your deposit history before trying again.” | **View deposit history**                                                  |
| Unknown or code-less problem                                                                        | “We couldn’t complete this request. Check your deposit history before trying again.”                       | **View deposit history** or **Contact support**                           |

For `daily_limit`, `monthly_limit`, `unsettled_deposit_limit`,
`unsettled_amount_limit`, `bank_unsettled_deposit_limit`, `open_exposure`, and
`aggregate_exposure`, re-read `limitsByPriority` and fill the copy from its public
`maxAmountUsd`, `suggestedAction`, and `clearsAt`. Do not expose which private control
produced that current limit.

## Email and push notifications

Channel ownership for linked-bank ACH debit is fixed:

* **Spritz sends transactional emails** for `achDebit.authorized`,
  `achDebit.deliveryProgress`, `achDebit.delivered`, `achDebit.refunded`, and
  `achDebit.returned` after email delivery is enabled for your production integrator.
* **Your integration sends push notifications** in your app from the same milestones.
* **Your integration owns the durable in-app history and status UI.**

Confirm with Spritz that ACH email delivery is enabled before production testing. Do
not send duplicate integrator emails for these milestones. In sandbox, assert the
milestone and copy inputs; email delivery itself is not part of the public sandbox
contract.

Do not derive these notifications by fetching state after `onramp.updated`. Persist
every semantic webhook `eventId` in a unique inbox and the highest accepted `sequence`
per deposit. In the same transaction, enqueue one push keyed by `eventId`. Duplicate
event IDs and sequences at or below the high-water mark produce no push. This prevents
both a repeated partial-delivery push and a stale partial push after full delivery.

### Authorized

For push, handle `achDebit.authorized`. Its snapshot matches the successful create,
not the earlier preparation.

<Tabs>
  <Tab title="Partial instant">
    **Title:** You authorized a \$506.00 bank debit

    “You authorized a $506.00 debit from Chase ••••6789 to purchase $500.00 of
    USDC. $100.00 will be delivered now and $400.00 after the bank debit settles.
    Fee: \$6.00. No action needed.”
  </Tab>

  <Tab title="Full instant">
    **Title:** You authorized a \$510.00 bank debit

    “You authorized a $510.00 debit from Chase ••••6789 to purchase $500.00 of
    USDC. The crypto will be delivered now while the bank debit processes. Fee:
    \$10.00. No action needed.”
  </Tab>

  <Tab title="After settlement">
    **Title:** You authorized a \$505.00 bank debit

    “You authorized a $505.00 debit from Chase ••••6789 to purchase $500.00 of
    USDC. The crypto will be delivered after the bank debit settles. Fee: \$5.00.
    No action needed.”
  </Tab>
</Tabs>

### Partial delivery

Handle `achDebit.deliveryProgress`. Spritz creates a new occurrence only when
`confirmedReleasedAmountUsd` increases above zero but remains below
`principalAmountUsd`.

**Title:** Part of your USDC was delivered

When the debit is not settled:

> $100.00 of USDC was delivered to your wallet. $400.00 remains pending until the bank
> debit settles. No action needed.

When the debit is settled:

> $100.00 of USDC was delivered to your wallet. The remaining $400.00 is being
> delivered. No action needed.

Each legitimate increase has a new `eventId` and higher `sequence`.

### Full crypto delivery

Handle `achDebit.delivered`. Its immutable snapshot has `releaseStatus: "completed"`
and `confirmedReleasedAmountUsd` at least `principalAmountUsd`.

**Title:** Your USDC was delivered

If the debit is not settled:

> Your full $500.00 of USDC was delivered to your wallet. Your $506.00 bank debit is
> still processing. No action needed.

If the debit is settled:

> Your $500.00 of USDC was delivered to your wallet, and your $506.00 bank debit
> settled. No action needed.

When an instant deposit later settles without another delivery change, update the
in-app bank status. A second email or push is usually unnecessary.

### Failure and refund

For a release failure, state exactly how much was confirmed and how much was not
delivered. Do not promise a refund merely because delivery failed. Failure copy is
currently for the in-app state only: Spritz does not send a failure email, and your
integration must not synthesize a push from `onramp.updated`. Do not add an integrator
email or push for failures until Spritz publishes a semantic failure event.

**Title:** Your USDC delivery could not be completed

With nothing confirmed:

> Your USDC delivery could not be completed. No USDC was delivered. Check the bank
> status shown with this deposit. Contact support if you need help.

After a partial confirmation:

> $100.00 of USDC was delivered to your wallet. The remaining $400.00 was not
> delivered. We’re looking into it. Contact support if you need help.

For a debit failure before settlement, the deposit is terminal. Do not retry it or tell
the user to retry the same authorization:

**Title:** Your bank debit failed

> Your \$506.00 bank debit failed. No new debit was started. Contact support if you need
> help.

Handle `achDebit.refunded`; do not infer the notification from current deposit status:

**Title:** Your \$506.00 bank debit refund was initiated

> A \$506.00 refund to Chase ••••6789 was initiated. Bank processing times vary. No
> action needed.

`status: "refunded"` and `achDebit.refunded` both mean the bank refund was initiated,
not that the user received it. This is the final public refund milestone; there is no
later refund-completed or refund-failed status or semantic event. Keep the deposit in
history with “initiated” wording, and route a reported non-receipt through support.

### ACH return

Handle `achDebit.returned`. Use its immutable `deposit` and `achReturn` snapshots for
the push; use the resource webhooks and API reads separately to refresh the UI.

**Title:** Your bank returned an ACH debit

Build the body from exactly two parts: one crypto-state sentence, followed by one
`userAction` sentence. Calculate amounts from the event's
`deposit.confirmedReleasedAmountUsd`; `achReturn.cryptoStateAtReturn` selects
the wording but is not itself an amount snapshot.

| `cryptoStateAtReturn` | First sentence                                                                                                                   |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `not_released`        | “Your bank returned the \$506.00 debit before any USDC was delivered.”                                                           |
| `in_flight`           | “Your bank returned the \$506.00 debit while USDC delivery was in progress; check your deposit for the latest confirmed amount.” |
| `partially_confirmed` | “Your bank returned the $506.00 debit after $100.00 of USDC was delivered; \$400.00 was not confirmed as delivered.”             |
| `fully_confirmed`     | “Your bank returned the $506.00 debit after $500.00 of USDC was delivered.”                                                      |

Never accuse the user of fraud or expose a return/risk reason.

| `userAction`      | Suggested message                                                                 |
| ----------------- | --------------------------------------------------------------------------------- |
| `none`            | “This bank account can no longer be used. Link another bank account to continue.” |
| `review_required` | “ACH deposits are paused while we review it. Contact support for help.”           |
| `restricted`      | “ACH deposits are temporarily restricted. Contact support for help.”              |
| `disabled`        | “ACH deposits are disabled. Contact support for help.”                            |

### Access restored

If support later restores access, update the in-app state only after fresh reads show
all three:

1. the user's `fiat_to_crypto` / `ach_debit` capability is `active`;
2. at least one funding source is `active`; and
3. that source's `limitsByPriority.normal.available` is `true`.

Do not synthesize an email or push from those reads. Wait for Spritz to publish a
semantic access-restored event. Never promise restoration while review is pending.

## Keep pre-deposit outcomes in the active flow

Do not send email or push for an outcome that occurs before a deposit exists. Show it
where the user is choosing a bank, amount, or authorization:

* bank-link and ownership outcomes;
* validation and limit errors;
* funding-source eligibility errors;
* high priority reduced or changed to normal;
* temporary integration or program pauses.

Follow `suggestedAction`, `clearsAt`, `availableAt`, and `permanent` when present. Do not
show internal decision details or suggest changing the amount to probe a decline.

<Note>
  The deposit API does not expose a stable expected-delivery deadline. Do not decide a
  deposit is late from a local timer. Continue showing its current bank and crypto
  state, and reconcile from the API after missed webhooks.
</Note>

## Frontend completion checklist

* [ ] Authorization shows the exact create-response debit, principal, fee, timing, and destination.
* [ ] Bank and crypto progress are rendered separately.
* [ ] Partial delivery shows confirmed and remaining amounts.
* [ ] A failed remainder is labeled “not delivered,” not “pending.”
* [ ] Normal, partial-instant, full-instant, and high-to-normal outcomes are supported.
* [ ] Returned and refunded deposits remain visible in history.
* [ ] Return handling branches on `userAction`, not raw reason text.
* [ ] Resource webhook processing reconciles current API state; semantic push processing permanently deduplicates `eventId` and rejects stale `sequence` values.
* [ ] Spritz ACH email delivery is confirmed enabled; the integrator sends no duplicate emails and owns push.
* [ ] Every message gives the next action or says “No action needed.”

## Related

<CardGroup cols={2}>
  <Card title="Linked bank on-ramp" icon="building-columns" href="/guides/use-cases/linked-bank-onramp">
    Build the complete backend and frontend flow.
  </Card>

  <Card title="ACH returns" icon="rotate-left" href="/guides/ach-returns">
    Handle return classes and user access changes.
  </Card>

  <Card title="Webhooks" icon="bell" href="/guides/webhooks">
    Verify, deduplicate, and reconcile deliveries.
  </Card>

  <Card title="Sandbox" icon="flask" href="/guides/sandbox">
    Run deterministic scenarios and the documented contract-only cases before launch.
  </Card>
</CardGroup>
