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
| 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 IN_CONTRACT) |
listing.expired | Listing 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.