Skip to main content
A linked bank on-ramp lets a user onboard to crypto straight from their bank account. They link the account once, authorize an ACH debit, and receive USDC at a wallet. No wallet signature is required; the authorization comes from the verified bank funding source. This is the canonical implementation guide. An integration is complete when its backend implements the flow and reconciliation below, and its frontend implements the ACH debit user experience.

Before you build

  • Create or connect the user as described in Onboarding.
  • Confirm GET /v1/integrator/ returns achDebitEnabled: true. If it is false, contact Spritz before building or testing ACH debit.
  • Wait until GET /v1/users/me shows a verified US user and the fiat_to_crypto / ach_debit capability as active. 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.*, and achDebit.* webhooks. Store Spritz resource IDs so every event can be reconciled by API read.
High priority is enabled only for reviewed integrations. Ask Spritz to enable it on your sandbox and production credentials. Use it only when 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 calls link-complete in the next step.
  • Plaid Hosted Link: when hostedLinkUrl is non-null, open it. Spritz receives Plaid’s completion asynchronously and exchanges the public token for you. Do not call link-complete for this mode. If it is null, use embedded/native Link with linkToken.
2

Finish the selected linking mode

3

Find the funding source and check limits

Linking creates a funding source. Wait for one with status: "active", then read its limits.
A funding source moves through pending, active, review_required, ineligible, disabled, or deleted. Only active sources can be debited. Check what the user can deposit:
Present two delivery choices, not three limits:
  • Standard: send priority: "normal" and validate the amount with normal.minAmountUsd and normal.maxAmountUsd.
  • Fastest available: offer this only when high.available is true, send priority: "high", and validate the amount with high.maxAmountUsd.
Do not show both maximums at once or label 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.57 principal + $7.43 fee = $750.00 bank debit.
  • Fastest available with $100.00 eligible for early release: $100.00 at 2% plus $641.58 at 1% produces an $8.42 fee, so $741.58 principal + $8.42 = $750.00 bank 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 summary and the ACH authorization message to the user. Once they authorize, commit the deposit with the preparationId.
Spritz rechecks the source, amount, and effective release availability before any money moves. A blocking check creates no deposit. A create attempt consumes its 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 problem type/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

At create, 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 set priority: "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 only active 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 a returnCode 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.

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.* by eventId and reject stale sequence values.
  • Returns disable the source in the UI and branch on userAction.
  • Every scenario in the sandbox test matrix passes before production.