# Callback delivery

> Callback delivery keeps imported row data in the browser and hands it directly to your application's `onData` function.

Canonical HTML: https://www.csvjetty.com/docs/callbacks

## onData

```jsx
<CSVJetty
  importerKey="YOUR_IMPORTER_KEY"
  onData={async ({ rows, importId, rowNumbers }) => {
    console.log(rows);
  }}
>
  Import contacts
</CSVJetty>
```

For callback delivery, `onData` is required. CSVJetty waits for its returned promise before completing that delivery step. Throw an error when your application could not save the batch.

## Save records through your API

```jsx
<CSVJetty
  importerKey="YOUR_IMPORTER_KEY"
  onData={async ({ rows, importId, rowNumbers }) => {
    const response = await fetch("/api/contacts/import", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ rows, importId, rowNumbers }),
    });

    if (!response.ok) {
      throw new Error("Could not save contacts.");
    }

    const body = await response.text();
    return body ? JSON.parse(body) : undefined;
  }}
>
  Import contacts
</CSVJetty>
```

Validate authorization, uniqueness, foreign keys, and business rules on your server before persistence.

## Reject individual records

Return an `errors` array to reject specific original CSV rows while allowing the rest of a batch to succeed.

```js
return {
  errors: [
    {
      row: 2,
      message: "Email already exists",
    },
  ],
};
```

Row numbers start at 1 after the CSV header and retain their original values when invalid rows are filtered out.

## Import payload

```js
{
  importId: "IMPORT_ID",
  fileName: "contacts.csv",
  rows: [
    {
      name: "Ada",
      email: "ada@example.com",
    },
  ],
  rowNumbers: [1],
  skippedRows: 0,
  uploadedFileHeaders: ["Full Name", "Email"],
  matchedColumnsMap: {
    name: "Full Name",
    email: "Email",
  },
}
```

When unmatched-column delivery is enabled, each row can also include `_unmatched`.

## Completion events

`onComplete` receives `{ importId, accepted, rejected }` after the import finishes. `onClose` runs when the importer modal closes.
