Before you build
- Create or connect the user as described in Onboarding.
- Confirm
GET /v1/integrator/returnsachDebitEnabled: true. If it is false, contact Spritz before building or testing ACH debit. - Wait until
GET /v1/users/meshows a verified US user and thefiat_to_crypto/ach_debitcapability asactive. ACH debit has no separate terms requirement for an enabled integrator. - Keep the integrator key, integrator secret, and user API key on your backend. Never expose them in a browser or mobile app.
- Let the client run Plaid Link and render authorization/UI. Send Plaid’s result to your backend, which calls Spritz with HMAC and the user’s authorization.
- Subscribe once per integrator to
onramp.*,achDebitReturn.*, andachDebit.*webhooks. Store Spritz resource IDs so every event can be reconciled by API read.
limitsByPriority.high.available
is true; normal priority is the complete default implementation.
The flow is a short server-side sequence with one client-side bank-linking step:
1
Create a link token
From your backend, create a Plaid link token for the user.Spritz requests Hosted Link for every link token. Choose exactly one completion
mode from the returned values:
- Embedded/native Plaid Link: initialize Plaid Link with
linkToken. Your app receives the public token and selected account IDs, then your backend callslink-completein the next step. - Plaid Hosted Link: when
hostedLinkUrlis non-null, open it. Spritz receives Plaid’s completion asynchronously and exchanges the public token for you. Do not calllink-completefor this mode. If it is null, use embedded/native Link withlinkToken.
2
Finish the selected linking mode
- Embedded/native Link
- Hosted Link
Plaid returns a public token and the selected account IDs to your client. Send
them to your backend, then complete the link once:
3
Find the funding source and check limits
Linking creates a funding source. Wait for one with A funding source moves through Present two delivery choices, not three limits:
status: "active", then read its limits.pending, active, review_required, ineligible,
disabled, or deleted. Only active sources can be debited. Check what the user
can deposit:- Standard: send
priority: "normal"and validate the amount withnormal.minAmountUsdandnormal.maxAmountUsd. - Fastest available: offer this only when
high.availableis true, sendpriority: "high", and validate the amount withhigh.maxAmountUsd.
high.maxAmountUsd as an “instant
limit.” It is the fee-adjusted maximum deposit for the Fastest available choice.
high.maxEarlyReleaseAmountUsd is only the largest portion that may release
before settlement; the user does not allocate that portion. The prepare response
supplies the exact early/later split to display before authorization.With quoteType: "exact_output", each maxAmountUsd is the USDC principal the
user can receive, excluding fees. Spritz has already reduced it enough for the
principal plus fee to fit the applicable bank-debit limit. For example, with a
$750.00 total-debit ceiling and no plan adjustment or subsidy:- Standard:
$742.57principal +$7.43fee =$750.00bank debit. - Fastest available with
$100.00eligible for early release:$100.00at 2% plus$641.58at 1% produces an$8.42fee, so$741.58principal +$8.42=$750.00bank debit.
maxAmountUsd is a principal ceiling for both quote types. With
quoteType: "exact_input", request amountUsd is instead the maximum total bank
debit, including fees, so do not compare it directly with maxAmountUsd. Send the
user’s total-debit budget to prepare; Spritz chooses the largest principal that
fits it and returns the authoritative principalAmountUsd, userFeeUsd, and
totalDebitAmountUsd in summary. Use exact_output when your amount picker needs
to validate the requested principal directly against maxAmountUsd.When available is false, branch on reason. suggestedAction is auto_ramp,
wait_for_settlement, or null. clearsAt is the earliest time the current reason
can stop applying; clearsAtIsEstimate: true means it is only a forecast. Re-fetch
before prepare and again after a blocked attempt. Never hardcode limits or infer the
policy that produced them.The response also contains legacy daily/monthly fields. They are deprecated; build
new UI from limitsByPriority.4
Prepare the deposit
Prepare a quote. This returns the ACH authorization text to show the user, along with a
preparationId you’ll use to commit.quoteType is exact_input (you specify the USD debited) or exact_output (you specify the crypto delivered). network supports solana, ethereum, polygon, base, avalanche, and arbitrum; asset is USDC.Map Standard to priority: "normal" and Fastest available to
priority: "high". For Fastest available, Spritz decides the split:
instantPortionUsd may release before settlement and settlementPortionUsd waits
for settlement. Show those two amounts and timings from this response. Do not ask
the user to choose the portions themselves.5
Show the authorization and create the deposit
Display the quote Spritz rechecks the source, amount, and effective release availability before any
money moves. A blocking check creates no deposit. A create attempt consumes its
summary and the ACH authorization message to the user. Once they authorize, commit the deposit with the preparationId.preparationId; prepare a new authorization for a new attempt. Reuse the same
Idempotency-Key and body only to recover from a timeout or lost response.A high-priority request can instead be reduced or downgraded without failing. The
create response then preserves requestedPriority: "high" while priority,
release portions, timing, and fees describe what will actually happen. Render the
create response again before showing confirmation; do not rely on the earlier
preparation summary.6
Track the deposit
The deposit response carries the full lifecycle. Track it by reading the deposit or by reacting to webhooks.
status is authorized, processing, partially_released, completed, failed,
refunded, or returned. The ACH pull is tracked independently in debitStatus
(authorized, submitting, submitted, settled, returned, failed). Crypto is
tracked in releaseStatus (not_started, queued, partial, completed,
failed).releasedAmountUsd includes submitted releases. Tell the user crypto was delivered
only as confirmedReleasedAmountUsd increases. Use payoutTxHash as blockchain
proof when present. For early_partial, the instant portion can confirm while the
settlement portion remains pending.A settled ACH debit can later become returned, even when the aggregate deposit previously showed completed, failed, or refunded because of its crypto-release path. Keep processing deposit and achDebitReturn.* webhook updates. Use debitStatus and releaseStatus to explain intermediate states.Reconcile after an outage by paging through GET /v1/deposits/. The list is
user-scoped, so an integrator-wide recovery must iterate your own user roster and
authorize each user’s read. Webhooks are notifications, not the only record of
deposits.When linking is blocked
link-complete can return a 409 after Plaid succeeds. Branch on code; retrying the
same account cannot clear these conflicts.
A
503 with PLAID_LINK_FAILED, PLAID_SANDBOX_LINK_UNAVAILABLE, or
PLAID_IDENTITY_UNAVAILABLE is different: the provider flow did not complete. Keep the
user on linking and allow a bounded retry or a different bank.
If a user deliberately unlinks a healthy account, its funding source becomes deleted.
Relinking the same account restores the original bank-account and funding-source IDs to
active; update the existing local payment method instead of inserting a duplicate.
Relinking does not clear a return or risk block: those attempts follow the blocked rows
above.
When a deposit is blocked
Spritz runs every deposit through eligibility, limit, and risk checks. A block returns an error before any money moves — no ACH pull is attempted. Most deposit failures collapse onto a few problemtype/status values, so branch on the
code extension when it is present.
A code-less 409 can occur when another create request for the same user is still in
progress. Do not assume whether the first request succeeded. Wait briefly, reconcile
with the same Idempotency-Key and GET /v1/deposits/, then retry the same body with
that key at most once. Do not create a new deposit intent until reconciliation is clear.
Both prepare and create can return:
Integration-level errors can also block prepare or create:
create can additionally return:
Deposit decisions
Atcreate, Spritz evaluates the deposit for ACH-return risk. These blocking outcomes
return 409 and do not create the deposit:
A decision can also change the funding source. Always re-read it after a blocked
create.
disabled / permanent: true requires another bank or support. ineligible
/ rerouted can recover at availableAt. Relinking the same account does not bypass
either state. Do not show the raw risk code or reason to the user.Instant release adjustments
High priority is a request, not a guarantee. The create response can reduce the instant portion or setpriority: "normal" without failing the deposit. The rest is released
after ACH settlement. Do not infer or expose the control that produced the adjustment;
render the returned result:
Always render timing, portions,
userFeeUsd, and totalDebitAmountUsd from the create
response. A downgrade is a successful deposit, not an error. Continue tracking it and
show standard settlement timing; do not retry solely to recover instant release.
Funding source status
A funding source moves through these states, and onlyactive can be debited. When it isn’t active, statusReason explains why.
statusReason takes one of:
Two further fields say what happens next:
Handling returns
ACH debits can be returned after the fact (for example, insufficient funds). When that happens, the deposit reports areturnCode and returnReason, the funding source becomes permanently disabled, and the achDebitReturn.created and achDebitReturn.updated webhooks fire. A cached funding-source read can briefly lag the returned deposit, so use either return webhook as the trigger for bounded GET /v1/funding-sources/:sourceId polling. Branch on the return record’s userAction: none can offer another bank, while review_required, restricted, and disabled must show the support path and stop new ACH debit attempts. See ACH returns for the exact frontend contract and Sandbox for deterministic return scenarios.
Related
ACH debit user experience
Implement every UI, email, and push state.
Sandbox
Run deterministic scenarios and the documented contract-only cases.
On-ramp
Auto-converting deposit accounts for push funding.
Webhooks
Track deposit status and ACH returns.
Definition of complete
- Backend creates/connects users, waits for active ACH debit capability, and keeps all secrets server-side.
- Embedded Link calls
link-complete; Hosted Link waits for Spritz’s asynchronous completion. Both store stable bank/source IDs. - Frontend handles every funding-source status and every link conflict above.
- Amount entry is driven by fresh
limitsByPriority; no limits are hardcoded. - Authorization displays the exact server-provided message and create-response summary.
- Deposit creation uses a persisted
Idempotency-Key. - Normal, partial-instant, full-instant, and high-to-normal results render correctly when enabled.
- Bank debit and crypto delivery are tracked as separate state machines.
- Resource webhook handlers verify signatures, fetch current state, and reconcile after outages; push handlers atomically deduplicate
achDebit.*byeventIdand reject stalesequencevalues. - Returns disable the source in the UI and branch on
userAction. - Every scenario in the sandbox test matrix passes before production.