Skip to content

Open Beta – help us test! All listings are examples only.

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 return 404. The operations are x-internal and 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 with 403 INSUFFICIENT_SCOPE.
  • Scope webhooks:write (implies webhooks:read); webhooks:delete to remove subscriptions.
  • A public HTTPS endpoint that can receive POST requests. 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

CodeHTTPWhat happenedFix
VALIDATION_ERROR400Bad URL (not HTTPS/SSRF) or unknown eventUse a public HTTPS URL and whitelisted events.
INSUFFICIENT_SCOPE403pk_ key or missing webhooks:writeUse a secret key with the scope.
NOT_FOUND404Flag off, or webhook/delivery not yoursConfirm the flag and ownership.
WEBHOOK_INACTIVE409Re-delivery on a disabled webhookPATCH 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-ID to de-duplicate; retries can deliver the same event more than once.
  • Subscribe narrowly. List only the events you act on.

Next steps