Skip to main content
Webhooks require a Business tier subscription.

Overview

Borough uses the Standard Webhooks 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:
npm install @borough/sdk standardwebhooks

Express

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)

// 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

EventTriggered when
listing.createdA new listing appeared in the search index
listing.price_decreasedListing price dropped by more than $25
listing.price_increasedListing price increased by more than $25
listing.status_changedListing status changed (e.g., ACTIVE to IN_CONTRACT)
listing.expiredListing went off-market

Event payload

All events share the same shape:
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:
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. 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.