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

# Onboarding customers

> Create a user, verify their identity, and read their capabilities. Or connect an existing Spritz user.

Every Spritz user belongs to your integration. Onboarding has three beats: create the
user, verify their identity, and read their capabilities to see what they can do.

If the person already has a Spritz account, with you or with another integrator, you
don't create a new user. You [connect to them](#connect-an-existing-spritz-user)
instead, with their consent.

## 1. Create a user

Create a user with their email address. Spritz returns a user API key (prefixed `ak_`)
that you use to authenticate requests on that user's behalf.

```bash theme={null}
curl -X POST https://platform.spritz.finance/v1/integrator/users \
  -H "Content-Type: application/json" \
  # plus your integrator signing headers (see Authentication)
  -d '{ "email": "user@example.com" }'
```

```json theme={null}
{
  "userId": "63d12d3b577fab6c6382136e",
  "email": "user@example.com",
  "apiKey": "ak_..."
}
```

Store the `apiKey` securely. You send it as the `Authorization` bearer for every request
you make on this user's behalf (see [Authentication](/guides/authentication)). You can
also pass an optional `timezone`.

## 2. Verify identity

Before a user can move money, they complete identity verification (KYC). Start a
verification session for the user:

```bash theme={null}
# Add HMAC headers and the user's Authorization as documented in Authentication.
# This request has no body, so its HMAC body hash is the empty string.
curl -X POST https://platform.spritz.finance/v1/users/me/verification-sessions/
```

```json theme={null}
{
  "sessionId": "inq_2Q3x7k9m1n",
  "provider": "persona",
  "sessionToken": "...",
  "verificationUrl": "https://verify.spritz.finance/...",
  "verificationUrlExpiresAt": "2026-06-30T12:34:56.000Z"
}
```

The response selects the provider for this session. Do not hard-code one provider:
`provider` is either `persona` or `plaid`, and both `sessionToken` and
`verificationUrl` are nullable.

Use this dispatch contract:

| Response                                            | Integration action                                                                                                                                                                                                  |
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `provider: "persona"` and `sessionToken` is present | Embed the Persona SDK. Pass `sessionId` as the inquiry ID and `sessionToken` as the session token.                                                                                                                  |
| `provider: "plaid"` and `sessionToken` is present   | Open Plaid Link and pass `sessionToken` as its `token`. Treat `sessionId` as an opaque verification-session ID.                                                                                                     |
| `verificationUrl` is present                        | Hosted fallback for either provider: open the returned URL in a browser tab or mobile web view. Do not construct a provider URL yourself.                                                                           |
| Both `sessionToken` and `verificationUrl` are null  | Do not open an SDK. Show “We couldn’t start identity verification. Try again.” with **Try again**, which creates a fresh session. If a fresh request has the same result, use your app's **Contact support** route. |

Prefer the provider SDK when you support it and a token is present; otherwise use the
hosted URL. A non-null `verificationUrlExpiresAt` is authoritative. If that time has
passed, create another session instead of opening the old URL. When it is null, still
request sessions just in time and never persist a URL as a permanent verification link.

Persona SDK references: [Web embedded flow](https://docs.withpersona.com/quickstart-embedded-flow),
[React Native](https://docs.withpersona.com/embedded-flow/react-native) and
[native mobile](https://docs.withpersona.com/embedded-flow/mobile-sdk). Plaid sessions
use the same Plaid Link client integration described by Plaid's SDK documentation; the
token comes from this verification-session response, not from the bank-account
link-token endpoint.

Creating a session can also return `409`. Branch on its stable `code`, never its
`title` or `detail`:

| `code`                             | Message                                                    | CTA                                                                                                       |
| ---------------------------------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `VERIFICATION_UNDER_REVIEW`        | “Your identity verification is under review.”              | **Check again** — re-read `GET /v1/users/me`; do not create another session while it remains under review |
| `VERIFICATION_SESSION_UNAVAILABLE` | “Identity verification can’t be started for this account.” | **Contact support** using the support route inside your app                                               |

The user's verification state is reported on their profile as `verification.status`:
`not_started`, `verified`, `failed`, `retry`, or `disabled`. A `retry` status means
they can try again; create a new session to do so.

## 3. Read capabilities

`GET /v1/users/me` returns the user's verification status and their **capabilities**.
Capabilities are the source of truth for what a user can do right now, and what's
blocking anything they can't.

```json theme={null}
{
  "id": "63d12d3b577fab6c6382136e",
  "email": "user@example.com",
  "verification": { "status": "verified", "country": "US" },
  "capabilities": [
    {
      "product": "crypto_to_fiat",
      "method": "ach_credit",
      "name": "ACH Bank Transfer",
      "description": "Pay out to a US bank account via ACH",
      "status": "active",
      "requirements": []
    },
    {
      "product": "fiat_to_crypto",
      "method": "ach_debit",
      "name": "ACH Debit",
      "status": "active",
      "requirements": []
    }
  ]
}
```

For an ACH-debit-enabled integrator, a verified US user receives the active
`fiat_to_crypto` / `ach_debit` capability with no separate terms requirement. Other
products and regions can still return `requirements_needed`; always drive the UI from
the actual capability object instead of assuming every method has the same gates.

Select ACH debit by the exact pair, not by array position or display name:

```ts theme={null}
const achDebit = user.capabilities.find(
  (capability) =>
    capability.product === "fiat_to_crypto" &&
    capability.method === "ach_debit",
);
```

`method` is optional in the generic capability schema because non-ramp products do not
have a transfer method. A missing `method` does not match ACH debit. If no capability
has the exact pair above, treat ACH debit as `not_available`: do not start bank linking,
and offer another supported deposit method.

Each capability is a **product**, and for ramps a **method**:

* **Products**: `fiat_to_crypto`, `crypto_to_fiat`, `bill_pay`, `crypto_card`
* **Methods**: `ach_credit`, `ach_debit`, `wire`, `sepa_credit_transfer`, `rtp`,
  `push_to_card`, `canadian_eft`

Its `status` tells you where it stands:

| Status                | Meaning                                               |
| --------------------- | ----------------------------------------------------- |
| `active`              | Ready to use now                                      |
| `requirements_needed` | The user has something to complete first              |
| `pending`             | In progress, for example verification under review    |
| `not_available`       | Not offered for this user (for example, their region) |

For the ACH debit entry point, use this minimum copy and action contract:

| State                                            | Message                                                                                      | CTA                                                                   |
| ------------------------------------------------ | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| Verification `not_started`                       | “Verify your identity to use ACH bank deposits.”                                             | **Verify identity** — create a verification session                   |
| Verification `retry`                             | “We couldn’t verify your identity. Try again to continue.”                                   | **Verify again** — create a new session                               |
| Verification `failed`                            | “We couldn’t verify your identity.”                                                          | **Contact support** in your app                                       |
| Verification `disabled`                          | “Identity verification is unavailable for this account.”                                     | **Contact support** in your app                                       |
| Verification `verified`, ACH capability `active` | No blocking message.                                                                         | Continue to bank linking                                              |
| ACH capability `requirements_needed`             | Show the selected requirement's `description`, or the type fallback below when it is absent. | Follow the selected requirement action below, then re-fetch the user. |
| ACH capability `pending`                         | “Your ACH bank deposit access is being reviewed.”                                            | **Check again**; do not start Plaid Link                              |
| ACH capability `not_available`                   | “ACH bank deposits aren’t available for this account.”                                       | Offer another supported deposit method                                |

“Contact support” means the integrator's configured in-app support route. Its support
team escalates to Spritz; do not invent a direct Spritz operations URL.

When a capability is `requirements_needed`, its `requirements[]` lists what to do.
`type` and `status` are always present. `description`, `actionUrl`, `retryable`, and the
capability's `nextRequirement` are optional by contract.

Choose the requirement whose `type` equals `nextRequirement` when that field is present
and still incomplete. Otherwise choose the first requirement whose `status` is not
`completed`. If there is no such requirement, re-fetch the user once. If the capability
still says `requirements_needed` with no incomplete requirement, show “We couldn’t load
the next verification step.” with **Contact support**; do not guess a step.

Use a returned `description` when present. Otherwise use this fallback and action
matrix:

| Requirement type          | Fallback message                                      | Action when `actionUrl` is absent                                                                                                             |
| ------------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `identity_verification`   | “Verify your identity to continue.”                   | Create a verification session as described in step 2.                                                                                         |
| `terms_acceptance`        | “Accept the required terms to continue.”              | The hosted terms flow is required to produce an agreement ID. Show **Contact support**; do not fabricate an agreement or mark terms accepted. |
| `additional_verification` | “Additional verification is required.”                | If `status` is `pending`, show **Check again**. Otherwise show **Contact support**.                                                           |
| `document_submission`     | “Supporting documents are required.”                  | If `status` is `pending`, show **Check again**. Otherwise show **Contact support**.                                                           |
| `regional_compliance`     | “Additional information is required for your region.” | Render the regional compliance form and submit it to `POST /v1/users/me/compliance`; see [EEA onboarding](/guides/eea-onboarding).            |
| `region_restriction`      | “This product isn’t available in your region.”        | No CTA clears it. Offer another supported product.                                                                                            |

When `actionUrl` is present, open it for every non-terminal requirement, then re-fetch
the user after the flow returns. A missing `retryable` is not permission to retry a
failed provider action automatically. Only retry when it is `true` or when the explicit
type/status handling above gives the user a retry action.

### Drive your UI from capabilities

Render your onboarding and product UI off the capabilities list rather than tracking
state yourself:

* Show a capability as available when its `status` is `active`.
* When it's `requirements_needed`, select and render the requirement using the fallback
  rules above. Never assume its optional presentation or action fields are present.
* After the user completes a requirement, re-fetch `GET /v1/users/me`, or react to the
  `capabilities.updated` [webhook](/guides/webhooks) so you update without polling.

## Connect an existing Spritz user

If the person already has a Spritz account, with you or with another integrator, use
**Integrator Connect** to gain access with their consent instead of creating a new
user. It's an OAuth-style authorization flow.

<Steps>
  <Step title="Create a connect session">
    From your backend, create a session with a pre-registered HTTPS redirect URI and an
    optional `state` value you can use to tie the result back to a request.

    ```bash theme={null}
    curl -X POST https://platform.spritz.finance/v1/integrator/connect/sessions \
      -H "Content-Type: application/json" \
      # plus your integrator signing headers (see Authentication)
      -d '{
        "redirectUri": "https://app.example.com/spritz/callback",
        "state": "opaque-value"
      }'
    ```

    ```json theme={null}
    {
      "sessionId": "...",
      "authorizationUrl": "https://app.spritz.finance/connect?session_id=...",
      "expiresAt": "2026-06-30T12:40:00.000Z"
    }
    ```
  </Step>

  <Step title="Send the user to approve">
    Redirect the user to `authorizationUrl`. They sign in to Spritz, review your
    integration, and approve. The session is valid for about 10 minutes.
  </Step>

  <Step title="Receive the authorization code">
    On approval, Spritz redirects the user back to your `redirectUri` with a `code` and
    the `state` you sent.
  </Step>

  <Step title="Exchange the code for a user key">
    From your backend, exchange the code for that user's API key. The code is
    single-use and expires after a few minutes.

    ```bash theme={null}
    curl -X POST https://platform.spritz.finance/v1/integrator/connect/token \
      -H "Content-Type: application/json" \
      # plus your integrator signing headers
      -d '{ "code": "..." }'
    ```

    ```json theme={null}
    {
      "apiKey": "ak_...",
      "userId": "63d12d3b577fab6c6382136e",
      "email": "user@example.com",
      "grantId": "..."
    }
    ```
  </Step>
</Steps>

You now hold a user API key for that user and can act on their behalf, exactly like a
user you created.

## Next

<CardGroup cols={2}>
  <Card title="Authentication" icon="key" href="/guides/authentication">
    Use the user API key to sign requests.
  </Card>

  <Card title="What we support" icon="globe" href="/guides/supported">
    Rails, tokens, and regions behind each capability.
  </Card>

  <Card title="Off-ramp" icon="arrow-right-from-bracket" href="/guides/use-cases/off-ramp">
    Put an active capability to work.
  </Card>

  <Card title="Webhooks" icon="bell" href="/guides/webhooks">
    React to capabilities.updated and verification changes.
  </Card>
</CardGroup>
