Webhooks

Subscribe to real-time notifications for new violations, enforcement actions, and compliance status changes. Requires Pro tier.

Endpoints

POST/v1/webhooksPro+

Create a new webhook subscription.

GET/v1/webhooksPro+

List all your webhook subscriptions.

GET/v1/webhooks/{webhook_id}Pro+

Get details for a specific webhook (secret is redacted).

PATCH/v1/webhooks/{webhook_id}Pro+

Update a webhook's URL, events, facility list, or active status.

DELETE/v1/webhooks/{webhook_id}Pro+

Delete a webhook subscription.

Event Types

EventDescription
violation.newA new violation is recorded for a watched facility
case.newA new enforcement case is filed involving a watched facility
case.updatedA case status changes (e.g., settled, closed)
facility.status_changedA facility's compliance status changes

Create a webhook

POST/v1/webhooks

Request Body (JSON)

urlrequiredstring

HTTPS URL to deliver webhook payloads to

e.g. https://myapp.com/webhooks/parley
eventsrequiredstring[]

Array of event types to subscribe to

facility_idsstring[]

Array of facility UUIDs to watch. Omit to watch all facilities.

Request
curl -X POST "https://api.parley.dev/v1/webhooks" \  -H "X-API-Key: prl_YOUR_KEY_HERE" \  -H "Content-Type: application/json" \  -d '{    "url": "https://myapp.com/webhooks/parley",    "events": ["violation.new", "case.new"],    "facility_ids": ["f47ac10b-58cc-4372-a567-0e02b2c3d479"]  }'
201Createdapplication/json
{  "data": {    "id": "wh_a1b2c3d4-...",    "url": "https://myapp.com/webhooks/parley",    "events": ["violation.new", "case.new"],    "facility_ids": ["f47ac10b-..."],    "secret": "whsec_9f8e7d6c5b4a3210...",    "is_active": true,    "created_at": "2026-03-20T12:00:00Z"  }}

Important: The secret field is only returned once, at creation time. Store it securely for verifying webhook signatures.

Delivery Payload

When an event matches your subscription, Parley sends an HTTP POST to your webhook URL with the following payload:

Webhook delivery body
{  "id": "evt_a1b2c3d4",  "type": "violation.new",  "created_at": "2026-03-20T12:00:00Z",  "data": {    "violation": {      "id": "a1b2c3d4-...",      "facility_id": "f47ac10b-...",      "facility_name": "ACME CHEMICAL PLANT",      "program": "CAA",      "violation_type": "High Priority Violation",      "violation_date": "2025-11-22",      "severity": "HIGH"    }  }}

Verifying Signatures

Every webhook delivery includes an X-Parley-Signature header containing an HMAC-SHA256 signature of the raw request body.

X-Parley-Signature: sha256=5d7226c3c7a2...

Verification example (Node.js)

verify-webhook.js
const crypto = require("crypto");function verifySignature(secret, body, signature) {  const expected = "sha256=" +    crypto.createHmac("sha256", secret)      .update(body)      .digest("hex");  return crypto.timingSafeEqual(    Buffer.from(expected),    Buffer.from(signature)  );}// In your Express handler:app.post("/webhooks/parley", (req, res) => {  const sig = req.headers["x-parley-signature"];  const valid = verifySignature(WEBHOOK_SECRET, req.rawBody, sig);  if (!valid) return res.status(401).send("Invalid signature");  const event = req.body;  console.log(event.type, event.data);  res.status(200).send("ok");});

Verification example (Python)

verify_webhook.py
import hmacimport hashlibdef verify_signature(secret: str, body: bytes, signature: str) -> bool:    expected = "sha256=" + hmac.new(        secret.encode(),        body,        hashlib.sha256    ).hexdigest()    return hmac.compare_digest(expected, signature)# In your Flask handler:@app.route("/webhooks/parley", methods=["POST"])def handle_webhook():    sig = request.headers.get("X-Parley-Signature")    if not verify_signature(WEBHOOK_SECRET, request.data, sig):        return "Invalid signature", 401    event = request.json    print(event["type"], event["data"])    return "ok", 200

Retry Policy

If your endpoint returns a non-2xx status code, Parley will retry delivery with exponential backoff:

AttemptDelay
Retry 11 minute
Retry 25 minutes
Retry 330 minutes
Retry 42 hours
Retry 512 hours

After 5 consecutive failures, the webhook is automatically deactivated. You can reactivate it by sending a PATCH request with "is_active": true.