# Webhook delivery

> Webhook delivery sends imported records from CSVJetty to an HTTPS endpoint you control.

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

## Configure a webhook

Select **Webhook** in the importer's delivery settings and enter your HTTPS endpoint. The React component does not need `onData`.

```jsx
import CSVJetty from "csvjetty";

export default function ImportContacts() {
  return (
    <CSVJetty importerKey="YOUR_IMPORTER_KEY">
      Import contacts
    </CSVJetty>
  );
}
```

## Webhook requests

CSVJetty sends records in batches of up to 1,000 rows. Large individual records can result in smaller batches.

Each request includes:

- `Idempotency-Key`: the import ID and batch number.
- `CSVJetty-Signature`: a timestamp and HMAC-SHA256 signature.

Webhook payloads include the normal import payload plus zero-based `batch` and `totalBatches` values.

## Verify signatures

Compute HMAC-SHA256 over the timestamp, a period, and the exact raw request body. Compare in constant time and reject timestamps older than five minutes.

```js
import crypto from "node:crypto";

export const config = {
  api: {
    bodyParser: false,
  },
};

async function readRawBody(req) {
  const chunks = [];
  for await (const chunk of req) chunks.push(Buffer.from(chunk));
  return Buffer.concat(chunks);
}

function validSignature(rawBody, header, secret) {
  const parts = Object.fromEntries(
    String(header || "")
      .split(",")
      .map((part) => part.trim().split("="))
  );

  const timestamp = Number(parts.t);
  const signature = parts.v1;

  if (!Number.isFinite(timestamp) || !signature) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(String(timestamp))
    .update(".")
    .update(rawBody)
    .digest();

  let received;
  try {
    received = Buffer.from(signature, "hex");
  } catch {
    return false;
  }

  return (
    expected.length === received.length &&
    crypto.timingSafeEqual(expected, received)
  );
}

export default async function handler(req, res) {
  if (req.method !== "POST") return res.status(405).end();

  const rawBody = await readRawBody(req);
  const signature = req.headers["csvjetty-signature"];

  if (!validSignature(rawBody, signature, process.env.CSVJETTY_WEBHOOK_SECRET)) {
    return res.status(401).end();
  }

  const payload = JSON.parse(rawBody.toString("utf8"));
  const idempotencyKey = req.headers["idempotency-key"];

  await saveImportBatch({ payload, idempotencyKey });

  return res.status(200).json({ errors: [] });
}
```

Verify the exact raw body before parsing and re-serializing JSON.

## Responses

Return any HTTP 2xx response with an empty body, or return an `errors` array.

```json
{
  "errors": []
}
```

To reject individual records, return objects containing the original CSV row number and an error message.

## Retries and idempotency

Deduplicate every request using `Idempotency-Key`. Saved batches can be retried from the CSVJetty dashboard within 24 hours of the first batch; batches that already succeeded are skipped.
