Webhooks
Receive YourNextHome events at your own HTTPS endpoint.
Webhooks let YourNextHome push events to a URL you control as soon as something happens — for v1, that means a new inquiry. You no longer need to poll our API to find out a lead came in.
What ships in v1
A single event type:
| Event | When it fires |
|---|---|
inquiry.created | A visitor submits the inquiry form on one of your viewers or embeds. |
The signing scheme, headers, and delivery guarantees described below are designed to extend — adding new event types later will not break existing integrations.
Configure an endpoint
- Sign in to the dashboard as an organization admin.
- Open Settings → Webhooks.
- Generate the signing secret and copy it once — we never show it again. Store it in your secrets manager.
- Click Add endpoint, paste your HTTPS URL, enable
inquiry.created, and save.
The URL must be HTTPS and must resolve to a public address. We refuse loopback (127.0.0.0/8), private (10/8, 172.16/12, 192.168/16, fc00::/7), and link-local (169.254/16, fe80::/10) ranges to prevent abuse.
Receiving a webhook
Each delivery is an HTTPS POST with a JSON body.
Request headers
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | YourNextHome-Webhooks/1.0 |
X-YNH-Event-Id | UUID, stable across retries — use it for receiver-side dedup |
X-YNH-Event-Type | e.g. inquiry.created |
X-YNH-Delivery-Id | UUID, unique per attempt |
X-YNH-Attempt | 1-based attempt counter |
X-YNH-Timestamp | Unix seconds, signed alongside the body |
X-YNH-Signature | t=<timestamp>,v1=<hex(hmac_sha256(secret, "<ts>.<body>"))> |
Body (inquiry.created)
{
"id": "11111111-2222-3333-4444-555555555555",
"type": "inquiry.created",
"created": "2026-05-13T10:00:00.000Z",
"data": {
"inquiry": {
"id": "...",
"name": "Jane Doe",
"email": "jane@example.com",
"phoneNumber": "+1 555 123 4567",
"info": "Interested in the corner unit.",
"apartmentIds": ["..."],
"estateId": "...",
"referralUserId": null,
"createdAt": "2026-05-13T09:59:58.500Z"
}
}
}We deliberately omit ip and userAgent — PII you don't need to receive.
Verify the signature
Always recompute the HMAC and compare with timingSafeEqual — never trust the X-YNH-Signature header verbatim.
import crypto from 'node:crypto';
function verify(req, secret) {
const sigHeader = req.headers['x-ynh-signature'];
const [tPart, v1Part] = sigHeader.split(',');
const ts = Number(tPart.slice(2));
const v1 = v1Part.slice(3);
// Reject events older than 5 minutes (clock-skew + replay protection)
if (Math.abs(Date.now() / 1000 - ts) > 300) {
throw new Error('stale webhook timestamp');
}
// req.rawBody must be the exact bytes we received — do not re-serialize
const expected = crypto
.createHmac('sha256', secret)
.update(`${ts}.${req.rawBody.toString('utf8')}`)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(v1, 'hex'))) {
throw new Error('bad webhook signature');
}
}A quick curl check against a freshly delivered payload (handy for ops):
TIMESTAMP=$(date -r 1715616000 +%s)
PAYLOAD='{"id":"...","type":"inquiry.created"}'
SECRET='whsec_replace_me'
SIG=$(printf '%s.%s' "$TIMESTAMP" "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
echo "t=$TIMESTAMP,v1=$SIG"Delivery guarantees
-
At-least-once. Receivers must dedup by
X-YNH-Event-Id. -
No ordering guarantee between events. A replay can arrive after a newer event.
-
Up to 6 attempts spaced out over ~30 hours, with ±20 % jitter on each delay:
Attempt Delay from previous Cumulative 1 0 0 2 1 min 1 min 3 5 min 6 min 4 30 min 36 min 5 3 h ~4 h 6 24 h ~28 h -
Success = HTTP 2xx. Anything else (4xx, 5xx, network error, timeout) counts as a failure and triggers the next attempt.
-
Response capture. We record HTTP status + the first 2 KB of the body — handy for debugging your handler.
-
Auto-disable. After 20 consecutive dead deliveries we disable the endpoint and stop sending. Re-enable it from the dashboard once you've fixed your handler.
-
Retention. We keep delivery logs for 30 days.
Rotating the secret
Open Settings → Webhooks → Rotate signing secret. The old secret stops working immediately — there is no overlap window in v1. Update every consumer to the new secret before rotating in production.
If you lose the secret, just rotate. There is no "reveal again" endpoint.
Replay
From the deliveries drawer on a given endpoint, click Replay on any past attempt. We insert a fresh delivery row that reuses the original X-YNH-Event-Id. Your receiver's dedup logic should treat the replay as a no-op for events you've already processed.
Troubleshooting
| Symptom | Fix |
|---|---|
| Endpoint is Auto-disabled | Look at recent deliveries for the failure reason, fix your handler, then re-enable from the dashboard. The failure counter resets on the next success. |
All attempts show endpoint disabled | The endpoint was paused/disabled between dispatch and the attempt. Re-enable and replay. |
| Signature check fails | You're probably parsing the body as JSON and re-serializing it before signing. Sign the raw bytes we sent. |
| Receiver returns 200 but never sees the row | Your handler is acking before processing. Process synchronously or use your own outbox. |