> ## Documentation Index
> Fetch the complete documentation index at: https://docs.isnap.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Example: handling webhooks

> Gives a complete Node.js server that verifies webhook deliveries with the SDK, skips the event_id values it has already processed, and handles two message events.

This script is an HTTP server that receives the deliveries of a webhook subscription. It verifies each request, skips the events it has already processed, and handles two message events. Use it as the starting point of your own endpoint. [Webhooks](/sdk/webhooks) describes the functions it uses.

> [!NOTE]
>
> The script processes the real events of your account. Message events carry the handles and the text of real messages.

## The script

```typescript handle-webhooks.ts theme={null}
import { createServer, type IncomingMessage } from 'node:http'
import {
  WebhookSignatureError,
  isMessageEvent,
  verifyWebhook,
  type WebhookEventBase
} from '@isnap/sdk'

const secret = process.env.ISNAP_WEBHOOK_SECRET
if (!secret) {
  console.error('Set ISNAP_WEBHOOK_SECRET to the signing secret of your webhook subscription.')
  process.exit(1)
}

// The ids of the events already processed. This set is lost on restart and is not
// shared between processes: replace it with storage of your own.
const processed = new Set<string>()

const processEvent = async (event: WebhookEventBase): Promise<void> => {
  // Other families, probes and event types this script does not know: nothing to do.
  if (!isMessageEvent(event)) return

  const { message } = event.data
  switch (event.event_type) {
    case 'message.received':
      console.log(`${message.id} received on ${message.line_id}`)
      break
    case 'message.failed':
      console.log(`${message.id} failed: ${message.error_code ?? 'reported by the device'}`)
      break
  }
}

const handle = async (req: IncomingMessage): Promise<number> => {
  if (req.method !== 'POST' || req.url !== '/webhooks/isnap') return 404

  // Keep the body as received. Verification fails on a body that was parsed and serialized again.
  const chunks: Buffer[] = []
  for await (const chunk of req) chunks.push(chunk as Buffer)
  const rawBody = Buffer.concat(chunks).toString('utf8')

  let event: WebhookEventBase
  try {
    event = verifyWebhook(rawBody, req.headers, secret)
  } catch (err) {
    if (err instanceof WebhookSignatureError) return 400
    throw err
  }

  // A retry or a replay carries the same event_id.
  if (processed.has(event.event_id)) return 200

  await processEvent(event)
  // Record the event_id only after the work succeeded. When processEvent throws,
  // the server responds 500, and the next attempt, if one remains, processes the event.
  processed.add(event.event_id)
  return 200
}

const server = createServer((req, res) => {
  handle(req).then(
    (status) => {
      res.writeHead(status).end()
    },
    () => {
      res.writeHead(500).end()
    }
  )
})

server.listen(3000, () => {
  console.log('Listening on port 3000, path /webhooks/isnap')
})
```

## Run it

Install the SDK, and `typescript` and `@types/node` as development dependencies. Set `"type": "module"` in the `package.json` of the project, then save the script as `handle-webhooks.ts` with the [`tsconfig.json` of the overview](/sdk/overview#typescript-settings).

**1. Expose the server**

The script listens on port 3000 over plain HTTP. A webhook subscription needs a public `https://` URL: put a proxy that terminates TLS in front of the server. [Webhooks overview](/webhooks/overview) lists what an endpoint must do.

**2. Create a webhook subscription**

Create a subscription for `https://<your host>/webhooks/isnap` with [`POST /v1/webhooks`](/api-reference/webhooks/create-webhook), or with `client.webhooks.create`. The response carries the signing secret: store it in `ISNAP_WEBHOOK_SECRET`. The API returns the secret at creation and at rotation only.

**3. Start the server**

Set `ISNAP_WEBHOOK_SECRET` in the environment, then start the script with one of the commands below.

```bash Bun theme={null}
bun handle-webhooks.ts
```

```bash Node.js theme={null}
npx tsc
node handle-webhooks.js
```

To check the verification before you subscribe, run the tests of [Test your verification](/webhooks/verifying-signatures#test-your-verification). Do not send the test vector to this server: its timestamp is in the past, so the server refuses it with `400`.

## What it does

| Step | Behavior |
| - | - |
| Raw body | The script reads the body as bytes and decodes it as UTF-8. It never parses the body before verification. |
| Verification | `verifyWebhook` checks the signature and the timestamp, and returns the event. |
| Deduplication | The script skips an `event_id` it has already processed, and responds `200`. |
| Processing | `isMessageEvent` gives `event.data.message` its type. The script handles `message.received` and `message.failed`, and ignores every other event type. |
| Response | `200` after the processing. Respond within 10 seconds: when your processing can take longer, store the event, respond, then process it. |

Ignore response fields, enum values and event types you do not know. The script acknowledges them with `200`.

Events do not announce every change: see [What events do not announce](/webhooks/event-catalog#what-events-do-not-announce), and reconcile from the resource operations.

## Errors it handles

| Case | Response | What iSnap does |
| - | - | - |
| `WebhookSignatureError`: a missing header, a timestamp outside the tolerance, a signature that does not match, or a malformed body. | `400` | Counts a failed attempt. iSnap retries the delivery on its retry schedule. |
| The processing throws. | `500` | Counts a failed attempt. iSnap retries the delivery on its retry schedule. The script records an `event_id` only after the processing succeeded, so the next attempt processes the event. After the last attempt the delivery ends `failed`: replay it, or read the event from the [events feed](/webhooks/pulling-events). |
| An event already processed. | `200` | Nothing more. |
| Another method or another path. | `404` | |

Never respond `410`: it makes the subscription inactive. [Retries](/webhooks/delivery#retries) gives the schedule of the attempts, and [Suspension](/webhooks/delivery#suspension) says what repeated failures do to a subscription.

## Before production

* **Deduplication.** Deduplicate on `event_id`. Replace the in-memory set with storage that your processes share and that survives a restart, with a unique constraint on the event id. See [Duplicates](/webhooks/delivery#duplicates).
* **Order.** Delivery order is not guaranteed. Order events by the timestamps inside `data`, not by arrival.
* **Secret.** Keep the signing secret in a secret manager. The script stops when `ISNAP_WEBHOOK_SECRET` is not set: keep that check.
* **Personal data.** The script prints message ids and line ids only. Do not copy a handle or a message text into logs, tickets or other tools, and refer to a contact by an id of your own.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.