> ## Documentation Index
> Fetch the complete documentation index at: https://rain-sandbox-trial.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Capturing End User Acceptance of Terms

> How Rain tracks a user's terms acceptance in the application flow, and how to embed the terms page in your own onboarding UI.

End users must accept your program's terms before they can transact. Rain collects and records that acceptance, starting with card terms, and an application is only `approved` once the user has both passed identity verification and accepted the terms. Acceptance is on by default for new programs; existing programs are migrating to it over time.

## Prerequisites

As part of program setup, you'll complete the terms intake form with your Implementation Manager, it defines the terms your users accept. Terms acceptance works in both sandbox and production.

If you plan to embed the terms page in your own onboarding UI instead of using Rain's hosted flow, also tell your Implementation Manager which domains will embed the page. Embedding is generally available on Enterprise plans, your Implementation Manager can confirm availability for your program.

This guide covers new implementations. If your program predates terms acceptance, your Implementation Manager will coordinate the migration with you.

## How acceptance affects application status

Terms acceptance adds one application status: `tosNotAccepted`. It means the user passed identity verification but hasn't accepted the terms yet, acceptance is the only thing between them and approval.

* You'll see `tosNotAccepted` in user application responses and in [`user.updated`](/changelog/webhooks/v1/user/updated) webhooks, alongside an `applicationCompletionLink` the user can follow to finish.
* Once the user accepts, Rain records the acceptance and refreshes the application status. Processing can take up to a minute. You'll receive an `approved` webhook when it completes, and the usual approval side effects run, so you can proceed as you do today.
* On programs migrating to terms acceptance, users who were already `approved` currently keep their status. This may change in the future; Rain will notify you before shipping any breaking change.

See [Application States](/docs/application-states#tosnotaccepted) for the full status reference.

## Standard Compliance

Nothing to integrate: under [Standard Compliance](/docs/hosted-flow#standard-compliance), Rain's hosted flow presents the terms step after identity verification, and the application transitions to `approved` shortly after the user accepts.

```mermaid theme={null}
sequenceDiagram
    participant User
    participant App as Your app
    participant Rain

    App->>Rain: Create application
    Rain-->>App: applicationStatus + applicationCompletionLink
    App->>User: Redirect to hosted flow
    User->>Rain: Complete identity verification (Sumsub)
    Rain-->>App: user.updated webhook: tosNotAccepted
    User->>Rain: Accept terms (hosted flow)
    Rain-->>App: user.updated webhook: approved
```

## Embed the terms page in your own flow

If you host your own onboarding UI under [Hybrid Compliance](/docs/hosted-flow#hybrid-compliance), embed the terms page instead of redirecting to the hosted flow. The embedded page renders the terms and checkboxes; your page owns everything around them, including the submit button. The two talk over `postMessage`.

Embedding must be enabled for your program before the page will render, see [Prerequisites](#prerequisites).

<Steps>
  <Step title="Derive the terms URL">
    While a user's application status is `tosNotAccepted`, user responses and webhooks include an `applicationCompletionLink` whose `url` ends in `/kyc`, Rain's hosted flow. The embeddable terms page lives at `/kyc/terms` on the same host. Replace the path and forward all `params` as query parameters:

    ```ts theme={null}
    function buildTermsUrl({ url, params }: { url: string; params: Record<string, string | undefined> }) {
      const termsUrl = new URL(url.replace(/\/kyc$/, "/kyc/terms"));
      for (const [key, value] of Object.entries(params)) {
        if (value != null) termsUrl.searchParams.set(key, value);
      }
      return termsUrl;
    }

    // `user` comes from the users API or a user.updated webhook payload.
    const termsUrl = buildTermsUrl(user.applicationCompletionLink);
    ```

    Forward every param the link gives you, unchanged. Two of them carry the request:

    | Parameter   | Description                                                  |
    | ----------- | ------------------------------------------------------------ |
    | `userId`    | The user accepting the terms                                 |
    | `signature` | A Rain-issued token authorizing the terms page for that user |

    The link only exists while the application is in a non-approved state, so derive it when you render your terms step, not ahead of time.

    <Warning>
      Pass `signature` through exactly as Rain issues it. Rain mints it per user; you can't construct it, and it isn't interchangeable between users. A URL built from a bare `userId` works today, but Rain is rolling out signature verification on the terms page, so forward the signature now and nothing breaks when it turns on.
    </Warning>
  </Step>

  <Step title="Embed and style the iframe">
    Add styling parameters to the derived URL so the embedded terms match your UI, then render it in an iframe:

    | Parameter         | Description                                              |
    | ----------------- | -------------------------------------------------------- |
    | `color`           | Text and checkbox accent color (any CSS color)           |
    | `backgroundColor` | Page background (`transparent` works well for embedding) |
    | `fontFamily`      | A Google Fonts family name, for example `Inter`          |
    | `fontSize`        | Terms text size, for example `14px`                      |
    | `fontWeight`      | Terms text weight, for example `400`                     |

    ```html theme={null}
    <iframe id="rain-terms" width="100%"></iframe>
    ```

    ```ts theme={null}
    const iframe = document.querySelector<HTMLIFrameElement>("#rain-terms");

    termsUrl.searchParams.set("backgroundColor", "transparent");
    termsUrl.searchParams.set("color", "#111111");
    termsUrl.searchParams.set("fontFamily", "Inter");

    if (iframe) iframe.src = termsUrl.toString();
    ```

    `URLSearchParams` URL-encodes values like `#111111` for you; encode them yourself (`%23111111`) only if you build the URL by hand.
  </Step>

  <Step title="Track progress with lifecycle messages">
    The terms page reports where the user is in the acceptance process by posting a `rain.kyc.terms.lifecycle` message to your page on every state transition:

    ```json theme={null}
    { "type": "rain.kyc.terms.lifecycle", "data": { "state": "ready" } }
    ```

    | State        | Meaning                                                              | What you should do                   |
    | ------------ | -------------------------------------------------------------------- | ------------------------------------ |
    | `loading`    | The terms are loading                                                | Show a loading state                 |
    | `reviewing`  | The terms are displayed; the user hasn't checked every required box  | Keep your submit button disabled     |
    | `ready`      | The user checked every required box                                  | Enable your submit button            |
    | `submitting` | Acceptance was dispatched and is being recorded                      | Show a spinner; disable resubmission |
    | `completed`  | Acceptance is recorded and the application status has been refreshed | Advance your flow                    |
    | `error`      | Something went wrong, `data.error` says what                         | See [Handle errors](#handle-errors)  |

    Only trust messages that come from the terms page: check the origin, and that the message was sent by your embedded iframe.

    ```ts theme={null}
    const termsOrigin = termsUrl.origin;

    window.addEventListener("message", (event) => {
      if (event.origin !== termsOrigin) return;
      if (!iframe || event.source !== iframe.contentWindow) return;
      if (event.data?.type !== "rain.kyc.terms.lifecycle") return;

      const { state, error } = event.data.data;
      updateSubmitButton(state); // for example, enable on "ready", spinner on "submitting"
    });
    ```
  </Step>

  <Step title="Submit acceptance">
    When the user clicks your submit button, post a `rain.kyc.terms.submit` message into the iframe. The page dispatches the acceptance and reports progress through the lifecycle messages above.

    ```ts theme={null}
    if (iframe?.contentWindow) {
      iframe.contentWindow.postMessage({ type: "rain.kyc.terms.submit" }, termsOrigin);
    }
    ```

    Recording an acceptance can take up to a minute, so keep your spinner tied to the `submitting` state rather than a fixed delay. If you add your own stall timeout, don't resubmit automatically, refetch the user's application status to reconcile before letting the user try again.
  </Step>

  <Step title="Handle completion">
    When you receive `completed`, the acceptance is recorded and Rain has refreshed the user's application status. Confirm the transition the same way you track every other status change, via the [`user.updated`](/changelog/webhooks/v1/user/updated) webhook or by refetching the user, and continue your flow once the application is `approved`, for example by issuing a card.
  </Step>
</Steps>

### Handle errors

When the lifecycle state is `error`, `data.error` tells you what happened:

| Error                  | Meaning                                                        | Recovery                                                                                                 |
| ---------------------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `load_failed`          | The terms failed to load                                       | The embedded page shows its own retry control; when the user retries, the lifecycle returns to `loading` |
| `acceptance_failed`    | The acceptance submission failed                               | Re-enable your submit button so the user can try again                                                   |
| `acceptance_not_ready` | Submission was requested before every required box was checked | Keep your submit button disabled until you receive `ready`                                               |

Errors are recoverable: after an `error`, the next lifecycle message tells you where the user is.

### Test in sandbox

The embedded flow works in both sandbox and production. To simulate the status itself, use the same sandbox fixture as other application statuses: give the user a last name containing `tosnotaccepted` (case-insensitive), such as `TestTosNotAccepted`, see [testing application statuses](/docs/signing-up-a-customer) for how fixtures work.

## What's next

<CardGroup cols={2}>
  <Card title="Application states" icon="list-check" href="/docs/application-states">
    What each application state means and what to do about it.
  </Card>

  <Card title="Signing up a customer" icon="user-plus" href="/docs/signing-up-a-customer">
    Create and manage applications through the API.
  </Card>

  <Card title="user.updated webhook" icon="webhook" href="/changelog/webhooks/v1/user/updated">
    Full payload reference for the user.updated event.
  </Card>

  <Card title="Webhooks overview" icon="bell" href="/docs/webhooks">
    Track application progress in real time.
  </Card>
</CardGroup>
