The custom-form module renders an author-defined form as a step in the verification journey. The fields, labels, and copy are defined in your workflow template and delivered to the SDK; the module renders one control per field, collects the answers, uploads any files, and submits the step. On success it emits `checktiv.custom_form.submitted` so your page can advance.

Unlike the IDV module (which runs inside a sandboxed iframe), the custom-form module renders real form controls into your page. That means you must include its stylesheet.

## Register the module and its stylesheet

Import the module for its self-registration side effect, and import the required stylesheet so the controls are styled:

```js
import { init } from '@checktiv/sdk-web';
import '@checktiv/sdk-web/custom-form'; // registers the custom_form module
import '@checktiv/sdk-web/custom-form/style.css'; // required: styles the rendered controls
```

On the CDN script-tag path (`window.Checktiv`), the module is registered for you, and the stylesheet ships as a sibling file to `<link>` alongside the bundle.

## Mount it

The default path is `mountProvisioned`: when the server declares a `custom_form` step, the SDK renders it and injects the applicant-safe form config and copy for you.

```js
const client = init({ publishableKey: 'ah_pk_us_test_...', getSessionToken });

client.mountProvisioned({
  target: document.getElementById('verify-container'),
  onEvent: (event) => {
    if (event.type === 'checktiv.custom_form.submitted') {
      // The step was accepted. Advance to the next step.
      window.location.reload();
    }
  },
});
```

If you resolve the applicant-safe config yourself (for example, a host that renders the form on a step it already controls), mount the module explicitly and pass the config and copy:

```js
client.mount('custom_form', {
  target: document.getElementById('verify-container'),
  config, // the applicant-safe form config: { fields, supportedLocales }
  copy, // per-field and chrome strings, resolved to the applicant's locale
  onEvent: (event) => {
    if (event.type === 'checktiv.custom_form.submitted') {
      window.location.reload();
    }
  },
});
```

## Config and copy are injected

The module is locale-agnostic by design. It never reads `navigator.language` or bundles a translation runtime. Instead:

- **Config** (`{ fields, supportedLocales }`) is the applicant-safe form definition. Author-only fields (rules, notification targets) are stripped before they reach the browser, so nothing sensitive is rendered.
- **Copy** is a map of plain strings your host already resolved to the applicant's locale (for example, `field.label[locale]`). Absent keys fall back to a built-in English table, which is what a CDN script-tag customer gets.

Every author-supplied string renders as plain text, never as HTML, so author copy cannot inject markup into your page.

## The submitted event advances the journey

The module emits three events, all in the `checktiv.custom_form.*` namespace:

| Event type                       | Meaning                                                                                                                                                                       |
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `checktiv.custom_form.ready`     | The form config resolved and the fields rendered. The applicant can begin.                                                                                                    |
| `checktiv.custom_form.submitted` | The step submit succeeded. Advance the journey (for example, reload into the next step). This is terminal for the step, not a verdict: rule evaluation happens on the server. |
| `checktiv.custom_form.error`     | A failure (session unresolved, upload failed, submit failed). Carries an actionable error.                                                                                    |

Do not exhaustively switch on `event.type`. New event types may be added without a major version bump. See [Versioning](/developers/sdks/versioning).

## Content-only screens still advance

A screen with zero input fields (only content blocks, such as a disclosure to acknowledge) still renders a Continue control. Pressing it submits an empty answer set and advances the journey. This is intentional: without a Continue control, a content-only step would never complete and the workflow would wait forever.

## File fields

When a field accepts a file, the module uploads the file from the browser directly to storage before it submits the answers, so file bytes never pass through your server. If an upload fails, the module emits `checktiv.custom_form.error` with `upload_failed`; the applicant can retry.

## Related pages

- [Modules overview](/developers/sdks/modules) - the full public surface and the self-registration model
- [Quickstart](/developers/sdks/quickstart) - end-to-end integration walkthrough
- [Error reference](/developers/sdks/error-reference) - every error code and its recovery step
- [Verdict and webhooks](/developers/sdks/verdict-and-webhooks) - the authoritative outcome arrives on your server