Skip to content

Webhook Verification

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.

The webhook helpers require the standardwebhooks peer dependency:

Terminal window
npm install @borough/sdk standardwebhooks
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);
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;
}
}
);
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

All events share the same shape:

interface WebhookEvent {
type: string;
data: {
listingId: string;
oldValue?: string;
newValue?: string;
};
timestamp: string; // ISO 8601
}

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 });
}

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.

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.