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

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: 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 600Fastestavailabledepositas600 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:

State machines

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

Bank debit

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

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

Top-level deposit

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: 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

Amount, availability, and deposit decisions

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.
Title: You authorized a $506.00 bank debit“You authorized a 506.00debitfromChase••••6789topurchase506.00 debit from Chase ••••6789 to purchase 500.00 of USDC. 100.00willbedeliverednowand100.00 will be delivered now and 400.00 after the bank debit settles. Fee: $6.00. No action needed.”

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.00ofUSDCwasdeliveredtoyourwallet.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.00ofUSDCwasdeliveredtoyourwallet.Theremaining100.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.00ofUSDCwasdeliveredtoyourwallet.Your500.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.00ofUSDCwasdeliveredtoyourwallet,andyour500.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.00ofUSDCwasdeliveredtoyourwallet.Theremaining100.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. Never accuse the user of fraud or expose a return/risk reason.

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.
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.

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.”

Linked bank on-ramp

Build the complete backend and frontend flow.

ACH returns

Handle return classes and user access changes.

Webhooks

Verify, deduplicate, and reconcile deliveries.

Sandbox

Run deterministic scenarios and the documented contract-only cases before launch.