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

# USD Onramp

> Buy stablecoin with USD via Plaid-linked ACH Pull, with KYC completed through a Partner URL.

USD onramp lets your users convert USD into stablecoin, pulled directly from a Plaid-linked bank account. Every onramp flow follows the same four steps: **Create User → KYC → Bank Account Linkage → Transaction.**

<Info>
  Both USDT and USDC are supported for USD onramp.
</Info>

<Steps>
  <Step title="Create User">
    ```http theme={null}
    POST /api/v2/user
    ```

    ```json theme={null}
    {
      "email": "user@example.com",
      "phone": "+12025550123"
    }
    ```

    Response:

    ```json theme={null}
    {
      "success": true,
      "data": {
        "user_id": "550e8400-e29b-41d4-a716-446655440000"
      }
    }
    ```

    Save `user_id` — every subsequent call needs it as the `X-User-Id` header.
  </Step>

  <Step title="KYC">
    <Warning>
      USD supports **only** the KYC Partner URL method — there is no KYC Widget or KYC Sharing option for USD. The user must submit US ID documents only.
    </Warning>

    Request a Partner KYC URL:

    ```http theme={null}
    POST /api/v2/user/kyc/partner-url
    ```

    Headers: `X-User-Id: <user_id>`

    This call is **asynchronous**. The first response may come back pending:

    ```json theme={null}
    {
      "success": true,
      "data": {
        "status": "PENDING"
      }
    }
    ```

    Poll the same endpoint again after a few seconds until the status becomes `ACTIVE`, at which point the response includes the KYC URL:

    ```json theme={null}
    {
      "success": true,
      "data": {
        "status": "ACTIVE",
        "url": "https://withpersona.com/verify?..."
      }
    }
    ```

    Redirect the user to `url` to complete identity verification. See [Partner URL](/user/kyc/partner-url) for the full reference.

    To return the user to your app afterwards, append `redirect-uri` as the last query parameter on the returned Persona URL:

    ```text theme={null}
    &redirect-uri=<url-encoded-url>
    ```
  </Step>

  <Step title="Bank Account Linkage">
    USD bank accounts are linked through Plaid, using the Bank Account Widget:

    ```javascript theme={null}
    const sigString = clientId + timestamp + "sdk" + userId
    const secret = HmacSHA256(sigString, clientSecret).toString().toUpperCase()
    // redirect to: https://app.saber.money/bank/add?client_id=...&user_id=...&timestamp=...&secret=...
    ```

    The URL is valid for **10 minutes** from generation. The user selects and authorizes their bank account via Plaid on this page.

    To return the user to your app once they exit the Plaid flow, append `redirect_url` as the last query parameter:

    ```text theme={null}
    &redirect_url=<url-encoded-url>
    ```

    Once the user adds their bank account and it's successfully linked and approved, the client can fetch the list of approved bank accounts:

    ```http theme={null}
    GET /api/v2/user/bank_account?status=APPROVED
    ```

    <Warning>
      A user can have up to **10 active bank accounts** at any point in time.
    </Warning>

    See [Bank Account — Through Hosted Widget](/user/bank-account/widget) for setup details.
  </Step>

  <Step title="Transaction">
    Two variants:

    * **Pool Buy** — collected funds are converted to stablecoin and deposited into the pool wallet on the Saber system.
    * **External Buy** — collected funds are converted to stablecoin and sent to a specified wallet address on a given network.

    The flow is identical for both — the only difference is that External Buy requires `wallet_address` and `network` in the request. See [Available Networks and Tokens](/client/pool-account/supported-networks) for supported networks and per-network limits.

    <Note>
      The request body must also include `source_id` — the ID of the linked, approved bank account funds should be pulled from.
    </Note>

    Two kinds of quotes are available — a **Live Quote** (informational, executes at market rate) or a **Locked Quote** (rate held \~30-60s, supplied as `quote_id` in the buy request):

    Live quote:

    ```http theme={null}
    GET /api/v2/wallet/s/quote?from_currency=USD&to_currency=USDT&from_amount=100
    ```

    Locked quote:

    ```http theme={null}
    GET /api/v2/wallet/w/quote?from_currency=USD&to_currency=USDT&from_amount=100
    ```

    Save `id` from the locked-quote response as `quote_id`.

    <Tabs>
      <Tab title="Pool Buy">
        ```http theme={null}
        POST /api/v2/wallet/transaction/pool/buy
        ```

        ```json theme={null}
        {
          "source_id": "<bank_id>",
          "fiat_symbol": "USD",
          "crypto_symbol": "USDT",
          "fiat_amount": 100,
          "payment_method": "bank_transfer_ach_pull",
          "quote_id": "<quote_id>"
        }
        ```

        <Note>
          `quote_id` is only required when using a Locked Quote — omit it when using a Live Quote.
        </Note>
      </Tab>

      <Tab title="External Buy">
        ```json theme={null}
        {
          "source_id": "<bank_id>",
          "fiat_symbol": "USD",
          "crypto_symbol": "USDT",
          "fiat_amount": 100,
          "payment_method": "bank_transfer_ach_pull",
          "quote_id": "<quote_id>",
          "wallet_address": "0x318d2aae4c99c2e74f7b5949fa1c34df837789b8",
          "network": "BSC"
        }
        ```
      </Tab>
    </Tabs>
  </Step>
</Steps>

## Checking status

```http theme={null}
GET /api/v2/wallet/transaction?transaction_type={POOL_BUY|EXTERNAL_BUY}&transaction_id={id}
```

Status values mirror the offramp equivalents (`CREATED`, `PROCESSING`, `COMPLETED`, `FAILED`, `CANCELLED`, `REFUND_INITIATED`, `REFUND_COMPLETED`, etc.).

Poll until the status reaches a terminal state, or configure a webhook URL to receive completion pushes instead — see [Order Lifecycle and Webhooks](/onramp/operations/lifecycle).

## User limits and EDD

SSN is optional at KYC time, so limits are tiered based on whether an SSN was submitted:

| US Customers                     | 24-Hour Limit | 7-Day Limit |
| -------------------------------- | ------------- | ----------- |
| Individual customers without SSN | \$2,999.99    | \$15,000.00 |
| Individual customers with SSN    | \$5,000.00    | \$15,000.00 |

<Info>
  There is no EDD widget for USD. For higher limits, the client requests an EDD link via the same async Partner URL pattern as KYC (`POST` → poll until `status: ACTIVE` → receive `url`):

  * **If an SSN was already submitted** at KYC, the user is taken directly to a broader questionnaire with a Persona link, where they upload the relevant supporting documents.
  * **If no SSN was submitted**, the first EDD pass collects the SSN; the second pass then runs the actual EDD process (questionnaire and document upload).
</Info>

## Related pages

<CardGroup cols={2}>
  <Card title="Onramp Operations" icon="arrow-right-arrow-left" href="/onramp/available-flows" cta="View guide" arrow="true" />

  <Card title="User KYC — Partner URL" icon="id-card" href="/user/kyc/partner-url" cta="View guide" arrow="true" />

  <Card title="Bank Account — Hosted Widget" icon="building-columns" href="/user/bank-account/widget" cta="View guide" arrow="true" />

  <Card title="User Limits and EDD" icon="gauge" href="/user/limits-edd" cta="View guide" arrow="true" />
</CardGroup>


## Related topics

- [Available Flows](/onramp/available-flows.md)
- [Overview](/onramp/overview.md)
- [KYC Partner URL](/user/kyc/partner-url.md)
