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

# Webhook Verification

> Verify Borough webhook signatures in Express and Next.js

<Note>Webhooks require a **Business** tier subscription.</Note>

## Overview

Borough uses the [Standard Webhooks](https://www.standardwebhooks.com/) specification for signing webhook payloads. The SDK provides middleware helpers for Express and Next.js to verify signatures and parse events.

## Installation

The webhook helpers require the `standardwebhooks` peer dependency:

```bash theme={null}
npm install @borough/sdk standardwebhooks
```

## Express

```typescript theme={null}
import express from "express";
import { webhookMiddleware } from "@borough/sdk/webhooks/express";

const app = express();

app.post(
  "/webhooks/borough",
  // IMPORTANT: Use raw body parser — signature is verified against the raw body
  express.raw({ type: "application/json" }),
  webhookMiddleware(process.env.BOROUGH_WEBHOOK_SECRET!, async (event) => {
    switch (event.type) {
      case "listing.price_decreased":
        console.log(
          `Listing ${event.data.listingId} price dropped to ${event.data.newValue}`
        );
        break;
      case "listing.created":
        console.log(`New listing: ${event.data.listingId}`);
        break;
      case "listing.status_changed":
        console.log(
          `Listing ${event.data.listingId}: ${event.data.oldValue} → ${event.data.newValue}`
        );
        break;
    }
  })
);

app.listen(3000);
```

## Next.js (App Router)

```typescript theme={null}
// app/api/webhooks/borough/route.ts
import { webhookHandler } from "@borough/sdk/webhooks/nextjs";

export const POST = webhookHandler(
  process.env.BOROUGH_WEBHOOK_SECRET!,
  async (event) => {
    switch (event.type) {
      case "listing.price_decreased":
        // Send alert, update database, etc.
        await notifyUser(event.data.listingId, event.data.newValue);
        break;
      case "listing.expired":
        await removeFromWatchlist(event.data.listingId);
        break;
    }
  }
);
```

## Event types

| Event                     | Triggered when                                       |
| ------------------------- | ---------------------------------------------------- |
| `listing.created`         | A new listing appeared in the search index           |
| `listing.price_decreased` | Listing price dropped by more than \$25              |
| `listing.price_increased` | Listing price increased by more than \$25            |
| `listing.status_changed`  | Listing status changed (e.g., ACTIVE to OFF\_MARKET) |
| `listing.expired`         | Listing went off-market                              |

## Event payload

All events share the same shape:

```typescript theme={null}
interface WebhookEvent {
  type: string;
  data: {
    listingId: string;
    oldValue?: string;
    newValue?: string;
  };
  timestamp: string; // ISO 8601
}
```

## Manual verification

If you're not using Express or Next.js, verify the signature manually:

```typescript theme={null}
import { Webhook } from "standardwebhooks";

const wh = new Webhook(process.env.BOROUGH_WEBHOOK_SECRET!);

// headers: webhook-id, webhook-timestamp, webhook-signature
// body: raw request body as string
try {
  const event = wh.verify(body, {
    "webhook-id": headers["webhook-id"],
    "webhook-timestamp": headers["webhook-timestamp"],
    "webhook-signature": headers["webhook-signature"],
  });
  // event is verified and safe to process
} catch (err) {
  // Signature verification failed — reject the request
  return new Response("Invalid signature", { status: 401 });
}
```

## Webhook secret

Your webhook secret is returned when you [create a subscription](/borough/api-reference/webhooks/create-webhook). It starts with `whsec_` and should be stored securely (e.g., environment variable). Each subscription has its own unique secret.

## Retry behavior

Failed deliveries (non-2xx responses) are retried up to **7 times** with increasing delays. Ensure your handler returns a 2xx status within 10 seconds to acknowledge receipt. After all retries are exhausted, the delivery is moved to a dead letter queue.
