React to events with webhooks
Subscribe to WOHNO events, verify the HMAC signature, and handle retries and re-delivery from your own endpoint or an automation tool like Zapier.
This guide shows how to receive real-time notifications when things change in WOHNO — a new application, a published listing, a booked appointment — instead of polling. You will create a webhook subscription, verify its HMAC signature, and handle delivery failures.
The problem this solves: you want your systems (a CRM, a Slack channel, a Zapier flow) to react the moment something happens in WOHNO.
Beta / dark-shipped. The webhooks management API (Plan 57) is gated behind the feature flag
WEBHOOKS_PUBLIC_API_ENABLED; while off, these endpoints return404. The operations arex-internaland not in the public reference. Ask your WOHNO contact to enable them.
Prerequisites
- A secret key (
sk_live_…) — webhooks management is sk-only. Publishable keys are rejected with403 INSUFFICIENT_SCOPE. - Scope
webhooks:write(implieswebhooks:read);webhooks:deleteto remove subscriptions. - A public HTTPS endpoint that can receive
POSTrequests. The URL is SSRF-checked at creation, so internal/private addresses are rejected.
Step 1 — Create a webhook subscription
List the event types you want under events (validated against WOHNO's event
whitelist):
curl -X POST https://wohno.de/api/v1/webhooks \
-H "X-API-Key: sk_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.example.com/wohno",
"events": ["listing.published", "application.status_changed", "appointment.booked"],
"description": "Production CRM sync",
"active": true
}'The 201 response contains the HMAC secret — this is the only time it
is ever returned. Store it securely; you need it to verify every incoming
delivery.
{
"data": {
"id": "wh_1a2b3c4d",
"url": "https://hooks.example.com/wohno",
"events": [
"listing.published",
"application.status_changed",
"appointment.booked"
],
"secret": "whsec_only_shown_once_xxxxxxxx",
"active": true
}
}If you lose the secret, you must create a new webhook (or rotate). GET and
PATCH never return it again.
Step 2 — Verify the HMAC signature on your endpoint
Every delivery carries a X-Webhook-ID and X-Event-Type header plus an
X-Webhook-Signature of the form sha256=<hex>. Compute HMAC-SHA256 over the
raw request body using your stored secret, and compare in constant time.
Reject anything that does not match.
// Node.js (Express) — verify before trusting the payload
import crypto from "node:crypto";
const WEBHOOK_SECRET = process.env.WOHNO_WEBHOOK_SECRET; // whsec_…
app.post("/wohno", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("X-Webhook-Signature") || ""; // "sha256=<hex>"
const signature = header.replace(/^sha256=/, "");
const expected = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(req.body) // raw Buffer, not parsed JSON
.digest("hex");
const ok =
signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!ok) return res.status(401).send("invalid signature");
const event = JSON.parse(req.body.toString());
const type = req.get("X-Event-Type");
const eventId = req.get("X-Webhook-ID"); // use for de-duplication
// Respond fast (2xx) and process asynchronously.
res.status(202).send("ok");
handleEvent(type, eventId, event);
});Always verify against the raw bytes — re-serializing parsed JSON can change the payload and break the signature.
Step 3 — Respond quickly and let retries work
Return a 2xx status as soon as you have stored the event; do the real work
afterwards. If your endpoint is slow, errors, or returns a non-2xx, WOHNO
retries the delivery automatically.
Step 4 — Inspect and re-deliver failures
List delivery attempts for a webhook (offset-paginated; payloads and response bodies are omitted as they may contain third-party data):
curl "https://wohno.de/api/v1/webhooks/wh_1a2b3c4d/deliveries?status=failed&page=1&per_page=50" \
-H "X-API-Key: sk_live_xxxxxxxxxxxxxxxxxxxxxxxx"Re-queue a specific failed delivery:
curl -X POST https://wohno.de/api/v1/webhooks/wh_1a2b3c4d/deliveries/dlv_99/redeliver \
-H "X-API-Key: sk_live_xxxxxxxxxxxxxxxxxxxxxxxx"This returns 202 with { "redelivery_id": "...", "status": "pending" }. A
409 WEBHOOK_INACTIVE means the webhook is disabled — re-enable it with PATCH
first.
Using Zapier or another automation tool
Point the webhook url at the tool's inbound-webhook URL. If the tool cannot
verify HMAC, terminate the signature check in a thin proxy you control and forward
only verified events onward — never trust an unverified payload.
Error handling
| Code | HTTP | What happened | Fix |
|---|---|---|---|
VALIDATION_ERROR | 400 | Bad URL (not HTTPS/SSRF) or unknown event | Use a public HTTPS URL and whitelisted events. |
INSUFFICIENT_SCOPE | 403 | pk_ key or missing webhooks:write | Use a secret key with the scope. |
NOT_FOUND | 404 | Flag off, or webhook/delivery not yours | Confirm the flag and ownership. |
WEBHOOK_INACTIVE | 409 | Re-delivery on a disabled webhook | PATCH active: true, then retry. |
See the conventions reference for the full error table.
Best practices
- Verify every delivery. An unverified payload is untrusted input.
- Be idempotent. Use
X-Webhook-IDto de-duplicate; retries can deliver the same event more than once. - Subscribe narrowly. List only the events you act on.
Next steps
- Sync listings from your CRM — pair webhooks with the write API.
- Pull applications into your ATS — react to
application.*events. - API Reference — webhook payload schemas.