---
updatedAt: 2026-08-26T06:42:00.000Z
agentTools:
  projectIndex: https://docs.oneschema.co/llms.txt
---

# Using Validation Webhooks

OneSchema’s validation API allows you to use your own API endpoint to create a column validator.

# Validation webhook

> 🚧 Webhook endpoints must be publicly accessible
>
> OneSchema webhooks, including validation webhooks, cannot be connected to `localhost` services, since the webhook endpoint needs to be accessible from the public internet. If you are developing locally, you can use a tunneling service such as [ngrok](https://ngrok.com/) to expose your local server to the internet.

Similar to our data export webhooks feature, you will only need a URL endpoint and a template to get started using this feature. Once your validation webhook is configured, every time your template is mapped to a sheet, your validation webhook will be run alongside our other validations in our validation library.

OneSchema's validation API operates by sending POST requests with JSON values to the supplied endpoints. The User-Agent HTTP header is set to `OneSchema/1.0`.

For row hooks, each request will have a customizable batch size which defaults to 1000 values. If a list has more rows than the batch size, multiple requests for a row hook will be sent. You have to select which columns should be sent to the validation hook. Row hooks are triggered on initial validation as well as after subsequent updates to any cell in the specified columns.

For column hooks, only one request will be sent with ALL of the rows on the spreadsheet even if the spreadsheet has more than 1000 rows. Column hooks are triggered once on initial validation and each time a cell in one of the configured columns changes.

> 📘
>
> Please contact your OneSchema representative for more info on setting up column hooks.

## Attaching a Validation webhook to a Template

<Glossary>Validation webhook</Glossary>s are configured on an individual template's configuration page. [More info here](https://docs.oneschema.co/docs/creating-and-editing-templates#validation-webhooks).

### Validation JSON request schema

| Parameter          | Type      | Description                                                                                                                                                                                                                                                                                                                                                           |
| :----------------- | :-------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| rows\*             | Object\[] | Array of rows to validate.                                                                                                                                                                                                                                                                                                                                            |
| rows\[].row\_id\*  | Integer   | Id of a single row. Use this value in the response data to specify errors.                                                                                                                                                                                                                                                                                            |
| rows\[].values\*   | Object    | A map from template column names to the value in that column for a single row. All values are represented as strings. Only columns that are part of the hook's configuration are included.                                                                                                                                                                            |
| columns\*          | Object\[] | Ordered array of mapped columns. Used for referencing [custom columns.](https://docs.oneschema.co/docs/validation-webhook#validation-hooks-with-custom-columns)                                                                                                                                                                                                       |
| template\_key\*    | String    | An optional key specified on the template that can be used instead of `template_id` to determine what template is being validated.                                                                                                                                                                                                                                    |
| hook\_id\*         | Integer   | A unique identifier for this validation hook.                                                                                                                                                                                                                                                                                                                         |
| hook\_name\*       | String    | The name of the hook, as configured on the validation hook UI.                                                                                                                                                                                                                                                                                                        |
| workspace\_id\*    | Integer   | The id of Workspace containing the Sheet containing the rows being validated.                                                                                                                                                                                                                                                                                         |
| sheet\_id\*        | Integer   | The id of the Sheet containing the rows being validated.                                                                                                                                                                                                                                                                                                              |
| sheet\_metadata\*  | Object    | A JSON Object containing any custom metadata associated with the Sheet at time of import. Includes two additional fields added automatically by OneSchema: `original_file_name` containing the name of the original uploaded file, and `original_sheet_name` containing the name of the sheet in the imported Excel file, if part of a multi-sheet Excel file upload. |
| embed\_user\_jwt\* | String    | If you are embedding OneSchema within your own site, this will contain the <Glossary>JWT</Glossary> token used to initialize a customer’s session.                                                                                                                                                                                                                    |
| request\_id\*      | String    | A unique ID for this webhook request.                                                                                                                                                                                                                                                                                                                                 |

## Example JSON POST request body

```json
{
  "rows": [
    {
      "row_id": 1,
      "values": {
        "contact_id": "1438263",
        "contact_name": "Aiko Tanaka",
        "business_vertical": "SaaS Sales"
      }
    },
    {
      "row_id": 2,
      "values": {
        "contact_id": "900372",
        "contact_name": "Babatunde Osei",
        "business_vertical": "Sportswear"
      }
    },
    {
      "row_id": 3,
      "values": {
        "contact_id": "6803830",
        "contact_name": "Chen Wei",
        "business_vertical": "Shipping Infrastructure"
      }
    },
    {
      "row_id": 4,
      "values": {
        "contact_id": "1684023",
        "contact_name": "Deepika Sharma",
        "business_vertical": "Marketing"
      }
    }
  ],
  "columns": [
    {
      "sheet_column_name": "Contact ID",
      "template_column_name": "Contact ID",
      "template_column_key": "contact_id"
    },
    {
      "sheet_column_name": "Contact Name",
      "template_column_name": "Contact Name",
      "template_column_key": "contact_name"
    },
    {
      "sheet_column_name": "Business Vertical",
      "template_column_name": "Business Vertical",
      "template_column_key": "business_vertical"
    }
  ],
  "template_key": "contact-template",
  "hook_id": 823,
  "hook_name": "Hook 1",
  "workspace_id": 23155,
  "sheet_id": 23145,
  "sheet_metadata": {
    "original_file_name": "contacts.csv"
  },
  "embed_user_jwt": "<EMBED_USER_JWT>",
  "request_id": "req_v85bd03e1-32aa-4600-8f9c-5ff0b0d206c5"
}
```

In order to validate the values, OneSchema will expect the endpoint to return an array of warnings and errors. Each warning and error should specify the `row_id`, the `column` the warning or error appeared in, the `severity` of the issue (`"warning"` or `"error"`), a `message` to be displayed in the UI (see [best practices for error messages](https://docs.oneschema.co/docs/error-message-best-practices)), and optionally a `suggestion` can be provided to the user.

Issues can be raised for multiple columns in a single row. For example, if a validation hook verifies that two columns `A` and `B` sum to a value in column `C`, the validation hook may choose to say only column `C` has an error, or that each of columns `A`, `B`, and `C` have an error, depending on the relationship between the columns and the semantics they want to expose.

### Validation Hook JSON Response Schema

<Table align={["left","left","left"]}>
  <thead>
    <tr>
      <th>
        Parameter
      </th>

      <th>
        Type
      </th>

      <th>
        Description
      </th>
    </tr>

  </thead>

  <tbody>
    <tr>
      <td>
        \[]
      </td>

      <td>
        Object\[]
      </td>

      <td>
        The response should be an array of validations.
      </td>
    </tr>

    <tr>
      <td>
        \[].row\_id
      </td>

      <td>
        Integer
      </td>

      <td>
        The `row_id` with the warning or error.
      </td>
    </tr>

    <tr>
      <td>
        \[].column
      </td>

      <td>
        String
      </td>

      <td>
        The key of the template column where the validation should be displayed.
      </td>
    </tr>

    <tr>
      <td>
        \[].column\_index
      </td>

      <td>
        Integer
      </td>

      <td>
        (Optional) The index of the column for this validation, based on the [columns](https://docs.oneschema.co/docs/validation-webhook#columns-object) array provided in the JSON POST request body. Required if the `column` is a custom column.
      </td>
    </tr>

    <tr>
      <td>
        \[].severity
      </td>

      <td>
        String
      </td>

      <td>
        Either `"warning"` (displayed in yellow) or `"error"` (displayed in red).
      </td>
    </tr>

    <tr>
      <td>
        \[].message
      </td>

      <td>
        String
      </td>

      <td>
        Message to be displayed to user when they click on the validation.
      </td>
    </tr>

    <tr>
      <td>
        \[].suggestion
      </td>

      <td>
        String | (String | Object)\[]
      </td>

      <td>
        (Optional) A suggestion or list of suggestions for a new value that will be displayed to the user. The user may choose whether or not to accept the suggested value.

        A suggestion in the list may also be an object, which annotates the suggested value in the UI:

        `value`: (Required) The suggested value itself, written to the cell when the user accepts it.

        `label`: (Optional) An object with a `text` (short plain text, for example the source the value came from). Shown as a colored pill next to the suggested value; the pill's color is derived from the text, so the same label is always the same color.

        `link`: (Optional) An object with a `url` (an absolute `http`/`https` URL) and optional `text`. Shown as an external-link icon that opens the URL in a new tab; the `text` appears in its popover, defaulting to "More info".
      </td>
    </tr>

    <tr>
      <td>
        \[].options
      </td>

      <td>
        Object
      </td>

      <td>
        (Optional) Additional information related to the error. Keys may include:

        `popover_title`: The title of the popover which appears when the user clicks an error or warning in OneSchema.
      </td>
    </tr>

  </tbody>
</Table>

## Example JSON Response

```json
[
  {
    "row_id": 2,
    "column": "employee_name",
    "severity": "warning",
    "message": "Employee no longer works for company."
  },
  {
    "row_id": 2,
    "column": "employee_end_date",
    "severity": "error",
    "message": "Employee gave notice on this date.",
    "suggestion": "04/15/2020"
  },
  {
    "row_id": 3,
    "column": "employee_name",
    "severity": "error",
    "message": "Employee's name has changed",
    "suggestion": ["Chen Wei", "Chen"]
  },
  {
    "row_id": 4,
    "column": "employee_end_date",
    "severity": "error",
    "message": "Date does not match our records.",
    "suggestion": [
      {
        "value": "04/15/2020",
        "label": { "text": "Payslip" },
        "link": {
          "url": "https://hr.example.com/employees/123",
          "text": "Open the payslip this date came from"
        }
      },
      { "value": "04/30/2020", "label": { "text": "Payroll account" } },
      "04/01/2020"
    ]
  }
]
```

## Securing your validation hook

To secure your webhook, we can optionally authenticate webhook requests using **Basic Authentication** or **Bearer User <Glossary>JWT</Glossary>**.

If choosing to use [Basic Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication), a secret key must be supplied, the secret key will be used as the basic auth password. No username is passed through.

If Bearer User <Glossary>JWT</Glossary>, the user <Glossary>JWT</Glossary> set up through the SDK will be used as a bearer token in the request header.

### How Bearer User JWT authentication works

The following diagram shows the end-to-end flow when using Bearer User <Glossary>JWT</Glossary> authentication for validation webhooks:

```mermaid
sequenceDiagram
    participant CustomerBackend as Your Backend
    participant SDK as OneSchema Importer (Browser)
    participant OneSchema as OneSchema
    participant Webhook as Your Validation Webhook

    Note over CustomerBackend,Webhook: 1. Importer Initialization

    CustomerBackend->>CustomerBackend: Generate a user JWT signed with your OneSchema client secret
    CustomerBackend->>SDK: Initialize the OneSchema importer with client_id, template_key, and user_jwt
    SDK->>OneSchema: Start embed session (passes user_jwt)
    OneSchema->>OneSchema: Verify JWT signature and store the token for later use
    OneSchema-->>SDK: Session ready

    Note over CustomerBackend,Webhook: 2. Data Upload and Mapping

    SDK->>OneSchema: User uploads file and maps columns
    OneSchema->>OneSchema: Store and process the uploaded data

    Note over CustomerBackend,Webhook: 3. Validation (including your webhook)

    OneSchema->>OneSchema: Run built-in validations (types, required fields, regex, etc.)

    loop For each batch of rows
        OneSchema->>Webhook: POST your_webhook_url<br/>Authorization: Bearer {user_jwt from step 1}<br/>Body: { rows, columns, request_id }

        alt Your webhook returns 2xx
            Webhook-->>OneSchema: 200 OK with row-level errors (if any)
            OneSchema->>OneSchema: Apply errors to the relevant rows
        else Your webhook returns 408 or 429
            Webhook-->>OneSchema: Timeout / rate-limited
            OneSchema->>OneSchema: Retry up to 3 times with exponential backoff
        else Your webhook returns other error (4xx/5xx)
            Webhook-->>OneSchema: Error response (not retried)
        end
    end

    Note over CustomerBackend,Webhook: 4. Completion

    alt Validation succeeded
        OneSchema-->>SDK: Show the review step to the user
    else Validation failed or timed out
        OneSchema-->>SDK: Surface validation errors to the user
    end
```

> ⚠️ <Glossary>JWT</Glossary> expiration
>
> The `user_jwt` you provide during importer initialization is stored and reused as-is for all validation webhook calls. OneSchema does not refresh or re-sign this token. If your JWT includes an expiration (`exp` claim), make sure it has a long enough lifetime to cover the full import session, including validation. For large files, validation can take several minutes after upload.

## Embedding

When using webhooks with embedded OneSchema, the <Glossary>JWT</Glossary> token that was used to initialize the session will also be sent in the top-level key `embed_user_jwt`.

## Retries

If the webhook call times out after 30 seconds, or if OneSchema cannot connect to the webhook endpoint, OneSchema will retry the call up to three times with exponential backoff.

A 408 (request timeout) or 429 (too many requests) response code will also prompt OneSchema to retry. Other HTTP error responses (including 5xx) are not retried.

The `request_id` will be the same for every retry.

## Validation hooks with custom columns

Sometimes, even your custom columns require validation hooks. For example, you may have a `user` custom column that requires validation against your internal database when mapped against a sheet column. However, since custom columns can be mapped multiple times within the same sheet, additional information is required in the validation webhook JSON response in order to properly display validation errors and warnings.

The POST request schema specifies an array of column mappings via the `columns` key.

### Columns object

| Parameter                | Type      | Description                                                              |
| :----------------------- | :-------- | :----------------------------------------------------------------------- |
| \[]\*                    | Object\[] | Ordered array of mappings.                                               |
| sheet\_column\_name\*    | String    | The sheet column name of a specific mapping                              |
| template\_column\_name\* | String    | The name of the template column this sheet column is mapped to.          |
| template\_column\_key\*  | String    | The template column key this sheet column is mapped to.                  |
| is\_custom               | Boolean   | (Optional) Whether this template column is a custom column.              |
| custom\_column\_name     | String    | (Optional) The uploaded sheet column name for this mapped custom column. |

### Custom column values

Like standard template columns, custom column values are also specified via the `rows` array provided in the POST request body. However, all custom column values will be grouped under a single umbrella object keyed by`$custom`. The `$custom` key specifies a mapping from `column_index` to sheet cell value. This allows you to disambiguate between multiple sheet columns which could be mapped to the same custom template column.

### Example JSON POST with custom columns

```json
{
  "rows": [
    {
      "row_id": 1,
      "values": {
        "business_vertical": "SaaS Sales",
        "$custom": {
          "3": "LOW",
          "4": "LOW"
        }
      }
    },
    {
      "row_id": 2,
      "values": {
        "business_vertical": "Sportswear",
        "$custom": {
          "3": "HIGH",
          "4": "HIGH"
        }
      }
    },
    {
      "row_id": 3,
      "values": {
        "business_vertical": "Shipping Infrastructure",
        "$custom": {
          "3": "LOW",
          "4": "HIGH"
        }
      }
    },
    {
      "row_id": 4,
      "values": {
        "business_vertical": "Marketing",
        "$custom": {
          "3": "HIGH",
          "4": "MED"
        }
      }
    }
  ],
  "columns": [
    {
      "sheet_column_name": "Contact ID",
      "template_column_name": "Contact ID",
      "template_column_key": "contact_id"
    },
    {
      "sheet_column_name": "Contact Name",
      "template_column_name": "Contact Name",
      "template_column_key": "contact_name"
    },
    {
      "sheet_column_name": "Business Vertical",
      "template_column_name": "Business Vertical",
      "template_column_key": "business_vertical"
    },
    {
      "sheet_column_name": "Secondary Priority",
      "template_column_name": "Tier",
      "template_column_key": "tier",
      "is_custom": true,
      "custom_column_name": "Secondary Priority"
    },
    {
      "sheet_column_name": "Priority",
      "template_column_name": "Tier",
      "template_column_key": "tier",
      "is_custom": true,
      "custom_column_name": "Priority"
    }
  ],
  "template_key": "contact-template",
  "hook_id": 823,
  "hook_name": "Hook 1",
  "workspace_id": 23155,
  "sheet_id": 23145,
  "sheet_metadata": {
    "original_file_name": "contacts.csv"
  },
  "embed_user_jwt": "<EMBED_USER_JWT>",
  "request_id": "req_v85bd03e1-32aa-4600-8f9c-5ff0b0d206c5"
}
```

As before, OneSchema will expect the endpoint to return an array of warnings and errors. In addition to specifying `row_id`, `column`, `severity`, and `message`, you must also provide a `column_index` in any errors for custom columns.

### Example JSON Response with custom columns

```json
[
  {
    "row_id": 2,
    "column": "contact_name",
    "severity": "warning",
    "message": "Employee no longer works for company."
  },
  {
    "row_id": 3,
    "column": "tier",
    "column_index": 3,
    "severity": "error",
    "message": "Mismatched priority levels."
  },
  {
    "row_id": 4,
    "column": "tier",
    "column_index": 4,
    "severity": "warning",
    "message": "Specify priority as HIGH or LOW."
  }
]
```

## Validation hooks with multi-mappable columns

Similar to custom columns, multi-mappable columns may be mapped multiple times in the same sheet. As such, additional information is required in the validation webhook JSON response in order to properly display validation errors and warnings.

The POST request schema specifies an array of column mappings via the `columns` key.

### Columns object

| Parameter                | Type      | Description                                                     |
| :----------------------- | :-------- | :-------------------------------------------------------------- |
| \[]\*                    | Object\[] | Ordered array of mappings.                                      |
| sheet\_column\_name\*    | String    | The sheet column name of a specific mapping                     |
| template\_column\_name\* | String    | The name of the template column this sheet column is mapped to. |
| template\_column\_key\*  | String    | The template column key this sheet column is mapped to.         |
| is\_multi\_mappable      | Boolean   | (Optional) Whether this template column is multi mappable.      |

### Multi mapped column values

Like standard template columns, multi-mappable column values are also specified via the rows array provided in the POST request body. Multi-mappable column values are reported as an array of objects; the `column_index` field identifies the index of the column in the columns array so that the original uploaded column can be identified. Note that multi-mappable columns values will always reported in array form even if there is only one corresponding uploaded column.

### Example JSON POST with multi-mapped values

```jsonc
{
  "request_id": "req_v39a92433-3061-4977-8f70-9ff981d528ba",
  "rows": [
    {
      "row_id": 1,
      "values": {
        "area_name": "Area 1",
        "ground_temperature": [
          {
            "value": "59.4",
            "column_index": 1,
          },
          {
            "value": "59.",
            "column_index": 2,
          },
          {
            "value": "60.9",
            "column_index": 3,
          },
        ],
      },
    },
    {
      "row_id": 2,
      "values": {
        "ground_temperature": [
          {
            "value": "56.8",
            "column_index": 1,
          },
          {
            "value": "57.0",
            "column_index": 2,
          },
          {
            "value": "57.6",
            "column_index": 3,
          },
        ],
        "area_name": "Area 2",
      },
    },
    {
      "row_id": 3,
      "values": {
        "ground_temperature": [
          {
            "value": "61.7",
            "column_index": 1,
          },
          {
            "value": "62.2",
            "column_index": 2,
          },
          {
            "value": "62.9",
            "column_index": 3,
          },
        ],
        "area_name": "Area 3",
      },
    },
  ],
  "template_key": "temperature_sensors",
  "hook_id": "<HOOK_ID>",
  "hook_name": "<HOOK_NAME>",
  "sheet_id": "<SHEET_ID>",
  "columns": [
    {
      "sheet_column_name": "Area name",
      "template_column_name": "Area name",
      "template_column_key": "area_name",
    },
    {
      "sheet_column_name": "Ground temp 1",
      "template_column_name": "Ground Temperature",
      "template_column_key": "ground_temperature",
      "is_multi_mappable": true,
    },
    {
      "sheet_column_name": "Ground temp 2",
      "template_column_name": "Ground Temperature",
      "template_column_key": "ground_temperature",
      "is_multi_mappable": true,
    },
    {
      "sheet_column_name": "Ground temp 3",
      "template_column_name": "Ground Temperature",
      "template_column_key": "ground_temperature",
      "is_multi_mappable": true,
    },
  ],
}
```

Again, OneSchema will expect the endpoint to return an array of warnings and errors. In addition to specifying `row_id`, `column`, `severity`, and `message`, you must also provide a `column_index` in any errors for multi mapped columns.

### Example JSON Response with multi-mapped values

```json
[
  {
    "row_id": 1,
    "column": "ground_temperature",
    "column_index": 2,
    "severity": "warning",
    "message": "Invalid temperature reading."
  }
]
```

## Validation Hooks for File Feeds

The response object of a validation hook attached to a file feed run will function similarly to validation hooks in an importer run, save for a couple differences.

In the context of a file feed run, there will be **no** `embed_user_jwt` attached to the validation webhook payload.

In the context of a file feed run, `file_feed_id`, `file_feed_import_id`and `file_feed_metadata` will be attached to the validation webhook payload. The exception is imports run in the pipeline builder. Those will not have an associated `file_feed_import_id`.

### File Feed Response Object (Additional Fields)

| Parameter                | Type    | Description                                                                                  |
| :----------------------- | :------ | :------------------------------------------------------------------------------------------- |
| file\_feed\_id\*         | Integer | Id of a file feed.                                                                           |
| file\_feed\_import\_id\* | Integer | Id of a file feed import. (Will not be present on pipeline builder test runs)                |
| file\_feed\_metadata\*   | Object  | Json that can be attached to a file feed via api in the custom\_metadata field or in the UI. |

### Example JSON Response

```json
{
  "request_id": "req_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "rows": [
    {
      "row_id": 1,
      "values": {
        "test": "value1"
      }
    },
    {
      "row_id": 2,
      "values": {
        "test": "value2"
      }
    }
  ],
  "template_key": "template_key_example",
  "hook_id": 12345,
  "hook_name": "hook_name_example",
  "workspace_id": 1,
  "sheet_id": 12345,
  "sheet_metadata": {
    "original_file_name": "original_file_name.json"
  },
  "file_feed_id": 12345,
  "file_feed_metadata": {},
  "columns": [
    {
      "sheet_column_name": "column_name_example",
      "template_column_name": "column_name_example",
      "template_column_key": "column_key_example"
    }
  ]
}
```