Webhooks
Receive real-time POST notifications when new permits are added that match your filters. Perfect for lead generation pipelines, monitoring competitor projects, and triggering workflows the moment a permit goes live.
How webhooks work
You register an endpoint URL with optional filters. Whenever a new permit lands in our database matching those filters, PermitStack POSTs a JSON event to your URL within ~60 seconds. Each request includes an HMAC-SHA256 signature you can verify to confirm authenticity.
- Register a webhook endpoint with filters (city, state, category, ZIP)
- Receive POST events as new matching permits arrive
- Verify the
X-PermitStack-Signatureheader to ensure the request came from us - Respond with 2xx within 10 seconds — anything else triggers retry
Quick start
Register a webhook via the dashboard (easiest) or the API:
curl -X POST https://api.permit-stack.com/v1/webhooks/ \
-H "X-API-Key: pk_yourkey..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/permits",
"city": "Los Angeles",
"state": "CA",
"category": "solar"
}'
That's it. When the next solar permit gets ingested for Los Angeles, your endpoint receives a POST.
Event payload
Each webhook delivery is a POST request with a JSON body:
{
"event": "permit.created",
"delivered_at": "2026-05-02T15:30:00.000Z",
"data": {
"id": "f7c3e8a9-1234-5678-9abc-def012345678",
"permit_number": "BLD-2026-04-12345",
"address": {
"street": "1234 SUNSET BLVD",
"city": "LOS ANGELES",
"state": "CA",
"zip": "90026"
},
"category": "SOLAR",
"status": "ISSUED",
"description": "INSTALL 8.4KW ROOF MOUNTED PV SOLAR SYSTEM",
"estimated_value": 32500.00,
"date_filed": "2026-04-15",
"date_issued": "2026-05-01",
"jurisdiction": "Los Angeles"
}
}
Headers sent with each delivery
| Header | Description |
|---|---|
Content-Type | application/json |
User-Agent | PermitStack-Webhooks/1.0 |
X-PermitStack-Event | Event type (e.g. permit.created, permit.test) |
X-PermitStack-Signature | HMAC-SHA256 of request body, signed with your secret |
X-PermitStack-Delivery-Id | Unique delivery ID (the permit ID for real events) |
Verifying the signature
Always verify X-PermitStack-Signature matches HMAC-SHA256 of the raw request body using your webhook's secret. This proves the request actually came from PermitStack and hasn't been tampered with.
Get your signing secret from the dashboard or via the API: GET /v1/webhooks/{id}/secret
import hashlib
import hmac
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = "your_secret_from_dashboard"
@app.route("/webhooks/permits", methods=["POST"])
def receive_webhook():
# Verify signature
signature = request.headers.get("X-PermitStack-Signature", "")
expected = hmac.new(
WEBHOOK_SECRET.encode(),
request.data,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(signature, expected):
abort(401, "Invalid signature")
# Process the permit
payload = request.json
permit = payload["data"]
print(f"New {permit['category']} permit at {permit['address']['street']}")
# Respond 2xx within 10 seconds
return "", 200
const express = require("express");
const crypto = require("crypto");
const app = express();
const WEBHOOK_SECRET = "your_secret_from_dashboard";
// Important: use raw body for signature verification
app.post(
"/webhooks/permits",
express.raw({ type: "application/json" }),
(req, res) => {
const signature = req.headers["x-permitstack-signature"] || "";
const expected = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(req.body)
.digest("hex");
if (
!crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
)
) {
return res.status(401).send("Invalid signature");
}
const payload = JSON.parse(req.body.toString());
const permit = payload.data;
console.log(`New ${permit.category} permit at ${permit.address.street}`);
res.status(200).end();
}
);
app.listen(3000);
<?php
$WEBHOOK_SECRET = "your_secret_from_dashboard";
$body = file_get_contents("php://input");
$signature = $_SERVER["HTTP_X_PERMITSTACK_SIGNATURE"] ?? "";
$expected = hash_hmac("sha256", $body, $WEBHOOK_SECRET);
if (!hash_equals($expected, $signature)) {
http_response_code(401);
echo "Invalid signature";
exit;
}
$payload = json_decode($body, true);
$permit = $payload["data"];
error_log("New {$permit['category']} permit at {$permit['address']['street']}");
http_response_code(200);
Filtering
All filters are optional. Omit a filter to match any value. Multiple filters combine with AND logic.
| Filter | Type | Example |
|---|---|---|
city | string | "Los Angeles" |
state | 2-letter code | "CA" |
category | string | "solar", "electrical", "hvac", "plumbing", "roofing", "new_construction", "addition", "alteration", "demolition" |
zip_code | string | "90026" |
Retries and reliability
If your endpoint returns a non-2xx status or doesn't respond within 10 seconds, PermitStack retries 3 times with exponential backoff (1s, 5s, 15s). After 3 failures, the failure is logged and we move on to the next permit.
If your webhook accumulates 10 consecutive failed deliveries, we automatically deactivate it to protect both sides. You'll see the failure reason and last status code in the dashboard. Once your endpoint is healthy again, reactivate by registering a new webhook with the same filters.
X-PermitStack-Delivery-Id header (which equals the permit ID) to deduplicate.
Testing your endpoint
Send a synthetic test event to verify your integration works before relying on real permits:
curl -X POST https://api.permit-stack.com/v1/webhooks/{webhook_id}/test \
-H "X-API-Key: pk_yourkey..."
The test event uses event: "permit.test" with a synthetic payload. Your handler should ignore test events in production logic but still verify the signature.
From the dashboard, click "Send test" on any registered webhook.
Limits
- Max 10 active webhooks per API key
- Endpoint must be HTTPS (HTTP not allowed)
- Endpoint must respond within 10 seconds
- Available on Developer tier ($49/mo) and above
Common questions
X-PermitStack-Delivery-Id header to deduplicate — it's the permit's UUID.last_error field.GET /v1/permits/search endpoint with date filters.Ready to integrate?
Webhooks are available on the Developer plan ($49/month) and above. Includes 10,000 requests/day, AI-enriched data, and priority email support.
Upgrade to Developer — $49/mo