Webhooks
Every event, the delivery format, signature verification and retries.
Webhooks
A webhook is the opposite direction from the API: instead of you calling us, we
POST to a URL of yours when something happens on your account. No API key is
involved — a delivery is trusted because it is signed, not because it is
authenticated.
Endpoints are managed in the account area under Settings → Webhooks, or over the API; see Webhook endpoints.
Events
Subscribe an endpoint to the events you care about; anything you did not subscribe to is never sent.
Domains
| Event | Sent when |
|---|---|
domain.created |
A listing is added |
domain.updated |
A listing changes |
domain.sold |
A listing moves to status sold |
domain.deleted |
A listing is deleted |
Leads
| Event | Sent when |
|---|---|
lead.created |
A new enquiry arrives |
lead.updated |
The lead record changes |
lead.status_changed |
Its status changes |
lead.deleted |
It is moved to the trash |
lead.replied |
You answer the buyer |
Messages
| Event | Sent when |
|---|---|
lead.message_received |
The buyer writes back |
lead.message_sent |
A reply goes out |
Subscription and payments
| Event | Sent when |
|---|---|
subscription.created |
A plan starts |
subscription.updated |
A plan changes |
subscription.cancelled |
A plan ends |
payment.received |
A payment succeeds |
payment.failed |
A payment fails |
Billing is not live yet, so these five are never delivered today. They can already be subscribed to, so an integration is ready when billing arrives.
What a delivery looks like
POST /hooks/ydomain HTTP/1.1
Host: example.com
Content-Type: application/json
User-Agent: ydomain-webhooks/1.0
X-Ydomain-Event: lead.created
X-Ydomain-Signature: t=1789595100,v1=6f1a8c4e2b7d09f3a5c8e1b4d7a2f9c6e3b0d8a5f2c7e4b1d9a6f3c0e7b4d1a8
{
"event": "lead.created",
"created_at": "2026-09-14T11:02:19+00:00",
"data": {
"id": "01920fc4-7b21-7d90-8e55-9a1c3f7d2b08",
"domain": "example.net",
"sender_name": "Pieter Jansen",
"offer_amount": 1500,
"offer_currency": "EUR"
}
}
The envelope is always event, created_at and data. What sits inside
data depends on the event: a lead event carries at least lead_id, a domain
event carries domain. Treat data as a pointer, not as the whole record —
read the record back over the API if you need every field.
Answer with any 2xx within ten seconds. Queue your own work instead of doing
it inline; a slow answer counts as a failure.
Verifying a delivery
Never trust a delivery you have not verified. Sign the timestamp and the raw body with the endpoint's signing secret and compare in constant time.
signed_payload = timestamp + "." + raw_request_body
expected = hmac_sha256(secret, signed_payload)
valid = hash_equals(expected, v1) and abs(now - timestamp) <= 300
PHP:
[$t, $v1] = sscanf($request->header('X-Ydomain-Signature'), 't=%d,v1=%s');
$expected = hash_hmac('sha256', $t.'.'.$request->getContent(), $secret);
$valid = hash_equals($expected, $v1) && abs(time() - $t) <= 300;
Node:
const [, t, v1] = /t=(\d+),v1=([a-f0-9]+)/.exec(req.headers['x-ydomain-signature']);
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const valid = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))
&& Math.abs(Date.now() / 1000 - Number(t)) <= 300;
Two things go wrong most often:
- Using the parsed and re-encoded JSON instead of the raw body. Key order and whitespace change, and the digest no longer matches. Capture the raw body before your framework parses it.
- Ignoring the timestamp. The tolerance is what stops a captured delivery being replayed at you later; five minutes is the intended window.
The secret is shown in the account area under Settings → Webhooks: open the endpoint and press Show. Rotate replaces it, at which point deliveries signed with the old secret stop validating, so update your receiver first.
Retries and failures
- A failed attempt is retried five times, with backoff at 10s, 1m, 5m, 30m and 2h.
- After 10 consecutive failures the endpoint is disabled. Saving it again re-enables it and resets the counter.
- Endpoints must be publicly routable HTTPS URLs. Private, loopback and link-local addresses are refused, and checked again on every attempt, so an endpoint whose DNS is later repointed inward stops being called.
- Redirects are not followed.
- Recent deliveries, their status codes and response snippets are listed under Settings → Webhooks.
Deliveries are at-least-once: a receiver that timed out after doing its work will see the same event again. Key your handling on the event plus the record id and make it idempotent.
Managing endpoints
Endpoints are added, edited and deleted in the account area under Settings → Webhooks. The same three actions are available over the HTTP API; see Webhook endpoints.