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

# Linked bank on-ramp

> Onboard users to crypto by linking their bank account and pulling funds via ACH debit.

A linked bank on-ramp lets a user onboard to crypto straight from their bank account. They link the account once, and you pull funds via ACH debit and deliver crypto to a wallet. No wallet signature is needed; the authorization comes from the verified bank [funding source](/guides/definitions#funding-source).

The flow is a short server-side sequence with one client-side bank-linking step:

<Steps>
  <Step title="Create a link token">
    From your backend, create a Plaid link token for the user.

    ```bash theme={null}
    curl -X POST https://platform.spritz.finance/v1/bank-accounts/link-token \
      -H "Content-Type: application/json" \
      # plus your integrator signing headers and the user's Authorization (see Authentication)
      -d '{}'
    ```

    ```json theme={null}
    {
      "linkToken": "link-sandbox-...",
      "hostedLinkUrl": "https://...",
      "expiration": "2026-06-30T12:40:00.000Z",
      "requestId": "..."
    }
    ```

    Use `linkToken` with Plaid Link in your app, or send the user to `hostedLinkUrl`.
  </Step>

  <Step title="Complete linking">
    Plaid returns a public token and the selected account(s) on the client. Send them to your backend and complete the link.

    ```bash theme={null}
    curl -X POST https://platform.spritz.finance/v1/bank-accounts/link-complete \
      -H "Content-Type: application/json" \
      # plus your integrator signing headers and the user's Authorization
      -d '{
        "publicToken": "public-sandbox-...",
        "accountIds": ["..."],
        "institutionId": "ins_...",
        "institutionName": "..."
      }'
    ```
  </Step>

  <Step title="Find the funding source and check limits">
    Linking creates a funding source. Wait for one with `status: "active"`, then read its limits.

    ```bash theme={null}
    curl "https://platform.spritz.finance/v1/funding-sources/" \
      # plus your integrator signing headers and the user's Authorization
    ```

    A funding source moves through `pending`, `active`, `review_required`, `ineligible`, or `disabled`. Only `active` sources can be debited. Check what the user can deposit:

    ```bash theme={null}
    curl "https://platform.spritz.finance/v1/funding-sources/{fundingSourceId}/deposit-limits" \
      # plus your integrator signing headers and the user's Authorization
    ```

    ```json theme={null}
    {
      "minimumDepositAmountUsd": "1.00",
      "transactionLimitUsd": "5000.00",
      "dailyLimitUsd": "10000.00",
      "dailyRemainingUsd": "10000.00",
      "monthlyLimitUsd": "25000.00",
      "monthlyRemainingUsd": "25000.00"
    }
    ```
  </Step>

  <Step title="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.

    ```bash theme={null}
    curl -X POST https://platform.spritz.finance/v1/deposits/direct/prepare \
      -H "Content-Type: application/json" \
      # plus your integrator signing headers and the user's Authorization
      -d '{
        "sourceId": "{fundingSourceId}",
        "address": "YourUsersSolanaWallet",
        "network": "solana",
        "asset": "USDC",
        "quoteType": "exact_input",
        "amountUsd": "100.00",
        "priority": "normal"
      }'
    ```

    ```json theme={null}
    {
      "preparationId": "...",
      "kind": "...",
      "expiresAt": "2026-06-30T12:40:00.000Z",
      "message": "ACH authorization text to display to the user",
      "summary": { "...": "..." }
    }
    ```

    `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`.
  </Step>

  <Step title="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`.

    ```bash theme={null}
    curl -X POST https://platform.spritz.finance/v1/deposits/direct \
      -H "Content-Type: application/json" \
      # plus your integrator signing headers and the user's Authorization
      -d '{ "preparationId": "..." }'
    ```

    Spritz runs risk checks before any money moves. If a check blocks the deposit, the API returns `409` before pulling funds; prepare a new quote to retry, since a blocked attempt consumes its `preparationId`.
  </Step>

  <Step title="Track the deposit">
    The deposit response carries the full lifecycle. Track it by reading the deposit or by reacting to [webhooks](/guides/webhooks).

    ```json theme={null}
    {
      "id": "...",
      "status": "authorized",
      "principalAmountUsd": "100.00",
      "expectedAssetAmount": "100.00",
      "network": "solana",
      "asset": "USDC",
      "address": "YourUsersSolanaWallet",
      "debitStatus": "authorized",
      "releaseStatus": "not_started"
    }
    ```

    `status` moves through `authorized`, `processing`, `partially_released`, `completed`, `returned`, or `failed`. The ACH pull is tracked in `debitStatus` (`authorized`, `submitting`, `submitted`, `settled`, `returned`, `failed`) and the crypto delivery in `releaseStatus`.
  </Step>
</Steps>

## When a deposit is blocked

Spritz runs every deposit through eligibility, limit, and risk checks. A block returns an [error](/guides/errors) **before any money moves** — no ACH pull is attempted. Every deposit failure collapses onto a few problem `type`/`status` values, so branch on the `code` extension member: it is the discriminator.

Both `prepare` and `create` can return:

| `code`                                | Status | Meaning                                                                                                       | What to do                                                                                                                         |
| ------------------------------------- | ------ | ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `validation_error`                    | `400`  | The request or amount failed validation — below the minimum, over a limit, or malformed. `detail` says which. | Fix the input; re-read the funding source's deposit limits.                                                                        |
| `source_not_found`                    | `404`  | The `sourceId` doesn't exist or isn't this user's.                                                            | Re-list funding sources.                                                                                                           |
| `source_not_eligible`                 | `409`  | The funding source isn't `active`.                                                                            | Read its `status` and `statusReason` (see [Funding source status](#funding-source-status)), then relink or choose another account. |
| `ach_debit_program_halted`            | `409`  | ACH debit is paused platform-wide for your program.                                                           | Temporary — retry later.                                                                                                           |
| `ach_debit_new_user_paused`           | `409`  | ACH debit is paused for new users in your program.                                                            | Temporary — retry later, or once the user is established.                                                                          |
| `ach_debit_user_access_disabled`      | `409`  | This user's ACH debit access is disabled, usually after a return.                                             | Not reinstated automatically; contact Spritz.                                                                                      |
| `ach_debit_source_identity_ambiguous` | `409`  | The bank identity maps to more than one active funding source.                                                | Remove the duplicate source.                                                                                                       |
| `ach_debit_source_unsettled_limit`    | `409`  | The source already has an unsettled debit in flight.                                                          | Wait for it to settle, then retry.                                                                                                 |

`create` can additionally return:

| `code`                     | Status | Meaning                                                                                                     |
| -------------------------- | ------ | ----------------------------------------------------------------------------------------------------------- |
| `preparation_not_found`    | `404`  | Unknown `preparationId`.                                                                                    |
| `preparation_expired`      | `409`  | The preparation passed its `expiresAt`. Prepare a new quote.                                                |
| `preparation_already_used` | `409`  | The preparation was already committed. A blocked attempt also consumes it, so prepare a new quote to retry. |

### Risk decisions

At `create`, Spritz evaluates the deposit for ACH-return risk. Any outcome other than authorized returns `409` and **does not create the deposit**:

| `code`                        | Meaning                                                                                                                                         | What to do                                                 |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| `risk_review_required`        | The deposit needs manual review.                                                                                                                | Don't retry automatically; the user may be contacted.      |
| `risk_rejected`               | Risk evaluation declined the deposit.                                                                                                           | Don't retry the same source and amount.                    |
| `risk_rerouted`               | Risk evaluation declined this deposit for the instant on-ramp. **Despite the name, no alternative route is attempted — treat it as a decline.** | Try a smaller amount later, or a different funding source. |
| `risk_evaluation_unavailable` | Risk couldn't be evaluated — missing bank data or a provider error.                                                                             | Transient — prepare a new quote and retry shortly.         |

<Note>
  A block driven by customer-return risk can also **disable the funding source** (`status: "disabled"`, `statusReason: "risk_blocked"`). Once that happens, deposits against that source return `source_not_eligible` until the user links a different account.
</Note>

## Funding source status

A funding source moves through these states, and only `active` can be debited. When it isn't active, `statusReason` explains why.

| `status`          | Debitable | Typical `statusReason`                              |
| ----------------- | --------- | --------------------------------------------------- |
| `pending`         | No        | `null` — ownership and verification still resolving |
| `active`          | Yes       | `null`                                              |
| `review_required` | No        | `ownership_review_required`                         |
| `ineligible`      | No        | `ownership_mismatch`                                |
| `disabled`        | No        | `risk_blocked`, `returned`, or `manually_disabled`  |

`statusReason` takes one of:

| Value                       | Meaning                                                          |
| --------------------------- | ---------------------------------------------------------------- |
| `ownership_mismatch`        | The account holder's name didn't match the verified Spritz user. |
| `ownership_review_required` | Ownership couldn't be confirmed automatically and needs review.  |
| `user_not_verified`         | The user isn't verified for ACH debit.                           |
| `duplicate_bank_account`    | The same bank account is already linked.                         |
| `returned`                  | A prior debit on this source or user was returned.               |
| `risk_blocked`              | Risk evaluation disabled the source.                             |
| `manually_disabled`         | An operator disabled the source.                                 |

`ownershipMatchStatus` (`matched`, `mismatch`, `review_required`, or `null`) reports the raw name-match result, independent of `status`.

## Handling returns

ACH debits can be returned after the fact (for example, insufficient funds). When that happens, the deposit reports a `returnCode` and `returnReason`, and the `achDebitReturn.created` and `achDebitReturn.updated` [webhooks](/guides/webhooks) fire. See [ACH returns](/guides/ach-returns) for the return codes, exposure signals, and how to reconcile.

## Related

<CardGroup cols={2}>
  <Card title="On-ramp" icon="arrow-right-to-bracket" href="/guides/use-cases/on-ramp">
    Auto-converting deposit accounts for push funding.
  </Card>

  <Card title="Webhooks" icon="bell" href="/guides/webhooks">
    Track deposit status and ACH returns.
  </Card>
</CardGroup>
