# Testing KBA with the Sandbox (/docs/guides/tutorials/testing-kba-with-the-sandbox)




Knowledge-based authentication (KBA) is hard to test. Every attempt is a paid lookup against a third-party identity provider, the questions are generated from real public records, and they change every session. None of that is something you can assert on in CI.

The **KBA sandbox** replaces the identity provider with a deterministic stand-in, for organizations you set aside for testing. Nothing else changes: you call the same [Verify Signer](/docs/reference/Rest-API/api-docs/recipients/verifySigner) endpoint, the recipient moves through the same auth states, and failures produce the same errors and webhooks they would in production.

You choose the outcome per request with `sandbox_test_type`:

<Cards>
  <InlineCard icon="<CircleCheck />" title="pass">
    KBA completes immediately. No questions are asked.
  </InlineCard>

  <InlineCard icon="<CircleX />" title="fail">
    KBA fails immediately, as if the identity provider couldn't find the person.
  </InlineCard>

  <InlineCard icon="<ListChecks />" title="questions (default)">
    Four questions with known answers, graded by the sandbox.
  </InlineCard>
</Cards>

***

## How a request is routed [#how-a-request-is-routed]

A KBA attempt runs in the sandbox only when **both** of these are true:

1. The organization has the `kba_sandbox` entitlement.
2. The signer submits the sandbox identity.

`sandbox_test_type` on the same request then picks the scenario. Everything else is rejected before anything is sent to the identity provider:

<figure className="my-8">
  <svg viewBox="0 0 840 384" width="100%" role="img" aria-label="Routing for the KBA identity step. Without the kba_sandbox entitlement, the request goes to the real identity provider, unless sandbox_test_type is sent, which returns a 400. With the entitlement, the signer must submit the sandbox identity, otherwise a 400; with it, the sandbox runs the pass, fail, or questions scenario.">
    <defs>
      <marker id="kba-sandbox-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
        <path d="M0 0 L10 5 L0 10 z" style="{fill: 'var(--color-fd-muted-foreground)'}" />
      </marker>
    </defs>

    {/* Request */}

    <rect x="260" y="12" width="320" height="56" rx="10" strokeWidth="1.5" style="{fill: 'var(--color-fd-card)', stroke: 'var(--color-fd-border)'}" />

    <text x="420" y="36" textAnchor="middle" fontSize="14" fontWeight="600" style="{fill: 'var(--color-fd-foreground)', fontFamily: 'ui-monospace, monospace'}">
      POST /v2/sign/verify
    </text>

    <text x="420" y="56" textAnchor="middle" fontSize="12" style="{fill: 'var(--color-fd-muted-foreground)'}">
      auth_method: kba, plus the signer's identity
    </text>

    <path d="M420 68 V104" strokeWidth="1.5" fill="none" markerEnd="url(#kba-sandbox-arrow)" style="{stroke: 'var(--color-fd-muted-foreground)'}" />

    {/* Is this a sandbox organization? */}

    <rect x="270" y="108" width="300" height="50" rx="25" strokeWidth="1.5" style="{fill: 'var(--color-fd-muted)', stroke: 'var(--color-fd-border)'}" />

    <text x="420" y="138" textAnchor="middle" fontSize="14" fontWeight="600" style="{fill: 'var(--color-fd-foreground)'}">
      Organization has kba_sandbox?
    </text>

    <path d="M420 158 V182 H214 V204" strokeWidth="1.5" fill="none" markerEnd="url(#kba-sandbox-arrow)" style="{stroke: 'var(--color-fd-muted-foreground)'}" />

    <path d="M420 182 H626 V204" strokeWidth="1.5" fill="none" markerEnd="url(#kba-sandbox-arrow)" style="{stroke: 'var(--color-fd-muted-foreground)'}" />

    <text x="300" y="176" textAnchor="middle" fontSize="12" fontWeight="600" style="{fill: 'var(--color-fd-muted-foreground)'}">
      No
    </text>

    <text x="540" y="176" textAnchor="middle" fontSize="12" fontWeight="600" style="{fill: 'var(--color-fd-muted-foreground)'}">
      Yes
    </text>

    {/* Left: regular organization */}

    <rect x="84" y="208" width="260" height="50" rx="25" strokeWidth="1.5" style="{fill: 'var(--color-fd-muted)', stroke: 'var(--color-fd-border)'}" />

    <text x="214" y="238" textAnchor="middle" fontSize="14" fontWeight="600" style="{fill: 'var(--color-fd-foreground)'}">
      sandbox_test_type sent?
    </text>

    <path d="M214 258 V282 H111 V304" strokeWidth="1.5" fill="none" markerEnd="url(#kba-sandbox-arrow)" style="{stroke: 'var(--color-fd-muted-foreground)'}" />

    <path d="M214 282 H317 V304" strokeWidth="1.5" fill="none" markerEnd="url(#kba-sandbox-arrow)" style="{stroke: 'var(--color-fd-muted-foreground)'}" />

    <text x="158" y="276" textAnchor="middle" fontSize="12" fontWeight="600" style="{fill: 'var(--color-fd-muted-foreground)'}">
      Yes
    </text>

    <text x="270" y="276" textAnchor="middle" fontSize="12" fontWeight="600" style="{fill: 'var(--color-fd-muted-foreground)'}">
      No
    </text>

    {/* Right: sandbox organization */}

    <rect x="496" y="208" width="260" height="50" rx="25" strokeWidth="1.5" style="{fill: 'var(--color-fd-muted)', stroke: 'var(--color-fd-border)'}" />

    <text x="626" y="238" textAnchor="middle" fontSize="14" fontWeight="600" style="{fill: 'var(--color-fd-foreground)'}">
      Sandbox identity submitted?
    </text>

    <path d="M626 258 V282 H523 V304" strokeWidth="1.5" fill="none" markerEnd="url(#kba-sandbox-arrow)" style="{stroke: 'var(--color-fd-muted-foreground)'}" />

    <path d="M626 282 H729 V304" strokeWidth="1.5" fill="none" markerEnd="url(#kba-sandbox-arrow)" style="{stroke: 'var(--color-fd-muted-foreground)'}" />

    <text x="570" y="276" textAnchor="middle" fontSize="12" fontWeight="600" style="{fill: 'var(--color-fd-muted-foreground)'}">
      No
    </text>

    <text x="682" y="276" textAnchor="middle" fontSize="12" fontWeight="600" style="{fill: 'var(--color-fd-muted-foreground)'}">
      Yes
    </text>

    {/* Outcomes */}

    <rect x="16" y="308" width="190" height="64" rx="10" strokeWidth="1.5" style="{fill: 'rgba(239, 68, 68, 0.1)', stroke: '#ef4444'}" />

    <text x="111" y="334" textAnchor="middle" fontSize="14" fontWeight="600" style="{fill: 'var(--color-fd-foreground)'}">
      400
    </text>

    <text x="111" y="356" textAnchor="middle" fontSize="12" style="{fill: 'var(--color-fd-muted-foreground)'}">
      Sandbox not enabled
    </text>

    <rect x="222" y="308" width="190" height="64" rx="10" strokeWidth="1.5" style="{fill: 'var(--color-fd-card)', stroke: 'var(--color-fd-border)'}" />

    <text x="317" y="334" textAnchor="middle" fontSize="14" fontWeight="600" style="{fill: 'var(--color-fd-foreground)'}">
      Real KBA
    </text>

    <text x="317" y="356" textAnchor="middle" fontSize="12" style="{fill: 'var(--color-fd-muted-foreground)'}">
      Identity provider lookup
    </text>

    <rect x="428" y="308" width="190" height="64" rx="10" strokeWidth="1.5" style="{fill: 'rgba(239, 68, 68, 0.1)', stroke: '#ef4444'}" />

    <text x="523" y="334" textAnchor="middle" fontSize="14" fontWeight="600" style="{fill: 'var(--color-fd-foreground)'}">
      400
    </text>

    <text x="523" y="356" textAnchor="middle" fontSize="12" style="{fill: 'var(--color-fd-muted-foreground)'}">
      Use the sandbox identity
    </text>

    <rect x="634" y="308" width="190" height="64" rx="10" strokeWidth="2" style="{fill: 'var(--color-fd-primary)', fillOpacity: 0.12, stroke: 'var(--color-fd-primary)'}" />

    <text x="729" y="334" textAnchor="middle" fontSize="14" fontWeight="600" style="{fill: 'var(--color-fd-foreground)'}">
      Sandbox
    </text>

    <text x="729" y="356" textAnchor="middle" fontSize="12" style="{fill: 'var(--color-fd-muted-foreground)', fontFamily: 'ui-monospace, monospace'}">
      pass · fail · questions
    </text>
  </svg>

  <figcaption className="mt-2 text-center text-sm text-fd-muted-foreground">
    Routing for the identity step. When the sandbox asks questions, it grades the answers too.
  </figcaption>
</figure>

Both `400`s are deliberate guard rails:

| Situation                                                                   | Response                                                                                | Why                                                                                    |
| --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `sandbox_test_type` sent, but the organization doesn't have the sandbox     | `400` "The KBA sandbox is not enabled for this organization."                           | A misconfigured test fails fast instead of running a real lookup with a fake identity. |
| The organization has the sandbox, but the signer submits any other identity | `400` "This organization is in KBA sandbox mode. Use the sandbox identity to test KBA." | A test organization can never run a real person through KBA.                           |

***

## Enable the sandbox [#enable-the-sandbox]

`kba_sandbox` is an organization [entitlement](/docs/guides/tutorials/entitlements). Verdocs grants it on request: contact your account team or [support@verdocs.com](mailto:support@verdocs.com). It's never included in a subscription, so it can't be switched on by accident.

<Callout type="warn" title="Use a dedicated test organization">
  Once an organization has the sandbox, **every** KBA signer in it must use the sandbox identity, and real signers are turned away. Don't enable it on an organization that sends real envelopes.
</Callout>

***

## The sandbox identity [#the-sandbox-identity]

The signer must submit these values exactly. The match is case-sensitive, and all seven fields are required.

| Field        | Value               |
| ------------ | ------------------- |
| `first_name` | `VERDOCS`           |
| `last_name`  | `SANDBOX`           |
| `address`    | `888 TURNKEY DRIVE` |
| `city`       | `NYC`               |
| `state`      | `NY`                |
| `zip`        | `99999`             |
| `dob`        | `01/13/1982`        |

***

## Walkthrough [#walkthrough]

<Steps>
  <Step>
    ### Create an envelope with a KBA recipient [#create-an-envelope-with-a-kba-recipient]

    Create the envelope as usual with [Create Envelope](/docs/reference/Rest-API/api-docs/envelopes/createEnvelope), and add `kba` to the recipient's `auth_methods`:

    ```json
    {
      "role_name": "Signer",
      "first_name": "Verdocs",
      "last_name": "Sandbox",
      "email": "kba-tests@example.com",
      "auth_methods": ["kba"]
    }
    ```
  </Step>

  <Step>
    ### Start a signing session [#start-a-signing-session]

    Start a session for the recipient with [Start Signing Session](/docs/reference/Rest-API/api-docs/recipients/startSigningSession), and keep the `access_token` from the response. Every call below uses it as the bearer token, not your API key.
  </Step>

  <Step>
    ### Submit the sandbox identity [#submit-the-sandbox-identity]

    Send the identity with the scenario you want. Leave out `sandbox_test_type` to get `questions`.

    <Tabs items="['HTTP', 'js-sdk']">
      <Tab value="HTTP">
        ```bash
        curl -X POST https://api.verdocs.com/v2/sign/verify \
          -H "Authorization: Bearer $SIGNING_TOKEN" \
          -H "Content-Type: application/json" \
          -d '{
            "auth_method": "kba",
            "first_name": "VERDOCS",
            "last_name": "SANDBOX",
            "address": "888 TURNKEY DRIVE",
            "city": "NYC",
            "state": "NY",
            "zip": "99999",
            "dob": "01/13/1982",
            "sandbox_test_type": "questions"
          }'
        ```
      </Tab>

      <Tab value="js-sdk">
        ```typescript
        import {verifySigner} from '@verdocs/js-sdk';

        // `endpoint` is a VerdocsEndpoint holding the signing session token
        const session = await verifySigner(endpoint, {
          auth_method: 'kba',
          first_name: 'VERDOCS',
          last_name: 'SANDBOX',
          address: '888 TURNKEY DRIVE',
          city: 'NYC',
          state: 'NY',
          zip: '99999',
          dob: '01/13/1982',
          sandbox_test_type: 'questions',
        });
        ```
      </Tab>
    </Tabs>

    The response is the usual signing session payload. Read the outcome from `recipient.auth_method_states.kba`. For `pass` and `fail` you're done here: see the [scenario reference](#scenario-reference).
  </Step>

  <Step>
    ### Answer the questions [#answer-the-questions]

    For `questions`, `recipient.kba_questions` holds four questions. Each one tells you its answer:

    ```json
    [
      {"type": "sandbox.question.1", "prompt": "The correct answer is 1", "answer": ["1", "2", "3", "4"]},
      {"type": "sandbox.question.2", "prompt": "The correct answer is 2", "answer": ["1", "2", "3", "4"]},
      {"type": "sandbox.question.3", "prompt": "The correct answer is 3", "answer": ["1", "2", "3", "4"]},
      {"type": "sandbox.question.4", "prompt": "The correct answer is 4", "answer": ["1", "2", "3", "4"]}
    ]
    ```

    Answer all four in one request, pairing each `type` with the option you pick:

    ```bash
    curl -X POST https://api.verdocs.com/v2/sign/verify \
      -H "Authorization: Bearer $SIGNING_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "auth_method": "kba",
        "responses": [
          {"type": "sandbox.question.1", "answer": "1"},
          {"type": "sandbox.question.2", "answer": "2"},
          {"type": "sandbox.question.3", "answer": "3"},
          {"type": "sandbox.question.4", "answer": "4"}
        ]
      }'
    ```

    To test a failure, change any one answer.
  </Step>
</Steps>

***

## Scenario reference [#scenario-reference]

**Identity step**

| `sandbox_test_type`      | Response                                                                 | `auth_method_states.kba` |
| ------------------------ | ------------------------------------------------------------------------ | ------------------------ |
| `pass`                   | `200` signing session                                                    | `complete`               |
| `fail`                   | `400` "Authentication failed. Please contact the sender for assistance." | `failed`                 |
| `questions` (or omitted) | `200` signing session, with four `kba_questions`                         | `questions`              |
| Anything else            | `400` validation error                                                   | Unchanged                |

**Answers step** (`questions` only)

| Answers                                        | Response                                                                 | `auth_method_states.kba` |
| ---------------------------------------------- | ------------------------------------------------------------------------ | ------------------------ |
| All four correct                               | `200` signing session                                                    | `complete`               |
| Any answer wrong                               | `400` "Authentication failed. Please contact the sender for assistance." | `failed`                 |
| Fewer than four, or a `type` that wasn't asked | `400` "Please answer every question."                                    | Unchanged: answer again  |

A failure marks the recipient failed and fires the `recipient_auth_fail` webhook, exactly as a real KBA failure does (see [The Recipient Auth Ladder](/docs/guides/tutorials/the-recipient-auth-ladder)).

***

## Things to know [#things-to-know]

* **The identity provider is never called.** Sandbox attempts don't leave Verdocs, so they have no lookup cost and always behave the same way.
* **`complete` and `failed` are final, as in production.** A failed recipient can't retry. Use a fresh envelope per test case, or [reset the envelope](/docs/reference/Rest-API/api-docs/envelopes/resetEnvelope), which clears every recipient's auth state.
* **Grading is stricter than production.** Any wrong answer fails, whereas production may allow a few. Don't use the sandbox to test pass thresholds.
* **The 24-hour window still applies.** KBA must be completed within 24 hours of starting it, as in production.
* **There's no differentiator step.** Production KBA can occasionally ask one extra question to tell people with similar records apart before the main questions. The sandbox goes straight to its four questions.
