---
title: Double-Entry Verification
product: Form Library
description: Compare two independent data entries in SurveyJS forms to identify data discrepancies. View this JavaScript demo to learn more.
framework: React
source: https://surveyjs.io/form-library/examples/double-entry-verification/reactjs
index: https://surveyjs.io/form-library/examples/overview.md
---

# Double-Entry Verification (React)

Double-entry verification—also known as double data entry or dual data entry—is a data quality control process in which the same information is entered independently twice, and the two entries are then compared to identify discrepancies. Its purpose is to detect human transcription errors, particularly when data is captured from paper forms, scanned documents, handwritten notes, or other source records. Unlike ordinary validation, which checks whether a value is valid, double-entry verification checks whether it was entered accurately. For example, the value `27` passes a numeric validator even if the source document says `72`.

A typical double-entry verification workflow includes two validation layers:

- Assisted confirmation\
Email and confirm email are compared with a client-side expression validator. In this demo, validators use [`"notificationType": "warning"`](/form-library/documentation/api-reference/textvalidator#notificationType), so validation messages do not block form submission. To prevent the form from being submitted until the issue is resolved, use the default `"error"` notification type. See [Validator Notification Types](/survey-creator/documentation/end-user-guide/data-validation-in-forms#validator-notification-types).

- Selective dual entry\
Critical fields are entered again on the second page and compared asynchronously in the [`onServerValidateQuestions`](/form-library/documentation/surveymodel#onServerValidateQuestions) event, which can return a custom validation message for each field.

## How This Demo Works

1. First entry\
Enter the participant ID, weight, height, date of birth, and email, or click **Fill Sample Data**.

2. Assisted check\
Confirm the email address. An expression validator checks that both email fields match.

3. Second entry\
Re-enter the critical fields without seeing the first-entry values, or click **Fill Sample Data**.

4. Server-side comparison\
When you complete the form, `onServerValidateQuestions` compares the paired values and reports mismatches as field-level errors.

## Connect to an API Endpoint

In regulated environments, a second operator often compares their entries with the first operator's saved submission on the server. To implement this workflow, call your comparison endpoint in `onServerValidateQuestions`, populate the `errors` object, and then call `complete()`:


```js
function validateDoubleEntryAsync(sender, { data, errors, complete }) {
  fetch("/api/validation/double-entry", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      // Prefer sender.data so first-entry values are available when only page 2 is validated
      participantId: sender.data.participantId,
      participantId2: data.participantId2,
      weightKg: sender.data.weightKg,
      weightKg2: data.weightKg2
      // ...other paired fields
    })
  })
    .then((response) => response.json())
    .then((result) => {
      // result.errors: { weightKg2: "Mismatch with first entry..." }
      Object.keys(result.errors || {}).forEach((name) => {
        errors[name] = result.errors[name];
      });
      complete();
    })
    .catch(() => {
      errors["weightKg2"] = "Comparison service is temporarily unavailable. Try again.";
      complete();
    });
}

survey.onServerValidateQuestions.add(validateDoubleEntryAsync);
```

## Related Examples

- [Server-Side Form Validation Using Expressions](/form-library/examples/javascript-async-form-validation/)
- [Server-Side Form Validation Using an Event](/form-library/examples/javascript-server-side-form-validation/)
- [Detect Mis-Keyed Form Entries](/form-library/examples/detect-miskeyed-form-entries/)
- [Detect Duplicate Form Records](/form-library/examples/detect-duplicate-form-records/)

## Files

### `public/index.html`

```html
<div id="surveyElement" style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; min-height: 100%; height:100%"></div>
```

### `src/SurveyComponent.jsx`

```js
import React from "react";
import { Model } from "survey-core";
import { Survey } from "survey-react-ui";
import "survey-core/survey-core.min.css";
import "./index.css";
import { json } from "./json";

// Assisted email confirm stays client-side; dual-entry pairs use onServerValidateQuestions.
// See the Documentation tab to connect a real comparison API (Operator A/B, audit trail, locking).

const DOUBLE_ENTRY_PAIRS = [
  { first: "participantId", second: "participantId2", label: "Participant ID" },
  { first: "weightKg", second: "weightKg2", label: "Weight (kg)" },
  { first: "heightCm", second: "heightCm2", label: "Height (cm)" },
  { first: "dateOfBirth", second: "dateOfBirth2", label: "Date of birth" }
];

function normalizeComparable(value) {
  if (value === undefined || value === null) {
    return "";
  }
  return String(value).trim();
}

function valuesMatch(left, right) {
  const a = normalizeComparable(left);
  const b = normalizeComparable(right);
  if (a === "" || b === "") {
    return true;
  }
  const aNum = Number(a);
  const bNum = Number(b);
  if (!Number.isNaN(aNum) && !Number.isNaN(bNum) && a !== "" && b !== "") {
    if (String(a).match(/^-?\d+(\.\d+)?$/) && String(b).match(/^-?\d+(\.\d+)?$/)) {
      return aNum === bNum;
    }
  }
  return a.toLowerCase() === b.toLowerCase();
}

// In-demo async comparison (replace with fetch to your comparison API in production).
function mockValidationApi(payload) {
  return new Promise((resolve) => {
    setTimeout(() => {
      const errors = {};

      DOUBLE_ENTRY_PAIRS.forEach((pair) => {
        const firstValue = payload[pair.first];
        const secondValue = payload[pair.second];
        if (!normalizeComparable(firstValue) || !normalizeComparable(secondValue)) {
          return;
        }
        if (valuesMatch(firstValue, secondValue)) {
          return;
        }
        errors[pair.second] =
          `Mismatch with first entry for ${pair.label}: entered "${normalizeComparable(secondValue)}" ` +
          `vs first entry "${normalizeComparable(firstValue)}". Re-check the source document.`;
      });

      resolve({ errors });
    }, 450);
  });
}

function validateDoubleEntryAsync(sender, { data, errors, complete }) {
  const surveyData = sender.data || {};
  const payload = {};
  DOUBLE_ENTRY_PAIRS.forEach((pair) => {
    payload[pair.first] = surveyData[pair.first];
    payload[pair.second] =
      data[pair.second] !== undefined ? data[pair.second] : surveyData[pair.second];
  });

  mockValidationApi(payload)
    .then((result) => {
      Object.keys(result.errors || {}).forEach((name) => {
        errors[name] = result.errors[name];
      });
      complete();
    })
    .catch(() => {
      errors["weightKg2"] = "Comparison service is temporarily unavailable. Try again.";
      complete();
    });
}

function SurveyComponent() {
    const survey = new Model(json);
    survey.onComplete.add((sender, options) => {
        console.log(JSON.stringify(sender.data, null, 3));
    });
    survey.onServerValidateQuestions.add(validateDoubleEntryAsync);
    
    const FIRST_ENTRY_SAMPLE = {
      participantId: "TRIAL-1042",
      weightKg: 72,
      heightCm: 180,
      dateOfBirth: "1987-04-12",
      email: "patient@example.com",
      emailConfirm: "patient@example.com"
    };
    
    const SECOND_ENTRY_SAMPLE = {
      participantId2: "TRIAL-1042",
      weightKg2: 27,
      heightCm2: 180,
      dateOfBirth2: "1987-04-12"
    };
    
    survey.addNavigationItem({
      id: "sv-nav-fill-sample",
      title: "Fill Sample Data",
      action: () => {
        const sample =
          survey.currentPage.name === "secondEntry"
            ? SECOND_ENTRY_SAMPLE
            : FIRST_ENTRY_SAMPLE;
        Object.keys(sample).forEach((name) => {
          survey.setValue(name, sample[name]);
        });
      },
      css: "nav-button",
      innerCss: "sd-btn"
    });
    
    return (<Survey model={survey} />);
}

export default SurveyComponent;
```

### `src/index.css`

```css
.nav-button {
  margin-left: auto;
  margin-right: 0;
}
```

### `src/index.js`

```js
import React from "react";
import { createRoot } from "react-dom/client";
import SurveyComponent from "./SurveyComponent";

const root = createRoot(document.getElementById("surveyElement"));
root.render(<SurveyComponent />);
```

### `src/json.js`

```js
export const json = {
  "title": "Patient Measurements — Double-Entry Check",
  "description": "Enter critical values, confirm the email address on page 1, then re-enter the critical fields on page 2. The second entry is compared with the first when you submit the form.",
  "checkErrorsMode": "onValueChanged",
  "completeText": "Submit",
  "pages": [
    {
      "name": "firstEntry",
      "title": "First entry",
      "description": "Enter values from the source document.",
      "elements": [
        {
          "type": "text",
          "name": "participantId",
          "title": "Participant ID",
          "isRequired": true,
          "placeholder": "e.g. TRIAL-1042"
        },
        {
          "type": "text",
          "name": "weightKg",
          "title": "Weight (kg)",
          "isRequired": true,
          "maskType": "numeric",
          "maskSettings": {
            "min": 0,
            "precision": 1
          },
          "placeholder": "e.g. 72",
          "inputTextAlignment": "left"
        },
        {
          "type": "text",
          "name": "heightCm",
          "title": "Height (cm)",
          "isRequired": true,
          "maskType": "numeric",
          "maskSettings": {
            "min": 0,
            "precision": 0
          },
          "placeholder": "e.g. 180",
          "inputTextAlignment": "left"
        },
        {
          "type": "text",
          "name": "dateOfBirth",
          "title": "Date of birth",
          "isRequired": true,
          "inputType": "date"
        },
        {
          "type": "text",
          "name": "email",
          "title": "Email",
          "isRequired": true,
          "inputType": "email",
          "validators": [{ "type": "email" }]
        },
        {
          "type": "text",
          "name": "emailConfirm",
          "title": "Confirm email",
          "description": "Assisted verification — client-side expression checks that both emails match.",
          "isRequired": true,
          "inputType": "email",
          "validators": [
            { "type": "email" },
            {
              "type": "expression",
              "text": "Email addresses do not match.",
              "expression": "{email} empty or {emailConfirm} empty or {email} = {emailConfirm}",
              "notificationType": "warning"
            }
          ]
        }
      ]
    },
    {
      "name": "secondEntry",
      "title": "Second entry",
      "description": "Re-enter the same values without referring to the first entry.",
      "elements": [
        {
          "type": "text",
          "name": "participantId2",
          "title": "Participant ID",
          "isRequired": true,
          "placeholder": "Re-enter value",
          "inputTextAlignment": "left"
        },
        {
          "type": "text",
          "name": "weightKg2",
          "title": "Weight (kg)",
          "isRequired": true,
          "maskType": "numeric",
          "maskSettings": {
            "min": 0,
            "precision": 1
          },
          "placeholder": "Re-enter value",
          "inputTextAlignment": "left"
        },
        {
          "type": "text",
          "name": "heightCm2",
          "title": "Height (cm)",
          "isRequired": true,
          "maskType": "numeric",
          "maskSettings": {
            "min": 0,
            "precision": 0
          },
          "placeholder": "Re-enter value",
          "inputTextAlignment": "left"
        },
        {
          "type": "text",
          "name": "dateOfBirth2",
          "title": "Date of birth",
          "isRequired": true,
          "inputType": "date"
        },
        {
          "type": "html",
          "name": "mismatchSummary",
          "html": "<p><em>Click <strong>Submit</strong> to compare both entries. Any mismatches are displayed next to the corresponding fields.</em></p>"
        }
      ]
    }
  ],
  "width": "750px"
}
;
```

### `src/theme.js`

```js
export const themeJson = {};
```

### `package.json`

```json
{
  "dependencies": {
    "react": "latest",
    "react-dom": "latest",
    "survey-core": "latest",
    "survey-react-ui": "latest"
  },
  "devDependencies": {
    "react-scripts": "latest"
  }
}
```

## Other Frameworks

- [Angular](https://surveyjs.io/form-library/examples/double-entry-verification/angular.md)
- [Vue 3](https://surveyjs.io/form-library/examples/double-entry-verification/vue3js.md)
- [jQuery](https://surveyjs.io/form-library/examples/double-entry-verification/jquery.md)
- [Vanilla JS](https://surveyjs.io/form-library/examples/double-entry-verification/vanillajs.md)
